feat(medusa, link-modules): sales channel <> cart link (#5459)
* feat: sales channel joiner config * feat: product sales channel link config, SC list method * feat: migration * fix: refactor list SC * refactor: SC repo api * chore: changeset * feat: add dedicated FF * wip: cart<>sc link and migration * chore: changeset * fix: update migration with the cart table constraints * feat: populate the pivot table * chore: remove relation from joiner config * fix: constraint name * fix: filter out link relations when calling internal services * feat: product<> sc join entity * fix: update case * fix: add FF on in the repository, fix tests * fix: assign id when FF is on * fix: target table * feat: product service - fetch SC with RQ * feat: admin list products & SC with isolated product domain * feat: get admin product * feat: store endpoints * fix: remove duplicate import * fix: remove "name" prop * feat: typeorm entity changes * feat: pivot table, entity, on cart create changes * feat: update carts' SC * feat: cart - getValidatedSalesChannel with RQ * feat: refactor * wip: changes to create cart workflow * fix: remove join table entity due to migrations failing * fix: product seeder if FF is on * feat: attach SC handler and test * fix: env * feat: workflow compensation, cart service retrieve with RQ * fix: remote joiner implode map * chore: update changesets * fix: remove methods from SC service/repo * feat: use remote link in handlers * fix: remove SC service calls * fix: link params * fix: migration add constraint to make link upsert pass * refactor: workflow product handlers to handle remote links * fix: condition * fix: use correct method * fix: build * wip: update FF * fix: update FF in the handlers * chore: migrate to medusav2 FF * chore: uncomment test * fix: product factory * fix: unlinking SC and product * fix: use module name variable * refactor: cleanup query definitions * fix: add constraint * wip: migrate FF * fix: comments * feat: cart entity callbacks, fix tests * fix: only create SC in test * wip: services updates, changes to models * chore: rename prop * fix: add hook * fix: address comments * fix: temp sc filtering * fix: use RQ to filter by SC * fix: relations on retrieve * feat: migration sync data, remove FF * fix: revert order of queries * fix: alter migration, relations in service * fix: revert id * fix: migrations * fix: make expand work * fix: remote link method call * fix: try making tests work without id in the pivot table * test: use remote link * test: relations changes * fix: preserve channel id column * fix: seeder and factory * fix: remove sales_channels from response * feat: support feature flag arrays * fix: cover everything with correct FF * fix: remove verbose * fix: unit and plugin tests * chore: comments * fix: reenable workflow handler, add comments, split cart create workflow tests * chore: reenable link in the create mehod, update changesets * fix: address feedback * fix: revert migration * fix: change the migration to follow link module * fix: migration syntax * fix: merge conflicts * fix: typo * feat: remove store sales channel foreign key * fix: merge migrations * fix: FF keys * refactor: cart service * refactor: FF missing key * fix: comments * fix: address PR comments * fix: new changesets * fix: revert flag router changes * chore: refactor `isFeatureEnabled` --------- Co-authored-by: Carlos R. L. Rodrigues <rodrigolr@gmail.com> Co-authored-by: Riqwan Thamir <rmthamir@gmail.com>
This commit is contained in:
28 files changed
+662
-89
No files matched your search
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/orchestration": patch
|
||||||
|
"@medusajs/link-modules": patch
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: SalesChannel <> Cart joiner config
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
ShippingMethodFactoryData,
|
ShippingMethodFactoryData,
|
||||||
simpleShippingMethodFactory,
|
simpleShippingMethodFactory,
|
||||||
} from "./simple-shipping-method-factory"
|
} from "./simple-shipping-method-factory"
|
||||||
|
import { generateEntityId } from "@medusajs/utils"
|
||||||
|
|
||||||
export type CartFactoryData = {
|
export type CartFactoryData = {
|
||||||
id?: string
|
id?: string
|
||||||
@@ -32,6 +33,8 @@ export type CartFactoryData = {
|
|||||||
sales_channel_id?: string
|
sales_channel_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isMedusaV2Enabled = process.env.MEDUSA_FF_MEDUSA_V2 == "true"
|
||||||
|
|
||||||
export const simpleCartFactory = async (
|
export const simpleCartFactory = async (
|
||||||
dataSource: DataSource,
|
dataSource: DataSource,
|
||||||
data: CartFactoryData = {},
|
data: CartFactoryData = {},
|
||||||
@@ -77,7 +80,7 @@ export const simpleCartFactory = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const id = data.id || `simple-cart-${Math.random() * 1000}`
|
const id = data.id || `simple-cart-${Math.random() * 1000}`
|
||||||
const toSave = manager.create(Cart, {
|
let toSave = {
|
||||||
id,
|
id,
|
||||||
email:
|
email:
|
||||||
typeof data.email !== "undefined" ? data.email : faker.internet.email(),
|
typeof data.email !== "undefined" ? data.email : faker.internet.email(),
|
||||||
@@ -85,7 +88,18 @@ export const simpleCartFactory = async (
|
|||||||
customer_id: customerId,
|
customer_id: customerId,
|
||||||
shipping_address_id: address.id,
|
shipping_address_id: address.id,
|
||||||
sales_channel_id: sales_channel?.id ?? data.sales_channel_id ?? null,
|
sales_channel_id: sales_channel?.id ?? data.sales_channel_id ?? null,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if (isMedusaV2Enabled) {
|
||||||
|
await manager.query(
|
||||||
|
`INSERT INTO "cart_sales_channel" (id, cart_id, sales_channel_id)
|
||||||
|
VALUES ('${generateEntityId(undefined, "cartsc")}', '${toSave.id}', '${
|
||||||
|
sales_channel?.id ?? data.sales_channel_id
|
||||||
|
}');`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
toSave = manager.create(Cart, toSave)
|
||||||
|
|
||||||
const cart = await manager.save(toSave)
|
const cart = await manager.save(toSave)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { Region } from "@medusajs/medusa"
|
||||||
|
import path from "path"
|
||||||
|
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
|
||||||
|
import { useApi } from "../../../../environment-helpers/use-api"
|
||||||
|
import { initDb, useDb } from "../../../../environment-helpers/use-db"
|
||||||
|
import {
|
||||||
|
simpleProductFactory,
|
||||||
|
simpleSalesChannelFactory,
|
||||||
|
} from "../../../../factories"
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
const env = {
|
||||||
|
MEDUSA_FF_MEDUSA_V2: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("/store/carts", () => {
|
||||||
|
let dbConnection
|
||||||
|
let shutdownServer
|
||||||
|
|
||||||
|
const doAfterEach = async () => {
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||||
|
dbConnection = await initDb({ cwd, env } as any)
|
||||||
|
shutdownServer = await startBootstrapApp({ cwd, env })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
await shutdownServer()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /store/carts", () => {
|
||||||
|
let prod1
|
||||||
|
let prodSale
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const manager = dbConnection.manager
|
||||||
|
await manager.insert(Region, {
|
||||||
|
id: "region",
|
||||||
|
name: "Test Region",
|
||||||
|
currency_code: "usd",
|
||||||
|
tax_rate: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
await manager.query(
|
||||||
|
`UPDATE "country"
|
||||||
|
SET region_id='region'
|
||||||
|
WHERE iso_2 = 'us'`
|
||||||
|
)
|
||||||
|
|
||||||
|
prod1 = await simpleProductFactory(dbConnection, {
|
||||||
|
id: "test-product",
|
||||||
|
variants: [{ id: "test-variant_1" }],
|
||||||
|
})
|
||||||
|
|
||||||
|
prodSale = await simpleProductFactory(dbConnection, {
|
||||||
|
id: "test-product-sale",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: "test-variant-sale",
|
||||||
|
prices: [{ amount: 1000, currency: "usd" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
id: "amazon-sc",
|
||||||
|
name: "Amazon store",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await doAfterEach()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should create a cart in a sales channel", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api.post("/store/carts", {
|
||||||
|
sales_channel_id: "amazon-sc",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
|
||||||
|
const getRes = await api.get(`/store/carts/${response.data.cart.id}`)
|
||||||
|
expect(getRes.status).toEqual(200)
|
||||||
|
expect(getRes.data.cart.sales_channel.id).toEqual("amazon-sc")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -23,7 +23,7 @@ describe("/store/carts", () => {
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||||
dbConnection = await initDb({ cwd })
|
dbConnection = await initDb({ cwd } as any)
|
||||||
shutdownServer = await startBootstrapApp({ cwd })
|
shutdownServer = await startBootstrapApp({ cwd })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,7 @@ const {
|
|||||||
startBootstrapApp,
|
startBootstrapApp,
|
||||||
} = require("../../../../environment-helpers/bootstrap-app")
|
} = require("../../../../environment-helpers/bootstrap-app")
|
||||||
const { initDb, useDb } = require("../../../../environment-helpers/use-db")
|
const { initDb, useDb } = require("../../../../environment-helpers/use-db")
|
||||||
const {
|
const { useApi } = require("../../../../environment-helpers/use-api")
|
||||||
setPort,
|
|
||||||
useApi,
|
|
||||||
useExpressServer,
|
|
||||||
} = require("../../../../environment-helpers/use-api")
|
|
||||||
|
|
||||||
const adminSeeder = require("../../../../helpers/admin-seeder")
|
const adminSeeder = require("../../../../helpers/admin-seeder")
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { exportWorkflow, pipe } from "@medusajs/workflows-sdk"
|
|||||||
enum CreateCartActions {
|
enum CreateCartActions {
|
||||||
setContext = "setContext",
|
setContext = "setContext",
|
||||||
attachLineItems = "attachLineItems",
|
attachLineItems = "attachLineItems",
|
||||||
|
attachToSalesChannel = "attachToSalesChannel",
|
||||||
findRegion = "findRegion",
|
findRegion = "findRegion",
|
||||||
findSalesChannel = "findSalesChannel",
|
findSalesChannel = "findSalesChannel",
|
||||||
createCart = "createCart",
|
createCart = "createCart",
|
||||||
@@ -58,10 +59,13 @@ const workflowSteps: TransactionStepsDefinition = {
|
|||||||
noCompensation: true,
|
noCompensation: true,
|
||||||
next: {
|
next: {
|
||||||
action: CreateCartActions.createCart,
|
action: CreateCartActions.createCart,
|
||||||
next: {
|
next: [
|
||||||
action: CreateCartActions.attachLineItems,
|
{
|
||||||
noCompensation: true,
|
action: CreateCartActions.attachLineItems,
|
||||||
},
|
noCompensation: true,
|
||||||
|
},
|
||||||
|
{ action: CreateCartActions.attachToSalesChannel },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -134,6 +138,10 @@ const handlers = new Map([
|
|||||||
invoke: pipe(
|
invoke: pipe(
|
||||||
{
|
{
|
||||||
invoke: [
|
invoke: [
|
||||||
|
{
|
||||||
|
from: CreateCartActions.findSalesChannel,
|
||||||
|
alias: CartHandlers.createCart.aliases.SalesChannel,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
from: CreateCartActions.findRegion,
|
from: CreateCartActions.findRegion,
|
||||||
alias: CartHandlers.createCart.aliases.Region,
|
alias: CartHandlers.createCart.aliases.Region,
|
||||||
@@ -186,6 +194,38 @@ const handlers = new Map([
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
CreateCartActions.attachToSalesChannel,
|
||||||
|
{
|
||||||
|
invoke: pipe(
|
||||||
|
{
|
||||||
|
invoke: [
|
||||||
|
{
|
||||||
|
from: CreateCartActions.createCart,
|
||||||
|
alias: CartHandlers.attachCartToSalesChannel.aliases.Cart,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: CreateCartActions.findSalesChannel,
|
||||||
|
alias: CartHandlers.attachCartToSalesChannel.aliases.SalesChannel,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
CartHandlers.attachCartToSalesChannel
|
||||||
|
),
|
||||||
|
compensate: pipe(
|
||||||
|
{
|
||||||
|
invoke: [
|
||||||
|
{
|
||||||
|
from: CreateCartActions.findSalesChannel,
|
||||||
|
alias:
|
||||||
|
CartHandlers.detachCartFromSalesChannel.aliases.SalesChannel,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
CartHandlers.detachCartFromSalesChannel
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
])
|
])
|
||||||
|
|
||||||
WorkflowManager.register(Workflows.CreateCart, workflowSteps, handlers)
|
WorkflowManager.register(Workflows.CreateCart, workflowSteps, handlers)
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { MedusaV2Flag } from "@medusajs/utils"
|
||||||
|
import { WorkflowArguments } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
type HandlerInputData = {
|
||||||
|
cart: {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
sales_channel: {
|
||||||
|
sales_channel_id: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Aliases {
|
||||||
|
Cart = "cart",
|
||||||
|
SalesChannel = "sales_channel",
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function attachCartToSalesChannel({
|
||||||
|
container,
|
||||||
|
data,
|
||||||
|
}: WorkflowArguments<HandlerInputData>): Promise<void> {
|
||||||
|
const featureFlagRouter = container.resolve("featureFlagRouter")
|
||||||
|
const remoteLink = container.resolve("remoteLink")
|
||||||
|
|
||||||
|
if (!featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const cart = data[Aliases.Cart]
|
||||||
|
const salesChannel = data[Aliases.SalesChannel]
|
||||||
|
|
||||||
|
await remoteLink.create({
|
||||||
|
cartService: {
|
||||||
|
cart_id: cart.id,
|
||||||
|
},
|
||||||
|
salesChannelService: {
|
||||||
|
sales_channel_id: salesChannel.sales_channel_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
attachCartToSalesChannel.aliases = Aliases
|
||||||
@@ -47,15 +47,13 @@ export async function createCart({
|
|||||||
const cartService = container.resolve("cartService")
|
const cartService = container.resolve("cartService")
|
||||||
const cartServiceTx = cartService.withTransaction(manager)
|
const cartServiceTx = cartService.withTransaction(manager)
|
||||||
|
|
||||||
const cart = await cartServiceTx.create({
|
return await cartServiceTx.create({
|
||||||
...data[Aliases.SalesChannel],
|
...data[Aliases.SalesChannel],
|
||||||
...data[Aliases.Addresses],
|
...data[Aliases.Addresses],
|
||||||
...data[Aliases.Customer],
|
...data[Aliases.Customer],
|
||||||
...data[Aliases.Region],
|
...data[Aliases.Region],
|
||||||
...data[Aliases.Context],
|
...data[Aliases.Context],
|
||||||
})
|
})
|
||||||
|
|
||||||
return cart
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createCart.aliases = Aliases
|
createCart.aliases = Aliases
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { MedusaV2Flag } from "@medusajs/utils"
|
||||||
|
import { WorkflowArguments } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
type HandlerInputData = {
|
||||||
|
cart: {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
sales_channel: {
|
||||||
|
sales_channel_id: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Aliases {
|
||||||
|
Cart = "cart",
|
||||||
|
SalesChannel = "sales_channel",
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function detachCartFromSalesChannel({
|
||||||
|
container,
|
||||||
|
data,
|
||||||
|
}: WorkflowArguments<HandlerInputData>): Promise<void> {
|
||||||
|
const featureFlagRouter = container.resolve("featureFlagRouter")
|
||||||
|
const remoteLink = container.resolve("remoteLink")
|
||||||
|
|
||||||
|
if (!featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const cart = data[Aliases.Cart]
|
||||||
|
const salesChannel = data[Aliases.SalesChannel]
|
||||||
|
|
||||||
|
await remoteLink.dismiss({
|
||||||
|
cartService: {
|
||||||
|
cart_id: cart.id,
|
||||||
|
},
|
||||||
|
salesChannelService: {
|
||||||
|
sales_channel_id: salesChannel.sales_channel_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
detachCartFromSalesChannel.aliases = Aliases
|
||||||
@@ -2,3 +2,5 @@ export * from "./attach-line-items-to-cart"
|
|||||||
export * from "./create-cart"
|
export * from "./create-cart"
|
||||||
export * from "./remove-cart"
|
export * from "./remove-cart"
|
||||||
export * from "./retrieve-cart"
|
export * from "./retrieve-cart"
|
||||||
|
export * from "./attach-cart-to-sales-channel"
|
||||||
|
export * from "./detach-cart-from-sales-channel"
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||||
|
import { LINKS } from "../links"
|
||||||
|
|
||||||
|
export const CartSalesChannel: ModuleJoinerConfig = {
|
||||||
|
serviceName: LINKS.CartSalesChannel,
|
||||||
|
isLink: true,
|
||||||
|
databaseConfig: {
|
||||||
|
tableName: "cart_sales_channel",
|
||||||
|
idPrefix: "cartsc",
|
||||||
|
},
|
||||||
|
alias: [
|
||||||
|
{
|
||||||
|
name: "cart_sales_channel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cart_sales_channels",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primaryKeys: ["id", "cart_id", "sales_channel_id"],
|
||||||
|
relationships: [
|
||||||
|
{
|
||||||
|
serviceName: "cartService",
|
||||||
|
isInternalService: true,
|
||||||
|
primaryKey: "id",
|
||||||
|
foreignKey: "cart_id",
|
||||||
|
alias: "cart",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: "salesChannelService",
|
||||||
|
isInternalService: true,
|
||||||
|
primaryKey: "id",
|
||||||
|
foreignKey: "sales_channel_id",
|
||||||
|
alias: "sales_channel",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extends: [
|
||||||
|
{
|
||||||
|
serviceName: "cartService",
|
||||||
|
fieldAlias: {
|
||||||
|
sales_channel: "sales_channel_link.sales_channel",
|
||||||
|
},
|
||||||
|
relationship: {
|
||||||
|
serviceName: LINKS.CartSalesChannel,
|
||||||
|
isInternalService: true,
|
||||||
|
primaryKey: "cart_id",
|
||||||
|
foreignKey: "id",
|
||||||
|
alias: "sales_channel_link",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
serviceName: "salesChannelService",
|
||||||
|
fieldAlias: {
|
||||||
|
carts: "cart_link.cart",
|
||||||
|
},
|
||||||
|
relationship: {
|
||||||
|
serviceName: LINKS.CartSalesChannel,
|
||||||
|
isInternalService: true,
|
||||||
|
primaryKey: "sales_channel_id",
|
||||||
|
foreignKey: "id",
|
||||||
|
alias: "cart_link",
|
||||||
|
isList: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -3,3 +3,4 @@ export * from "./product-variant-inventory-item"
|
|||||||
export * from "./product-variant-price-set"
|
export * from "./product-variant-price-set"
|
||||||
export * from "./product-shipping-profile"
|
export * from "./product-shipping-profile"
|
||||||
export * from "./product-sales-channel"
|
export * from "./product-sales-channel"
|
||||||
|
export * from "./cart-sales-channel"
|
||||||
@@ -28,4 +28,10 @@ export const LINKS = {
|
|||||||
"salesChannelService",
|
"salesChannelService",
|
||||||
"sales_channel_id"
|
"sales_channel_id"
|
||||||
),
|
),
|
||||||
|
CartSalesChannel: composeLinkName(
|
||||||
|
"cartService",
|
||||||
|
"cart_id",
|
||||||
|
"salesChannelService",
|
||||||
|
"sales_channel_id"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@ import { StorePostCartsCartShippingMethodReq } from "./add-shipping-method"
|
|||||||
import { StorePostCartsCartPaymentSessionReq } from "./set-payment-session"
|
import { StorePostCartsCartPaymentSessionReq } from "./set-payment-session"
|
||||||
import { StorePostCartsCartLineItemsItemReq } from "./update-line-item"
|
import { StorePostCartsCartLineItemsItemReq } from "./update-line-item"
|
||||||
import { StorePostCartsCartPaymentSessionUpdateReq } from "./update-payment-session"
|
import { StorePostCartsCartPaymentSessionUpdateReq } from "./update-payment-session"
|
||||||
|
import { MedusaV2Flag } from "@medusajs/utils"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
@@ -26,7 +27,11 @@ export default (app, container) => {
|
|||||||
app.use("/carts", route)
|
app.use("/carts", route)
|
||||||
|
|
||||||
if (featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key)) {
|
if (featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key)) {
|
||||||
defaultStoreCartRelations.push("sales_channel")
|
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
|
||||||
|
defaultStoreCartRelations.push("sales_channels")
|
||||||
|
} else {
|
||||||
|
defaultStoreCartRelations.push("sales_channel")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject plugin routes
|
// Inject plugin routes
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { Modules } from "@medusajs/modules-sdk"
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||||
|
|
||||||
|
import { Cart } from "../models"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
serviceName: "cartService",
|
serviceName: "cartService",
|
||||||
primaryKeys: ["id"],
|
primaryKeys: ["id"],
|
||||||
linkableKeys: { cart_id: "Cart" },
|
linkableKeys: { cart_id: "Cart" },
|
||||||
alias: [
|
alias: {
|
||||||
{
|
name: ["cart", "carts"],
|
||||||
name: "cart",
|
args: { entity: Cart.name },
|
||||||
},
|
},
|
||||||
],
|
|
||||||
relationships: [
|
relationships: [
|
||||||
{
|
{
|
||||||
serviceName: Modules.PRODUCT,
|
serviceName: Modules.PRODUCT,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export class addTableProductShippingProfile1680857773273
|
|||||||
DROP INDEX IF EXISTS "idx_product_shipping_profile_product_id";
|
DROP INDEX IF EXISTS "idx_product_shipping_profile_product_id";
|
||||||
DROP INDEX IF EXISTS "idx_product_shipping_profile_profile_id";
|
DROP INDEX IF EXISTS "idx_product_shipping_profile_profile_id";
|
||||||
|
|
||||||
ALTER TABLE "product" ADD COLUMN IF NOT EXISTS "profile_id";
|
ALTER TABLE "product" ADD COLUMN IF NOT EXISTS "profile_id" CHARACTER VARYING;
|
||||||
|
|
||||||
UPDATE "product" SET "profile_id" = "product_shipping_profile"."profile_id"
|
UPDATE "product" SET "profile_id" = "product_shipping_profile"."profile_id"
|
||||||
FROM "product_shipping_profile"
|
FROM "product_shipping_profile"
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||||
|
import { MedusaV2Flag } from "@medusajs/utils"
|
||||||
|
|
||||||
|
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
|
||||||
|
|
||||||
|
export const featureFlag = [SalesChannelFeatureFlag.key, MedusaV2Flag.key]
|
||||||
|
|
||||||
|
export class CartSalesChannelsLink1698160215000 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS "cart_sales_channel"
|
||||||
|
(
|
||||||
|
"id" character varying NOT NULL,
|
||||||
|
"cart_id" character varying NOT NULL,
|
||||||
|
"sales_channel_id" character varying NOT NULL,
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
"deleted_at" TIMESTAMP WITH TIME ZONE,
|
||||||
|
CONSTRAINT "cart_sales_channel_pk" PRIMARY KEY ("cart_id", "sales_channel_id"),
|
||||||
|
CONSTRAINT "cart_sales_channel_cart_id_unique" UNIQUE ("cart_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "IDX_id_cart_sales_channel" ON "cart_sales_channel" ("id");
|
||||||
|
|
||||||
|
insert into "cart_sales_channel" (id, cart_id, sales_channel_id)
|
||||||
|
(select 'cartsc_' || substr(md5(random()::text), 0, 27), id, sales_channel_id from "cart" WHERE sales_channel_id IS NOT NULL);
|
||||||
|
|
||||||
|
ALTER TABLE IF EXISTS "cart" DROP CONSTRAINT IF EXISTS "FK_a2bd3c26f42e754b9249ba78fd6";
|
||||||
|
|
||||||
|
ALTER TABLE IF EXISTS "store" DROP CONSTRAINT IF EXISTS "FK_61b0f48cccbb5f41c750bac7286";
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
UPDATE "cart" SET "sales_channel_id" = "cart_sales_channel"."sales_channel_id"
|
||||||
|
FROM "cart_sales_channel"
|
||||||
|
WHERE "cart"."id" = "cart_sales_channel"."cart_id";
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "cart_sales_channel";
|
||||||
|
|
||||||
|
ALTER TABLE IF EXISTS "cart" ADD CONSTRAINT "FK_a2bd3c26f42e754b9249ba78fd6" FOREIGN KEY ("sales_channel_id") REFERENCES "sales_channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
|
||||||
|
|
||||||
|
ALTER TABLE IF EXISTS "store" ADD CONSTRAINT "FK_61b0f48cccbb5f41c750bac7286" FOREIGN KEY ("default_sales_channel_id") REFERENCES "sales_channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { BeforeInsert, Column, Index, PrimaryColumn } from "typeorm"
|
||||||
|
import { MedusaV2Flag, SalesChannelFeatureFlag } from "@medusajs/utils"
|
||||||
|
|
||||||
|
import { generateEntityId } from "../utils"
|
||||||
|
import { SoftDeletableEntity } from "../interfaces"
|
||||||
|
import { FeatureFlagEntity } from "../utils/feature-flag-decorators"
|
||||||
|
|
||||||
|
@FeatureFlagEntity([MedusaV2Flag.key, SalesChannelFeatureFlag.key])
|
||||||
|
export class CartSalesChannel extends SoftDeletableEntity {
|
||||||
|
@Column()
|
||||||
|
id: string
|
||||||
|
|
||||||
|
@Index("cart_sales_channel_cart_id_unique", {
|
||||||
|
unique: true,
|
||||||
|
})
|
||||||
|
@PrimaryColumn()
|
||||||
|
cart_id: string
|
||||||
|
|
||||||
|
@PrimaryColumn()
|
||||||
|
sales_channel_id: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiIgnore
|
||||||
|
*/
|
||||||
|
@BeforeInsert()
|
||||||
|
private beforeInsert(): void {
|
||||||
|
this.id = generateEntityId(this.id, "cartsc")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -233,6 +233,7 @@
|
|||||||
import {
|
import {
|
||||||
AfterLoad,
|
AfterLoad,
|
||||||
BeforeInsert,
|
BeforeInsert,
|
||||||
|
BeforeUpdate,
|
||||||
Column,
|
Column,
|
||||||
Entity,
|
Entity,
|
||||||
Index,
|
Index,
|
||||||
@@ -241,13 +242,10 @@ import {
|
|||||||
ManyToMany,
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
OneToOne
|
OneToOne,
|
||||||
} from "typeorm"
|
} from "typeorm"
|
||||||
|
import { MedusaV2Flag, SalesChannelFeatureFlag } from "@medusajs/utils"
|
||||||
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column"
|
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column"
|
||||||
import {
|
|
||||||
FeatureFlagColumn,
|
|
||||||
FeatureFlagDecorators,
|
|
||||||
} from "../utils/feature-flag-decorators"
|
|
||||||
|
|
||||||
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
||||||
import { generateEntityId } from "../utils/generate-entity-id"
|
import { generateEntityId } from "../utils/generate-entity-id"
|
||||||
@@ -261,6 +259,10 @@ import { PaymentSession } from "./payment-session"
|
|||||||
import { Region } from "./region"
|
import { Region } from "./region"
|
||||||
import { SalesChannel } from "./sales-channel"
|
import { SalesChannel } from "./sales-channel"
|
||||||
import { ShippingMethod } from "./shipping-method"
|
import { ShippingMethod } from "./shipping-method"
|
||||||
|
import {
|
||||||
|
FeatureFlagColumn,
|
||||||
|
FeatureFlagDecorators,
|
||||||
|
} from "../utils/feature-flag-decorators"
|
||||||
|
|
||||||
export enum CartType {
|
export enum CartType {
|
||||||
DEFAULT = "default",
|
DEFAULT = "default",
|
||||||
@@ -387,15 +389,37 @@ export class Cart extends SoftDeletableEntity {
|
|||||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||||
metadata: Record<string, unknown>
|
metadata: Record<string, unknown>
|
||||||
|
|
||||||
@FeatureFlagColumn("sales_channels", { type: "varchar", nullable: true })
|
@FeatureFlagColumn(SalesChannelFeatureFlag.key, {
|
||||||
|
type: "varchar",
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
sales_channel_id: string | null
|
sales_channel_id: string | null
|
||||||
|
|
||||||
@FeatureFlagDecorators("sales_channels", [
|
@FeatureFlagDecorators(SalesChannelFeatureFlag.key, [
|
||||||
ManyToOne(() => SalesChannel),
|
ManyToOne(() => SalesChannel),
|
||||||
JoinColumn({ name: "sales_channel_id" }),
|
JoinColumn({ name: "sales_channel_id" }),
|
||||||
])
|
])
|
||||||
sales_channel: SalesChannel
|
sales_channel: SalesChannel
|
||||||
|
|
||||||
|
@FeatureFlagDecorators(
|
||||||
|
[MedusaV2Flag.key, SalesChannelFeatureFlag.key],
|
||||||
|
[
|
||||||
|
ManyToMany(() => SalesChannel, { cascade: ["remove", "soft-remove"] }),
|
||||||
|
JoinTable({
|
||||||
|
name: "cart_sales_channel",
|
||||||
|
joinColumn: {
|
||||||
|
name: "cart_id",
|
||||||
|
referencedColumnName: "id",
|
||||||
|
},
|
||||||
|
inverseJoinColumn: {
|
||||||
|
name: "sales_channel_id",
|
||||||
|
referencedColumnName: "id",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
sales_channels?: SalesChannel[]
|
||||||
|
|
||||||
shipping_total?: number
|
shipping_total?: number
|
||||||
discount_total?: number
|
discount_total?: number
|
||||||
raw_discount_total?: number
|
raw_discount_total?: number
|
||||||
@@ -412,18 +436,41 @@ export class Cart extends SoftDeletableEntity {
|
|||||||
/**
|
/**
|
||||||
* @apiIgnore
|
* @apiIgnore
|
||||||
*/
|
*/
|
||||||
@AfterLoad()
|
@BeforeInsert()
|
||||||
private afterLoad(): void {
|
private beforeInsert(): void {
|
||||||
if (this.payment_sessions) {
|
this.id = generateEntityId(this.id, "cart")
|
||||||
this.payment_session = this.payment_sessions.find((p) => p.is_selected)!
|
|
||||||
|
if (this.sales_channel_id || this.sales_channel) {
|
||||||
|
this.sales_channels = [
|
||||||
|
{ id: this.sales_channel_id || this.sales_channel?.id },
|
||||||
|
] as SalesChannel[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @apiIgnore
|
* @apiIgnore
|
||||||
*/
|
*/
|
||||||
@BeforeInsert()
|
@BeforeUpdate()
|
||||||
private beforeInsert(): void {
|
private beforeUpdate(): void {
|
||||||
this.id = generateEntityId(this.id, "cart")
|
if (this.sales_channel_id || this.sales_channel) {
|
||||||
|
this.sales_channels = [
|
||||||
|
{ id: this.sales_channel_id || this.sales_channel?.id },
|
||||||
|
] as SalesChannel[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiIgnore
|
||||||
|
*/
|
||||||
|
@AfterLoad()
|
||||||
|
private afterLoad(): void {
|
||||||
|
if (this.payment_sessions) {
|
||||||
|
this.payment_session = this.payment_sessions.find((p) => p.is_selected)!
|
||||||
|
}
|
||||||
|
if (this.sales_channels) {
|
||||||
|
this.sales_channel = this.sales_channels?.[0]
|
||||||
|
this.sales_channel_id = this.sales_channel?.id
|
||||||
|
delete this.sales_channels
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
import { BeforeInsert, Column, JoinTable, ManyToMany, OneToMany } from "typeorm"
|
import { BeforeInsert, Column, JoinTable, ManyToMany, OneToMany } from "typeorm"
|
||||||
|
|
||||||
import { FeatureFlagEntity } from "../utils/feature-flag-decorators"
|
import { MedusaV2Flag } from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
FeatureFlagDecorators,
|
||||||
|
FeatureFlagEntity,
|
||||||
|
} from "../utils/feature-flag-decorators"
|
||||||
import { SoftDeletableEntity } from "../interfaces"
|
import { SoftDeletableEntity } from "../interfaces"
|
||||||
import { DbAwareColumn, generateEntityId } from "../utils"
|
import { DbAwareColumn, generateEntityId } from "../utils"
|
||||||
import { SalesChannelLocation } from "./sales-channel-location"
|
import { SalesChannelLocation } from "./sales-channel-location"
|
||||||
import { Product } from "./product"
|
import { Product } from "./product"
|
||||||
|
import { Cart } from "./cart"
|
||||||
|
|
||||||
@FeatureFlagEntity("sales_channels")
|
@FeatureFlagEntity("sales_channels")
|
||||||
export class SalesChannel extends SoftDeletableEntity {
|
export class SalesChannel extends SoftDeletableEntity {
|
||||||
@@ -34,6 +39,22 @@ export class SalesChannel extends SoftDeletableEntity {
|
|||||||
})
|
})
|
||||||
products: Product[]
|
products: Product[]
|
||||||
|
|
||||||
|
@FeatureFlagDecorators(MedusaV2Flag.key, [
|
||||||
|
ManyToMany(() => Cart),
|
||||||
|
JoinTable({
|
||||||
|
name: "cart_sales_channel",
|
||||||
|
joinColumn: {
|
||||||
|
name: "sales_channel_id",
|
||||||
|
referencedColumnName: "id",
|
||||||
|
},
|
||||||
|
inverseJoinColumn: {
|
||||||
|
name: "cart_id",
|
||||||
|
referencedColumnName: "id",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
carts: Cart[]
|
||||||
|
|
||||||
@OneToMany(
|
@OneToMany(
|
||||||
() => SalesChannelLocation,
|
() => SalesChannelLocation,
|
||||||
(scLocation) => scLocation.sales_channel,
|
(scLocation) => scLocation.sales_channel,
|
||||||
|
|||||||
@@ -2668,6 +2668,7 @@ describe("CartService", () => {
|
|||||||
.register("newTotalsService", asClass(NewTotalsService))
|
.register("newTotalsService", asClass(NewTotalsService))
|
||||||
.register("cartService", asClass(CartService))
|
.register("cartService", asClass(CartService))
|
||||||
.register("remoteQuery", asValue(null))
|
.register("remoteQuery", asValue(null))
|
||||||
|
.register("remoteLink", asValue(null))
|
||||||
.register("pricingModuleService", asValue(undefined))
|
.register("pricingModuleService", asValue(undefined))
|
||||||
.register("pricingService", asClass(PricingService))
|
.register("pricingService", asClass(PricingService))
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ import { PaymentSessionRepository } from "../repositories/payment-session"
|
|||||||
import { ShippingMethodRepository } from "../repositories/shipping-method"
|
import { ShippingMethodRepository } from "../repositories/shipping-method"
|
||||||
import { PaymentSessionInput } from "../types/payment"
|
import { PaymentSessionInput } from "../types/payment"
|
||||||
import { validateEmail } from "../utils/is-email"
|
import { validateEmail } from "../utils/is-email"
|
||||||
|
import { RemoteQueryFunction } from "@medusajs/types"
|
||||||
|
import { RemoteLink } from "@medusajs/modules-sdk"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
@@ -98,6 +100,8 @@ type InjectedDependencies = {
|
|||||||
priceSelectionStrategy: IPriceSelectionStrategy
|
priceSelectionStrategy: IPriceSelectionStrategy
|
||||||
productVariantInventoryService: ProductVariantInventoryService
|
productVariantInventoryService: ProductVariantInventoryService
|
||||||
pricingService: PricingService
|
pricingService: PricingService
|
||||||
|
remoteQuery: RemoteQueryFunction
|
||||||
|
remoteLink: RemoteLink
|
||||||
}
|
}
|
||||||
|
|
||||||
type TotalsConfig = {
|
type TotalsConfig = {
|
||||||
@@ -139,6 +143,8 @@ class CartService extends TransactionBaseService {
|
|||||||
protected readonly priceSelectionStrategy_: IPriceSelectionStrategy
|
protected readonly priceSelectionStrategy_: IPriceSelectionStrategy
|
||||||
protected readonly lineItemAdjustmentService_: LineItemAdjustmentService
|
protected readonly lineItemAdjustmentService_: LineItemAdjustmentService
|
||||||
protected readonly featureFlagRouter_: FlagRouter
|
protected readonly featureFlagRouter_: FlagRouter
|
||||||
|
protected remoteQuery_: RemoteQueryFunction
|
||||||
|
protected remoteLink_: RemoteLink
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
protected readonly productVariantInventoryService_: ProductVariantInventoryService
|
protected readonly productVariantInventoryService_: ProductVariantInventoryService
|
||||||
protected readonly pricingService_: PricingService
|
protected readonly pricingService_: PricingService
|
||||||
@@ -169,6 +175,8 @@ class CartService extends TransactionBaseService {
|
|||||||
salesChannelService,
|
salesChannelService,
|
||||||
featureFlagRouter,
|
featureFlagRouter,
|
||||||
storeService,
|
storeService,
|
||||||
|
remoteQuery,
|
||||||
|
remoteLink,
|
||||||
productVariantInventoryService,
|
productVariantInventoryService,
|
||||||
pricingService,
|
pricingService,
|
||||||
}: InjectedDependencies) {
|
}: InjectedDependencies) {
|
||||||
@@ -202,6 +210,8 @@ class CartService extends TransactionBaseService {
|
|||||||
this.storeService_ = storeService
|
this.storeService_ = storeService
|
||||||
this.productVariantInventoryService_ = productVariantInventoryService
|
this.productVariantInventoryService_ = productVariantInventoryService
|
||||||
this.pricingService_ = pricingService
|
this.pricingService_ = pricingService
|
||||||
|
this.remoteQuery_ = remoteQuery
|
||||||
|
this.remoteLink_ = remoteLink
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -216,6 +226,7 @@ class CartService extends TransactionBaseService {
|
|||||||
const cartRepo = this.activeManager_.withRepository(this.cartRepository_)
|
const cartRepo = this.activeManager_.withRepository(this.cartRepository_)
|
||||||
|
|
||||||
const query = buildQuery(selector, config)
|
const query = buildQuery(selector, config)
|
||||||
|
|
||||||
return await cartRepo.find(query)
|
return await cartRepo.find(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +368,10 @@ class CartService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key)
|
this.featureFlagRouter_.isFeatureEnabled(
|
||||||
|
SalesChannelFeatureFlag.key
|
||||||
|
) &&
|
||||||
|
!this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key)
|
||||||
) {
|
) {
|
||||||
rawCart.sales_channel_id = (
|
rawCart.sales_channel_id = (
|
||||||
await this.getValidatedSalesChannel(data.sales_channel_id)
|
await this.getValidatedSalesChannel(data.sales_channel_id)
|
||||||
@@ -365,8 +379,7 @@ class CartService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.customer_id || data.customer) {
|
if (data.customer_id || data.customer) {
|
||||||
const customer =
|
const customer = (data.customer ??
|
||||||
(data.customer ??
|
|
||||||
(data.customer_id &&
|
(data.customer_id &&
|
||||||
(await this.customerService_
|
(await this.customerService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
@@ -476,6 +489,27 @@ class CartService extends TransactionBaseService {
|
|||||||
|
|
||||||
const createdCart = cartRepo.create(rawCart)
|
const createdCart = cartRepo.create(rawCart)
|
||||||
const cart = await cartRepo.save(createdCart)
|
const cart = await cartRepo.save(createdCart)
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.featureFlagRouter_.isFeatureEnabled([
|
||||||
|
SalesChannelFeatureFlag.key,
|
||||||
|
MedusaV2Flag.key,
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
const salesChannel = await this.getValidatedSalesChannel(
|
||||||
|
data.sales_channel_id
|
||||||
|
)
|
||||||
|
|
||||||
|
await this.remoteLink_.create({
|
||||||
|
cartService: {
|
||||||
|
cart_id: cart.id,
|
||||||
|
},
|
||||||
|
salesChannelService: {
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
await this.eventBus_
|
await this.eventBus_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.emit(CartService.Events.CREATED, {
|
.emit(CartService.Events.CREATED, {
|
||||||
@@ -491,9 +525,20 @@ class CartService extends TransactionBaseService {
|
|||||||
): Promise<SalesChannel | never> {
|
): Promise<SalesChannel | never> {
|
||||||
let salesChannel: SalesChannel
|
let salesChannel: SalesChannel
|
||||||
if (isDefined(salesChannelId)) {
|
if (isDefined(salesChannelId)) {
|
||||||
salesChannel = await this.salesChannelService_
|
if (this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key)) {
|
||||||
.withTransaction(this.activeManager_)
|
const query = {
|
||||||
.retrieve(salesChannelId)
|
sales_channel: {
|
||||||
|
__args: {
|
||||||
|
id: salesChannelId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
;[salesChannel] = await this.remoteQuery_(query)
|
||||||
|
} else {
|
||||||
|
salesChannel = await this.salesChannelService_
|
||||||
|
.withTransaction(this.activeManager_)
|
||||||
|
.retrieve(salesChannelId)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
salesChannel = (
|
salesChannel = (
|
||||||
await this.storeService_.withTransaction(this.activeManager_).retrieve({
|
await this.storeService_.withTransaction(this.activeManager_).retrieve({
|
||||||
@@ -584,7 +629,7 @@ class CartService extends TransactionBaseService {
|
|||||||
* Returns true if all products in the cart can be fulfilled with the current
|
* Returns true if all products in the cart can be fulfilled with the current
|
||||||
* shipping methods.
|
* shipping methods.
|
||||||
* @param shippingMethods - the set of shipping methods to check from
|
* @param shippingMethods - the set of shipping methods to check from
|
||||||
* @param lineItem - the line item
|
* @param lineItemShippingProfiledId - the line item
|
||||||
* @return boolean representing whether shipping method is validated
|
* @return boolean representing whether shipping method is validated
|
||||||
*/
|
*/
|
||||||
protected validateLineItemShipping_(
|
protected validateLineItemShipping_(
|
||||||
@@ -655,17 +700,22 @@ class CartService extends TransactionBaseService {
|
|||||||
lineItem: LineItem,
|
lineItem: LineItem,
|
||||||
config = { validateSalesChannels: true }
|
config = { validateSalesChannels: true }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const select: (keyof Cart)[] = ["id"]
|
const fields: (keyof Cart)[] = ["id"]
|
||||||
|
const relations: (keyof Cart)[] = ["shipping_methods"]
|
||||||
|
|
||||||
if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) {
|
if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) {
|
||||||
select.push("sales_channel_id")
|
if (this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key)) {
|
||||||
|
relations.push("sales_channels")
|
||||||
|
} else {
|
||||||
|
fields.push("sales_channel_id")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.atomicPhase_(
|
return await this.atomicPhase_(
|
||||||
async (transactionManager: EntityManager) => {
|
async (transactionManager: EntityManager) => {
|
||||||
let cart = await this.retrieve(cartId, {
|
let cart = await this.retrieve(cartId, {
|
||||||
select,
|
select: fields,
|
||||||
relations: ["shipping_methods"],
|
relations,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) {
|
if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) {
|
||||||
@@ -797,17 +847,22 @@ class CartService extends TransactionBaseService {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const items: LineItem[] = Array.isArray(lineItems) ? lineItems : [lineItems]
|
const items: LineItem[] = Array.isArray(lineItems) ? lineItems : [lineItems]
|
||||||
|
|
||||||
const select: (keyof Cart)[] = ["id", "customer_id", "region_id"]
|
const fields: (keyof Cart)[] = ["id", "customer_id", "region_id"]
|
||||||
|
const relations: (keyof Cart)[] = ["shipping_methods"]
|
||||||
|
|
||||||
if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) {
|
if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) {
|
||||||
select.push("sales_channel_id")
|
if (this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key)) {
|
||||||
|
relations.push("sales_channels")
|
||||||
|
} else {
|
||||||
|
fields.push("sales_channel_id")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.atomicPhase_(
|
return await this.atomicPhase_(
|
||||||
async (transactionManager: EntityManager) => {
|
async (transactionManager: EntityManager) => {
|
||||||
let cart = await this.retrieve(cartId, {
|
let cart = await this.retrieve(cartId, {
|
||||||
select,
|
select: fields,
|
||||||
relations: ["shipping_methods"],
|
relations,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) {
|
if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) {
|
||||||
@@ -994,7 +1049,7 @@ class CartService extends TransactionBaseService {
|
|||||||
* Updates a cart's existing line item.
|
* Updates a cart's existing line item.
|
||||||
* @param cartId - the id of the cart to update
|
* @param cartId - the id of the cart to update
|
||||||
* @param lineItemId - the id of the line item to update.
|
* @param lineItemId - the id of the line item to update.
|
||||||
* @param lineItemUpdate - the line item to update. Must include an id field.
|
* @param update - the line item to update. Must include an id field.
|
||||||
* @return the result of the update operation
|
* @return the result of the update operation
|
||||||
*/
|
*/
|
||||||
async updateLineItem(
|
async updateLineItem(
|
||||||
@@ -1223,8 +1278,39 @@ class CartService extends TransactionBaseService {
|
|||||||
isDefined(data.sales_channel_id) &&
|
isDefined(data.sales_channel_id) &&
|
||||||
data.sales_channel_id != cart.sales_channel_id
|
data.sales_channel_id != cart.sales_channel_id
|
||||||
) {
|
) {
|
||||||
|
const salesChannel = await this.getValidatedSalesChannel(
|
||||||
|
data.sales_channel_id
|
||||||
|
)
|
||||||
|
|
||||||
await this.onSalesChannelChange(cart, data.sales_channel_id)
|
await this.onSalesChannelChange(cart, data.sales_channel_id)
|
||||||
cart.sales_channel_id = data.sales_channel_id
|
|
||||||
|
/**
|
||||||
|
* TODO: remove this once update cart workflow is build
|
||||||
|
* since this will be handled in a handler by the workflow
|
||||||
|
*/
|
||||||
|
if (this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key)) {
|
||||||
|
if (cart.sales_channel_id) {
|
||||||
|
await this.remoteLink_.dismiss({
|
||||||
|
cartService: {
|
||||||
|
cart_id: cart.id,
|
||||||
|
},
|
||||||
|
salesChannelService: {
|
||||||
|
sales_channel_id: cart.sales_channel_id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.remoteLink_.create({
|
||||||
|
cartService: {
|
||||||
|
cart_id: cart.id,
|
||||||
|
},
|
||||||
|
salesChannelService: {
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
cart.sales_channel_id = salesChannel.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDefined(data.discounts) && data.discounts.length) {
|
if (isDefined(data.discounts) && data.discounts.length) {
|
||||||
@@ -2247,15 +2333,15 @@ class CartService extends TransactionBaseService {
|
|||||||
const lineItemServiceTx =
|
const lineItemServiceTx =
|
||||||
this.lineItemService_.withTransaction(transactionManager)
|
this.lineItemService_.withTransaction(transactionManager)
|
||||||
|
|
||||||
let productShippinProfileMap = new Map<string, string>()
|
let productShippingProfileMap = new Map<string, string>()
|
||||||
|
|
||||||
if (this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key)) {
|
if (this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key)) {
|
||||||
productShippinProfileMap =
|
productShippingProfileMap =
|
||||||
await this.shippingProfileService_.getMapProfileIdsByProductIds(
|
await this.shippingProfileService_.getMapProfileIdsByProductIds(
|
||||||
cart.items.map((item) => item.variant.product_id)
|
cart.items.map((item) => item.variant.product_id)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
productShippinProfileMap = new Map<string, string>(
|
productShippingProfileMap = new Map<string, string>(
|
||||||
cart.items.map((item) => [
|
cart.items.map((item) => [
|
||||||
item.variant?.product?.id,
|
item.variant?.product?.id,
|
||||||
item.variant?.product?.profile_id,
|
item.variant?.product?.profile_id,
|
||||||
@@ -2268,7 +2354,7 @@ class CartService extends TransactionBaseService {
|
|||||||
return lineItemServiceTx.update(item.id, {
|
return lineItemServiceTx.update(item.id, {
|
||||||
has_shipping: this.validateLineItemShipping_(
|
has_shipping: this.validateLineItemShipping_(
|
||||||
methods,
|
methods,
|
||||||
productShippinProfileMap.get(item.variant?.product_id)!
|
productShippingProfileMap.get(item.variant?.product_id)!
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class SalesChannelLocationService extends TransactionBaseService {
|
|||||||
.retrieve(salesChannelId)
|
.retrieve(salesChannelId)
|
||||||
|
|
||||||
if (this.stockLocationService_) {
|
if (this.stockLocationService_) {
|
||||||
// trhows error if not found
|
// throws error if not found
|
||||||
await this.stockLocationService_.retrieve(locationId, undefined, {
|
await this.stockLocationService_.retrieve(locationId, undefined, {
|
||||||
transactionManager: this.activeManager_,
|
transactionManager: this.activeManager_,
|
||||||
})
|
})
|
||||||
@@ -124,7 +124,7 @@ class SalesChannelLocationService extends TransactionBaseService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists the sales channels associated with a stock location.
|
* Lists the sales channels associated with a stock location.
|
||||||
* @param {string} salesChannelId - The ID of the stock location.
|
* @param {string} locationId - The ID of the stock location.
|
||||||
* @returns {Promise<string[]>} A promise that resolves with an array of sales channel IDs.
|
* @returns {Promise<string[]>} A promise that resolves with an array of sales channel IDs.
|
||||||
*/
|
*/
|
||||||
async listSalesChannelIds(locationId: string): Promise<string[]> {
|
async listSalesChannelIds(locationId: string): Promise<string[]> {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function FeatureFlagColumn(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FeatureFlagDecorators(
|
export function FeatureFlagDecorators(
|
||||||
featureFlag: string,
|
featureFlag: string | string[],
|
||||||
decorators: PropertyDecorator[]
|
decorators: PropertyDecorator[]
|
||||||
): PropertyDecorator {
|
): PropertyDecorator {
|
||||||
return function (target, propertyName) {
|
return function (target, propertyName) {
|
||||||
@@ -68,7 +68,7 @@ export function FeatureFlagClassDecorators(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FeatureFlagEntity(
|
export function FeatureFlagEntity(
|
||||||
featureFlag: string,
|
featureFlag: string | string[],
|
||||||
name?: string,
|
name?: string,
|
||||||
options?: EntityOptions
|
options?: EntityOptions
|
||||||
): ClassDecorator {
|
): ClassDecorator {
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ export function remoteQueryFetchData(container: MedusaContainer) {
|
|||||||
...RemoteQuery.getAllFieldsAndRelations(expand),
|
...RemoteQuery.getAllFieldsAndRelations(expand),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const expandRelations = Object.keys(expand.expands ?? {})
|
||||||
|
|
||||||
|
// filter out links from relations because TypeORM will throw if the relation doesn't exist
|
||||||
|
|
||||||
|
options.relations = options.relations.filter(
|
||||||
|
(relation) => !expandRelations.some((ex) => relation.startsWith(ex))
|
||||||
|
)
|
||||||
|
|
||||||
|
options.select = options.relations.filter(
|
||||||
|
(field) => !expandRelations.some((ex) => field.startsWith(ex))
|
||||||
|
)
|
||||||
|
|
||||||
if (ids) {
|
if (ids) {
|
||||||
filters[keyField] = ids
|
filters[keyField] = ids
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,16 +23,16 @@ export type RemoteFetchDataCallback = (
|
|||||||
path?: string
|
path?: string
|
||||||
}>
|
}>
|
||||||
|
|
||||||
|
type InternalImplodeMapping = {
|
||||||
|
location: string[]
|
||||||
|
property: string
|
||||||
|
path: string[]
|
||||||
|
isList?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export class RemoteJoiner {
|
export class RemoteJoiner {
|
||||||
private serviceConfigCache: Map<string, JoinerServiceConfig> = new Map()
|
private serviceConfigCache: Map<string, JoinerServiceConfig> = new Map()
|
||||||
|
|
||||||
private implodeMapping: {
|
|
||||||
location: string[]
|
|
||||||
property: string
|
|
||||||
path: string[]
|
|
||||||
isList?: boolean
|
|
||||||
}[] = []
|
|
||||||
|
|
||||||
private static filterFields(
|
private static filterFields(
|
||||||
data: any,
|
data: any,
|
||||||
fields: string[],
|
fields: string[],
|
||||||
@@ -355,7 +355,8 @@ export class RemoteJoiner {
|
|||||||
|
|
||||||
private handleFieldAliases(
|
private handleFieldAliases(
|
||||||
items: any[],
|
items: any[],
|
||||||
parsedExpands: Map<string, RemoteExpandProperty>
|
parsedExpands: Map<string, RemoteExpandProperty>,
|
||||||
|
implodeMapping: InternalImplodeMapping[]
|
||||||
) {
|
) {
|
||||||
const getChildren = (item: any, prop: string) => {
|
const getChildren = (item: any, prop: string) => {
|
||||||
if (Array.isArray(item)) {
|
if (Array.isArray(item)) {
|
||||||
@@ -373,7 +374,7 @@ export class RemoteJoiner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cleanup: [any, string][] = []
|
const cleanup: [any, string][] = []
|
||||||
for (const alias of this.implodeMapping) {
|
for (const alias of implodeMapping) {
|
||||||
const propPath = alias.path
|
const propPath = alias.path
|
||||||
|
|
||||||
let itemsLocation = items
|
let itemsLocation = items
|
||||||
@@ -432,7 +433,8 @@ export class RemoteJoiner {
|
|||||||
|
|
||||||
private async handleExpands(
|
private async handleExpands(
|
||||||
items: any[],
|
items: any[],
|
||||||
parsedExpands: Map<string, RemoteExpandProperty>
|
parsedExpands: Map<string, RemoteExpandProperty>,
|
||||||
|
implodeMapping: InternalImplodeMapping[] = []
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!parsedExpands) {
|
if (!parsedExpands) {
|
||||||
return
|
return
|
||||||
@@ -458,7 +460,7 @@ export class RemoteJoiner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handleFieldAliases(items, parsedExpands)
|
this.handleFieldAliases(items, parsedExpands, implodeMapping)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async expandProperty(
|
private async expandProperty(
|
||||||
@@ -567,13 +569,15 @@ export class RemoteJoiner {
|
|||||||
initialService: RemoteExpandProperty,
|
initialService: RemoteExpandProperty,
|
||||||
query: RemoteJoinerQuery,
|
query: RemoteJoinerQuery,
|
||||||
serviceConfig: JoinerServiceConfig,
|
serviceConfig: JoinerServiceConfig,
|
||||||
expands: RemoteJoinerQuery["expands"]
|
expands: RemoteJoinerQuery["expands"],
|
||||||
|
implodeMapping: InternalImplodeMapping[]
|
||||||
): Map<string, RemoteExpandProperty> {
|
): Map<string, RemoteExpandProperty> {
|
||||||
const parsedExpands = this.parseProperties(
|
const parsedExpands = this.parseProperties(
|
||||||
initialService,
|
initialService,
|
||||||
query,
|
query,
|
||||||
serviceConfig,
|
serviceConfig,
|
||||||
expands
|
expands,
|
||||||
|
implodeMapping
|
||||||
)
|
)
|
||||||
|
|
||||||
const groupedExpands = this.groupExpands(parsedExpands)
|
const groupedExpands = this.groupExpands(parsedExpands)
|
||||||
@@ -585,7 +589,8 @@ export class RemoteJoiner {
|
|||||||
initialService: RemoteExpandProperty,
|
initialService: RemoteExpandProperty,
|
||||||
query: RemoteJoinerQuery,
|
query: RemoteJoinerQuery,
|
||||||
serviceConfig: JoinerServiceConfig,
|
serviceConfig: JoinerServiceConfig,
|
||||||
expands: RemoteJoinerQuery["expands"]
|
expands: RemoteJoinerQuery["expands"],
|
||||||
|
implodeMapping: InternalImplodeMapping[]
|
||||||
): Map<string, RemoteExpandProperty> {
|
): Map<string, RemoteExpandProperty> {
|
||||||
const parsedExpands = new Map<string, any>()
|
const parsedExpands = new Map<string, any>()
|
||||||
parsedExpands.set(BASE_PATH, initialService)
|
parsedExpands.set(BASE_PATH, initialService)
|
||||||
@@ -612,7 +617,7 @@ export class RemoteJoiner {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
this.implodeMapping.push({
|
implodeMapping.push({
|
||||||
location: currentPath,
|
location: currentPath,
|
||||||
property: prop,
|
property: prop,
|
||||||
path: fullPath,
|
path: fullPath,
|
||||||
@@ -799,6 +804,7 @@ export class RemoteJoiner {
|
|||||||
(arg) => !serviceConfig.primaryKeys.includes(arg.name)
|
(arg) => !serviceConfig.primaryKeys.includes(arg.name)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const implodeMapping: InternalImplodeMapping[] = []
|
||||||
const parsedExpands = this.parseExpands(
|
const parsedExpands = this.parseExpands(
|
||||||
{
|
{
|
||||||
property: "",
|
property: "",
|
||||||
@@ -809,7 +815,8 @@ export class RemoteJoiner {
|
|||||||
},
|
},
|
||||||
queryObj,
|
queryObj,
|
||||||
serviceConfig,
|
serviceConfig,
|
||||||
queryObj.expands!
|
queryObj.expands!,
|
||||||
|
implodeMapping
|
||||||
)
|
)
|
||||||
|
|
||||||
const root = parsedExpands.get(BASE_PATH)!
|
const root = parsedExpands.get(BASE_PATH)!
|
||||||
@@ -823,7 +830,11 @@ export class RemoteJoiner {
|
|||||||
|
|
||||||
const data = response.path ? response.data[response.path!] : response.data
|
const data = response.path ? response.data[response.path!] : response.data
|
||||||
|
|
||||||
await this.handleExpands(Array.isArray(data) ? data : [data], parsedExpands)
|
await this.handleExpands(
|
||||||
|
Array.isArray(data) ? data : [data],
|
||||||
|
parsedExpands,
|
||||||
|
implodeMapping
|
||||||
|
)
|
||||||
|
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export function isObject(obj: unknown): obj is object {
|
export function isObject(obj: any): obj is object {
|
||||||
return typeof obj === "object" && !!obj
|
return obj != null && obj?.constructor?.name === "Object"
|
||||||
}
|
}
|
||||||
@@ -19,22 +19,24 @@ export class FlagRouter implements FeatureFlagTypes.IFlagRouter {
|
|||||||
* @param flag - The flag to check
|
* @param flag - The flag to check
|
||||||
* @return {boolean} - Whether the flag is enabled or not
|
* @return {boolean} - Whether the flag is enabled or not
|
||||||
*/
|
*/
|
||||||
public isFeatureEnabled(flag: string | Record<string, string>): boolean {
|
public isFeatureEnabled(
|
||||||
if (isString(flag)) {
|
flag: string | string[] | Record<string, string>
|
||||||
return !!this.flags[flag]
|
): boolean {
|
||||||
}
|
|
||||||
|
|
||||||
if (isObject(flag)) {
|
if (isObject(flag)) {
|
||||||
const [nestedFlag, value] = Object.entries(flag)[0]
|
const [nestedFlag, value] = Object.entries(flag)[0]
|
||||||
|
|
||||||
if (typeof this.flags[nestedFlag] === "boolean") {
|
if (typeof this.flags[nestedFlag] === "boolean") {
|
||||||
return this.flags[nestedFlag] as boolean
|
return this.flags[nestedFlag] as boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!this.flags[nestedFlag]?.[value]
|
return !!this.flags[nestedFlag]?.[value]
|
||||||
}
|
}
|
||||||
|
|
||||||
throw Error("Flag must be a string or an object")
|
const flags = (Array.isArray(flag) ? flag : [flag]) as string[]
|
||||||
|
return flags.every((flag_) => {
|
||||||
|
if (!isString(flag_)) {
|
||||||
|
throw Error("Flag must be a string an array of string or an object")
|
||||||
|
}
|
||||||
|
return !!this.flags[flag_]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in new issue
Block a user