feat(medusa,orchestration): Decouple Product in Cart domain (#4945)

This commit is contained in:
Carlos R. L. Rodrigues
2023-09-08 12:24:46 -03:00
committed by GitHub
parent 1958809fa9
commit 4b0e3fb2a7
22 changed files with 459 additions and 221 deletions

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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({

View File

@@ -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"),
})

View File

@@ -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<string, unknown> = {}
@@ -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, {

View File

@@ -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<void> {
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<void> {
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;
`)
}
}

View File

@@ -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
}
}
}

View File

@@ -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<number> {
async queryConditionTable({
type,
conditionId,
resourceId,
}): Promise<number> {
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,
})

View File

@@ -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(() => {

View File

@@ -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({}))

View File

@@ -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(() => {

View File

@@ -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<WithRequiredProperty<Cart, "total">> {
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<string, string>()
if (
this.featureFlagRouter_.isFeatureEnabled(
IsolateProductDomainFeatureFlag.key
)
) {
productShippinProfileMap =
await this.shippingProfileService_.getMapProfileIdsByProductIds(
cart.items.map((item) => item.variant.product_id)
)
} else {
productShippinProfileMap = new Map<string, string>(
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)!
),
})
})
)

View File

@@ -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<boolean> {
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
)
})
}

View File

@@ -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

View File

@@ -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)
)
}
)
}

View File

@@ -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"
)
}
}

View File

@@ -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<ShippingOption>
): Promise<ShippingOption[]> {
const products = await this.productService_.list(
{
id: productIds,
async getMapProfileIdsByProductIds(
productIds: string[]
): Promise<Map<string, string>> {
const mappedProfiles = new Map<string, string>()
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<ShippingOption[]> {
return await this.atomicPhase_(async (manager) => {
const profileIds = this.getProfilesInCart(cart)
const profileIds = await this.getProfilesInCart(cart)
const selector: Selector<ShippingOption> = {
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<string>()
protected async getProfilesInCart(cart: Cart): Promise<string[]> {
let profileIds = new Set<string>()
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]
}

View File

@@ -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) => {

View File

@@ -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,

View File

@@ -557,14 +557,15 @@ export class RemoteJoiner {
parsedExpands: Map<string, RemoteExpandProperty>
): Map<string, RemoteExpandProperty> {
const mergedExpands = new Map<string, RemoteExpandProperty>(parsedExpands)
const mergedPaths = new Map<string, string>()
const mergedPaths = new Map<string, RemoteExpandProperty>()
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
}

View File

@@ -8,7 +8,7 @@
"outputs": [
"!node_modules/**",
"!src/**",
"/*/**"
"*/**"
]
},
"test": {