diff --git a/integration-tests/api/__tests__/store/cart/cart.js b/integration-tests/api/__tests__/store/cart/cart.js index 2832bd3833..a17602d7f4 100644 --- a/integration-tests/api/__tests__/store/cart/cart.js +++ b/integration-tests/api/__tests__/store/cart/cart.js @@ -556,6 +556,7 @@ describe("/store/carts", () => { { id: "test-li", variant_id: "test-variant", + product_id: "test-product", quantity: 1, unit_price: 100, adjustments: [ @@ -644,6 +645,7 @@ describe("/store/carts", () => { id: "line-item-2", cart_id: discountCart.id, variant_id: "test-variant-quantity", + product_id: "test-product", unit_price: 950, quantity: 1, adjustments: [ @@ -713,6 +715,7 @@ describe("/store/carts", () => { id: "line-item-2", cart_id: discountCart.id, variant_id: "test-variant-quantity", + product_id: "test-product", unit_price: 1000, quantity: 1, adjustments: [ @@ -804,6 +807,7 @@ describe("/store/carts", () => { unit_price: 1000, quantity: 1, variant_id: "test-variant-quantity", + product_id: "test-product", cart_id: "test-cart-w-total-fixed-discount", }) @@ -847,6 +851,7 @@ describe("/store/carts", () => { unit_price: 1000, quantity: 1, variant_id: "test-variant-quantity", + product_id: "test-product", cart_id: "test-cart-w-total-percentage-discount", }) @@ -890,6 +895,7 @@ describe("/store/carts", () => { unit_price: 1000, quantity: 1, variant_id: "test-variant-quantity", + product_id: "test-product", cart_id: "test-cart-w-item-fixed-discount", }) @@ -933,6 +939,7 @@ describe("/store/carts", () => { unit_price: 1000, quantity: 1, variant_id: "test-variant-quantity", + product_id: "test-product", cart_id: "test-cart-w-item-percentage-discount", }) @@ -1050,6 +1057,7 @@ describe("/store/carts", () => { line_items: [ { variant_id: "test-variant", + product_id: "test-product", unit_price: 100, }, ], @@ -1110,6 +1118,7 @@ describe("/store/carts", () => { line_items: [ { variant_id: "test-variant", + product_id: "test-product", unit_price: 100, }, ], @@ -1260,6 +1269,7 @@ describe("/store/carts", () => { line_items: [ { variant_id: "test-variant", + product_id: "test-product", unit_price: 100, }, ], @@ -1327,6 +1337,7 @@ describe("/store/carts", () => { line_items: [ { variant_id: "test-variant", + product_id: "test-product", unit_price: 100, }, ], @@ -1387,6 +1398,7 @@ describe("/store/carts", () => { line_items: [ { variant_id: "test-variant", + product_id: "test-product", unit_price: 100, }, ], @@ -1457,6 +1469,7 @@ describe("/store/carts", () => { line_items: [ { variant_id: "test-variant", + product_id: "test-product", unit_price: 100, }, ], @@ -2207,6 +2220,7 @@ describe("/store/carts", () => { line_items: [ { variant_id: product.variants[0].id, + product_id: product.id, quantity: 1, unit_price: 1000, }, @@ -2251,6 +2265,7 @@ describe("/store/carts", () => { line_items: [ { variant_id: product.variants[0].id, + product_id: product.id, quantity: 1, unit_price: 1000, }, @@ -2702,7 +2717,13 @@ describe("/store/carts", () => { const product = await simpleProductFactory(dbConnection) const cart = await simpleCartFactory(dbConnection, { region: region.id, - line_items: [{ variant_id: product.variants[0].id, quantity: 1 }], + line_items: [ + { + variant_id: product.variants[0].id, + product_id: product.id, + quantity: 1, + }, + ], shipping_address: { country_code: "us", }, @@ -2737,7 +2758,13 @@ describe("/store/carts", () => { const product = await simpleProductFactory(dbConnection) const cart = await simpleCartFactory(dbConnection, { region: region.id, - line_items: [{ variant_id: product.variants[0].id, quantity: 1 }], + line_items: [ + { + variant_id: product.variants[0].id, + product_id: product.id, + quantity: 1, + }, + ], }) await simpleShippingOptionFactory(dbConnection, { region_id: region.id, @@ -2769,7 +2796,13 @@ describe("/store/carts", () => { const product = await simpleProductFactory(dbConnection) const cart = await simpleCartFactory(dbConnection, { region: region.id, - line_items: [{ variant_id: product.variants[0].id, quantity: 1 }], + line_items: [ + { + variant_id: product.variants[0].id, + product_id: product.id, + quantity: 1, + }, + ], }) await simpleShippingOptionFactory(dbConnection, { region_id: region.id, diff --git a/integration-tests/factories/simple-line-item-factory.ts b/integration-tests/factories/simple-line-item-factory.ts index ef45a333f9..7fb1ddf994 100644 --- a/integration-tests/factories/simple-line-item-factory.ts +++ b/integration-tests/factories/simple-line-item-factory.ts @@ -1,6 +1,6 @@ -import { DataSource } from "typeorm" -import faker from "faker" import { LineItem, LineItemAdjustment, LineItemTaxLine } from "@medusajs/medusa" +import faker from "faker" +import { DataSource } from "typeorm" type TaxLineFactoryData = { rate: number @@ -18,6 +18,7 @@ export type LineItemFactoryData = { cart_id?: string order_id?: string variant_id: string | null + product_id: string | null title?: string description?: string thumbnail?: string @@ -75,7 +76,8 @@ export const simpleLineItemFactory = async ( adjustments: data.adjustments, includes_tax: data.includes_tax, order_edit_id: data.order_edit_id, - is_giftcard: data.is_giftcard || false + is_giftcard: data.is_giftcard || false, + product_id: data.product_id, }) const line = await manager.save(toSave) diff --git a/integration-tests/helpers/cart-seeder.js b/integration-tests/helpers/cart-seeder.js index 1476c219eb..b539c68772 100644 --- a/integration-tests/helpers/cart-seeder.js +++ b/integration-tests/helpers/cart-seeder.js @@ -834,6 +834,7 @@ module.exports = async (dataSource, data = {}) => { unit_price: 8000, quantity: 1, variant_id: "test-variant", + product_id: "test-product", cart_id: "test-cart-2", }) await manager.save(li) @@ -887,6 +888,7 @@ module.exports = async (dataSource, data = {}) => { unit_price: 8000, quantity: 1, variant_id: "test-variant", + product_id: "test-product", cart_id: "test-cart-3", }) await manager.save(li2) @@ -951,6 +953,7 @@ module.exports = async (dataSource, data = {}) => { quantity: 1, variant_id: "test-variant-sale-cg", cart_id: "test-cart-3", + product_id: "test-product", metadata: { "some-existing": "prop" }, }) await manager.save(li3) diff --git a/packages/medusa/src/api/routes/admin/draft-orders/create-line-item.ts b/packages/medusa/src/api/routes/admin/draft-orders/create-line-item.ts index 9086771440..2d34cc8897 100644 --- a/packages/medusa/src/api/routes/admin/draft-orders/create-line-item.ts +++ b/packages/medusa/src/api/routes/admin/draft-orders/create-line-item.ts @@ -1,19 +1,19 @@ -import { - CartService, - DraftOrderService, - LineItemService, -} from "../../../../services" import { IsInt, IsObject, IsOptional, IsString } from "class-validator" import { defaultAdminDraftOrdersCartFields, defaultAdminDraftOrdersCartRelations, defaultAdminDraftOrdersFields, } from "." +import { + CartService, + DraftOrderService, + LineItemService, +} from "../../../../services" -import { EntityManager } from "typeorm" import { MedusaError } from "medusa-core-utils" -import { validator } from "../../../../utils/validator" +import { EntityManager } from "typeorm" import { cleanResponseData } from "../../../../utils/clean-response-data" +import { validator } from "../../../../utils/validator" /** * @oas [post] /admin/draft-orders/{id}/line-items @@ -119,7 +119,9 @@ export default async (req, res) => { await cartService .withTransaction(manager) - .addLineItem(draftOrder.cart_id, line, { validateSalesChannels: false }) + .addOrUpdateLineItems(draftOrder.cart_id, line, { + validateSalesChannels: false, + }) } else { // custom line items can be added to a draft order await lineItemService.withTransaction(manager).create({ diff --git a/packages/medusa/src/api/routes/store/carts/create-line-item/utils/handler-steps.ts b/packages/medusa/src/api/routes/store/carts/create-line-item/utils/handler-steps.ts index a5f8c55c19..2c9dfd2974 100644 --- a/packages/medusa/src/api/routes/store/carts/create-line-item/utils/handler-steps.ts +++ b/packages/medusa/src/api/routes/store/carts/create-line-item/utils/handler-steps.ts @@ -39,7 +39,7 @@ export async function handleAddOrUpdateLineItem( metadata: data.metadata, }) - await txCartService.addLineItem(cart.id, line, { + await txCartService.addOrUpdateLineItems(cart.id, line, { validateSalesChannels: featureFlagRouter.isFeatureEnabled("sales_channels"), }) diff --git a/packages/medusa/src/api/routes/store/shipping-options/list-options.ts b/packages/medusa/src/api/routes/store/shipping-options/list-options.ts index 28f55437f2..0313d5dfdf 100644 --- a/packages/medusa/src/api/routes/store/shipping-options/list-options.ts +++ b/packages/medusa/src/api/routes/store/shipping-options/list-options.ts @@ -1,8 +1,14 @@ +import { FlagRouter } from "@medusajs/utils" import { IsBooleanString, IsOptional, IsString } from "class-validator" -import { PricingService, ProductService } from "../../../../services" +import { defaultRelations } from "." +import IsolateProductDomainFeatureFlag from "../../../../loaders/feature-flags/isolate-product-domain" +import { + PricingService, + ProductService, + ShippingProfileService, +} from "../../../../services" import ShippingOptionService from "../../../../services/shipping-option" import { validator } from "../../../../utils/validator" -import { defaultRelations } from "." /** * @oas [get] /store/shipping-options @@ -61,6 +67,10 @@ export default async (req, res) => { const shippingOptionService: ShippingOptionService = req.scope.resolve( "shippingOptionService" ) + const shippingProfileService: ShippingProfileService = req.scope.resolve( + "shippingProfileService" + ) + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") // should be selector const query: Record = {} @@ -76,8 +86,17 @@ export default async (req, res) => { query.admin_only = false if (productIds.length) { - const prods = await productService.list({ id: productIds }) - query.profile_id = prods.map((p) => p.profile_id) + if ( + featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key) + ) { + const productShippinProfileMap = + await shippingProfileService.getMapProfileIdsByProductIds(productIds) + + query.profile_id = [...productShippinProfileMap.values()] + } else { + const prods = await productService.list({ id: productIds }) + query.profile_id = prods.map((p) => p.profile_id) + } } const options = await shippingOptionService.list(query, { diff --git a/packages/medusa/src/migrations/1692870898424-line-item-product-id.ts b/packages/medusa/src/migrations/1692870898424-line-item-product-id.ts new file mode 100644 index 0000000000..c82f934257 --- /dev/null +++ b/packages/medusa/src/migrations/1692870898424-line-item-product-id.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import IsolateProductDomain from "../loaders/feature-flags/isolate-product-domain" + +export const featureFlag = IsolateProductDomain.key + +export class LineItemProductId1692870898424 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "line_item" DROP CONSTRAINT IF EXISTS "FK_5371cbaa3be5200f373d24e3d5b"; + ALTER TABLE "line_item" ADD COLUMN IF NOT EXISTS "product_id" text; + + UPDATE "line_item" SET "product_id" = pv."product_id" + FROM "product_variant" pv + WHERE "line_item"."variant_id" = "pv"."id"; + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "line_item" DROP COLUMN IF EXISTS "product_id"; + ALTER TABLE "line_item" ADD CONSTRAINT "FK_5371cbaa3be5200f373d24e3d5b" FOREIGN KEY ("variant_id") REFERENCES "product_variant" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + `) + } +} diff --git a/packages/medusa/src/models/line-item.ts b/packages/medusa/src/models/line-item.ts index 3be2a03143..7f0187ba44 100644 --- a/packages/medusa/src/models/line-item.ts +++ b/packages/medusa/src/models/line-item.ts @@ -1,5 +1,8 @@ import { + AfterLoad, + AfterUpdate, BeforeInsert, + BeforeUpdate, Check, Column, Entity, @@ -11,9 +14,11 @@ import { import { BaseEntity } from "../interfaces" import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing" -import { generateEntityId } from "../utils" -import { DbAwareColumn } from "../utils/db-aware-column" -import { FeatureFlagColumn } from "../utils/feature-flag-decorators" +import { DbAwareColumn, generateEntityId } from "../utils" +import { + FeatureFlagColumn, + FeatureFlagDecorators, +} from "../utils/feature-flag-decorators" import { Cart } from "./cart" import { ClaimOrder } from "./claim-order" import { LineItemAdjustment } from "./line-item-adjustment" @@ -22,6 +27,8 @@ import { Order } from "./order" import { OrderEdit } from "./order-edit" import { ProductVariant } from "./product-variant" import { Swap } from "./swap" +import IsolateProductDomain from "../loaders/feature-flags/isolate-product-domain" +import { featureFlagRouter } from "../loaders/feature-flags" @Check(`"fulfilled_quantity" <= "quantity"`) @Check(`"shipped_quantity" <= "fulfilled_quantity"`) @@ -124,6 +131,9 @@ export class LineItem extends BaseEntity { @JoinColumn({ name: "variant_id" }) variant: ProductVariant + @FeatureFlagColumn(IsolateProductDomain.key, { nullable: true, type: "text" }) + product_id: string | null + @Column({ type: "int" }) quantity: number @@ -155,6 +165,39 @@ export class LineItem extends BaseEntity { @BeforeInsert() private beforeInsert(): void { this.id = generateEntityId(this.id, "item") + + // This is to maintain compatibility while isolating the product domain + if (featureFlagRouter.isFeatureEnabled(IsolateProductDomain.key)) { + if ( + this.variant && + Object.keys(this.variant).length === 1 && + this.variant.product_id + ) { + this.variant = undefined as any + } + } + } + + @FeatureFlagDecorators(IsolateProductDomain.key, [BeforeUpdate]) + beforeUpdate(): void { + if ( + this.variant && + Object.keys(this.variant).length === 1 && + this.variant.product_id + ) { + this.variant = undefined as any + } + } + + @FeatureFlagDecorators(IsolateProductDomain.key, [AfterLoad, AfterUpdate]) + afterUpdateOrLoad(): void { + if (this.variant) { + return + } + + if (this.product_id) { + this.variant = { product_id: this.product_id } as any + } } } diff --git a/packages/medusa/src/repositories/discount-condition.ts b/packages/medusa/src/repositories/discount-condition.ts index 6b34d27d83..41fd386a04 100644 --- a/packages/medusa/src/repositories/discount-condition.ts +++ b/packages/medusa/src/repositories/discount-condition.ts @@ -1,4 +1,8 @@ +import { MedusaModule, Modules } from "@medusajs/modules-sdk" import { DeleteResult, EntityTarget, In, Not } from "typeorm" +import { dataSource } from "../loaders/database" +import { featureFlagRouter } from "../loaders/feature-flags" +import IsolateProductDomainFeatureFlag from "../loaders/feature-flags/isolate-product-domain" import { Discount, DiscountCondition, @@ -11,7 +15,6 @@ import { DiscountConditionType, } from "../models" import { isString } from "../utils" -import { dataSource } from "../loaders/database" export enum DiscountConditionJoinTableForeignKey { PRODUCT_ID = "product_id", @@ -53,6 +56,7 @@ export const DiscountConditionRepository = dataSource joinTableForeignKey: DiscountConditionJoinTableForeignKey conditionTable: DiscountConditionResourceType joinTableKey: string + relatedTable: string } { let conditionTable: DiscountConditionResourceType = DiscountConditionProduct @@ -61,6 +65,7 @@ export const DiscountConditionRepository = dataSource let joinTableForeignKey: DiscountConditionJoinTableForeignKey = DiscountConditionJoinTableForeignKey.PRODUCT_ID let joinTableKey = "id" + let relatedTable = "" // On the joined table (e.g. `product`), what key should be match on // (e.g `type_id` for product types and `id` for products) @@ -80,6 +85,7 @@ export const DiscountConditionRepository = dataSource joinTableForeignKey = DiscountConditionJoinTableForeignKey.PRODUCT_TYPE_ID joinTable = "product" + relatedTable = "types" conditionTable = DiscountConditionProductType break @@ -89,6 +95,7 @@ export const DiscountConditionRepository = dataSource joinTableForeignKey = DiscountConditionJoinTableForeignKey.PRODUCT_COLLECTION_ID joinTable = "product" + relatedTable = "collections" conditionTable = DiscountConditionProductCollection break @@ -99,6 +106,7 @@ export const DiscountConditionRepository = dataSource joinTableForeignKey = DiscountConditionJoinTableForeignKey.PRODUCT_TAG_ID joinTable = "product_tags" + relatedTable = "tags" conditionTable = DiscountConditionProductTag break @@ -123,6 +131,7 @@ export const DiscountConditionRepository = dataSource resourceKey, joinTableForeignKey, conditionTable, + relatedTable, } }, @@ -204,15 +213,56 @@ export const DiscountConditionRepository = dataSource .getMany() }, - async queryConditionTable({ type, condId, resourceId }): Promise { + async queryConditionTable({ + type, + conditionId, + resourceId, + }): Promise { const { conditionTable, joinTable, joinTableForeignKey, resourceKey, joinTableKey, + relatedTable, } = this.getJoinTableResourceIdentifiers(type) + if ( + type !== DiscountConditionType.CUSTOMER_GROUPS && + featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key) + ) { + const module = MedusaModule.getModuleInstance(Modules.PRODUCT)[ + Modules.PRODUCT + ] + const prop = relatedTable + const resource = await module.retrieve(resourceId, { + select: [`${prop ? prop + "." : ""}id`], + relations: prop ? [prop] : [], + }) + if (!resource) { + return 0 + } + + const relatedResourceIds = prop + ? resource[prop].map((relatedResource) => relatedResource.id) + : [resource.id] + + if (!relatedResourceIds.length) { + return 0 + } + + return await this.manager + .createQueryBuilder(conditionTable, "dc") + .where( + `dc.condition_id = :conditionId AND dc.${joinTableForeignKey} IN (:...relatedResourceIds)`, + { + conditionId, + relatedResourceIds, + } + ) + .getCount() + } + return await this.manager .createQueryBuilder(conditionTable, "dc") .innerJoin( @@ -224,7 +274,7 @@ export const DiscountConditionRepository = dataSource } ) .where(`dc.condition_id = :conditionId`, { - conditionId: condId, + conditionId, }) .getCount() }, @@ -258,7 +308,7 @@ export const DiscountConditionRepository = dataSource const numConditions = await this.queryConditionTable({ type: condition.type, - condId: condition.id, + conditionId: condition.id, resourceId: productId, }) @@ -307,7 +357,7 @@ export const DiscountConditionRepository = dataSource for (const condition of discountConditions) { const numConditions = await this.queryConditionTable({ type: "customer_groups", - condId: condition.id, + conditionId: condition.id, resourceId: customerId, }) diff --git a/packages/medusa/src/services/__mocks__/shipping-profile.js b/packages/medusa/src/services/__mocks__/shipping-profile.js index 36cc7827d5..5245160459 100644 --- a/packages/medusa/src/services/__mocks__/shipping-profile.js +++ b/packages/medusa/src/services/__mocks__/shipping-profile.js @@ -143,9 +143,6 @@ export const ShippingProfileServiceMock = { fetchCartOptions: jest.fn().mockImplementation(() => { return Promise.resolve([{ id: IdMap.getId("cartShippingOption") }]) }), - fetchOptionsByProductIds: jest.fn().mockImplementation(() => { - return Promise.resolve([{ id: IdMap.getId("cartShippingOption") }]) - }), } const mock = jest.fn().mockImplementation(() => { diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index 0f9fd912c9..1bbe0d87fb 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -1,27 +1,28 @@ -import _ from "lodash" +import { FlagRouter } from "@medusajs/utils" import { asClass, asValue, createContainer } from "awilix" +import _ from "lodash" import { MedusaError } from "medusa-core-utils" import { IdMap, MockManager, MockRepository } from "medusa-test-utils" -import { FlagRouter } from "@medusajs/utils" -import CartService from "../cart" -import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant-inventory" +import { IsNull, Not } from "typeorm" +import { PaymentSessionStatus } from "../../models" +import TaxCalculationStrategy from "../../strategies/tax-calculation" +import { cacheServiceMock } from "../__mocks__/cache" +import { CustomerServiceMock } from "../__mocks__/customer" +import { EventBusServiceMock } from "../__mocks__/event-bus" +import { LineItemServiceMock } from "../__mocks__/line-item" import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment" import { newTotalsServiceMock } from "../__mocks__/new-totals" -import { taxProviderServiceMock } from "../__mocks__/tax-provider" -import { PaymentSessionStatus } from "../../models" -import { NewTotalsService, TaxProviderService } from "../index" -import { cacheServiceMock } from "../__mocks__/cache" -import { EventBusServiceMock } from "../__mocks__/event-bus" import { PaymentProviderServiceMock } from "../__mocks__/payment-provider" import { ProductServiceMock } from "../__mocks__/product" import { ProductVariantServiceMock } from "../__mocks__/product-variant" +import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant-inventory" import { RegionServiceMock } from "../__mocks__/region" -import { LineItemServiceMock } from "../__mocks__/line-item" import { ShippingOptionServiceMock } from "../__mocks__/shipping-option" -import { CustomerServiceMock } from "../__mocks__/customer" -import TaxCalculationStrategy from "../../strategies/tax-calculation" +import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile" +import { taxProviderServiceMock } from "../__mocks__/tax-provider" +import CartService from "../cart" +import { NewTotalsService, TaxProviderService } from "../index" import SystemTaxService from "../system-tax" -import { IsNull, Not } from "typeorm" const eventBusService = { emit: jest.fn(), @@ -2628,6 +2629,7 @@ describe("CartService", () => { .register("regionService", asValue(RegionServiceMock)) .register("lineItemService", asValue(LineItemServiceMock)) .register("shippingOptionService", asValue(ShippingOptionServiceMock)) + .register("shippingProfileService", asValue(ShippingProfileServiceMock)) .register("customerService", asValue(CustomerServiceMock)) .register("discountService", asValue({})) .register("giftCardService", asValue({})) diff --git a/packages/medusa/src/services/__tests__/shipping-profile.js b/packages/medusa/src/services/__tests__/shipping-profile.js index 4a3310b1c1..8df12303c2 100644 --- a/packages/medusa/src/services/__tests__/shipping-profile.js +++ b/packages/medusa/src/services/__tests__/shipping-profile.js @@ -1,3 +1,4 @@ +import { FlagRouter } from "@medusajs/utils" import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import ShippingProfileService from "../shipping-profile" @@ -15,6 +16,7 @@ describe("ShippingProfileService", () => { const profileService = new ShippingProfileService({ manager: MockManager, shippingProfileRepository: profRepo, + featureFlagRouter: new FlagRouter({}), }) await profileService.retrieve(IdMap.getId("validId")) @@ -53,6 +55,7 @@ describe("ShippingProfileService", () => { shippingProfileRepository: profRepo, productService, shippingOptionService, + featureFlagRouter: new FlagRouter({}), }) beforeEach(() => { @@ -106,6 +109,7 @@ describe("ShippingProfileService", () => { const profileService = new ShippingProfileService({ manager: MockManager, shippingProfileRepository: profRepo, + featureFlagRouter: new FlagRouter({}), }) beforeEach(() => { @@ -136,6 +140,7 @@ describe("ShippingProfileService", () => { manager: MockManager, shippingProfileRepository: profRepo, productService, + featureFlagRouter: new FlagRouter({}), }) beforeEach(() => { @@ -219,6 +224,7 @@ describe("ShippingProfileService", () => { shippingProfileRepository: profRepo, shippingOptionService, customShippingOptionService, + featureFlagRouter: new FlagRouter({}), }) beforeEach(() => { @@ -311,6 +317,7 @@ describe("ShippingProfileService", () => { manager: MockManager, shippingProfileRepository: profRepo, shippingOptionService, + featureFlagRouter: new FlagRouter({}), }) beforeEach(() => { @@ -335,6 +342,7 @@ describe("ShippingProfileService", () => { const profileService = new ShippingProfileService({ manager: MockManager, shippingProfileRepository: profRepo, + featureFlagRouter: new FlagRouter({}), }) afterEach(() => { diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index 793cf3f652..7f1a0f596a 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -18,11 +18,13 @@ import { RegionService, SalesChannelService, ShippingOptionService, + ShippingProfileService, StoreService, TaxProviderService, TotalsService, } from "." import { IPriceSelectionStrategy, TransactionBaseService } from "../interfaces" +import IsolateProductDomainFeatureFlag from "../loaders/feature-flags/isolate-product-domain" import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" import { Address, @@ -79,6 +81,7 @@ type InjectedDependencies = { regionService: RegionService lineItemService: LineItemService shippingOptionService: ShippingOptionService + shippingProfileService: ShippingProfileService customerService: CustomerService discountService: DiscountService giftCardService: GiftCardService @@ -119,6 +122,7 @@ class CartService extends TransactionBaseService { protected readonly paymentProviderService_: PaymentProviderService protected readonly customerService_: CustomerService protected readonly shippingOptionService_: ShippingOptionService + protected readonly shippingProfileService_: ShippingProfileService protected readonly discountService_: DiscountService protected readonly giftCardService_: GiftCardService protected readonly taxProviderService_: TaxProviderService @@ -143,6 +147,7 @@ class CartService extends TransactionBaseService { regionService, lineItemService, shippingOptionService, + shippingProfileService, customerService, discountService, giftCardService, @@ -172,6 +177,7 @@ class CartService extends TransactionBaseService { this.paymentProviderService_ = paymentProviderService this.customerService_ = customerService this.shippingOptionService_ = shippingOptionService + this.shippingProfileService_ = shippingProfileService this.discountService_ = discountService this.giftCardService_ = giftCardService this.totalsService_ = totalsService @@ -222,6 +228,21 @@ class CartService extends TransactionBaseService { ) } + if ( + this.featureFlagRouter_.isFeatureEnabled( + IsolateProductDomainFeatureFlag.key + ) + ) { + if (Array.isArray(options.relations)) { + for (let i = 0; i < options.relations.length; i++) { + if (options.relations[i].startsWith("items.variant")) { + options.relations[i] = "items" + } + } + } + options.relations = [...new Set(options.relations)] + } + const { totalsToSelect } = this.transformQueryForTotals_(options) if (totalsToSelect.length) { @@ -293,10 +314,25 @@ class CartService extends TransactionBaseService { ): Promise> { const relations = this.getTotalsRelations(options) - const cart = await this.retrieve(cartId, { - ...options, - relations, - }) + const opt = { ...options, relations } + + if ( + this.featureFlagRouter_.isFeatureEnabled( + IsolateProductDomainFeatureFlag.key + ) + ) { + if (Array.isArray(opt.relations)) { + for (let i = 0; i < opt.relations.length; i++) { + if (opt.relations[i].startsWith("items.variant")) { + opt.relations[i] = "items" + } + } + } + + opt.relations = [...new Set(opt.relations)] + } + + const cart = await this.retrieve(cartId, opt) return await this.decorateTotals(cart, totalsConfig) } @@ -547,23 +583,21 @@ class CartService extends TransactionBaseService { */ protected validateLineItemShipping_( shippingMethods: ShippingMethod[], - lineItem: LineItem + lineItemShippingProfiledId: string ): boolean { - if (!lineItem.variant_id) { + if (!lineItemShippingProfiledId) { return true } if ( shippingMethods && shippingMethods.length && - lineItem.variant && - lineItem.variant.product + lineItemShippingProfiledId ) { - const productProfile = lineItem.variant.product.profile_id const selectedProfiles = shippingMethods.map( ({ shipping_option }) => shipping_option.profile_id ) - return selectedProfiles.includes(productProfile) + return selectedProfiles.includes(lineItemShippingProfiledId) } return false @@ -1083,15 +1117,6 @@ class CartService extends TransactionBaseService { "discounts.rule", ] - if ( - this.featureFlagRouter_.isFeatureEnabled( - SalesChannelFeatureFlag.key - ) && - data.sales_channel_id - ) { - relations.push("items.variant.product.profiles") - } - const cart = await this.retrieve(cartId, { relations, }) @@ -2163,10 +2188,33 @@ class CartService extends TransactionBaseService { const lineItemServiceTx = this.lineItemService_.withTransaction(transactionManager) + let productShippinProfileMap = new Map() + + if ( + this.featureFlagRouter_.isFeatureEnabled( + IsolateProductDomainFeatureFlag.key + ) + ) { + productShippinProfileMap = + await this.shippingProfileService_.getMapProfileIdsByProductIds( + cart.items.map((item) => item.variant.product_id) + ) + } else { + productShippinProfileMap = new Map( + cart.items.map((item) => [ + item.variant?.product?.id, + item.variant?.product?.profile_id, + ]) + ) + } + await Promise.all( cart.items.map(async (item) => { return lineItemServiceTx.update(item.id, { - has_shipping: this.validateLineItemShipping_(methods, item), + has_shipping: this.validateLineItemShipping_( + methods, + productShippinProfileMap.get(item.variant?.product_id)! + ), }) }) ) diff --git a/packages/medusa/src/services/discount.ts b/packages/medusa/src/services/discount.ts index 3f45cca302..27895dab33 100644 --- a/packages/medusa/src/services/discount.ts +++ b/packages/medusa/src/services/discount.ts @@ -3,31 +3,31 @@ import { parse, toSeconds } from "iso8601-duration" import { isEmpty, omit } from "lodash" import { MedusaError, isDefined } from "medusa-core-utils" import { - DeepPartial, - EntityManager, - FindOptionsWhere, - ILike, - In, + DeepPartial, + EntityManager, + FindOptionsWhere, + ILike, + In, } from "typeorm" import { - NewTotalsService, - ProductService, - RegionService, - TotalsService, + NewTotalsService, + ProductService, + RegionService, + TotalsService, } from "." import { TransactionBaseService } from "../interfaces" import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing" import { - Cart, - Discount, - DiscountConditionType, - LineItem, - Region, + Cart, + Discount, + DiscountConditionType, + LineItem, + Region, } from "../models" import { - AllocationType as DiscountAllocation, - DiscountRule, - DiscountRuleType, + AllocationType as DiscountAllocation, + DiscountRule, + DiscountRuleType, } from "../models/discount-rule" import { DiscountRepository } from "../repositories/discount" import { DiscountConditionRepository } from "../repositories/discount-condition" @@ -35,12 +35,12 @@ import { DiscountRuleRepository } from "../repositories/discount-rule" import { GiftCardRepository } from "../repositories/gift-card" import { FindConfig, Selector } from "../types/common" import { - CreateDiscountInput, - CreateDiscountRuleInput, - CreateDynamicDiscountInput, - FilterableDiscountProps, - UpdateDiscountInput, - UpdateDiscountRuleInput, + CreateDiscountInput, + CreateDiscountRuleInput, + CreateDynamicDiscountInput, + FilterableDiscountProps, + UpdateDiscountInput, + UpdateDiscountRuleInput, } from "../types/discount" import { CalculationContextData } from "../types/totals" import { buildQuery, setMetadata } from "../utils" @@ -576,7 +576,7 @@ class DiscountService extends TransactionBaseService { async validateDiscountForProduct( discountRuleId: string, - productId: string | undefined + productId?: string ): Promise { return await this.atomicPhase_(async (manager) => { const discountConditionRepo = manager.withRepository( @@ -589,15 +589,9 @@ class DiscountService extends TransactionBaseService { return false } - const product = await this.productService_ - .withTransaction(manager) - .retrieve(productId, { - relations: ["tags"], - }) - return await discountConditionRepo.isValidForProduct( discountRuleId, - product.id + productId ) }) } diff --git a/packages/medusa/src/services/line-item-adjustment.ts b/packages/medusa/src/services/line-item-adjustment.ts index 6f370714dd..52b9b89bf2 100644 --- a/packages/medusa/src/services/line-item-adjustment.ts +++ b/packages/medusa/src/services/line-item-adjustment.ts @@ -1,14 +1,14 @@ import { isDefined, MedusaError } from "medusa-core-utils" import { EntityManager, FindOperator, In } from "typeorm" +import { TransactionBaseService } from "../interfaces" import { Cart, DiscountRuleType, LineItem, LineItemAdjustment } from "../models" import { LineItemAdjustmentRepository } from "../repositories/line-item-adjustment" import { FindConfig } from "../types/common" import { FilterableLineItemAdjustmentProps } from "../types/line-item-adjustment" -import DiscountService from "./discount" -import { TransactionBaseService } from "../interfaces" -import { buildQuery, setMetadata } from "../utils" import { CalculationContextData } from "../types/totals" +import { buildQuery, setMetadata } from "../utils" +import DiscountService from "./discount" type LineItemAdjustmentServiceProps = { manager: EntityManager diff --git a/packages/medusa/src/services/line-item.ts b/packages/medusa/src/services/line-item.ts index f1094b96ce..664c8a6a89 100644 --- a/packages/medusa/src/services/line-item.ts +++ b/packages/medusa/src/services/line-item.ts @@ -4,12 +4,13 @@ import { DeepPartial } from "typeorm/common/DeepPartial" import { FlagRouter } from "@medusajs/utils" import { TransactionBaseService } from "../interfaces" +import IsolateProductDomainFeatureFlag from "../loaders/feature-flags/isolate-product-domain" import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing" import { - LineItem, - LineItemAdjustment, - LineItemTaxLine, - ProductVariant, + LineItem, + LineItemAdjustment, + LineItemTaxLine, + ProductVariant, } from "../models" import { CartRepository } from "../repositories/cart" import { LineItemRepository } from "../repositories/line-item" @@ -19,11 +20,11 @@ import { GenerateInputData, GenerateLineItemContext } from "../types/line-item" import { ProductVariantPricing } from "../types/pricing" import { buildQuery, isString, setMetadata } from "../utils" import { - PricingService, - ProductService, - ProductVariantService, - RegionService, - TaxProviderService, + PricingService, + ProductService, + ProductVariantService, + RegionService, + TaxProviderService, } from "./index" import LineItemAdjustmentService from "./line-item-adjustment" @@ -364,6 +365,14 @@ class LineItemService extends TransactionBaseService { should_merge: shouldMerge, } + if ( + this.featureFlagRouter_.isFeatureEnabled( + IsolateProductDomainFeatureFlag.key + ) + ) { + rawLineItem.product_id = variant.product_id + } + if ( this.featureFlagRouter_.isFeatureEnabled( TaxInclusivePricingFeatureFlag.key @@ -471,7 +480,9 @@ class LineItemService extends TransactionBaseService { return await lineItemRepository .findOne({ where: { id } }) - .then((lineItem) => lineItem && lineItemRepository.remove(lineItem)) + .then( + async (lineItem) => lineItem && lineItemRepository.remove(lineItem) + ) } ) } diff --git a/packages/medusa/src/services/new-totals.ts b/packages/medusa/src/services/new-totals.ts index aa192f4cde..da5c5a512c 100644 --- a/packages/medusa/src/services/new-totals.ts +++ b/packages/medusa/src/services/new-totals.ts @@ -2,20 +2,20 @@ import { FlagRouter } from "@medusajs/utils" import { MedusaError, isDefined } from "medusa-core-utils" import { EntityManager } from "typeorm" import { - ITaxCalculationStrategy, - TaxCalculationContext, - TransactionBaseService, + ITaxCalculationStrategy, + TaxCalculationContext, + TransactionBaseService, } from "../interfaces" import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing" import { - Discount, - DiscountRuleType, - GiftCard, - LineItem, - LineItemTaxLine, - Region, - ShippingMethod, - ShippingMethodTaxLine, + Discount, + DiscountRuleType, + GiftCard, + LineItem, + LineItemTaxLine, + Region, + ShippingMethod, + ShippingMethodTaxLine, } from "../models" import { LineAllocationsMap } from "../types/totals" import { calculatePriceTaxAmount } from "../utils" @@ -675,7 +675,7 @@ export default class NewTotalsService extends TransactionBaseService { if (!totals.tax_lines) { throw new MedusaError( MedusaError.Types.UNEXPECTED_STATE, - "Tax Lines must be joined to calculate taxes" + "Tax Lines must be joined to calculate shipping taxes" ) } } diff --git a/packages/medusa/src/services/shipping-profile.ts b/packages/medusa/src/services/shipping-profile.ts index b65ce9195d..41f5128f54 100644 --- a/packages/medusa/src/services/shipping-profile.ts +++ b/packages/medusa/src/services/shipping-profile.ts @@ -1,6 +1,8 @@ -import { isDefined, MedusaError } from "medusa-core-utils" -import { EntityManager } from "typeorm" +import { FlagRouter, isDefined } from "@medusajs/utils" +import { MedusaError } from "medusa-core-utils" +import { EntityManager, In } from "typeorm" import { TransactionBaseService } from "../interfaces" +import IsolateProductDomainFeatureFlag from "../loaders/feature-flags/isolate-product-domain" import { Cart, CustomShippingOption, @@ -27,6 +29,7 @@ type InjectedDependencies = { customShippingOptionService: CustomShippingOptionService shippingProfileRepository: typeof ShippingProfileRepository productRepository: typeof ProductRepository + featureFlagRouter: FlagRouter } /** @@ -41,6 +44,7 @@ class ShippingProfileService extends TransactionBaseService { // eslint-disable-next-line max-len protected readonly shippingProfileRepository_: typeof ShippingProfileRepository protected readonly productRepository_: typeof ProductRepository + protected readonly featureFlagRouter_: FlagRouter constructor({ shippingProfileRepository, @@ -48,6 +52,7 @@ class ShippingProfileService extends TransactionBaseService { productRepository, shippingOptionService, customShippingOptionService, + featureFlagRouter, }: InjectedDependencies) { // eslint-disable-next-line prefer-rest-params super(arguments[0]) @@ -57,6 +62,7 @@ class ShippingProfileService extends TransactionBaseService { this.productRepository_ = productRepository this.shippingOptionService_ = shippingOptionService this.customShippingOptionService_ = customShippingOptionService + this.featureFlagRouter_ = featureFlagRouter } /** @@ -79,49 +85,39 @@ class ShippingProfileService extends TransactionBaseService { return shippingProfileRepo.find(query) } - async fetchOptionsByProductIds( - productIds: string[], - filter: Selector - ): Promise { - const products = await this.productService_.list( - { - id: productIds, + async getMapProfileIdsByProductIds( + productIds: string[] + ): Promise> { + const mappedProfiles = new Map() + + if (!productIds?.length) { + return mappedProfiles + } + + const shippingProfiles = await this.shippingProfileRepository_.find({ + select: { + id: true, + products: { + id: true, + }, }, - { - relations: [ - "profile", - "profile.shipping_options", - "profile.shipping_options.requirements", - ], - } - ) + where: { + products: { + id: In(productIds), + }, + }, + relations: { + products: true, + }, + }) - const profiles = products.map((p) => p.profile) - - const shippingOptions = profiles.reduce( - (acc: ShippingOption[], next: ShippingProfile) => - acc.concat(next.shipping_options), - [] - ) - - const options = await Promise.all( - shippingOptions.map(async (option) => { - let canSend = true - if (filter.region_id) { - if (filter.region_id !== option.region_id) { - canSend = false - } - } - - if (option.deleted_at !== null) { - canSend = false - } - - return canSend ? option : null + shippingProfiles.forEach((profile) => { + profile.products.forEach((product) => { + mappedProfiles.set(product.id, profile.id) }) - ) + }) - return options.filter(Boolean) as ShippingOption[] + return mappedProfiles } /** @@ -425,7 +421,7 @@ class ShippingProfileService extends TransactionBaseService { */ async fetchCartOptions(cart): Promise { return await this.atomicPhase_(async (manager) => { - const profileIds = this.getProfilesInCart(cart) + const profileIds = await this.getProfilesInCart(cart) const selector: Selector = { profile_id: profileIds, @@ -489,14 +485,25 @@ class ShippingProfileService extends TransactionBaseService { * @param cart - the cart to extract products from * @return a list of product ids */ - protected getProfilesInCart(cart: Cart): string[] { - const profileIds = new Set() + protected async getProfilesInCart(cart: Cart): Promise { + let profileIds = new Set() - cart.items.forEach((item) => { - if (item.variant?.product) { - profileIds.add(item.variant.product.profile_id) - } - }) + if ( + this.featureFlagRouter_.isFeatureEnabled( + IsolateProductDomainFeatureFlag.key + ) + ) { + const productShippinProfileMap = await this.getMapProfileIdsByProductIds( + cart.items.map((item) => item.variant?.product_id) + ) + profileIds = new Set([...productShippinProfileMap.values()]) + } else { + cart.items.forEach((item) => { + if (item.variant?.product) { + profileIds.add(item.variant.product.profile_id) + } + }) + } return [...profileIds] } diff --git a/packages/medusa/src/services/tax-provider.ts b/packages/medusa/src/services/tax-provider.ts index 1e93c45518..17e93c7881 100644 --- a/packages/medusa/src/services/tax-provider.ts +++ b/packages/medusa/src/services/tax-provider.ts @@ -247,46 +247,39 @@ class TaxProviderService extends TransactionBaseService { lineItems: LineItem[], calculationContext: TaxCalculationContext ): Promise<(ShippingMethodTaxLine | LineItemTaxLine)[]> { - const productIds = lineItems - .map((l) => l?.variant?.product_id) - .filter((p) => p) + const productIds = [ + ...new Set( + lineItems.map((item) => item?.variant?.product_id).filter((p) => p) + ), + ] const productRatesMap = await this.getRegionRatesForProduct( productIds, calculationContext.region ) - const calculationLines = await Promise.all( - lineItems.map(async (l) => { - if (l.is_return) { - return null - } + const calculationLines = lineItems.map((item) => { + if (item.is_return) { + return null + } - if (l.variant_id && !l.variant) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Unable to get the tax lines for the item ${l.id}, it contains a variant_id but the variant is missing.` - ) - } - - if (l.variant?.product_id) { - return { - item: l, - rates: productRatesMap.get(l.variant.product_id) ?? [], - } - } - - /* - * If the line item is custom and therefore not associated with a - * product we assume no taxes - we should consider adding rate overrides - * to custom lines at some point - */ + if (item.variant?.product_id) { return { - item: l, - rates: [], + item: item, + rates: productRatesMap.get(item.variant?.product_id) ?? [], } - }) - ) + } + + /* + * If the line item is custom and therefore not associated with a + * product we assume no taxes - we should consider adding rate overrides + * to custom lines at some point + */ + return { + item: item, + rates: [], + } + }) const shippingCalculationLines = await Promise.all( calculationContext.shipping_methods.map(async (sm) => { diff --git a/packages/medusa/src/services/totals.ts b/packages/medusa/src/services/totals.ts index 8c6c0c9737..20e2617aa9 100644 --- a/packages/medusa/src/services/totals.ts +++ b/packages/medusa/src/services/totals.ts @@ -1,30 +1,30 @@ import { isDefined, MedusaError } from "@medusajs/utils" import { EntityManager } from "typeorm" import { - ITaxCalculationStrategy, - TaxCalculationContext, - TransactionBaseService, + ITaxCalculationStrategy, + TaxCalculationContext, + TransactionBaseService, } from "../interfaces" import { - Cart, - ClaimOrder, - Discount, - DiscountRuleType, - LineItem, - LineItemTaxLine, - Order, - ShippingMethod, - ShippingMethodTaxLine, - Swap, + Cart, + ClaimOrder, + Discount, + DiscountRuleType, + LineItem, + LineItemTaxLine, + Order, + ShippingMethod, + ShippingMethodTaxLine, + Swap, } from "../models" import { isCart } from "../types/cart" import { isOrder } from "../types/orders" import { - CalculationContextData, - LineAllocationsMap, - LineDiscount, - LineDiscountAmount, - SubtotalOptions, + CalculationContextData, + LineAllocationsMap, + LineDiscount, + LineDiscountAmount, + SubtotalOptions, } from "../types/totals" import { NewTotalsService, TaxProviderService } from "./index" @@ -622,6 +622,7 @@ class TotalsService extends TransactionBaseService { * @param value - discount value * @param discountType - the type of discount (fixed or percentage) * @return triples of lineitem, variant and applied discount + * @deprecated - in favour of DiscountService.calculateDiscountForLineItem */ calculateDiscount_( lineItem: LineItem, diff --git a/packages/orchestration/src/joiner/remote-joiner.ts b/packages/orchestration/src/joiner/remote-joiner.ts index 14ba840f72..14f08b2fbe 100644 --- a/packages/orchestration/src/joiner/remote-joiner.ts +++ b/packages/orchestration/src/joiner/remote-joiner.ts @@ -557,14 +557,15 @@ export class RemoteJoiner { parsedExpands: Map ): Map { const mergedExpands = new Map(parsedExpands) - const mergedPaths = new Map() + const mergedPaths = new Map() for (const [path, expand] of mergedExpands.entries()) { const currentServiceName = expand.serviceConfig.serviceName let parentPath = expand.parent while (parentPath) { - const parentExpand = mergedExpands.get(parentPath) + const parentExpand = + mergedExpands.get(parentPath) ?? mergedPaths.get(parentPath) if ( !parentExpand || parentExpand.serviceConfig.serviceName !== currentServiceName @@ -588,7 +589,7 @@ export class RemoteJoiner { targetExpand.args = expand.args mergedExpands.delete(path) - mergedPaths.set(path, parentPath) + mergedPaths.set(path, expand) parentPath = parentExpand.parent } diff --git a/turbo.json b/turbo.json index ea6e7c1025..dd3dd38d02 100644 --- a/turbo.json +++ b/turbo.json @@ -8,7 +8,7 @@ "outputs": [ "!node_modules/**", "!src/**", - "/*/**" + "*/**" ] }, "test": {