feat(medusa): Add ProductVariantInventoryService (#2883)
* add mw feature flag * add services * add types * add module interfaces * add interface export * add models for mw * to be ammended * remove featureflag * use correct count * update cart completion strategy * swap service conversion * update return service * update order service * update claim service * add exception to claim item * update cart service * add indicies * add changeset * nullable changes in store * store model update * fix unit tests * remove old inventory service * format integration test * update snapshots * remove old inventory service tests * update snapshots * remove old code * option updates * naming * add jsdoc to pv inventory service * rename class variables * pr feedback * rename option to context * if(variant_id) instead of if(typeof varia...) * update tests * add jsdoc * go for custom * update code for readability
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
Add services:
|
||||
- `sales-channel-inventory`
|
||||
- `sales-channel-location`
|
||||
- `product-variant-inventory`
|
||||
@@ -25,6 +25,7 @@ Object {
|
||||
},
|
||||
],
|
||||
"default_currency_code": "usd",
|
||||
"default_location_id": null,
|
||||
"id": Any<String>,
|
||||
"invite_link_template": null,
|
||||
"metadata": null,
|
||||
@@ -59,6 +60,7 @@ Object {
|
||||
"symbol_native": "¥",
|
||||
},
|
||||
"default_currency_code": "jpy",
|
||||
"default_location_id": null,
|
||||
"id": Any<String>,
|
||||
"invite_link_template": null,
|
||||
"metadata": null,
|
||||
@@ -93,6 +95,7 @@ Object {
|
||||
"symbol_native": "kr",
|
||||
},
|
||||
"default_currency_code": "dkk",
|
||||
"default_location_id": null,
|
||||
"id": Any<String>,
|
||||
"invite_link_template": null,
|
||||
"metadata": null,
|
||||
@@ -121,6 +124,7 @@ Object {
|
||||
"symbol_native": "$",
|
||||
},
|
||||
"default_currency_code": "usd",
|
||||
"default_location_id": null,
|
||||
"feature_flags": Any<Array>,
|
||||
"fulfillment_providers": Array [
|
||||
Object {
|
||||
|
||||
@@ -157,6 +157,7 @@ Object {
|
||||
"return_id": StringMatching /\\^ret_\\*/,
|
||||
},
|
||||
],
|
||||
"location_id": null,
|
||||
"metadata": null,
|
||||
"no_notification": true,
|
||||
"order_id": null,
|
||||
@@ -333,6 +334,7 @@ Object {
|
||||
"return_id": StringMatching /\\^ret_\\*/,
|
||||
},
|
||||
],
|
||||
"location_id": null,
|
||||
"metadata": null,
|
||||
"no_notification": true,
|
||||
"order_id": null,
|
||||
|
||||
@@ -1055,9 +1055,13 @@ describe("/store/carts", () => {
|
||||
regions: ["test-region"],
|
||||
}
|
||||
|
||||
const cartId = "discount-cart"
|
||||
const cartId = "discount-cart"
|
||||
|
||||
const discount = await simpleDiscountFactory(dbConnection, discountData, 100)
|
||||
const discount = await simpleDiscountFactory(
|
||||
dbConnection,
|
||||
discountData,
|
||||
100
|
||||
)
|
||||
const discountCart = await simpleCartFactory(
|
||||
dbConnection,
|
||||
{
|
||||
@@ -1088,14 +1092,10 @@ describe("/store/carts", () => {
|
||||
|
||||
const api = useApi()
|
||||
|
||||
let response = await api
|
||||
.post(
|
||||
`/store/carts/${cartId}/line-items`,
|
||||
{
|
||||
quantity: 1,
|
||||
variant_id: "test-variant-quantity",
|
||||
},
|
||||
)
|
||||
let response = await api.post(`/store/carts/${cartId}/line-items`, {
|
||||
quantity: 1,
|
||||
variant_id: "test-variant-quantity",
|
||||
})
|
||||
|
||||
expect(response.data.cart.items.length).toEqual(1)
|
||||
expect(response.data.cart.items).toEqual(
|
||||
@@ -1111,13 +1111,9 @@ describe("/store/carts", () => {
|
||||
])
|
||||
)
|
||||
|
||||
response = await api
|
||||
.post(
|
||||
`/store/carts/${cartId}`,
|
||||
{
|
||||
discounts: [],
|
||||
},
|
||||
)
|
||||
response = await api.post(`/store/carts/${cartId}`, {
|
||||
discounts: [],
|
||||
})
|
||||
|
||||
expect(response.data.cart.items.length).toEqual(1)
|
||||
expect(response.data.cart.items[0].adjustments).toHaveLength(0)
|
||||
@@ -2201,7 +2197,11 @@ describe("/store/carts", () => {
|
||||
|
||||
it("removes line item adjustments upon discount deletion", async () => {
|
||||
const cartId = "discount-cart"
|
||||
const discount = await simpleDiscountFactory(dbConnection, discountData, 100)
|
||||
const discount = await simpleDiscountFactory(
|
||||
dbConnection,
|
||||
discountData,
|
||||
100
|
||||
)
|
||||
const discountCart = await simpleCartFactory(
|
||||
dbConnection,
|
||||
{
|
||||
@@ -2232,14 +2232,10 @@ describe("/store/carts", () => {
|
||||
|
||||
const api = useApi()
|
||||
|
||||
let response = await api
|
||||
.post(
|
||||
`/store/carts/${cartId}/line-items`,
|
||||
{
|
||||
quantity: 1,
|
||||
variant_id: "test-variant-quantity",
|
||||
},
|
||||
)
|
||||
let response = await api.post(`/store/carts/${cartId}/line-items`, {
|
||||
quantity: 1,
|
||||
variant_id: "test-variant-quantity",
|
||||
})
|
||||
|
||||
expect(response.data.cart.items.length).toEqual(1)
|
||||
expect(response.data.cart.items).toEqual(
|
||||
@@ -2255,8 +2251,9 @@ describe("/store/carts", () => {
|
||||
])
|
||||
)
|
||||
|
||||
response = await api
|
||||
.delete(`/store/carts/${cartId}/discounts/${discountData.code}`)
|
||||
response = await api.delete(
|
||||
`/store/carts/${cartId}/discounts/${discountData.code}`
|
||||
)
|
||||
|
||||
expect(response.data.cart.items.length).toEqual(1)
|
||||
expect(response.data.cart.items[0].adjustments).toHaveLength(0)
|
||||
|
||||
@@ -566,6 +566,7 @@ Object {
|
||||
"return_id": Any<String>,
|
||||
},
|
||||
],
|
||||
"location_id": null,
|
||||
"metadata": null,
|
||||
"no_notification": null,
|
||||
"order_id": Any<String>,
|
||||
@@ -709,6 +710,7 @@ Object {
|
||||
"return_id": Any<String>,
|
||||
},
|
||||
],
|
||||
"location_id": null,
|
||||
"metadata": null,
|
||||
"no_notification": null,
|
||||
"order_id": Any<String>,
|
||||
@@ -1737,6 +1739,7 @@ Object {
|
||||
"return_id": Any<String>,
|
||||
},
|
||||
],
|
||||
"location_id": null,
|
||||
"metadata": null,
|
||||
"no_notification": null,
|
||||
"order_id": Any<String>,
|
||||
@@ -1880,6 +1883,7 @@ Object {
|
||||
"return_id": Any<String>,
|
||||
},
|
||||
],
|
||||
"location_id": null,
|
||||
"metadata": null,
|
||||
"no_notification": null,
|
||||
"order_id": Any<String>,
|
||||
@@ -2400,6 +2404,7 @@ Object {
|
||||
"return_id": Any<String>,
|
||||
},
|
||||
],
|
||||
"location_id": null,
|
||||
"metadata": null,
|
||||
"no_notification": null,
|
||||
"order_id": null,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
export * from "./api"
|
||||
export * from "./interfaces"
|
||||
export * from "./types/inventory"
|
||||
export * from "./types/stock-location"
|
||||
export * from "./types/common"
|
||||
export * from "./types/price-list"
|
||||
export * from "./types/batch-job"
|
||||
export * from "./types/global"
|
||||
export * from "./models"
|
||||
export * from "./services"
|
||||
export * from "./utils"
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from "./cache"
|
||||
export * from "./stock-location"
|
||||
export * from "./inventory"
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { FindConfig } from "../../types/common"
|
||||
|
||||
import {
|
||||
InventoryItemDTO,
|
||||
ReservationItemDTO,
|
||||
InventoryLevelDTO,
|
||||
FilterableInventoryItemProps,
|
||||
CreateInventoryItemInput,
|
||||
CreateReservationItemInput,
|
||||
FilterableInventoryLevelProps,
|
||||
FilterableReservationItemProps,
|
||||
CreateInventoryLevelInput,
|
||||
UpdateInventoryLevelInput,
|
||||
} from "../../types/inventory"
|
||||
|
||||
export interface IInventoryService {
|
||||
listInventoryItems(
|
||||
selector: FilterableInventoryItemProps,
|
||||
config?: FindConfig<InventoryItemDTO>
|
||||
): Promise<[InventoryItemDTO[], number]>
|
||||
|
||||
listReservationItems(
|
||||
selector: FilterableReservationItemProps,
|
||||
config?: FindConfig<ReservationItemDTO>
|
||||
): Promise<[ReservationItemDTO[], number]>
|
||||
|
||||
listInventoryLevels(
|
||||
selector: FilterableInventoryLevelProps,
|
||||
config?: FindConfig<InventoryLevelDTO>
|
||||
): Promise<[InventoryLevelDTO[], number]>
|
||||
|
||||
retrieveInventoryItem(
|
||||
itemId: string,
|
||||
config?: FindConfig<InventoryItemDTO>
|
||||
): Promise<InventoryItemDTO>
|
||||
|
||||
retrieveInventoryLevel(
|
||||
itemId: string,
|
||||
locationId: string
|
||||
): Promise<InventoryLevelDTO>
|
||||
|
||||
createReservationItem(
|
||||
input: CreateReservationItemInput
|
||||
): Promise<ReservationItemDTO>
|
||||
|
||||
createInventoryItem(
|
||||
input: CreateInventoryItemInput
|
||||
): Promise<InventoryItemDTO>
|
||||
|
||||
createInventoryLevel(
|
||||
data: CreateInventoryLevelInput
|
||||
): Promise<InventoryLevelDTO>
|
||||
|
||||
updateInventoryLevel(
|
||||
itemId: string,
|
||||
locationId: string,
|
||||
update: UpdateInventoryLevelInput
|
||||
): Promise<InventoryLevelDTO>
|
||||
|
||||
updateInventoryItem(
|
||||
itemId: string,
|
||||
input: CreateInventoryItemInput
|
||||
): Promise<InventoryItemDTO>
|
||||
|
||||
deleteReservationItemsByLineItem(lineItemId: string): Promise<void>
|
||||
|
||||
deleteReservationItem(id: string): Promise<void>
|
||||
|
||||
deleteInventoryItem(itemId: string): Promise<void>
|
||||
|
||||
deleteInventoryLevel(itemId: string, locationId: string): Promise<void>
|
||||
|
||||
adjustInventory(
|
||||
itemId: string,
|
||||
locationId: string,
|
||||
adjustment: number
|
||||
): Promise<InventoryLevelDTO>
|
||||
|
||||
confirmInventory(
|
||||
itemId: string,
|
||||
locationIds: string[],
|
||||
quantity: number
|
||||
): Promise<boolean>
|
||||
|
||||
retrieveAvailableQuantity(
|
||||
itemId: string,
|
||||
locationIds: string[]
|
||||
): Promise<number>
|
||||
|
||||
retrieveStockedQuantity(
|
||||
itemId: string,
|
||||
locationIds: string[]
|
||||
): Promise<number>
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { FindConfig } from "../../types/common"
|
||||
|
||||
import {
|
||||
StockLocationDTO,
|
||||
FilterableStockLocationProps,
|
||||
CreateStockLocationInput,
|
||||
UpdateStockLocationInput,
|
||||
} from "../../types/stock-location"
|
||||
|
||||
export interface IStockLocationService {
|
||||
list(
|
||||
selector: FilterableStockLocationProps,
|
||||
config?: FindConfig<StockLocationDTO>
|
||||
): Promise<StockLocationDTO[]>
|
||||
|
||||
listAndCount(
|
||||
selector: FilterableStockLocationProps,
|
||||
config?: FindConfig<StockLocationDTO>
|
||||
): Promise<[StockLocationDTO[], number]>
|
||||
|
||||
retrieve(id: string): Promise<StockLocationDTO>
|
||||
|
||||
create(input: CreateStockLocationInput): Promise<StockLocationDTO>
|
||||
|
||||
update(id: string, input: UpdateStockLocationInput): Promise<StockLocationDTO>
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
|
||||
export class multiLocation1671711415179 implements MigrationInterface {
|
||||
name = "multiLocation1666251508718"
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "sales_channel_location" ("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(), "sales_channel_id" text NOT NULL, "location_id" text NOT NULL, CONSTRAINT "PK_afd2c2c52634bc8280a9c9ee533" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_6caaa358f12ed0b846f00e2dcd" ON "sales_channel_location" ("sales_channel_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_c2203162ca946a71aeb98390b0" ON "sales_channel_location" ("location_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "product_variant_inventory_item" ("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(), "inventory_item_id" text NOT NULL, "variant_id" text NOT NULL, "quantity" integer NOT NULL DEFAULT '1', CONSTRAINT "UQ_c9be7c1b11a1a729eb51d1b6bca" UNIQUE ("variant_id", "inventory_item_id"), CONSTRAINT "PK_9a1188b8d36f4d198303b4f7efa" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_c74e8c2835094a37dead376a3b" ON "product_variant_inventory_item" ("inventory_item_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_bf5386e7f2acc460adbf96d6f3" ON "product_variant_inventory_item" ("variant_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "return" ADD "location_id" character varying`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "store" ADD "default_location_id" character varying`
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "store" DROP COLUMN "default_location_id"`
|
||||
)
|
||||
await queryRunner.query(`ALTER TABLE "return" DROP COLUMN "location_id"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_bf5386e7f2acc460adbf96d6f3"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_c74e8c2835094a37dead376a3b"`)
|
||||
await queryRunner.query(`DROP TABLE "product_variant_inventory_item"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_c2203162ca946a71aeb98390b0"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_6caaa358f12ed0b846f00e2dcd"`)
|
||||
await queryRunner.query(`DROP TABLE "sales_channel_location"`)
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ export * from "./product-tax-rate"
|
||||
export * from "./product-type"
|
||||
export * from "./product-type-tax-rate"
|
||||
export * from "./product-variant"
|
||||
export * from "./product-variant-inventory-item"
|
||||
export * from "./publishable-api-key"
|
||||
export * from "./publishable-api-key-sales-channel"
|
||||
export * from "./refund"
|
||||
@@ -60,6 +61,7 @@ export * from "./return"
|
||||
export * from "./return-item"
|
||||
export * from "./return-reason"
|
||||
export * from "./sales-channel"
|
||||
export * from "./sales-channel-location"
|
||||
export * from "./shipping-method"
|
||||
export * from "./shipping-method-tax-line"
|
||||
export * from "./shipping-option"
|
||||
|
||||
@@ -134,7 +134,7 @@ export class LineItem extends BaseEntity {
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
variant_id: string
|
||||
variant_id: string | null
|
||||
|
||||
@ManyToOne(() => ProductVariant, { eager: true })
|
||||
@JoinColumn({ name: "variant_id" })
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Index, Unique, BeforeInsert, Column, Entity } from "typeorm"
|
||||
import { BaseEntity } from "../interfaces/models/base-entity"
|
||||
import { DbAwareColumn, generateEntityId } from "../utils"
|
||||
|
||||
@Entity()
|
||||
@Unique(["variant_id", "inventory_item_id"])
|
||||
export class ProductVariantInventoryItem extends BaseEntity {
|
||||
@Index()
|
||||
@DbAwareColumn({ type: "text" })
|
||||
inventory_item_id: string
|
||||
|
||||
@Index()
|
||||
@DbAwareColumn({ type: "text" })
|
||||
variant_id: string
|
||||
|
||||
@Column({ type: "int", default: 1 })
|
||||
quantity: number
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert(): void {
|
||||
this.id = generateEntityId(this.id, "pvitem")
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,10 @@ export class Return extends BaseEntity {
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
shipping_data: Record<string, unknown>
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
location_id: string
|
||||
|
||||
@Column({ type: "int" })
|
||||
refund_amount: number
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { BeforeInsert, Index, Column } from "typeorm"
|
||||
|
||||
import { FeatureFlagEntity } from "../utils/feature-flag-decorators"
|
||||
import { BaseEntity } from "../interfaces"
|
||||
import { generateEntityId } from "../utils"
|
||||
|
||||
export class SalesChannelLocation extends BaseEntity {
|
||||
@Index()
|
||||
@Column({ type: "text" })
|
||||
sales_channel_id: string
|
||||
|
||||
@Index()
|
||||
@Column({ type: "text" })
|
||||
location_id: string
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert(): void {
|
||||
this.id = generateEntityId(this.id, "scloc")
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,9 @@ export class Store extends BaseEntity {
|
||||
@Column({ nullable: true })
|
||||
invite_link_template: string
|
||||
|
||||
@Column({ nullable: true })
|
||||
default_location_id: string
|
||||
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: Record<string, unknown>
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ export class ProductVariantRepository extends Repository<ProductVariant> {
|
||||
entitiesIds,
|
||||
idsOrOptionsWithoutRelations as FindConditions<ProductVariant>
|
||||
)
|
||||
return [toReturn, toReturn.length]
|
||||
return [toReturn, count]
|
||||
}
|
||||
|
||||
const groupedRelations = this.getGroupedRelations(
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
export const InventoryServiceMock = {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
adjustInventory: jest.fn().mockReturnValue((_variantId, _quantity) => {
|
||||
return Promise.resolve({})
|
||||
}),
|
||||
confirmInventory: jest.fn().mockImplementation((variantId, quantity) => {
|
||||
if (quantity < 10) {
|
||||
return true
|
||||
} else {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Variant with id: ${variantId} does not have the required inventory`
|
||||
)
|
||||
}
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
export const ProductVariantInventoryServiceMock = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
adjustInventory: jest.fn().mockReturnValue((_variantId, _quantity) => {
|
||||
return Promise.resolve({})
|
||||
}),
|
||||
confirmInventory: jest
|
||||
.fn()
|
||||
.mockImplementation((variantId, quantity, options) => {
|
||||
if (quantity < 10) {
|
||||
return true
|
||||
} else {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Variant with id: ${variantId} does not have the required inventory`
|
||||
)
|
||||
}
|
||||
}),
|
||||
releaseReservationsByLineItem: jest.fn().mockImplementation((lineItem) => {}),
|
||||
reserveQuantity: jest
|
||||
.fn()
|
||||
.mockImplementation((variantId, quantity, options) => {}),
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { MedusaError } from "medusa-core-utils"
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import { FlagRouter } from "../../utils/flag-router"
|
||||
import CartService from "../cart"
|
||||
import { InventoryServiceMock } from "../__mocks__/inventory"
|
||||
import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant-inventory"
|
||||
import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment"
|
||||
import { newTotalsServiceMock } from "../__mocks__/new-totals"
|
||||
import { taxProviderServiceMock } from "../__mocks__/tax-provider"
|
||||
@@ -314,18 +314,20 @@ describe("CartService", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const inventoryService = {
|
||||
...InventoryServiceMock,
|
||||
confirmInventory: jest.fn().mockImplementation((variantId, _quantity) => {
|
||||
if (variantId !== IdMap.getId("cannot-cover")) {
|
||||
return true
|
||||
} else {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Variant with id: ${variantId} does not have the required inventory`
|
||||
)
|
||||
}
|
||||
}),
|
||||
const productVariantInventoryService = {
|
||||
...ProductVariantInventoryServiceMock,
|
||||
confirmInventory: jest
|
||||
.fn()
|
||||
.mockImplementation((variantId, _quantity, options) => {
|
||||
if (variantId !== IdMap.getId("cannot-cover")) {
|
||||
return true
|
||||
} else {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Variant with id: ${variantId} does not have the required inventory`
|
||||
)
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
const cartRepository = MockRepository({
|
||||
@@ -387,7 +389,7 @@ describe("CartService", () => {
|
||||
newTotalsService: newTotalsServiceMock,
|
||||
eventBusService,
|
||||
shippingOptionService,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
productVariantService,
|
||||
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
|
||||
taxProviderService: taxProviderServiceMock,
|
||||
@@ -528,24 +530,6 @@ describe("CartService", () => {
|
||||
)} does not have the required inventory`
|
||||
)
|
||||
})
|
||||
|
||||
it("throws if inventory isn't covered", async () => {
|
||||
const lineItem = {
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
quantity: 1,
|
||||
variant_id: IdMap.getId("cannot-cover"),
|
||||
}
|
||||
|
||||
await expect(
|
||||
cartService.addLineItem(IdMap.getId("cartWithLine"), lineItem)
|
||||
).rejects.toThrow(
|
||||
`Variant with id: ${IdMap.getId(
|
||||
"cannot-cover"
|
||||
)} does not have the required inventory`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("addLineItem w. SalesChannel", () => {
|
||||
@@ -572,8 +556,8 @@ describe("CartService", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const inventoryService = {
|
||||
...InventoryServiceMock,
|
||||
const productVariantInventoryService = {
|
||||
...ProductVariantInventoryServiceMock,
|
||||
confirmInventory: jest.fn().mockImplementation((variantId, _quantity) => {
|
||||
if (variantId !== IdMap.getId("cannot-cover")) {
|
||||
return true
|
||||
@@ -625,7 +609,7 @@ describe("CartService", () => {
|
||||
newTotalsService: newTotalsServiceMock,
|
||||
eventBusService,
|
||||
shippingOptionService,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
productVariantService,
|
||||
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
|
||||
taxProviderService: taxProviderServiceMock,
|
||||
@@ -876,12 +860,26 @@ describe("CartService", () => {
|
||||
quantity: 1,
|
||||
})
|
||||
),
|
||||
retrieve: jest.fn().mockImplementation((lineItemId) => {
|
||||
if (lineItemId === IdMap.getId("existing")) {
|
||||
return Promise.resolve({
|
||||
id: lineItemId,
|
||||
cart_id: IdMap.getId("cannot"),
|
||||
variant_id: IdMap.getId("cannot-cover"),
|
||||
})
|
||||
}
|
||||
return Promise.resolve({
|
||||
id: lineItemId,
|
||||
cart_id: IdMap.getId("cartWithLine"),
|
||||
is_return: false,
|
||||
})
|
||||
}),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
const inventoryService = {
|
||||
...InventoryServiceMock,
|
||||
const productVariantInventoryService = {
|
||||
...ProductVariantInventoryServiceMock,
|
||||
confirmInventory: jest
|
||||
.fn()
|
||||
.mockImplementation((id) => id !== IdMap.getId("cannot-cover")),
|
||||
@@ -905,7 +903,7 @@ describe("CartService", () => {
|
||||
total: 100,
|
||||
items: [
|
||||
{
|
||||
id: IdMap.getId("existing"),
|
||||
id: IdMap.getId("existingUpdate"),
|
||||
variant_id: IdMap.getId("good"),
|
||||
subtotal: 100,
|
||||
quantity: 1,
|
||||
@@ -921,7 +919,7 @@ describe("CartService", () => {
|
||||
lineItemService,
|
||||
eventBusService,
|
||||
newTotalsService: newTotalsServiceMock,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
|
||||
taxProviderService: taxProviderServiceMock,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
@@ -934,7 +932,7 @@ describe("CartService", () => {
|
||||
it("successfully updates existing line item", async () => {
|
||||
await cartService.updateLineItem(
|
||||
IdMap.getId("cartWithLine"),
|
||||
IdMap.getId("existing"),
|
||||
IdMap.getId("existingUpdate"),
|
||||
{ quantity: 2 }
|
||||
)
|
||||
|
||||
@@ -946,13 +944,13 @@ describe("CartService", () => {
|
||||
|
||||
expect(lineItemService.update).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemService.update).toHaveBeenCalledWith(
|
||||
IdMap.getId("existing"),
|
||||
IdMap.getId("existingUpdate"),
|
||||
{ quantity: 2 }
|
||||
)
|
||||
|
||||
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledTimes(1)
|
||||
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledWith({
|
||||
item_id: [IdMap.getId("existing")],
|
||||
item_id: [IdMap.getId("existingUpdate")],
|
||||
})
|
||||
|
||||
expect(
|
||||
@@ -1244,6 +1242,7 @@ describe("CartService", () => {
|
||||
items: [
|
||||
{
|
||||
id: IdMap.getId("testitem"),
|
||||
variant_id: IdMap.getId("good"),
|
||||
},
|
||||
{
|
||||
id: IdMap.getId("fail"),
|
||||
@@ -1527,24 +1526,20 @@ describe("CartService", () => {
|
||||
await cartService.setPaymentSessions(IdMap.getId("cartWithLine"))
|
||||
|
||||
expect(paymentProviderService.createSession).toHaveBeenCalledTimes(2)
|
||||
expect(paymentProviderService.createSession).toHaveBeenCalledWith(
|
||||
{
|
||||
cart: cart1,
|
||||
customer: cart1.customer,
|
||||
amount: cart1.total,
|
||||
currency_code: cart1.region.currency_code,
|
||||
provider_id: "provider_1",
|
||||
}
|
||||
)
|
||||
expect(paymentProviderService.createSession).toHaveBeenCalledWith(
|
||||
{
|
||||
cart: cart1,
|
||||
customer: cart1.customer,
|
||||
amount: cart1.total,
|
||||
currency_code: cart1.region.currency_code,
|
||||
provider_id: "provider_2",
|
||||
}
|
||||
)
|
||||
expect(paymentProviderService.createSession).toHaveBeenCalledWith({
|
||||
cart: cart1,
|
||||
customer: cart1.customer,
|
||||
amount: cart1.total,
|
||||
currency_code: cart1.region.currency_code,
|
||||
provider_id: "provider_1",
|
||||
})
|
||||
expect(paymentProviderService.createSession).toHaveBeenCalledWith({
|
||||
cart: cart1,
|
||||
customer: cart1.customer,
|
||||
amount: cart1.total,
|
||||
currency_code: cart1.region.currency_code,
|
||||
provider_id: "provider_2",
|
||||
})
|
||||
})
|
||||
|
||||
it("filters sessions not available in the region", async () => {
|
||||
@@ -2293,7 +2288,9 @@ describe("CartService", () => {
|
||||
})
|
||||
|
||||
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledTimes(1)
|
||||
expect(LineItemAdjustmentServiceMock.createAdjustments).toHaveBeenCalledTimes(1)
|
||||
expect(
|
||||
LineItemAdjustmentServiceMock.createAdjustments
|
||||
).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||
expect(eventBusService.emit).toHaveBeenCalledWith(
|
||||
@@ -2368,7 +2365,9 @@ describe("CartService", () => {
|
||||
await cartService.removeDiscount(IdMap.getId("fr-cart"), "1234")
|
||||
|
||||
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledTimes(1)
|
||||
expect(LineItemAdjustmentServiceMock.createAdjustments).toHaveBeenCalledTimes(1)
|
||||
expect(
|
||||
LineItemAdjustmentServiceMock.createAdjustments
|
||||
).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||
expect(eventBusService.emit).toHaveBeenCalledWith(
|
||||
|
||||
@@ -5,7 +5,7 @@ import ClaimItemService from "../claim-item"
|
||||
const withTransactionMock = jest.fn()
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
withTransactionMock("eventBus")
|
||||
return this
|
||||
},
|
||||
@@ -25,20 +25,20 @@ describe("ClaimItemService", () => {
|
||||
|
||||
const claimTagRepo = MockRepository({
|
||||
findOne: () => Promise.resolve(),
|
||||
create: d => d,
|
||||
create: (d) => d,
|
||||
})
|
||||
|
||||
const claimImgRepo = MockRepository({
|
||||
findOne: () => Promise.resolve(),
|
||||
create: d => d,
|
||||
create: (d) => d,
|
||||
})
|
||||
|
||||
const claimItemRepo = MockRepository({
|
||||
create: d => ({ id: "ci_1234", ...d }),
|
||||
create: (d) => ({ id: "ci_1234", ...d }),
|
||||
})
|
||||
|
||||
const lineItemService = {
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
withTransactionMock("lineItem")
|
||||
return this
|
||||
},
|
||||
@@ -59,7 +59,10 @@ describe("ClaimItemService", () => {
|
||||
|
||||
it("successfully creates a claim item", async () => {
|
||||
lineItemService.retrieve = jest.fn(() =>
|
||||
Promise.resolve({ fulfilled_quantity: 1 })
|
||||
Promise.resolve({
|
||||
variant_id: "var_1",
|
||||
fulfilled_quantity: 1,
|
||||
})
|
||||
)
|
||||
await claimItemService.create(testItem)
|
||||
|
||||
@@ -80,6 +83,7 @@ describe("ClaimItemService", () => {
|
||||
expect(claimItemRepo.create).toHaveBeenCalledWith({
|
||||
claim_order_id: "claim_13",
|
||||
item_id: "itm_1",
|
||||
variant_id: "var_1",
|
||||
reason: "production_failure",
|
||||
note: "Details",
|
||||
quantity: 1,
|
||||
@@ -90,7 +94,10 @@ describe("ClaimItemService", () => {
|
||||
|
||||
it("normalizes claim tag value", async () => {
|
||||
lineItemService.retrieve = jest.fn(() =>
|
||||
Promise.resolve({ fulfilled_quantity: 1 })
|
||||
Promise.resolve({
|
||||
variant_id: "var_1",
|
||||
fulfilled_quantity: 1,
|
||||
})
|
||||
)
|
||||
await claimItemService.create({
|
||||
...testItem,
|
||||
@@ -104,7 +111,10 @@ describe("ClaimItemService", () => {
|
||||
|
||||
it("fails if fulfilled_quantity < quantity", async () => {
|
||||
lineItemService.retrieve = jest.fn(() =>
|
||||
Promise.resolve({ fulfilled_quantity: 0 })
|
||||
Promise.resolve({
|
||||
variant_id: "var_1",
|
||||
fulfilled_quantity: 0,
|
||||
})
|
||||
)
|
||||
await expect(claimItemService.create(testItem)).rejects.toThrow(
|
||||
"Cannot claim more of an item than has been fulfilled"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||
import ClaimService from "../claim"
|
||||
import { InventoryServiceMock } from "../__mocks__/inventory"
|
||||
import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant-inventory"
|
||||
|
||||
const withTransactionMock = jest.fn()
|
||||
const eventBusService = {
|
||||
@@ -79,7 +79,11 @@ describe("ClaimService", () => {
|
||||
}
|
||||
|
||||
const lineItemService = {
|
||||
generate: jest.fn((d, _, q) => ({ variant_id: d, quantity: q })),
|
||||
generate: jest.fn((d, _, q) => ({
|
||||
id: "test_item",
|
||||
variant_id: d,
|
||||
quantity: q,
|
||||
})),
|
||||
retrieve: () => Promise.resolve({}),
|
||||
list: () => Promise.resolve([{}]),
|
||||
withTransaction: function () {
|
||||
@@ -88,8 +92,8 @@ describe("ClaimService", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const inventoryService = {
|
||||
...InventoryServiceMock,
|
||||
const productVariantInventoryService = {
|
||||
...ProductVariantInventoryServiceMock,
|
||||
withTransaction: function () {
|
||||
withTransactionMock("inventory")
|
||||
return this
|
||||
@@ -113,7 +117,7 @@ describe("ClaimService", () => {
|
||||
returnService,
|
||||
lineItemService,
|
||||
claimItemService,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
eventBusService,
|
||||
})
|
||||
|
||||
@@ -151,17 +155,15 @@ describe("ClaimService", () => {
|
||||
1
|
||||
)
|
||||
|
||||
expect(inventoryService.confirmInventory).toHaveBeenCalledTimes(1)
|
||||
expect(inventoryService.confirmInventory).toHaveBeenCalledWith(
|
||||
"var_123",
|
||||
1
|
||||
)
|
||||
expect(withTransactionMock).toHaveBeenCalledWith("inventory")
|
||||
expect(inventoryService.adjustInventory).toHaveBeenCalledTimes(1)
|
||||
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
|
||||
"var_123",
|
||||
-1
|
||||
)
|
||||
expect(
|
||||
productVariantInventoryService.reserveQuantity
|
||||
).toHaveBeenCalledTimes(1)
|
||||
expect(
|
||||
productVariantInventoryService.reserveQuantity
|
||||
).toHaveBeenCalledWith("var_123", 1, {
|
||||
lineItemId: "test_item",
|
||||
salesChannelId: undefined,
|
||||
})
|
||||
|
||||
expect(withTransactionMock).toHaveBeenCalledWith("claimItem")
|
||||
expect(claimItemService.create).toHaveBeenCalledTimes(1)
|
||||
@@ -184,6 +186,7 @@ describe("ClaimService", () => {
|
||||
order_id: "1234",
|
||||
additional_items: [
|
||||
{
|
||||
id: "test_item",
|
||||
variant_id: "var_123",
|
||||
quantity: 1,
|
||||
},
|
||||
@@ -262,7 +265,6 @@ describe("ClaimService", () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
console.warn(res)
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual(
|
||||
`Variant with id: var_123 does not have the required inventory`
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { MockManager } from "medusa-test-utils"
|
||||
import InventoryService from "../inventory"
|
||||
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
|
||||
|
||||
describe("InventoryService", () => {
|
||||
describe("confirmInventory", () => {
|
||||
const inventoryService = new InventoryService({
|
||||
manager: MockManager,
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns false when inventory is managed, and no back orders are allowed and the quantity is larger than inventory", async () => {
|
||||
await expect(
|
||||
inventoryService.confirmInventory("no_bo", 10)
|
||||
).rejects.toThrow(
|
||||
`Variant with id: no_bo does not have the required inventory`
|
||||
)
|
||||
})
|
||||
it("returns true when variant is out of stock but allows back orders", async () => {
|
||||
const result = await inventoryService.confirmInventory("bo", 100)
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it("returns true when variant is out of stock but inventory quantity is not managed", async () => {
|
||||
const result = await inventoryService.confirmInventory("no_manage", 10000)
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it("returns true when managed variant inventory_quantity > requested quantity", async () => {
|
||||
const result = await inventoryService.confirmInventory("10_man", 5)
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it("returns false when managed variant inventory_quantity < requested quantity", async () => {
|
||||
await expect(
|
||||
inventoryService.confirmInventory("10_man", 50)
|
||||
).rejects.toThrow(
|
||||
`Variant with id: 10_man does not have the required inventory`
|
||||
)
|
||||
})
|
||||
})
|
||||
describe("adjustInventory", () => {
|
||||
const inventoryService = new InventoryService({
|
||||
manager: MockManager,
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should not call update in productVariantService because variant is not managed", async () => {
|
||||
await inventoryService.adjustInventory("no_manage", 1000)
|
||||
expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it("should call update in productVariantService once", async () => {
|
||||
await inventoryService.adjustInventory("10_man", 10)
|
||||
expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(1)
|
||||
expect(ProductVariantServiceMock.update).toHaveBeenCalledWith(
|
||||
{
|
||||
id: "10_man",
|
||||
title: "variant_popular",
|
||||
inventory_quantity: 10,
|
||||
allow_backorder: false,
|
||||
manage_inventory: true,
|
||||
},
|
||||
{
|
||||
inventory_quantity: 20,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("should update update once for 1man", async () => {
|
||||
await inventoryService.adjustInventory("1_man", -1)
|
||||
expect(ProductVariantServiceMock.update).toHaveBeenCalledWith(
|
||||
{
|
||||
id: "1_man",
|
||||
title: "variant_popular",
|
||||
inventory_quantity: 1,
|
||||
allow_backorder: false,
|
||||
manage_inventory: true,
|
||||
},
|
||||
{
|
||||
inventory_quantity: 0,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import OrderService from "../order"
|
||||
import { InventoryServiceMock } from "../__mocks__/inventory"
|
||||
import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant-inventory"
|
||||
import { LineItemServiceMock } from "../__mocks__/line-item"
|
||||
import { newTotalsServiceMock } from "../__mocks__/new-totals"
|
||||
import { taxProviderServiceMock } from "../__mocks__/tax-provider"
|
||||
@@ -53,8 +53,8 @@ describe("OrderService", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const inventoryService = {
|
||||
...InventoryServiceMock,
|
||||
const productVariantInventoryService = {
|
||||
...ProductVariantInventoryServiceMock,
|
||||
}
|
||||
|
||||
describe("createFromCart", () => {
|
||||
@@ -150,7 +150,7 @@ describe("OrderService", () => {
|
||||
regionService,
|
||||
eventBusService,
|
||||
cartService,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -193,7 +193,9 @@ describe("OrderService", () => {
|
||||
discount_total: 0,
|
||||
}
|
||||
|
||||
orderService.cartService_.retrieveWithTotals = jest.fn(() => Promise.resolve(cart))
|
||||
orderService.cartService_.retrieveWithTotals = jest.fn(() =>
|
||||
Promise.resolve(cart)
|
||||
)
|
||||
|
||||
await orderService.createFromCart("cart_id")
|
||||
const order = {
|
||||
@@ -224,16 +226,6 @@ describe("OrderService", () => {
|
||||
}
|
||||
)
|
||||
|
||||
expect(inventoryService.adjustInventory).toHaveBeenCalledTimes(2)
|
||||
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
|
||||
"variant-2",
|
||||
-1
|
||||
)
|
||||
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
|
||||
"variant-1",
|
||||
-1
|
||||
)
|
||||
|
||||
expect(lineItemService.update).toHaveBeenCalledTimes(2)
|
||||
expect(lineItemService.update).toHaveBeenCalledWith("item_1", {
|
||||
order_id: "id",
|
||||
@@ -256,21 +248,24 @@ describe("OrderService", () => {
|
||||
const lineItemWithGiftCard = {
|
||||
id: "item_1",
|
||||
variant_id: "variant-1",
|
||||
quantity: 2,
|
||||
// quantity: 2,
|
||||
is_giftcard: true,
|
||||
subtotal: giftCardValue * totalGiftCardsPurchased,
|
||||
quantity: totalGiftCardsPurchased,
|
||||
metadata: {},
|
||||
tax_lines: [{
|
||||
rate: taxLineRateOne
|
||||
}, {
|
||||
rate: taxLineRateTwo
|
||||
}]
|
||||
tax_lines: [
|
||||
{
|
||||
rate: taxLineRateOne,
|
||||
},
|
||||
{
|
||||
rate: taxLineRateTwo,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const lineItemWithoutGiftCard = {
|
||||
...lineItemWithGiftCard,
|
||||
is_giftcard: false
|
||||
is_giftcard: false,
|
||||
}
|
||||
|
||||
const cartWithGiftcard = {
|
||||
@@ -302,23 +297,29 @@ describe("OrderService", () => {
|
||||
}
|
||||
|
||||
it("creates gift cards when a lineItem contains a gift card variant", async () => {
|
||||
orderService.cartService_.retrieveWithTotals = jest.fn(() => Promise.resolve(cartWithGiftcard))
|
||||
orderService.cartService_.retrieveWithTotals = jest.fn(() =>
|
||||
Promise.resolve(cartWithGiftcard)
|
||||
)
|
||||
|
||||
await orderService.createFromCart("id")
|
||||
|
||||
expect(giftCardService.create).toHaveBeenCalledTimes(totalGiftCardsPurchased)
|
||||
expect(giftCardService.create).toHaveBeenCalledTimes(
|
||||
totalGiftCardsPurchased
|
||||
)
|
||||
expect(giftCardService.create).toHaveBeenCalledWith({
|
||||
order_id: "id",
|
||||
region_id: "test",
|
||||
value: giftCardValue,
|
||||
balance: giftCardValue,
|
||||
metadata: {},
|
||||
tax_rate: expectedGiftCardTaxRate
|
||||
tax_rate: expectedGiftCardTaxRate,
|
||||
})
|
||||
})
|
||||
|
||||
it("does not create gift cards when a lineItem doesn't contains a gift card variant", async () => {
|
||||
orderService.cartService_.retrieveWithTotals = jest.fn(() => Promise.resolve(cartWithoutGiftcard))
|
||||
orderService.cartService_.retrieveWithTotals = jest.fn(() =>
|
||||
Promise.resolve(cartWithoutGiftcard)
|
||||
)
|
||||
|
||||
await orderService.createFromCart("id")
|
||||
|
||||
@@ -487,45 +488,6 @@ describe("OrderService", () => {
|
||||
|
||||
expect(orderRepo.save).toHaveBeenCalledWith(order)
|
||||
})
|
||||
|
||||
it("fails because an item does not have the required inventory", async () => {
|
||||
const cart = {
|
||||
id: "cart_id",
|
||||
email: "test@test.com",
|
||||
customer_id: "cus_1234",
|
||||
payment: {
|
||||
id: "testpayment",
|
||||
amount: 100,
|
||||
status: "authorized",
|
||||
},
|
||||
region_id: "test",
|
||||
region: {
|
||||
id: "test",
|
||||
currency_code: "eur",
|
||||
name: "test",
|
||||
tax_rate: 25,
|
||||
},
|
||||
gift_cards: [],
|
||||
shipping_address_id: "1234",
|
||||
billing_address_id: "1234",
|
||||
discounts: [],
|
||||
shipping_methods: [{ id: "method_1" }],
|
||||
items: [
|
||||
{ id: "item_1", variant_id: "variant-1", quantity: 12 },
|
||||
{ id: "item_2", variant_id: "variant-2", quantity: 1 },
|
||||
],
|
||||
total: 100,
|
||||
}
|
||||
orderService.cartService_.retrieveWithTotals = () => Promise.resolve(cart)
|
||||
const res = orderService.createFromCart(cart)
|
||||
await expect(res).rejects.toThrow(
|
||||
"Variant with id: variant-1 does not have the required inventory"
|
||||
)
|
||||
// check to see if payment is cancelled
|
||||
expect(
|
||||
orderService.paymentProviderService_.cancelPayment
|
||||
).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieve", () => {
|
||||
@@ -723,7 +685,7 @@ describe("OrderService", () => {
|
||||
paymentProviderService,
|
||||
fulfillmentService,
|
||||
eventBusService,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -743,16 +705,6 @@ describe("OrderService", () => {
|
||||
id: "payment_test",
|
||||
})
|
||||
|
||||
expect(inventoryService.adjustInventory).toHaveBeenCalledTimes(2)
|
||||
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
|
||||
"variant-1",
|
||||
12
|
||||
)
|
||||
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
|
||||
"variant-2",
|
||||
1
|
||||
)
|
||||
|
||||
expect(orderRepo.save).toHaveBeenCalledTimes(1)
|
||||
expect(orderRepo.save).toHaveBeenCalledWith({
|
||||
fulfillment_status: "canceled",
|
||||
|
||||
@@ -1,150 +1,11 @@
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import idMap from "medusa-test-utils/dist/id-map"
|
||||
import ReturnService from "../return"
|
||||
import { InventoryServiceMock } from "../__mocks__/inventory"
|
||||
|
||||
import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant-inventory"
|
||||
describe("ReturnService", () => {
|
||||
// describe("requestReturn", () => {
|
||||
// const returnRepository = MockRepository({})
|
||||
|
||||
// const fulfillmentProviderService = {
|
||||
// createReturn: jest.fn().mockImplementation(data => {
|
||||
// return Promise.resolve(data)
|
||||
// }),
|
||||
// }
|
||||
|
||||
// const shippingOptionService = {
|
||||
// retrieve: jest.fn().mockImplementation(data => {
|
||||
// return Promise.resolve({
|
||||
// id: IdMap.getId("default"),
|
||||
// name: "default_profile",
|
||||
// provider_id: "default",
|
||||
// })
|
||||
// }),
|
||||
// }
|
||||
|
||||
// const totalsService = {
|
||||
// getTotal: jest.fn().mockImplementation(cart => {
|
||||
// return 1000
|
||||
// }),
|
||||
// getSubtotal: jest.fn().mockImplementation(cart => {
|
||||
// return 75
|
||||
// }),
|
||||
// getRefundTotal: jest.fn().mockImplementation((order, lineItems) => {
|
||||
// return 1000
|
||||
// }),
|
||||
// getRefundedTotal: jest.fn().mockImplementation((order, lineItems) => {
|
||||
// return 0
|
||||
// }),
|
||||
// }
|
||||
|
||||
// const returnService = new ReturnService({
|
||||
// manager: MockManager,
|
||||
// totalsService,
|
||||
// shippingOptionService,
|
||||
// fulfillmentProviderService,
|
||||
// returnRepository,
|
||||
// })
|
||||
|
||||
// beforeEach(async () => {
|
||||
// jest.clearAllMocks()
|
||||
// })
|
||||
|
||||
// it("successfully requests a return", async () => {
|
||||
// await returnService.requestReturn(
|
||||
// {
|
||||
// id: IdMap.getId("test-order"),
|
||||
// items: [{ id: IdMap.getId("existingLine"), quantity: 10 }],
|
||||
// tax_rate: 0.25,
|
||||
// payment_status: "captured",
|
||||
// },
|
||||
// [
|
||||
// {
|
||||
// item_id: IdMap.getId("existingLine"),
|
||||
// quantity: 10,
|
||||
// },
|
||||
// ],
|
||||
// { id: "some-shipping-method", price: 150 }
|
||||
// )
|
||||
|
||||
// expect(returnRepository.create).toHaveBeenCalledTimes(1)
|
||||
// expect(returnRepository.create).toHaveBeenCalledWith({
|
||||
// status: "requested",
|
||||
// items: [],
|
||||
// order_id: IdMap.getId("test-order"),
|
||||
// shipping_method: {
|
||||
// id: "some-shipping-method",
|
||||
// price: 150,
|
||||
// },
|
||||
// shipping_data: {
|
||||
// id: "some-shipping-method",
|
||||
// price: 150,
|
||||
// },
|
||||
// refund_amount: 1000 - 150 * (1 + 0.25),
|
||||
// })
|
||||
// })
|
||||
|
||||
// it("successfully requests a return with custom refund amount", async () => {
|
||||
// await returnService.requestReturn(
|
||||
// {
|
||||
// id: IdMap.getId("test-order"),
|
||||
// items: [{ id: IdMap.getId("existingLine"), quantity: 10 }],
|
||||
// tax_rate: 0.25,
|
||||
// payment_status: "captured",
|
||||
// },
|
||||
// [
|
||||
// {
|
||||
// item_id: IdMap.getId("existingLine"),
|
||||
// quantity: 10,
|
||||
// },
|
||||
// ],
|
||||
// { id: "some-shipping-method", price: 150 },
|
||||
// 500
|
||||
// )
|
||||
|
||||
// expect(returnRepository.create).toHaveBeenCalledTimes(1)
|
||||
// expect(returnRepository.create).toHaveBeenCalledWith({
|
||||
// status: "requested",
|
||||
// items: [],
|
||||
// order_id: IdMap.getId("test-order"),
|
||||
// shipping_method: {
|
||||
// id: "some-shipping-method",
|
||||
// price: 150,
|
||||
// },
|
||||
// shipping_data: expect.anything(),
|
||||
// refund_amount: 500 - 150 * (1 + 0.25),
|
||||
// })
|
||||
// })
|
||||
|
||||
// it("throws if refund amount is above captured amount", async () => {
|
||||
// try {
|
||||
// await returnService.requestReturn(
|
||||
// {
|
||||
// id: IdMap.getId("test-order"),
|
||||
// items: [{ id: IdMap.getId("existingLine"), quantity: 10 }],
|
||||
// tax_rate: 0.25,
|
||||
// payment_status: "captured",
|
||||
// },
|
||||
// [
|
||||
// {
|
||||
// item_id: IdMap.getId("existingLine"),
|
||||
// quantity: 10,
|
||||
// },
|
||||
// ],
|
||||
// { id: "some-shipping-method", price: 150 },
|
||||
// 2000
|
||||
// )
|
||||
// } catch (error) {
|
||||
// expect(error.message).toBe(
|
||||
// "Cannot refund more than the original payment"
|
||||
// )
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
|
||||
describe("receive", () => {
|
||||
const returnRepository = MockRepository({
|
||||
findOne: query => {
|
||||
findOne: (query) => {
|
||||
switch (query.where.id) {
|
||||
case IdMap.getId("test-return-2"):
|
||||
return Promise.resolve({
|
||||
@@ -188,7 +49,7 @@ describe("ReturnService", () => {
|
||||
})
|
||||
|
||||
const totalsService = {
|
||||
getTotal: jest.fn().mockImplementation(cart => {
|
||||
getTotal: jest.fn().mockImplementation((cart) => {
|
||||
return 1000
|
||||
}),
|
||||
getRefundedTotal: jest.fn().mockImplementation((order, lineItems) => {
|
||||
@@ -216,36 +77,36 @@ describe("ReturnService", () => {
|
||||
payments: [{ id: "payment_test" }],
|
||||
})
|
||||
}),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const lineItemService = {
|
||||
retrieve: jest.fn().mockImplementation(data => {
|
||||
retrieve: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve({ ...data, returned_quantity: 0 })
|
||||
}),
|
||||
update: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const inventoryService = {
|
||||
adjustInventory: jest.fn((variantId, quantity) => {
|
||||
return Promise.resolve({})
|
||||
}),
|
||||
confirmInventory: jest.fn((variantId, quantity) => {
|
||||
if (quantity < 10) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
// const inventoryService = {
|
||||
// adjustInventory: jest.fn((variantId, quantity) => {
|
||||
// return Promise.resolve({})
|
||||
// }),
|
||||
// confirmInventory: jest.fn((variantId, quantity) => {
|
||||
// if (quantity < 10) {
|
||||
// return true
|
||||
// } else {
|
||||
// return false
|
||||
// }
|
||||
// }),
|
||||
// withTransaction: function () {
|
||||
// return this
|
||||
// },
|
||||
// }
|
||||
|
||||
const returnService = new ReturnService({
|
||||
manager: MockManager,
|
||||
@@ -253,7 +114,7 @@ describe("ReturnService", () => {
|
||||
lineItemService,
|
||||
orderService,
|
||||
returnRepository,
|
||||
inventoryService,
|
||||
productVariantInventoryService: ProductVariantInventoryServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -296,11 +157,12 @@ describe("ReturnService", () => {
|
||||
}
|
||||
)
|
||||
|
||||
expect(inventoryService.adjustInventory).toHaveBeenCalledTimes(1)
|
||||
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
|
||||
"test-variant",
|
||||
10
|
||||
)
|
||||
expect(
|
||||
ProductVariantInventoryServiceMock.adjustInventory
|
||||
).toHaveBeenCalledTimes(1)
|
||||
expect(
|
||||
ProductVariantInventoryServiceMock.adjustInventory
|
||||
).toHaveBeenCalledWith("test-variant", undefined, 10)
|
||||
})
|
||||
|
||||
it("successfully receives a return with requires_action status", async () => {
|
||||
@@ -313,15 +175,15 @@ describe("ReturnService", () => {
|
||||
1000
|
||||
)
|
||||
|
||||
expect(inventoryService.adjustInventory).toHaveBeenCalledTimes(2)
|
||||
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
|
||||
"test-variant",
|
||||
10
|
||||
)
|
||||
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
|
||||
"test-variant-2",
|
||||
10
|
||||
)
|
||||
expect(
|
||||
ProductVariantInventoryServiceMock.adjustInventory
|
||||
).toHaveBeenCalledTimes(2)
|
||||
expect(
|
||||
ProductVariantInventoryServiceMock.adjustInventory
|
||||
).toHaveBeenCalledWith("test-variant", undefined, 10)
|
||||
expect(
|
||||
ProductVariantInventoryServiceMock.adjustInventory
|
||||
).toHaveBeenCalledWith("test-variant-2", undefined, 10)
|
||||
|
||||
expect(returnRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(returnRepository.save).toHaveBeenCalledWith({
|
||||
@@ -368,7 +230,7 @@ describe("ReturnService", () => {
|
||||
|
||||
describe("canceled", () => {
|
||||
const returnRepository = MockRepository({
|
||||
findOne: query => {
|
||||
findOne: (query) => {
|
||||
switch (query.where.id) {
|
||||
case IdMap.getId("test-return"):
|
||||
return Promise.resolve({
|
||||
@@ -384,7 +246,7 @@ describe("ReturnService", () => {
|
||||
return Promise.resolve({})
|
||||
}
|
||||
},
|
||||
save: f => f,
|
||||
save: (f) => f,
|
||||
})
|
||||
|
||||
const returnService = new ReturnService({
|
||||
@@ -415,7 +277,7 @@ describe("ReturnService", () => {
|
||||
|
||||
describe("fulfilled", () => {
|
||||
const returnRepository = MockRepository({
|
||||
findOne: query => {
|
||||
findOne: (query) => {
|
||||
switch (query.where.id) {
|
||||
case IdMap.getId("test-return"):
|
||||
return Promise.resolve({
|
||||
@@ -445,7 +307,7 @@ describe("ReturnService", () => {
|
||||
|
||||
describe("update", () => {
|
||||
const returnRepository = MockRepository({
|
||||
findOne: query => {
|
||||
findOne: (query) => {
|
||||
switch (query.where.id) {
|
||||
case IdMap.getId("test-return"):
|
||||
return Promise.resolve({
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
|
||||
import SwapService from "../swap"
|
||||
import { InventoryServiceMock } from "../__mocks__/inventory"
|
||||
import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant-inventory"
|
||||
import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment"
|
||||
import {
|
||||
CustomShippingOptionService,
|
||||
EventBusService,
|
||||
FulfillmentService,
|
||||
InventoryService,
|
||||
LineItemService,
|
||||
OrderService,
|
||||
PaymentProviderService,
|
||||
ProductVariantInventoryService,
|
||||
ReturnService,
|
||||
ShippingOptionService,
|
||||
TotalsService,
|
||||
@@ -104,7 +103,8 @@ const paymentProviderService = {
|
||||
|
||||
const orderService = {} as unknown as OrderService
|
||||
const returnService = {} as unknown as ReturnService
|
||||
const inventoryService = {} as unknown as InventoryService
|
||||
const productVariantInventoryService =
|
||||
{} as unknown as ProductVariantInventoryService
|
||||
const fulfillmentService = {} as unknown as FulfillmentService
|
||||
const lineItemAdjustmentService = {} as unknown as LineItemAdjustmentService
|
||||
|
||||
@@ -119,7 +119,7 @@ const defaultProps = {
|
||||
totalsService: totalsService,
|
||||
eventBusService: eventBusService,
|
||||
lineItemService: lineItemService,
|
||||
inventoryService: inventoryService,
|
||||
productVariantInventoryService: productVariantInventoryService,
|
||||
fulfillmentService: fulfillmentService,
|
||||
shippingOptionService: shippingOptionService,
|
||||
paymentProviderService: paymentProviderService,
|
||||
@@ -319,9 +319,14 @@ describe("SwapService", () => {
|
||||
expect(
|
||||
LineItemAdjustmentServiceMock.createAdjustmentForLineItem
|
||||
).toHaveBeenCalledWith(
|
||||
{ id: "cart", items: [{
|
||||
id: "test-item",
|
||||
}] },
|
||||
{
|
||||
id: "cart",
|
||||
items: [
|
||||
{
|
||||
id: "test-item",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "test-item",
|
||||
}
|
||||
@@ -852,12 +857,12 @@ describe("SwapService", () => {
|
||||
},
|
||||
} as unknown as PaymentProviderService
|
||||
|
||||
const inventoryService = {
|
||||
...InventoryServiceMock,
|
||||
const productVariantInventoryService = {
|
||||
...ProductVariantInventoryServiceMock,
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
} as unknown as InventoryService
|
||||
} as unknown as ProductVariantInventoryService
|
||||
|
||||
describe("success", () => {
|
||||
const cart = {
|
||||
@@ -889,7 +894,7 @@ describe("SwapService", () => {
|
||||
cartService,
|
||||
paymentProviderService,
|
||||
shippingOptionService,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
})
|
||||
|
||||
it("creates a shipment", async () => {
|
||||
@@ -899,14 +904,12 @@ describe("SwapService", () => {
|
||||
good: "yes",
|
||||
})
|
||||
|
||||
expect(inventoryService.confirmInventory).toHaveBeenCalledWith(
|
||||
"variant",
|
||||
2
|
||||
)
|
||||
expect(inventoryService.adjustInventory).toHaveBeenCalledWith(
|
||||
"variant",
|
||||
-2
|
||||
)
|
||||
expect(
|
||||
productVariantInventoryService.reserveQuantity
|
||||
).toHaveBeenCalledWith("variant", 2, {
|
||||
lineItemId: "1",
|
||||
salesChannelId: undefined,
|
||||
})
|
||||
|
||||
expect(swapRepo.save).toHaveBeenCalledWith({
|
||||
...existing,
|
||||
@@ -950,7 +953,7 @@ describe("SwapService", () => {
|
||||
cartService,
|
||||
paymentProviderService,
|
||||
shippingOptionService,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
})
|
||||
|
||||
it("fails to register cart completion when swap is canceled", async () => {
|
||||
|
||||
@@ -42,8 +42,11 @@ import CustomerService from "./customer"
|
||||
import DiscountService from "./discount"
|
||||
import EventBusService from "./event-bus"
|
||||
import GiftCardService from "./gift-card"
|
||||
import { NewTotalsService, SalesChannelService } from "./index"
|
||||
import InventoryService from "./inventory"
|
||||
import {
|
||||
NewTotalsService,
|
||||
ProductVariantInventoryService,
|
||||
SalesChannelService,
|
||||
} from "./index"
|
||||
import LineItemService from "./line-item"
|
||||
import LineItemAdjustmentService from "./line-item-adjustment"
|
||||
import PaymentProviderService from "./payment-provider"
|
||||
@@ -79,10 +82,10 @@ type InjectedDependencies = {
|
||||
giftCardService: GiftCardService
|
||||
totalsService: TotalsService
|
||||
newTotalsService: NewTotalsService
|
||||
inventoryService: InventoryService
|
||||
customShippingOptionService: CustomShippingOptionService
|
||||
lineItemAdjustmentService: LineItemAdjustmentService
|
||||
priceSelectionStrategy: IPriceSelectionStrategy
|
||||
productVariantInventoryService: ProductVariantInventoryService
|
||||
}
|
||||
|
||||
type TotalsConfig = {
|
||||
@@ -122,11 +125,12 @@ class CartService extends TransactionBaseService {
|
||||
protected readonly taxProviderService_: TaxProviderService
|
||||
protected readonly totalsService_: TotalsService
|
||||
protected readonly newTotalsService_: NewTotalsService
|
||||
protected readonly inventoryService_: InventoryService
|
||||
protected readonly customShippingOptionService_: CustomShippingOptionService
|
||||
protected readonly priceSelectionStrategy_: IPriceSelectionStrategy
|
||||
protected readonly lineItemAdjustmentService_: LineItemAdjustmentService
|
||||
protected readonly featureFlagRouter_: FlagRouter
|
||||
// eslint-disable-next-line max-len
|
||||
protected readonly productVariantInventoryService_: ProductVariantInventoryService
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
@@ -148,13 +152,13 @@ class CartService extends TransactionBaseService {
|
||||
newTotalsService,
|
||||
addressRepository,
|
||||
paymentSessionRepository,
|
||||
inventoryService,
|
||||
customShippingOptionService,
|
||||
lineItemAdjustmentService,
|
||||
priceSelectionStrategy,
|
||||
salesChannelService,
|
||||
featureFlagRouter,
|
||||
storeService,
|
||||
productVariantInventoryService,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
@@ -177,7 +181,6 @@ class CartService extends TransactionBaseService {
|
||||
this.newTotalsService_ = newTotalsService
|
||||
this.addressRepository_ = addressRepository
|
||||
this.paymentSessionRepository_ = paymentSessionRepository
|
||||
this.inventoryService_ = inventoryService
|
||||
this.customShippingOptionService_ = customShippingOptionService
|
||||
this.taxProviderService_ = taxProviderService
|
||||
this.lineItemAdjustmentService_ = lineItemAdjustmentService
|
||||
@@ -185,6 +188,7 @@ class CartService extends TransactionBaseService {
|
||||
this.salesChannelService_ = salesChannelService
|
||||
this.featureFlagRouter_ = featureFlagRouter
|
||||
this.storeService_ = storeService
|
||||
this.productVariantInventoryService_ = productVariantInventoryService
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -584,6 +588,10 @@ class CartService extends TransactionBaseService {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!lineItem.variant_id) {
|
||||
return true
|
||||
}
|
||||
|
||||
const lineItemVariant = lineItem.variant?.product_id
|
||||
? lineItem.variant
|
||||
: await this.productVariantService_
|
||||
@@ -627,11 +635,18 @@ class CartService extends TransactionBaseService {
|
||||
|
||||
if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) {
|
||||
if (config.validateSalesChannels) {
|
||||
if (!(await this.validateLineItem(cart, lineItem))) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`The product "${lineItem.title}" must belongs to the sales channel on which the cart has been created.`
|
||||
if (lineItem.variant_id) {
|
||||
const lineItemIsValid = await this.validateLineItem(
|
||||
cart,
|
||||
lineItem as LineItemValidateData
|
||||
)
|
||||
|
||||
if (!lineItemIsValid) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`The product "${lineItem.title}" must belongs to the sales channel on which the cart has been created.`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -664,9 +679,22 @@ class CartService extends TransactionBaseService {
|
||||
: lineItem.quantity
|
||||
|
||||
// Confirm inventory or throw error
|
||||
await this.inventoryService_
|
||||
.withTransaction(transactionManager)
|
||||
.confirmInventory(lineItem.variant_id, quantity)
|
||||
if (lineItem.variant_id) {
|
||||
const isCovered =
|
||||
await this.productVariantInventoryService_.confirmInventory(
|
||||
lineItem.variant_id,
|
||||
quantity,
|
||||
{ salesChannelId: cart.sales_channel_id }
|
||||
)
|
||||
|
||||
if (!isCovered) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Variant with id: ${lineItem.variant_id} does not have the required inventory`,
|
||||
MedusaError.Codes.INSUFFICIENT_INVENTORY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (currentItem) {
|
||||
await lineItemServiceTx.update(currentItem.id, {
|
||||
@@ -737,7 +765,13 @@ class CartService extends TransactionBaseService {
|
||||
if (config.validateSalesChannels) {
|
||||
const areValid = await Promise.all(
|
||||
items.map(async (item) => {
|
||||
return await this.validateLineItem(cart, item)
|
||||
if (item.variant_id) {
|
||||
return await this.validateLineItem(
|
||||
cart,
|
||||
item as LineItemValidateData
|
||||
)
|
||||
}
|
||||
return true
|
||||
})
|
||||
)
|
||||
|
||||
@@ -762,8 +796,10 @@ class CartService extends TransactionBaseService {
|
||||
|
||||
const lineItemServiceTx =
|
||||
this.lineItemService_.withTransaction(transactionManager)
|
||||
const inventoryServiceTx =
|
||||
this.inventoryService_.withTransaction(transactionManager)
|
||||
const productVariantInventoryServiceTx =
|
||||
this.productVariantInventoryService_.withTransaction(
|
||||
transactionManager
|
||||
)
|
||||
|
||||
const existingItems = await lineItemServiceTx.list(
|
||||
{
|
||||
@@ -797,10 +833,22 @@ class CartService extends TransactionBaseService {
|
||||
? (currentItem.quantity += item.quantity)
|
||||
: item.quantity
|
||||
|
||||
await inventoryServiceTx.confirmInventory(
|
||||
item.variant_id,
|
||||
item.quantity
|
||||
)
|
||||
if (item.variant_id) {
|
||||
const isSufficient =
|
||||
await productVariantInventoryServiceTx.confirmInventory(
|
||||
item.variant_id,
|
||||
item.quantity,
|
||||
{ salesChannelId: cart.sales_channel_id }
|
||||
)
|
||||
|
||||
if (!isSufficient) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Variant with id: ${item.variant_id} does not have the required inventory`,
|
||||
MedusaError.Codes.INSUFFICIENT_INVENTORY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (currentItem) {
|
||||
lineItemsToUpdate[currentItem.id] = {
|
||||
@@ -873,13 +921,12 @@ class CartService extends TransactionBaseService {
|
||||
): Promise<Cart> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const cart = await this.retrieve(cartId, {
|
||||
relations: ["items", "items.adjustments", "payment_sessions"],
|
||||
const lineItem = await this.lineItemService_.retrieve(lineItemId, {
|
||||
select: ["id", "quantity", "variant_id", "cart_id"],
|
||||
})
|
||||
|
||||
// Ensure that the line item exists in the cart
|
||||
const lineItemExists = cart.items.find((i) => i.id === lineItemId)
|
||||
if (!lineItemExists) {
|
||||
if (lineItem.cart_id !== cartId) {
|
||||
// Ensure that the line item exists in the cart
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"A line item with the provided id doesn't exist in the cart"
|
||||
@@ -887,18 +934,31 @@ class CartService extends TransactionBaseService {
|
||||
}
|
||||
|
||||
if (lineItemUpdate.quantity) {
|
||||
const hasInventory = await this.inventoryService_
|
||||
.withTransaction(transactionManager)
|
||||
.confirmInventory(
|
||||
lineItemExists.variant_id,
|
||||
lineItemUpdate.quantity
|
||||
)
|
||||
if (lineItem.variant_id) {
|
||||
const select: (keyof Cart)[] = ["id"]
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled(
|
||||
SalesChannelFeatureFlag.key
|
||||
)
|
||||
) {
|
||||
select.push("sales_channel_id")
|
||||
}
|
||||
|
||||
if (!hasInventory) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Inventory doesn't cover the desired quantity"
|
||||
)
|
||||
const cart = await this.retrieve(cartId, { select: select })
|
||||
|
||||
const hasInventory =
|
||||
await this.productVariantInventoryService_.confirmInventory(
|
||||
lineItem.variant_id,
|
||||
lineItemUpdate.quantity,
|
||||
{ salesChannelId: cart.sales_channel_id }
|
||||
)
|
||||
|
||||
if (!hasInventory) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Inventory doesn't cover the desired quantity"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2009,6 +2069,10 @@ class CartService extends TransactionBaseService {
|
||||
cart.items = (
|
||||
await Promise.all(
|
||||
cart.items.map(async (item) => {
|
||||
if (!item.variant_id) {
|
||||
return item
|
||||
}
|
||||
|
||||
const availablePrice = await this.priceSelectionStrategy_
|
||||
.withTransaction(transactionManager)
|
||||
.calculateVariantPrice(item.variant_id, {
|
||||
@@ -2024,14 +2088,13 @@ class CartService extends TransactionBaseService {
|
||||
availablePrice !== undefined &&
|
||||
availablePrice.calculatedPrice !== null
|
||||
) {
|
||||
return lineItemServiceTx.update(item.id, {
|
||||
return await lineItemServiceTx.update(item.id, {
|
||||
has_shipping: false,
|
||||
unit_price: availablePrice.calculatedPrice,
|
||||
})
|
||||
} else {
|
||||
await lineItemServiceTx.delete(item.id)
|
||||
return
|
||||
}
|
||||
|
||||
return await lineItemServiceTx.delete(item.id)
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
@@ -70,6 +70,13 @@ class ClaimItemService extends TransactionBaseService {
|
||||
.withTransaction(manager)
|
||||
.retrieve(item_id)
|
||||
|
||||
if (!lineItem.variant_id) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Cannot claim a custom line item"
|
||||
)
|
||||
}
|
||||
|
||||
if (lineItem.fulfilled_quantity < quantity) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
|
||||
@@ -21,9 +21,9 @@ import ClaimItemService from "./claim-item"
|
||||
import EventBusService from "./event-bus"
|
||||
import FulfillmentService from "./fulfillment"
|
||||
import FulfillmentProviderService from "./fulfillment-provider"
|
||||
import InventoryService from "./inventory"
|
||||
import LineItemService from "./line-item"
|
||||
import PaymentProviderService from "./payment-provider"
|
||||
import ProductVariantInventoryService from "./product-variant-inventory"
|
||||
import RegionService from "./region"
|
||||
import ReturnService from "./return"
|
||||
import ShippingOptionService from "./shipping-option"
|
||||
@@ -40,7 +40,7 @@ type InjectedDependencies = {
|
||||
eventBusService: EventBusService
|
||||
fulfillmentProviderService: FulfillmentProviderService
|
||||
fulfillmentService: FulfillmentService
|
||||
inventoryService: InventoryService
|
||||
productVariantInventoryService: ProductVariantInventoryService
|
||||
lineItemService: LineItemService
|
||||
paymentProviderService: PaymentProviderService
|
||||
regionService: RegionService
|
||||
@@ -71,7 +71,6 @@ export default class ClaimService extends TransactionBaseService {
|
||||
protected readonly eventBus_: EventBusService
|
||||
protected readonly fulfillmentProviderService_: FulfillmentProviderService
|
||||
protected readonly fulfillmentService_: FulfillmentService
|
||||
protected readonly inventoryService_: InventoryService
|
||||
protected readonly lineItemService_: LineItemService
|
||||
protected readonly paymentProviderService_: PaymentProviderService
|
||||
protected readonly regionService_: RegionService
|
||||
@@ -79,6 +78,8 @@ export default class ClaimService extends TransactionBaseService {
|
||||
protected readonly shippingOptionService_: ShippingOptionService
|
||||
protected readonly taxProviderService_: TaxProviderService
|
||||
protected readonly totalsService_: TotalsService
|
||||
// eslint-disable-next-line max-len
|
||||
protected readonly productVariantInventoryService_: ProductVariantInventoryService
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
@@ -90,7 +91,7 @@ export default class ClaimService extends TransactionBaseService {
|
||||
eventBusService,
|
||||
fulfillmentProviderService,
|
||||
fulfillmentService,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
lineItemService,
|
||||
paymentProviderService,
|
||||
regionService,
|
||||
@@ -112,7 +113,7 @@ export default class ClaimService extends TransactionBaseService {
|
||||
this.eventBus_ = eventBusService
|
||||
this.fulfillmentProviderService_ = fulfillmentProviderService
|
||||
this.fulfillmentService_ = fulfillmentService
|
||||
this.inventoryService_ = inventoryService
|
||||
this.productVariantInventoryService_ = productVariantInventoryService
|
||||
this.lineItemService_ = lineItemService
|
||||
this.paymentProviderService_ = paymentProviderService
|
||||
this.regionService_ = regionService
|
||||
@@ -334,16 +335,6 @@ export default class ClaimService extends TransactionBaseService {
|
||||
|
||||
let newItems: LineItem[] = []
|
||||
if (isDefined(additional_items)) {
|
||||
const inventoryServiceTx =
|
||||
this.inventoryService_.withTransaction(transactionManager)
|
||||
|
||||
for (const item of additional_items) {
|
||||
await inventoryServiceTx.confirmInventory(
|
||||
item.variant_id,
|
||||
item.quantity
|
||||
)
|
||||
}
|
||||
|
||||
newItems = await Promise.all(
|
||||
additional_items.map(async (i) =>
|
||||
lineItemServiceTx.generate(
|
||||
@@ -354,12 +345,20 @@ export default class ClaimService extends TransactionBaseService {
|
||||
)
|
||||
)
|
||||
|
||||
for (const newItem of newItems) {
|
||||
await inventoryServiceTx.adjustInventory(
|
||||
newItem.variant_id,
|
||||
-newItem.quantity
|
||||
)
|
||||
}
|
||||
await Promise.all(
|
||||
newItems.map(async (newItem) => {
|
||||
if (newItem.variant_id) {
|
||||
await this.productVariantInventoryService_.reserveQuantity(
|
||||
newItem.variant_id,
|
||||
newItem.quantity,
|
||||
{
|
||||
lineItemId: newItem.id,
|
||||
salesChannelId: order.sales_channel_id,
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const evaluatedNoNotification =
|
||||
|
||||
@@ -17,7 +17,6 @@ export { default as FulfillmentService } from "./fulfillment"
|
||||
export { default as FulfillmentProviderService } from "./fulfillment-provider"
|
||||
export { default as GiftCardService } from "./gift-card"
|
||||
export { default as IdempotencyKeyService } from "./idempotency-key"
|
||||
export { default as InventoryService } from "./inventory"
|
||||
export { default as LineItemService } from "./line-item"
|
||||
export { default as LineItemAdjustmentService } from "./line-item-adjustment"
|
||||
export { default as MiddlewareService } from "./middleware"
|
||||
@@ -35,11 +34,14 @@ export { default as PricingService } from "./pricing"
|
||||
export { default as ProductService } from "./product"
|
||||
export { default as ProductCollectionService } from "./product-collection"
|
||||
export { default as ProductTypeService } from "./product-type"
|
||||
export { default as ProductVariantInventoryService } from "./product-variant-inventory"
|
||||
export { default as ProductVariantService } from "./product-variant"
|
||||
|
||||
export { default as RegionService } from "./region"
|
||||
export { default as ReturnService } from "./return"
|
||||
export { default as ReturnReasonService } from "./return-reason"
|
||||
export { default as SalesChannelInventoryService } from "./sales-channel-inventory"
|
||||
export { default as SalesChannelLocationService } from "./sales-channel-location"
|
||||
export { default as SalesChannelService } from "./sales-channel"
|
||||
export { default as SearchService } from "./search"
|
||||
export { default as ShippingOptionService } from "./shipping-option"
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import { EntityManager } from "typeorm"
|
||||
import ProductVariantService from "./product-variant"
|
||||
import { ProductVariant } from "../models"
|
||||
|
||||
type InventoryServiceProps = {
|
||||
manager: EntityManager
|
||||
productVariantService: ProductVariantService
|
||||
}
|
||||
|
||||
class InventoryService extends TransactionBaseService {
|
||||
protected readonly productVariantService_: ProductVariantService
|
||||
|
||||
protected manager_: EntityManager
|
||||
protected transactionManager_: EntityManager | undefined
|
||||
|
||||
constructor({ manager, productVariantService }: InventoryServiceProps) {
|
||||
super(arguments[0])
|
||||
|
||||
this.manager_ = manager
|
||||
this.productVariantService_ = productVariantService
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the inventory of a variant based on a given adjustment.
|
||||
* @param variantId - the id of the variant to update
|
||||
* @param adjustment - the number to adjust the inventory quantity by
|
||||
* @return resolves to the update result.
|
||||
*/
|
||||
async adjustInventory(
|
||||
variantId: string,
|
||||
adjustment: number
|
||||
): Promise<ProductVariant | undefined> {
|
||||
if (!variantId) {
|
||||
return
|
||||
}
|
||||
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const variant = await this.productVariantService_
|
||||
.withTransaction(manager)
|
||||
.retrieve(variantId)
|
||||
// if inventory is managed then update
|
||||
if (variant.manage_inventory) {
|
||||
return await this.productVariantService_
|
||||
.withTransaction(manager)
|
||||
.update(variant, {
|
||||
inventory_quantity: variant.inventory_quantity + adjustment,
|
||||
})
|
||||
} else {
|
||||
return variant
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the inventory of a variant can cover a given quantity. Will
|
||||
* return true if the variant doesn't have managed inventory or if the variant
|
||||
* allows backorders or if the inventory quantity is greater than `quantity`.
|
||||
* @param variantId - the id of the variant to check
|
||||
* @param quantity - the number of units to check availability for
|
||||
* @return true if the inventory covers the quantity
|
||||
*/
|
||||
async confirmInventory(
|
||||
variantId: string | null | undefined,
|
||||
quantity: number
|
||||
): Promise<boolean> {
|
||||
// if variantId is undefined then confirm inventory as it
|
||||
// is a custom item that is not managed
|
||||
if (!isDefined(variantId) || variantId === null) {
|
||||
return true
|
||||
}
|
||||
|
||||
const variant = await this.productVariantService_
|
||||
.withTransaction(this.manager_)
|
||||
.retrieve(variantId, {
|
||||
select: [
|
||||
"id",
|
||||
"inventory_quantity",
|
||||
"allow_backorder",
|
||||
"manage_inventory",
|
||||
],
|
||||
})
|
||||
|
||||
const { inventory_quantity, allow_backorder, manage_inventory } = variant
|
||||
const isCovered =
|
||||
!manage_inventory || allow_backorder || inventory_quantity >= quantity
|
||||
if (!isCovered) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Variant with id: ${variant.id} does not have the required inventory`,
|
||||
MedusaError.Codes.INSUFFICIENT_INVENTORY
|
||||
)
|
||||
}
|
||||
|
||||
return isCovered
|
||||
}
|
||||
}
|
||||
|
||||
export default InventoryService
|
||||
@@ -38,13 +38,13 @@ import EventBusService from "./event-bus"
|
||||
import FulfillmentService from "./fulfillment"
|
||||
import FulfillmentProviderService from "./fulfillment-provider"
|
||||
import GiftCardService from "./gift-card"
|
||||
import InventoryService from "./inventory"
|
||||
import LineItemService from "./line-item"
|
||||
import PaymentProviderService from "./payment-provider"
|
||||
import RegionService from "./region"
|
||||
import ShippingOptionService from "./shipping-option"
|
||||
import ShippingProfileService from "./shipping-profile"
|
||||
import TotalsService from "./totals"
|
||||
import ProductVariantInventoryService from "./product-variant-inventory"
|
||||
import { NewTotalsService, TaxProviderService } from "./index"
|
||||
|
||||
export const ORDER_CART_ALREADY_EXISTS_ERROR = "Order from cart already exists"
|
||||
@@ -68,9 +68,9 @@ type InjectedDependencies = {
|
||||
addressRepository: typeof AddressRepository
|
||||
giftCardService: GiftCardService
|
||||
draftOrderService: DraftOrderService
|
||||
inventoryService: InventoryService
|
||||
eventBusService: EventBusService
|
||||
featureFlagRouter: FlagRouter
|
||||
productVariantInventoryService: ProductVariantInventoryService
|
||||
}
|
||||
|
||||
type TotalsConfig = {
|
||||
@@ -117,9 +117,10 @@ class OrderService extends TransactionBaseService {
|
||||
protected readonly addressRepository_: typeof AddressRepository
|
||||
protected readonly giftCardService_: GiftCardService
|
||||
protected readonly draftOrderService_: DraftOrderService
|
||||
protected readonly inventoryService_: InventoryService
|
||||
protected readonly eventBus_: EventBusService
|
||||
protected readonly featureFlagRouter_: FlagRouter
|
||||
// eslint-disable-next-line max-len
|
||||
protected readonly productVariantInventoryService_: ProductVariantInventoryService
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
@@ -140,9 +141,9 @@ class OrderService extends TransactionBaseService {
|
||||
addressRepository,
|
||||
giftCardService,
|
||||
draftOrderService,
|
||||
inventoryService,
|
||||
eventBusService,
|
||||
featureFlagRouter,
|
||||
productVariantInventoryService,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
@@ -166,8 +167,8 @@ class OrderService extends TransactionBaseService {
|
||||
this.cartService_ = cartService
|
||||
this.addressRepository_ = addressRepository
|
||||
this.draftOrderService_ = draftOrderService
|
||||
this.inventoryService_ = inventoryService
|
||||
this.featureFlagRouter_ = featureFlagRouter
|
||||
this.productVariantInventoryService_ = productVariantInventoryService
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -540,7 +541,6 @@ class OrderService extends TransactionBaseService {
|
||||
async createFromCart(cartOrId: string | Cart): Promise<Order | never> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const cartServiceTx = this.cartService_.withTransaction(manager)
|
||||
const inventoryServiceTx = this.inventoryService_.withTransaction(manager)
|
||||
|
||||
const exists = !!(await this.retrieveByCartId(
|
||||
isString(cartOrId) ? cartOrId : cartOrId?.id,
|
||||
@@ -571,23 +571,6 @@ class OrderService extends TransactionBaseService {
|
||||
|
||||
const { payment, region, total } = cart
|
||||
|
||||
await Promise.all(
|
||||
cart.items.map(async (lineItem) => {
|
||||
return await inventoryServiceTx.confirmInventory(
|
||||
lineItem.variant_id,
|
||||
lineItem.quantity
|
||||
)
|
||||
})
|
||||
).catch(async (err) => {
|
||||
if (payment) {
|
||||
await this.paymentProviderService_
|
||||
.withTransaction(manager)
|
||||
.cancelPayment(payment)
|
||||
}
|
||||
await cartServiceTx.update(cart.id, { payment_authorized_at: null })
|
||||
throw err
|
||||
})
|
||||
|
||||
// Would be the case if a discount code is applied that covers the item
|
||||
// total
|
||||
if (total !== 0) {
|
||||
@@ -707,22 +690,18 @@ class OrderService extends TransactionBaseService {
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
cart.items.map((lineItem) => {
|
||||
const lineItemPromises: unknown[] = [
|
||||
cart.items.map((lineItem): unknown[] => {
|
||||
const toReturn: unknown[] = [
|
||||
lineItemServiceTx.update(lineItem.id, { order_id: order.id }),
|
||||
inventoryServiceTx.adjustInventory(
|
||||
lineItem.variant_id,
|
||||
-lineItem.quantity
|
||||
),
|
||||
]
|
||||
|
||||
if (lineItem.is_giftcard) {
|
||||
lineItemPromises.push(
|
||||
toReturn.push(
|
||||
this.createGiftCardsFromLineItem_(order, lineItem, manager)
|
||||
)
|
||||
}
|
||||
|
||||
return lineItemPromises
|
||||
return toReturn
|
||||
}),
|
||||
cart.shipping_methods.map(async (method) => {
|
||||
// TODO: Due to cascade insert we have to remove the tax_lines that have been added by the cart decorate totals.
|
||||
@@ -1159,10 +1138,19 @@ class OrderService extends TransactionBaseService {
|
||||
throwErrorIf(order.swaps, notCanceled, "swaps")
|
||||
throwErrorIf(order.claims, notCanceled, "claims")
|
||||
|
||||
const inventoryServiceTx = this.inventoryService_.withTransaction(manager)
|
||||
for (const item of order.items) {
|
||||
await inventoryServiceTx.adjustInventory(item.variant_id, item.quantity)
|
||||
}
|
||||
const inventoryServiceTx =
|
||||
this.productVariantInventoryService_.withTransaction(manager)
|
||||
await Promise.all(
|
||||
order.items.map(async (item) => {
|
||||
if (item.variant_id) {
|
||||
return await inventoryServiceTx.releaseReservationsByLineItem(
|
||||
item.id,
|
||||
item.variant_id,
|
||||
item.quantity
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const paymentProviderServiceTx =
|
||||
this.paymentProviderService_.withTransaction(manager)
|
||||
|
||||
@@ -0,0 +1,429 @@
|
||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||
import { EntityManager, In } from "typeorm"
|
||||
import {
|
||||
IStockLocationService,
|
||||
IInventoryService,
|
||||
TransactionBaseService,
|
||||
} from "../interfaces"
|
||||
import { ProductVariantInventoryItem } from "../models/product-variant-inventory-item"
|
||||
import { ProductVariantService, SalesChannelLocationService } from "./"
|
||||
import { InventoryItemDTO, ReserveQuantityContext } from "../types/inventory"
|
||||
import { ProductVariant } from "../models"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
salesChannelLocationService: SalesChannelLocationService
|
||||
productVariantService: ProductVariantService
|
||||
stockLocationService: IStockLocationService
|
||||
inventoryService: IInventoryService
|
||||
}
|
||||
|
||||
class ProductVariantInventoryService extends TransactionBaseService {
|
||||
protected manager_: EntityManager
|
||||
protected transactionManager_: EntityManager | undefined
|
||||
|
||||
protected readonly salesChannelLocationService_: SalesChannelLocationService
|
||||
protected readonly productVariantService_: ProductVariantService
|
||||
protected readonly stockLocationService_: IStockLocationService
|
||||
protected readonly inventoryService_: IInventoryService
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
stockLocationService,
|
||||
salesChannelLocationService,
|
||||
productVariantService,
|
||||
inventoryService,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
|
||||
this.manager_ = manager
|
||||
this.salesChannelLocationService_ = salesChannelLocationService
|
||||
this.stockLocationService_ = stockLocationService
|
||||
this.productVariantService_ = productVariantService
|
||||
this.inventoryService_ = inventoryService
|
||||
}
|
||||
|
||||
/**
|
||||
* confirms if requested inventory is available
|
||||
* @param variantId id of the variant to confirm inventory for
|
||||
* @param quantity quantity of inventory to confirm is available
|
||||
* @param context optionally include a sales channel if applicable
|
||||
* @returns boolean indicating if inventory is available
|
||||
*/
|
||||
async confirmInventory(
|
||||
variantId: string,
|
||||
quantity: number,
|
||||
context: { salesChannelId?: string | null } = {}
|
||||
): Promise<Boolean> {
|
||||
if (!variantId) {
|
||||
return true
|
||||
}
|
||||
|
||||
const manager = this.transactionManager_ || this.manager_
|
||||
const productVariant = await this.productVariantService_
|
||||
.withTransaction(manager)
|
||||
.retrieve(variantId, {
|
||||
select: [
|
||||
"id",
|
||||
"allow_backorder",
|
||||
"manage_inventory",
|
||||
"inventory_quantity",
|
||||
],
|
||||
})
|
||||
|
||||
// If the variant allows backorders or if inventory isn't managed we
|
||||
// don't need to check inventory
|
||||
if (productVariant.allow_backorder || !productVariant.manage_inventory) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!this.inventoryService_) {
|
||||
return productVariant.inventory_quantity >= quantity
|
||||
}
|
||||
|
||||
const variantInventory = await this.listByVariant(variantId)
|
||||
|
||||
// If there are no inventory items attached to the variant we default
|
||||
// to true
|
||||
if (variantInventory.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
let locations: string[] = []
|
||||
if (context.salesChannelId) {
|
||||
locations = await this.salesChannelLocationService_.listLocations(
|
||||
context.salesChannelId
|
||||
)
|
||||
} else {
|
||||
const stockLocations = await this.stockLocationService_.list(
|
||||
{},
|
||||
{ select: ["id"] }
|
||||
)
|
||||
locations = stockLocations.map((l) => l.id)
|
||||
}
|
||||
|
||||
const hasInventory = await Promise.all(
|
||||
variantInventory.map(async (inventoryPart) => {
|
||||
const itemQuantity = inventoryPart.quantity * quantity
|
||||
return await this.inventoryService_.confirmInventory(
|
||||
inventoryPart.inventory_item_id,
|
||||
locations,
|
||||
itemQuantity
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
return hasInventory.every(Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* list registered inventory items
|
||||
* @param itemIds list inventory item ids
|
||||
* @returns list of inventory items
|
||||
*/
|
||||
async listByItem(itemIds: string[]): Promise<ProductVariantInventoryItem[]> {
|
||||
const manager = this.transactionManager_ || this.manager_
|
||||
|
||||
const variantInventoryRepo = manager.getRepository(
|
||||
ProductVariantInventoryItem
|
||||
)
|
||||
|
||||
const variantInventory = await variantInventoryRepo.find({
|
||||
where: { inventory_item_id: In(itemIds) },
|
||||
})
|
||||
|
||||
return variantInventory
|
||||
}
|
||||
|
||||
/**
|
||||
* List inventory items for a specific variant
|
||||
* @param variantId variant id
|
||||
* @returns variant inventory items for the variant id
|
||||
*/
|
||||
private async listByVariant(
|
||||
variantId: string
|
||||
): Promise<ProductVariantInventoryItem[]> {
|
||||
const manager = this.transactionManager_ || this.manager_
|
||||
|
||||
const variantInventoryRepo = manager.getRepository(
|
||||
ProductVariantInventoryItem
|
||||
)
|
||||
|
||||
const variantInventory = await variantInventoryRepo.find({
|
||||
where: { variant_id: variantId },
|
||||
})
|
||||
|
||||
return variantInventory
|
||||
}
|
||||
|
||||
/**
|
||||
* lists variant by inventory item id
|
||||
* @param itemId item id
|
||||
* @returns a list of product variants that are associated with the item id
|
||||
*/
|
||||
async listVariantsByItem(itemId: string): Promise<ProductVariant[]> {
|
||||
if (!this.inventoryService_) {
|
||||
return []
|
||||
}
|
||||
|
||||
const variantInventory = await this.listByItem([itemId])
|
||||
const items = await this.productVariantService_.list({
|
||||
id: variantInventory.map((i) => i.variant_id),
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* lists inventory items for a given variant
|
||||
* @param variantId variant id
|
||||
* @returns lidt of inventory items for the variant
|
||||
*/
|
||||
async listInventoryItemsByVariant(
|
||||
variantId: string
|
||||
): Promise<InventoryItemDTO[]> {
|
||||
if (!this.inventoryService_) {
|
||||
return []
|
||||
}
|
||||
|
||||
const variantInventory = await this.listByVariant(variantId)
|
||||
const [items] = await this.inventoryService_.listInventoryItems({
|
||||
id: variantInventory.map((i) => i.inventory_item_id),
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a variant to an inventory item
|
||||
* @param variantId variant id
|
||||
* @param inventoryItemId inventory item id
|
||||
* @param quantity quantity of variant to attach
|
||||
* @returns the variant inventory item
|
||||
*/
|
||||
async attachInventoryItem(
|
||||
variantId: string,
|
||||
inventoryItemId: string,
|
||||
quantity?: number
|
||||
): Promise<ProductVariantInventoryItem> {
|
||||
const manager = this.transactionManager_ || this.manager_
|
||||
|
||||
// Verify that variant exists
|
||||
await this.productVariantService_
|
||||
.withTransaction(manager)
|
||||
.retrieve(variantId, {
|
||||
select: ["id"],
|
||||
})
|
||||
|
||||
// Verify that item exists
|
||||
await this.inventoryService_.retrieveInventoryItem(inventoryItemId, {
|
||||
select: ["id"],
|
||||
})
|
||||
|
||||
const variantInventoryRepo = manager.getRepository(
|
||||
ProductVariantInventoryItem
|
||||
)
|
||||
|
||||
const existing = await variantInventoryRepo.findOne({
|
||||
where: {
|
||||
variant_id: variantId,
|
||||
inventory_item_id: inventoryItemId,
|
||||
},
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
let quantityToStore = 1
|
||||
if (typeof quantity !== "undefined") {
|
||||
if (quantity < 1) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Quantity must be greater than 0"
|
||||
)
|
||||
} else {
|
||||
quantityToStore = quantity
|
||||
}
|
||||
}
|
||||
|
||||
const variantInventory = variantInventoryRepo.create({
|
||||
variant_id: variantId,
|
||||
inventory_item_id: inventoryItemId,
|
||||
quantity: quantityToStore,
|
||||
})
|
||||
|
||||
return await variantInventoryRepo.save(variantInventory)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a variant from an inventory item
|
||||
* @param variantId variant id
|
||||
* @param inventoryItemId inventory item id
|
||||
*/
|
||||
async detachInventoryItem(
|
||||
variantId: string,
|
||||
inventoryItemId: string
|
||||
): Promise<void> {
|
||||
const manager = this.transactionManager_ || this.manager_
|
||||
|
||||
const variantInventoryRepo = manager.getRepository(
|
||||
ProductVariantInventoryItem
|
||||
)
|
||||
|
||||
const existing = await variantInventoryRepo.findOne({
|
||||
where: {
|
||||
variant_id: variantId,
|
||||
inventory_item_id: inventoryItemId,
|
||||
},
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
await variantInventoryRepo.remove(existing)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reserves a quantity of a variant
|
||||
* @param variantId variant id
|
||||
* @param quantity quantity to reserve
|
||||
* @param context optional parameters
|
||||
*/
|
||||
async reserveQuantity(
|
||||
variantId: string,
|
||||
quantity: number,
|
||||
context: ReserveQuantityContext = {}
|
||||
): Promise<void> {
|
||||
const manager = this.transactionManager_ || this.manager_
|
||||
|
||||
if (!this.inventoryService_) {
|
||||
const variantServiceTx =
|
||||
this.productVariantService_.withTransaction(manager)
|
||||
const variant = await variantServiceTx.retrieve(variantId, {
|
||||
select: ["id", "inventory_quantity"],
|
||||
})
|
||||
await variantServiceTx.update(variant.id, {
|
||||
inventory_quantity: variant.inventory_quantity - quantity,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const toReserve = {
|
||||
type: "order",
|
||||
line_item_id: context.lineItemId,
|
||||
}
|
||||
|
||||
const variantInventory = await this.listByVariant(variantId)
|
||||
|
||||
if (variantInventory.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let locationId = context.locationId
|
||||
if (!isDefined(locationId) && context.salesChannelId) {
|
||||
const locations = await this.salesChannelLocationService_
|
||||
.withTransaction(manager)
|
||||
.listLocations(context.salesChannelId)
|
||||
|
||||
if (!locations.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Must provide location_id or sales_channel_id to a Sales Channel that has associated Stock Locations"
|
||||
)
|
||||
}
|
||||
|
||||
locationId = locations[0]
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
variantInventory.map(async (inventoryPart) => {
|
||||
const itemQuantity = inventoryPart.quantity * quantity
|
||||
return await this.inventoryService_.createReservationItem({
|
||||
...toReserve,
|
||||
location_id: locationId as string,
|
||||
item_id: inventoryPart.inventory_item_id,
|
||||
quantity: itemQuantity,
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove reservation of variant quantity
|
||||
* @param lineItemId line item id
|
||||
* @param variantId variant id
|
||||
* @param quantity quantity to release
|
||||
*/
|
||||
async releaseReservationsByLineItem(
|
||||
lineItemId: string,
|
||||
variantId: string,
|
||||
quantity: number
|
||||
): Promise<void> {
|
||||
if (!this.inventoryService_) {
|
||||
const variant = await this.productVariantService_.retrieve(variantId, {
|
||||
select: ["id", "inventory_quantity", "manage_inventory"],
|
||||
})
|
||||
|
||||
if (!variant.manage_inventory) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.productVariantService_.update(variantId, {
|
||||
inventory_quantity: variant.inventory_quantity + quantity,
|
||||
})
|
||||
} else {
|
||||
await this.inventoryService_.deleteReservationItemsByLineItem(lineItemId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts inventory of a variant on a location
|
||||
* @param variantId variant id
|
||||
* @param locationId location id
|
||||
* @param quantity quantity to adjust
|
||||
*/
|
||||
async adjustInventory(
|
||||
variantId: string,
|
||||
locationId: string,
|
||||
quantity: number
|
||||
): Promise<void> {
|
||||
const manager = this.transactionManager_ || this.manager_
|
||||
if (!this.inventoryService_) {
|
||||
const variant = await this.productVariantService_
|
||||
.withTransaction(manager)
|
||||
.retrieve(variantId, {
|
||||
select: ["id", "inventory_quantity", "manage_inventory"],
|
||||
})
|
||||
|
||||
if (!variant.manage_inventory) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.productVariantService_
|
||||
.withTransaction(manager)
|
||||
.update(variantId, {
|
||||
inventory_quantity: variant.inventory_quantity + quantity,
|
||||
})
|
||||
} else {
|
||||
const variantInventory = await this.listByVariant(variantId)
|
||||
|
||||
if (variantInventory.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
variantInventory.map(async (inventoryPart) => {
|
||||
const itemQuantity = inventoryPart.quantity * quantity
|
||||
return await this.inventoryService_.adjustInventory(
|
||||
inventoryPart.inventory_item_id,
|
||||
locationId,
|
||||
itemQuantity
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductVariantInventoryService
|
||||
@@ -1,5 +1,6 @@
|
||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||
import { DeepPartial, EntityManager } from "typeorm"
|
||||
import { ProductVariantInventoryService } from "."
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import {
|
||||
FulfillmentStatus,
|
||||
@@ -17,7 +18,6 @@ import { OrdersReturnItem } from "../types/orders"
|
||||
import { CreateReturnInput, UpdateReturnInput } from "../types/return"
|
||||
import { buildQuery, setMetadata } from "../utils"
|
||||
import FulfillmentProviderService from "./fulfillment-provider"
|
||||
import InventoryService from "./inventory"
|
||||
import LineItemService from "./line-item"
|
||||
import OrderService from "./order"
|
||||
import ReturnReasonService from "./return-reason"
|
||||
@@ -35,8 +35,8 @@ type InjectedDependencies = {
|
||||
returnReasonService: ReturnReasonService
|
||||
taxProviderService: TaxProviderService
|
||||
fulfillmentProviderService: FulfillmentProviderService
|
||||
inventoryService: InventoryService
|
||||
orderService: OrderService
|
||||
productVariantInventoryService: ProductVariantInventoryService
|
||||
}
|
||||
|
||||
type Transformer = (
|
||||
@@ -57,8 +57,9 @@ class ReturnService extends TransactionBaseService {
|
||||
protected readonly shippingOptionService_: ShippingOptionService
|
||||
protected readonly fulfillmentProviderService_: FulfillmentProviderService
|
||||
protected readonly returnReasonService_: ReturnReasonService
|
||||
protected readonly inventoryService_: InventoryService
|
||||
protected readonly orderService_: OrderService
|
||||
// eslint-disable-next-line
|
||||
protected readonly productVariantInventoryService_: ProductVariantInventoryService
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
@@ -70,9 +71,10 @@ class ReturnService extends TransactionBaseService {
|
||||
returnReasonService,
|
||||
taxProviderService,
|
||||
fulfillmentProviderService,
|
||||
inventoryService,
|
||||
orderService,
|
||||
productVariantInventoryService,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
|
||||
this.manager_ = manager
|
||||
@@ -84,8 +86,8 @@ class ReturnService extends TransactionBaseService {
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
this.fulfillmentProviderService_ = fulfillmentProviderService
|
||||
this.returnReasonService_ = returnReasonService
|
||||
this.inventoryService_ = inventoryService
|
||||
this.orderService_ = orderService
|
||||
this.productVariantInventoryService_ = productVariantInventoryService
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -669,12 +671,14 @@ class ReturnService extends TransactionBaseService {
|
||||
})
|
||||
}
|
||||
|
||||
const inventoryServiceTx = this.inventoryService_.withTransaction(manager)
|
||||
const inventoryServiceTx =
|
||||
this.productVariantInventoryService_.withTransaction(manager)
|
||||
for (const line of newLines) {
|
||||
const orderItem = order.items.find((i) => i.id === line.item_id)
|
||||
if (orderItem) {
|
||||
if (orderItem && orderItem?.variant_id) {
|
||||
await inventoryServiceTx.adjustInventory(
|
||||
orderItem.variant_id,
|
||||
returnObj.location_id,
|
||||
line.received_quantity
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
import { IInventoryService } from "../interfaces/services"
|
||||
|
||||
import { SalesChannelLocationService, EventBusService } from "./"
|
||||
|
||||
type InjectedDependencies = {
|
||||
inventoryService: IInventoryService
|
||||
salesChannelLocationService: SalesChannelLocationService
|
||||
eventBusService: EventBusService
|
||||
manager: EntityManager
|
||||
}
|
||||
|
||||
class SalesChannelInventoryService {
|
||||
protected manager_: EntityManager
|
||||
|
||||
protected readonly salesChannelLocationService_: SalesChannelLocationService
|
||||
protected readonly eventBusService_: EventBusService
|
||||
protected readonly inventoryService_: IInventoryService
|
||||
|
||||
constructor({
|
||||
salesChannelLocationService,
|
||||
inventoryService,
|
||||
eventBusService,
|
||||
manager,
|
||||
}: InjectedDependencies) {
|
||||
this.manager_ = manager
|
||||
this.salesChannelLocationService_ = salesChannelLocationService
|
||||
this.eventBusService_ = eventBusService
|
||||
this.inventoryService_ = inventoryService
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the available quantity of an item across all sales channel locations
|
||||
* @param salesChannelId Sales channel id
|
||||
* @param itemId Item id
|
||||
* @returns available quantity of item across all sales channel locations
|
||||
*/
|
||||
async retrieveAvailableItemQuantity(
|
||||
salesChannelId: string,
|
||||
itemId: string
|
||||
): Promise<number> {
|
||||
const locations = await this.salesChannelLocationService_.listLocations(
|
||||
salesChannelId
|
||||
)
|
||||
|
||||
return await this.inventoryService_.retrieveAvailableQuantity(
|
||||
itemId,
|
||||
locations
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SalesChannelInventoryService
|
||||
@@ -0,0 +1,95 @@
|
||||
import { EntityManager } from "typeorm"
|
||||
import { IStockLocationService, TransactionBaseService } from "../interfaces"
|
||||
import { SalesChannelService, EventBusService } from "./"
|
||||
|
||||
import { SalesChannelLocation } from "../models"
|
||||
|
||||
type InjectedDependencies = {
|
||||
stockLocationService: IStockLocationService
|
||||
salesChannelService: SalesChannelService
|
||||
eventBusService: EventBusService
|
||||
manager: EntityManager
|
||||
}
|
||||
|
||||
class SalesChannelLocationService extends TransactionBaseService {
|
||||
protected manager_: EntityManager
|
||||
protected transactionManager_: EntityManager | undefined
|
||||
|
||||
protected readonly salesChannelService_: SalesChannelService
|
||||
protected readonly eventBusService_: EventBusService
|
||||
protected readonly stockLocationService_: IStockLocationService
|
||||
|
||||
constructor({
|
||||
salesChannelService,
|
||||
stockLocationService,
|
||||
eventBusService,
|
||||
manager,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
|
||||
this.manager_ = manager
|
||||
this.salesChannelService_ = salesChannelService
|
||||
this.eventBusService_ = eventBusService
|
||||
this.stockLocationService_ = stockLocationService
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes location from sales channel
|
||||
* @param salesChannelId sales channel id
|
||||
* @param locationId location id
|
||||
*/
|
||||
async removeLocation(
|
||||
salesChannelId: string,
|
||||
locationId: string
|
||||
): Promise<void> {
|
||||
const manager = this.transactionManager_ || this.manager_
|
||||
await manager.delete(SalesChannelLocation, {
|
||||
sales_channel_id: salesChannelId,
|
||||
location_id: locationId,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Links location to sales channel
|
||||
* @param salesChannelId sales channel id
|
||||
* @param locationId location id
|
||||
*/
|
||||
async associateLocation(
|
||||
salesChannelId: string,
|
||||
locationId: string
|
||||
): Promise<void> {
|
||||
const manager = this.transactionManager_ || this.manager_
|
||||
const salesChannel = await this.salesChannelService_
|
||||
.withTransaction(manager)
|
||||
.retrieve(salesChannelId)
|
||||
const stockLocation = await this.stockLocationService_.retrieve(locationId)
|
||||
|
||||
const salesChannelLocation = manager.create(SalesChannelLocation, {
|
||||
sales_channel_id: salesChannel.id,
|
||||
location_id: stockLocation.id,
|
||||
})
|
||||
|
||||
await manager.save(salesChannelLocation)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all locations associated with sales channel by id
|
||||
* @param salesChannelId sales channel id
|
||||
* @returns list of location ids associated with sales channel
|
||||
*/
|
||||
async listLocations(salesChannelId: string): Promise<string[]> {
|
||||
const manager = this.transactionManager_ || this.manager_
|
||||
const salesChannel = await this.salesChannelService_
|
||||
.withTransaction(manager)
|
||||
.retrieve(salesChannelId)
|
||||
|
||||
const locations = await manager.find(SalesChannelLocation, {
|
||||
where: { sales_channel_id: salesChannel.id },
|
||||
})
|
||||
|
||||
return locations.map((l) => l.location_id)
|
||||
}
|
||||
}
|
||||
|
||||
export default SalesChannelLocationService
|
||||
@@ -12,10 +12,10 @@ import {
|
||||
CustomShippingOptionService,
|
||||
EventBusService,
|
||||
FulfillmentService,
|
||||
InventoryService,
|
||||
LineItemService,
|
||||
OrderService,
|
||||
PaymentProviderService,
|
||||
ProductVariantInventoryService,
|
||||
ReturnService,
|
||||
ShippingOptionService,
|
||||
TotalsService,
|
||||
@@ -48,7 +48,7 @@ type InjectedProps = {
|
||||
totalsService: TotalsService
|
||||
eventBusService: EventBusService
|
||||
lineItemService: LineItemService
|
||||
inventoryService: InventoryService
|
||||
productVariantInventoryService: ProductVariantInventoryService
|
||||
fulfillmentService: FulfillmentService
|
||||
shippingOptionService: ShippingOptionService
|
||||
paymentProviderService: PaymentProviderService
|
||||
@@ -83,12 +83,13 @@ class SwapService extends TransactionBaseService {
|
||||
protected readonly returnService_: ReturnService
|
||||
protected readonly totalsService_: TotalsService
|
||||
protected readonly lineItemService_: LineItemService
|
||||
protected readonly inventoryService_: InventoryService
|
||||
protected readonly fulfillmentService_: FulfillmentService
|
||||
protected readonly shippingOptionService_: ShippingOptionService
|
||||
protected readonly paymentProviderService_: PaymentProviderService
|
||||
protected readonly lineItemAdjustmentService_: LineItemAdjustmentService
|
||||
protected readonly customShippingOptionService_: CustomShippingOptionService
|
||||
// eslint-disable-next-line max-len
|
||||
protected readonly productVariantInventoryService_: ProductVariantInventoryService
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
@@ -102,7 +103,7 @@ class SwapService extends TransactionBaseService {
|
||||
shippingOptionService,
|
||||
fulfillmentService,
|
||||
orderService,
|
||||
inventoryService,
|
||||
productVariantInventoryService,
|
||||
customShippingOptionService,
|
||||
lineItemAdjustmentService,
|
||||
}: InjectedProps) {
|
||||
@@ -120,7 +121,7 @@ class SwapService extends TransactionBaseService {
|
||||
this.fulfillmentService_ = fulfillmentService
|
||||
this.orderService_ = orderService
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
this.inventoryService_ = inventoryService
|
||||
this.productVariantInventoryService_ = productVariantInventoryService
|
||||
this.eventBus_ = eventBusService
|
||||
this.customShippingOptionService_ = customShippingOptionService
|
||||
this.lineItemAdjustmentService_ = lineItemAdjustmentService
|
||||
@@ -357,6 +358,12 @@ class SwapService extends TransactionBaseService {
|
||||
if (additionalItems) {
|
||||
newItems = await Promise.all(
|
||||
additionalItems.map(async ({ variant_id, quantity }) => {
|
||||
if (variant_id === null) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"You must include a variant when creating additional items on a swap"
|
||||
)
|
||||
}
|
||||
return this.lineItemService_
|
||||
.withTransaction(manager)
|
||||
.generate(variant_id, order.region_id, quantity)
|
||||
@@ -734,29 +741,6 @@ class SwapService extends TransactionBaseService {
|
||||
|
||||
const items = cart.items
|
||||
|
||||
if (!swap.allow_backorder) {
|
||||
const inventoryServiceTx =
|
||||
this.inventoryService_.withTransaction(manager)
|
||||
const paymentProviderServiceTx =
|
||||
this.paymentProviderService_.withTransaction(manager)
|
||||
const cartServiceTx = this.cartService_.withTransaction(manager)
|
||||
|
||||
for (const item of items) {
|
||||
try {
|
||||
await inventoryServiceTx.confirmInventory(
|
||||
item.variant_id,
|
||||
item.quantity
|
||||
)
|
||||
} catch (err) {
|
||||
if (payment) {
|
||||
await paymentProviderServiceTx.cancelPayment(payment)
|
||||
}
|
||||
await cartServiceTx.update(cart.id, { payment_authorized_at: null })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const total = cart.total!
|
||||
|
||||
if (total > 0) {
|
||||
@@ -790,15 +774,20 @@ class SwapService extends TransactionBaseService {
|
||||
order_id: swap.order_id,
|
||||
})
|
||||
|
||||
const inventoryServiceTx =
|
||||
this.inventoryService_.withTransaction(manager)
|
||||
|
||||
for (const item of items) {
|
||||
await inventoryServiceTx.adjustInventory(
|
||||
item.variant_id,
|
||||
-item.quantity
|
||||
)
|
||||
}
|
||||
await Promise.all(
|
||||
items.map(async (item) => {
|
||||
if (item.variant_id) {
|
||||
await this.productVariantInventoryService_.reserveQuantity(
|
||||
item.variant_id,
|
||||
item.quantity,
|
||||
{
|
||||
lineItemId: item.id,
|
||||
salesChannelId: cart.sales_channel_id,
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
swap.difference_due = total
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MockManager } from "medusa-test-utils"
|
||||
import CartCompletionStrategy from "../cart-completion"
|
||||
import { newTotalsServiceMock } from "../../services/__mocks__/new-totals"
|
||||
import { ProductVariantInventoryServiceMock } from "../../services/__mocks__/product-variant-inventory"
|
||||
|
||||
const IdempotencyKeyServiceMock = {
|
||||
withTransaction: function () {
|
||||
@@ -183,9 +184,11 @@ describe("CartCompletionStrategy", () => {
|
||||
return this
|
||||
},
|
||||
createTaxLines: jest.fn(() => {
|
||||
cart.items[0].tax_lines = [{
|
||||
id: "tax_lines"
|
||||
}]
|
||||
cart.items[0].tax_lines = [
|
||||
{
|
||||
id: "tax_lines",
|
||||
},
|
||||
]
|
||||
return Promise.resolve(cart)
|
||||
}),
|
||||
deleteTaxLines: jest.fn(() => Promise.resolve(cart)),
|
||||
@@ -207,12 +210,16 @@ describe("CartCompletionStrategy", () => {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
retrieveByCartId: jest.fn((id) =>
|
||||
Promise.resolve({ id, allow_backorder: true })
|
||||
),
|
||||
registerCartCompletion: jest.fn(() => Promise.resolve({})),
|
||||
retrieve: jest.fn(() => Promise.resolve({})),
|
||||
}
|
||||
const idempotencyKeyServiceMock = IdempotencyKeyServiceMock
|
||||
|
||||
const completionStrat = new CartCompletionStrategy({
|
||||
productVariantInventoryService: ProductVariantInventoryServiceMock,
|
||||
cartService: cartServiceMock,
|
||||
idempotencyKeyService: idempotencyKeyServiceMock,
|
||||
orderService: orderServiceMock,
|
||||
|
||||
@@ -14,8 +14,14 @@ import {
|
||||
AbstractCartCompletionStrategy,
|
||||
CartCompletionResponse,
|
||||
} from "../interfaces"
|
||||
import {
|
||||
PaymentProviderService,
|
||||
ProductVariantInventoryService,
|
||||
} from "../services"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productVariantInventoryService: ProductVariantInventoryService
|
||||
paymentProviderService: PaymentProviderService
|
||||
idempotencyKeyService: IdempotencyKeyService
|
||||
cartService: CartService
|
||||
orderService: OrderService
|
||||
@@ -26,12 +32,17 @@ type InjectedDependencies = {
|
||||
class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
protected manager_: EntityManager
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
protected readonly productVariantInventoryService_: ProductVariantInventoryService
|
||||
protected readonly paymentProviderService_: PaymentProviderService
|
||||
protected readonly idempotencyKeyService_: IdempotencyKeyService
|
||||
protected readonly cartService_: CartService
|
||||
protected readonly orderService_: OrderService
|
||||
protected readonly swapService_: SwapService
|
||||
|
||||
constructor({
|
||||
productVariantInventoryService,
|
||||
paymentProviderService,
|
||||
idempotencyKeyService,
|
||||
cartService,
|
||||
orderService,
|
||||
@@ -40,6 +51,8 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
}: InjectedDependencies) {
|
||||
super()
|
||||
|
||||
this.paymentProviderService_ = paymentProviderService
|
||||
this.productVariantInventoryService_ = productVariantInventoryService
|
||||
this.idempotencyKeyService_ = idempotencyKeyService
|
||||
this.cartService_ = cartService
|
||||
this.orderService_ = orderService
|
||||
@@ -245,26 +258,90 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
}
|
||||
|
||||
const orderServiceTx = this.orderService_.withTransaction(manager)
|
||||
const swapServiceTx = this.swapService_.withTransaction(manager)
|
||||
const cartServiceTx = this.cartService_.withTransaction(manager)
|
||||
|
||||
const cart = await this.cartService_
|
||||
.withTransaction(manager)
|
||||
.retrieveWithTotals(id, {
|
||||
relations: ["region", "payment", "payment_sessions"],
|
||||
})
|
||||
const cart = await cartServiceTx.retrieveWithTotals(id, {
|
||||
relations: ["region", "payment", "payment_sessions"],
|
||||
})
|
||||
|
||||
let allowBackorder = false
|
||||
let swapId: string
|
||||
|
||||
if (cart.type === "swap") {
|
||||
const swap = await swapServiceTx.retrieveByCartId(id)
|
||||
allowBackorder = swap.allow_backorder
|
||||
swapId = swap.id
|
||||
}
|
||||
|
||||
if (!allowBackorder) {
|
||||
const productVariantInventoryServiceTx =
|
||||
this.productVariantInventoryService_.withTransaction(manager)
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
cart.items.map(async (item) => {
|
||||
if (item.variant_id) {
|
||||
const inventoryConfirmed =
|
||||
await productVariantInventoryServiceTx.confirmInventory(
|
||||
item.variant_id,
|
||||
item.quantity,
|
||||
{ salesChannelId: cart.sales_channel_id }
|
||||
)
|
||||
|
||||
if (!inventoryConfirmed) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Variant with id: ${item.variant_id} does not have the required inventory`,
|
||||
MedusaError.Codes.INSUFFICIENT_INVENTORY
|
||||
)
|
||||
}
|
||||
|
||||
await productVariantInventoryServiceTx.reserveQuantity(
|
||||
item.variant_id,
|
||||
item.quantity,
|
||||
{
|
||||
lineItemId: item.id,
|
||||
salesChannelId: cart.sales_channel_id,
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
if (error && error.code === MedusaError.Codes.INSUFFICIENT_INVENTORY) {
|
||||
if (cart.payment) {
|
||||
await this.paymentProviderService_
|
||||
.withTransaction(manager)
|
||||
.cancelPayment(cart.payment)
|
||||
}
|
||||
await cartServiceTx.update(cart.id, {
|
||||
payment_authorized_at: null,
|
||||
})
|
||||
|
||||
return {
|
||||
response_code: 409,
|
||||
response_body: {
|
||||
message: error.message,
|
||||
type: error.type,
|
||||
code: error.code,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If cart is part of swap, we register swap as complete
|
||||
if (cart.type === "swap") {
|
||||
try {
|
||||
const swapId = cart.metadata?.swap_id
|
||||
let swap = await this.swapService_
|
||||
.withTransaction(manager)
|
||||
.registerCartCompletion(swapId as string)
|
||||
let swap = await swapServiceTx.registerCartCompletion(swapId as string)
|
||||
|
||||
swap = await this.swapService_
|
||||
.withTransaction(manager)
|
||||
.retrieve(swap.id, {
|
||||
relations: ["shipping_address"],
|
||||
})
|
||||
swap = await swapServiceTx.retrieve(swap.id, {
|
||||
relations: ["shipping_address"],
|
||||
})
|
||||
|
||||
return {
|
||||
response_code: 200,
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { NumericalComparisonOperator, StringComparisonOperator } from "./common"
|
||||
|
||||
export type InventoryItemDTO = {
|
||||
id: string
|
||||
sku?: string | null
|
||||
origin_country?: string | null
|
||||
hs_code?: string | null
|
||||
requires_shipping: boolean
|
||||
mid_code?: string | null
|
||||
material?: string | null
|
||||
weight?: number | null
|
||||
length?: number | null
|
||||
height?: number | null
|
||||
width?: number | null
|
||||
metadata: Record<string, unknown> | null
|
||||
created_at: string | Date
|
||||
updated_at: string | Date
|
||||
deleted_at: string | Date | null
|
||||
}
|
||||
|
||||
export type ReservationItemDTO = {
|
||||
id: string
|
||||
location_id: string
|
||||
item_id: string
|
||||
metadata: Record<string, unknown> | null
|
||||
created_at: string | Date
|
||||
updated_at: string | Date
|
||||
deleted_at: string | Date | null
|
||||
}
|
||||
|
||||
export type InventoryLevelDTO = {
|
||||
id: string
|
||||
item_id: string
|
||||
location_id: string
|
||||
stocked_quantity: number
|
||||
incoming_quantity: number
|
||||
metadata: Record<string, unknown> | null
|
||||
created_at: string | Date
|
||||
updated_at: string | Date
|
||||
deleted_at: string | Date | null
|
||||
}
|
||||
|
||||
export type FilterableReservationItemProps = {
|
||||
id?: string | string[]
|
||||
type?: string | string[]
|
||||
line_item_id?: string | string[]
|
||||
item_id?: string | string[]
|
||||
location_id?: string | string[]
|
||||
quantity?: number | NumericalComparisonOperator
|
||||
}
|
||||
|
||||
export type FilterableInventoryItemProps = {
|
||||
id?: string | string[]
|
||||
location_id?: string | string[]
|
||||
q?: string
|
||||
sku?: string | string[] | StringComparisonOperator
|
||||
origin_country?: string | string[]
|
||||
hs_code?: string | string[] | StringComparisonOperator
|
||||
requires_shipping?: boolean
|
||||
}
|
||||
|
||||
export type CreateInventoryItemInput = {
|
||||
sku?: string
|
||||
origin_country?: string
|
||||
mid_code?: string
|
||||
material?: string
|
||||
weight?: number
|
||||
length?: number
|
||||
height?: number
|
||||
width?: number
|
||||
metadata?: Record<string, unknown> | null
|
||||
hs_code?: string
|
||||
requires_shipping?: boolean
|
||||
}
|
||||
|
||||
export type CreateReservationItemInput = {
|
||||
type?: string
|
||||
line_item_id?: string
|
||||
item_id: string
|
||||
location_id: string
|
||||
quantity: number
|
||||
metadata?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export type FilterableInventoryLevelProps = {
|
||||
item_id?: string | string[]
|
||||
location_id?: string | string[]
|
||||
stocked_quantity?: number | NumericalComparisonOperator
|
||||
incoming_quantity?: number | NumericalComparisonOperator
|
||||
}
|
||||
|
||||
export type CreateInventoryLevelInput = {
|
||||
item_id: string
|
||||
location_id: string
|
||||
stocked_quantity: number
|
||||
incoming_quantity: number
|
||||
}
|
||||
|
||||
export type UpdateInventoryLevelInput = {
|
||||
stocked_quantity?: number
|
||||
incoming_quantity?: number
|
||||
}
|
||||
|
||||
export type ReserveQuantityContext = {
|
||||
locationId?: string
|
||||
lineItemId?: string
|
||||
salesChannelId?: string | null
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { StringComparisonOperator } from "./common"
|
||||
|
||||
export type StockLocationAddressDTO = {
|
||||
id?: string
|
||||
address_1: string
|
||||
address_2?: string
|
||||
city?: string
|
||||
country_code?: string
|
||||
phone?: string
|
||||
postal_code?: string
|
||||
province?: string
|
||||
}
|
||||
|
||||
export type StockLocationDTO = {
|
||||
id: string
|
||||
name: string
|
||||
metadata: Record<string, unknown> | null
|
||||
address_id: string
|
||||
created_at: string | Date
|
||||
updated_at: string | Date
|
||||
deleted_at: string | Date | null
|
||||
}
|
||||
|
||||
export type FilterableStockLocationProps = {
|
||||
id?: string | string[]
|
||||
name?: string | string[] | StringComparisonOperator
|
||||
}
|
||||
|
||||
export type StockLocationAddressInput = {
|
||||
address_1: string
|
||||
address_2?: string
|
||||
city?: string
|
||||
country_code?: string
|
||||
phone?: string
|
||||
province?: string
|
||||
postal_code?: string
|
||||
}
|
||||
|
||||
export type CreateStockLocationInput = {
|
||||
name: string
|
||||
address?: string | StockLocationAddressInput
|
||||
}
|
||||
|
||||
export type UpdateStockLocationInput = {
|
||||
name?: string
|
||||
address_id?: string
|
||||
address?: StockLocationAddressInput
|
||||
}
|
||||
@@ -7,3 +7,4 @@ export * from "./is-string"
|
||||
export * from "./calculate-price-tax-amount"
|
||||
export * from "./csv-cell-content-formatter"
|
||||
export * from "./exception-formatter"
|
||||
export * from "./db-aware-column"
|
||||
|
||||
Reference in New Issue
Block a user