refactor(medusa): Cleanup and fix CartService (#1306)

* refactor(medusa): Cleanup + fix

* styles(medusa): Lint

* refactor(medusa): Finalize cleanup

* feat(medusa): Prefer the usage of bulk operations instead of sequential/conccurent operations

* feat(medusa): Improve cart service

* refactor(medusa): Explicitly specifying protected methods when needed as well as enfore the usage of the local transactionManager_ in those methods

* tests(medusa): Fix tests according to the new changes

* feat(medusa): Cleanup after rebase

* test(medusa): Fix cart service tests
This commit is contained in:
Adrien de Peretti
2022-04-13 18:35:13 +02:00
committed by GitHub
parent 3f19a3c4d5
commit 12c06b4c9d
12 changed files with 1142 additions and 995 deletions
@@ -24,7 +24,7 @@ Object {
} }
`; `;
exports[`/store/carts shipping address + region updates updates region only - single to multipe countries 1`] = ` exports[`/store/carts shipping address + region updates updates region only - single to multiple countries 1`] = `
Object { Object {
"address_1": null, "address_1": null,
"address_2": null, "address_2": null,
+11 -13
View File
@@ -924,19 +924,19 @@ describe("/store/carts", () => {
}) })
it("fails on apply discount if limit has been reached", async () => { it("fails on apply discount if limit has been reached", async () => {
expect.assertions(2)
const api = useApi() const api = useApi()
await api const err = await api
.post("/store/carts/test-cart", { .post("/store/carts/test-cart", {
discounts: [{ code: "SPENT" }], discounts: [{ code: "SPENT" }],
}) })
.catch((error) => { .catch((err) => err)
expect(error.response.status).toEqual(400)
expect(error.response.data.message).toEqual( expect(err).toBeTruthy()
"Discount has been used maximum allowed times" expect(err.response.status).toEqual(400)
) expect(err.response.data.message).toEqual(
}) "Discount has been used maximum allowed times"
)
}) })
it("successfully passes customer conditions with `in` operator and applies discount", async () => { it("successfully passes customer conditions with `in` operator and applies discount", async () => {
@@ -1755,7 +1755,7 @@ describe("/store/carts", () => {
}), }),
]) ])
) )
expect(cartWithGiftcard.data.cart.total).toBe(1900) // 1000 (giftcard) + 900 (standard item with 10% discount) expect(cartWithGiftcard.data.cart.total).toBe(2900) // 1000 (giftcard) + 900 (standard item with 10% discount) + 1000 Shipping
expect(cartWithGiftcard.data.cart.discount_total).toBe(100) expect(cartWithGiftcard.data.cart.discount_total).toBe(100)
expect(cartWithGiftcard.status).toEqual(200) expect(cartWithGiftcard.status).toEqual(200)
}) })
@@ -1925,7 +1925,7 @@ describe("/store/carts", () => {
await doAfterEach() await doAfterEach()
}) })
it("updates region only - single to multipe countries", async () => { it("updates region only - single to multiple countries", async () => {
const api = useApi() const api = useApi()
const { data, status } = await api const { data, status } = await api
@@ -1947,7 +1947,7 @@ describe("/store/carts", () => {
}) })
}) })
it("updates region only - single to multipe countries", async () => { it("should reset the shipping_address on null value", async () => {
const api = useApi() const api = useApi()
const { data, status } = await api const { data, status } = await api
@@ -1962,7 +1962,5 @@ describe("/store/carts", () => {
expect(status).toEqual(200) expect(status).toEqual(200)
expect(data.cart.shipping_address).toEqual(null) expect(data.cart.shipping_address).toEqual(null)
}) })
// it("updates cart.customer_id on cart retrieval if cart.customer_id differ from session customer", async () => {})
}) })
}) })
@@ -228,8 +228,7 @@ class BaseService {
} }
try { try {
const result = await this.manager_.transaction((m) => doWork(m)) return await this.manager_.transaction((m) => doWork(m))
return result
} catch (error) { } catch (error) {
if (errorHandler) { if (errorHandler) {
const result = await errorHandler(error) const result = await errorHandler(error)
@@ -90,23 +90,20 @@ export default async (req, res) => {
// Update the cart // Update the cart
const { shipping_address, billing_address, ...rest } = validated const { shipping_address, billing_address, ...rest } = validated
const toUpdate: CartUpdateProps = { const cartDataToUpdate: CartUpdateProps = { ...rest };
...rest,
}
if (typeof shipping_address === "string") { if (typeof shipping_address === "string") {
toUpdate.shipping_address_id = shipping_address cartDataToUpdate.shipping_address_id = shipping_address
} else { } else {
toUpdate.shipping_address = shipping_address cartDataToUpdate.shipping_address = shipping_address
} }
if (typeof billing_address === "string") { if (typeof billing_address === "string") {
toUpdate.billing_address_id = billing_address cartDataToUpdate.billing_address_id = billing_address
} else { } else {
toUpdate.billing_address = billing_address cartDataToUpdate.billing_address = billing_address
} }
await cartService.update(id, toUpdate) await cartService.update(id, cartDataToUpdate)
// If the cart has payment sessions update these // If the cart has payment sessions update these
const updated = await cartService.retrieve(id, { const updated = await cartService.retrieve(id, {
+8 -2
View File
@@ -4,7 +4,9 @@ import { MedusaContainer } from "../types/global"
import DefaultSearchService from "../services/search" import DefaultSearchService from "../services/search"
import { Logger } from "../types/global" import { Logger } from "../types/global"
async function loadProductsIntoSearchEngine(container: MedusaContainer): Promise<void> { async function loadProductsIntoSearchEngine(
container: MedusaContainer
): Promise<void> {
const searchService = container.resolve<DefaultSearchService>("searchService") const searchService = container.resolve<DefaultSearchService>("searchService")
const productService = container.resolve<ProductService>("productService") const productService = container.resolve<ProductService>("productService")
@@ -61,7 +63,11 @@ async function loadProductsIntoSearchEngine(container: MedusaContainer): Promise
} }
} }
export default async ({ container }: { container: MedusaContainer }): Promise<void> => { export default async ({
container,
}: {
container: MedusaContainer
}): Promise<void> => {
const searchService = container.resolve<DefaultSearchService>("searchService") const searchService = container.resolve<DefaultSearchService>("searchService")
const logger = container.resolve<Logger>("logger") const logger = container.resolve<Logger>("logger")
if (searchService.isDefault) { if (searchService.isDefault) {
+2 -2
View File
@@ -5,7 +5,7 @@ import { Cart } from "../models/cart"
@EntityRepository(Cart) @EntityRepository(Cart)
export class CartRepository extends Repository<Cart> { export class CartRepository extends Repository<Cart> {
public async findWithRelations( public async findWithRelations(
relations: Array<keyof Cart> = [], relations: string[] = [],
optionsWithoutRelations: Omit<FindManyOptions<Cart>, "relations"> = {} optionsWithoutRelations: Omit<FindManyOptions<Cart>, "relations"> = {}
): Promise<Cart[]> { ): Promise<Cart[]> {
const entities = await this.find(optionsWithoutRelations) const entities = await this.find(optionsWithoutRelations)
@@ -38,7 +38,7 @@ export class CartRepository extends Repository<Cart> {
} }
public async findOneWithRelations( public async findOneWithRelations(
relations: Array<keyof Cart> = [], relations: string[] = [],
optionsWithoutRelations: Omit<FindManyOptions<Cart>, "relations"> = {} optionsWithoutRelations: Omit<FindManyOptions<Cart>, "relations"> = {}
): Promise<Cart> { ): Promise<Cart> {
// Limit 1 // Limit 1
+29 -11
View File
@@ -193,6 +193,9 @@ describe("CartService", () => {
describe("create", () => { describe("create", () => {
const regionService = { const regionService = {
withTransaction: function() {
return this
},
retrieve: () => { retrieve: () => {
return { return {
id: IdMap.getId("testRegion"), id: IdMap.getId("testRegion"),
@@ -324,7 +327,7 @@ describe("CartService", () => {
} }
const shippingOptionService = { const shippingOptionService = {
deleteShippingMethod: jest.fn(), deleteShippingMethods: jest.fn(),
withTransaction: function() { withTransaction: function() {
return this return this
}, },
@@ -378,6 +381,7 @@ describe("CartService", () => {
totalsService, totalsService,
cartRepository, cartRepository,
lineItemService, lineItemService,
lineItemRepository: MockRepository(),
eventBusService, eventBusService,
shippingOptionService, shippingOptionService,
inventoryService, inventoryService,
@@ -476,7 +480,7 @@ describe("CartService", () => {
await cartService.addLineItem(IdMap.getId("cartWithLine"), lineItem) await cartService.addLineItem(IdMap.getId("cartWithLine"), lineItem)
expect(lineItemService.update).toHaveBeenCalledTimes(2) expect(lineItemService.update).toHaveBeenCalledTimes(1)
expect(lineItemService.update).toHaveBeenCalledWith( expect(lineItemService.update).toHaveBeenCalledWith(
IdMap.getId("merger"), IdMap.getId("merger"),
{ {
@@ -581,7 +585,7 @@ describe("CartService", () => {
}) })
const shippingOptionService = { const shippingOptionService = {
deleteShippingMethod: jest.fn(), deleteShippingMethods: jest.fn(),
withTransaction: function() { withTransaction: function() {
return this return this
}, },
@@ -592,6 +596,7 @@ describe("CartService", () => {
totalsService, totalsService,
cartRepository, cartRepository,
lineItemService, lineItemService,
lineItemRepository: MockRepository(),
shippingOptionService, shippingOptionService,
eventBusService, eventBusService,
lineItemAdjustmentService: LineItemAdjustmentServiceMock, lineItemAdjustmentService: LineItemAdjustmentServiceMock,
@@ -641,15 +646,15 @@ describe("CartService", () => {
IdMap.getId("itemToRemove") IdMap.getId("itemToRemove")
) )
expect(shippingOptionService.deleteShippingMethod).toHaveBeenCalledTimes( expect(shippingOptionService.deleteShippingMethods).toHaveBeenCalledTimes(
1 1
) )
expect(shippingOptionService.deleteShippingMethod).toHaveBeenCalledWith({ expect(shippingOptionService.deleteShippingMethods).toHaveBeenCalledWith([{
id: IdMap.getId("ship-method"), id: IdMap.getId("ship-method"),
shipping_option: { shipping_option: {
profile_id: IdMap.getId("prevPro"), profile_id: IdMap.getId("prevPro"),
}, },
}) }])
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledTimes(1) expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledTimes(1)
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledWith({ expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledWith({
@@ -1095,6 +1100,9 @@ describe("CartService", () => {
countries: [{ iso_2: "us" }], countries: [{ iso_2: "us" }],
}) })
), ),
withTransaction: function() {
return this
},
} }
const cartRepository = MockRepository({ const cartRepository = MockRepository({
findOneWithRelations: () => findOneWithRelations: () =>
@@ -1131,6 +1139,9 @@ describe("CartService", () => {
} }
const priceSelectionStrat = { const priceSelectionStrat = {
withTransaction: function() {
return this
},
calculateVariantPrice: async (variantId, context) => { calculateVariantPrice: async (variantId, context) => {
if (variantId === IdMap.getId("fail")) { if (variantId === IdMap.getId("fail")) {
throw new MedusaError( throw new MedusaError(
@@ -1152,6 +1163,7 @@ describe("CartService", () => {
lineItemService, lineItemService,
productVariantService, productVariantService,
eventBusService, eventBusService,
paymentSessionRepository: MockRepository(),
priceSelectionStrategy: priceSelectionStrat, priceSelectionStrategy: priceSelectionStrat,
}) })
@@ -1192,7 +1204,7 @@ describe("CartService", () => {
shipping_address: { shipping_address: {
country_code: "us", country_code: "us",
}, },
items: [IdMap.getId("testitem"), null], items: [IdMap.getId("testitem")],
payment_session: null, payment_session: null,
payment_sessions: [], payment_sessions: [],
gift_cards: [], gift_cards: [],
@@ -1513,13 +1525,16 @@ describe("CartService", () => {
}, },
}) })
}), }),
deleteShippingMethod: jest.fn(), deleteShippingMethods: jest.fn(),
withTransaction: function() { withTransaction: function() {
return this return this
}, },
} }
const customShippingOptionService = { const customShippingOptionService = {
withTransaction: function() {
return this
},
list: jest.fn().mockImplementation(({ cart_id }) => { list: jest.fn().mockImplementation(({ cart_id }) => {
if (cart_id === IdMap.getId("cart-with-custom-so")) { if (cart_id === IdMap.getId("cart-with-custom-so")) {
return [ return [
@@ -1575,7 +1590,7 @@ describe("CartService", () => {
expect( expect(
shippingOptionService.createShippingMethod shippingOptionService.createShippingMethod
).toHaveBeenCalledWith(IdMap.getId("profile1"), data, { cart: cart2 }) ).toHaveBeenCalledWith(IdMap.getId("profile1"), data, { cart: cart2 })
expect(shippingOptionService.deleteShippingMethod).toHaveBeenCalledWith({ expect(shippingOptionService.deleteShippingMethods).toHaveBeenCalledWith({
id: IdMap.getId("ship1"), id: IdMap.getId("ship1"),
shipping_option: { shipping_option: {
profile_id: IdMap.getId("profile1"), profile_id: IdMap.getId("profile1"),
@@ -1594,7 +1609,7 @@ describe("CartService", () => {
data data
) )
expect(shippingOptionService.deleteShippingMethod).toHaveBeenCalledTimes( expect(shippingOptionService.deleteShippingMethods).toHaveBeenCalledTimes(
0 0
) )
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledTimes( expect(shippingOptionService.createShippingMethod).toHaveBeenCalledTimes(
@@ -1616,7 +1631,7 @@ describe("CartService", () => {
data data
) )
expect(shippingOptionService.deleteShippingMethod).toHaveBeenCalledTimes( expect(shippingOptionService.deleteShippingMethods).toHaveBeenCalledTimes(
0 0
) )
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledTimes( expect(shippingOptionService.createShippingMethod).toHaveBeenCalledTimes(
@@ -1738,6 +1753,9 @@ describe("CartService", () => {
}) })
const discountService = { const discountService = {
withTransaction: function () {
return this
},
retrieveByCode: jest.fn().mockImplementation((code) => { retrieveByCode: jest.fn().mockImplementation((code) => {
if (code === "US10") { if (code === "US10") {
return Promise.resolve({ return Promise.resolve({
@@ -1228,7 +1228,7 @@ describe("OrderService", () => {
.mockImplementation((optionId, data, config) => .mockImplementation((optionId, data, config) =>
Promise.resolve({ shipping_option: { profile_id: optionId } }) Promise.resolve({ shipping_option: { profile_id: optionId } })
), ),
deleteShippingMethod: jest deleteShippingMethods: jest
.fn() .fn()
.mockImplementation(() => Promise.resolve({})), .mockImplementation(() => Promise.resolve({})),
@@ -1276,7 +1276,7 @@ describe("OrderService", () => {
} }
) )
expect(optionService.deleteShippingMethod).not.toHaveBeenCalled() expect(optionService.deleteShippingMethods).not.toHaveBeenCalled()
}) })
it("successfully removes shipping method if same option profile", async () => { it("successfully removes shipping method if same option profile", async () => {
@@ -1305,8 +1305,8 @@ describe("OrderService", () => {
} }
) )
expect(optionService.deleteShippingMethod).toHaveBeenCalledTimes(1) expect(optionService.deleteShippingMethods).toHaveBeenCalledTimes(1)
expect(optionService.deleteShippingMethod).toHaveBeenCalledWith({ expect(optionService.deleteShippingMethods).toHaveBeenCalledWith({
shipping_option: { shipping_option: {
profile_id: IdMap.getId("method1"), profile_id: IdMap.getId("method1"),
}, },
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -873,7 +873,7 @@ class OrderService extends BaseService {
) { ) {
await this.shippingOptionService_ await this.shippingOptionService_
.withTransaction(manager) .withTransaction(manager)
.deleteShippingMethod(sm) .deleteShippingMethods(sm)
} else { } else {
methods.push(sm) methods.push(sm)
} }
@@ -208,12 +208,16 @@ class ShippingOptionService extends BaseService {
/** /**
* Removes a given shipping method * Removes a given shipping method
* @param {ShippingMethod} sm - the shipping method to remove * @param {ShippingMethod | Array<ShippingMethod>} shippingMethods - the shipping method to remove
*/ */
async deleteShippingMethod(sm) { async deleteShippingMethods(shippingMethods) {
if (!Array.isArray(shippingMethods)) {
shippingMethods = [shippingMethods]
}
return this.atomicPhase_(async (manager) => { return this.atomicPhase_(async (manager) => {
const methodRepo = manager.getCustomRepository(this.methodRepository_) const methodRepo = manager.getCustomRepository(this.methodRepository_)
return methodRepo.remove(sm) return methodRepo.remove(shippingMethods)
}) })
} }
+9 -5
View File
@@ -1,10 +1,14 @@
import { asFunction, asValue, AwilixContainer } from "awilix" import { AwilixContainer } from "awilix"
import { Logger as _Logger } from "winston" import { Logger as _Logger } from "winston"
export type ClassConstructor<T> = { export type ClassConstructor<T> = {
new (...args: any[]): T; new (...args: unknown[]): T
}; }
export type MedusaContainer = AwilixContainer & { registerAdd: <T>(name: string, registration: T ) => MedusaContainer } export type MedusaContainer = AwilixContainer & {
registerAdd: <T>(name: string, registration: T) => MedusaContainer
}
export type Logger = _Logger & { progress: (activityId: string, msg: string) => void } export type Logger = _Logger & {
progress: (activityId: string, msg: string) => void
}