From 530bbd4cac7e498ad6a876c15c1cd6a8dfb78031 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Thu, 14 Apr 2022 20:53:35 +0200 Subject: [PATCH] refactor: LineItemService migration to TS + refactoring + fix (#1254) * refactor(medusa): LineItemService migration to TS + refactoring * feat(medusa): Cleanup line-item service * feat(medusa): Rebase develop * test(medusa): Fix integration cart tests * fix(medusa): Cart service updateUnitPrices --- integration-tests/api/__tests__/store/cart.js | 2 +- .../routes/store/carts/create-line-item.ts | 2 +- packages/medusa/src/services/cart.ts | 4 +- packages/medusa/src/services/line-item.ts | 322 ++++++++++++++++++ 4 files changed, 326 insertions(+), 4 deletions(-) create mode 100644 packages/medusa/src/services/line-item.ts diff --git a/integration-tests/api/__tests__/store/cart.js b/integration-tests/api/__tests__/store/cart.js index 5bcb854bd2..4edc9a8baa 100644 --- a/integration-tests/api/__tests__/store/cart.js +++ b/integration-tests/api/__tests__/store/cart.js @@ -1739,7 +1739,7 @@ describe("/store/carts", () => { .catch((err) => console.log(err)) // Ensure that the discount is only applied to the standard item - const itemId = cartWithGiftcard.data.cart.items[0].id + const itemId = cartWithGiftcard.data.cart.items.find(item => !item.is_giftcard).id expect(cartWithGiftcard.data.cart.items).toEqual( expect.arrayContaining([ expect.objectContaining({ diff --git a/packages/medusa/src/api/routes/store/carts/create-line-item.ts b/packages/medusa/src/api/routes/store/carts/create-line-item.ts index 0f6966bff0..8153eef1a2 100644 --- a/packages/medusa/src/api/routes/store/carts/create-line-item.ts +++ b/packages/medusa/src/api/routes/store/carts/create-line-item.ts @@ -74,5 +74,5 @@ export class StorePostCartsCartLineItemsReq { quantity: number @IsOptional() - metadata?: object + metadata?: Record | undefined } diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index bcdde39bae..6e5d0c4284 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -1717,11 +1717,11 @@ class CartService extends BaseService { await this.lineItemService_ .withTransaction(this.transactionManager_) .delete(item.id) - return null + return } }) ) - ).filter((item): item is LineItem => item) + ).filter((item): item is LineItem => !!item) } } diff --git a/packages/medusa/src/services/line-item.ts b/packages/medusa/src/services/line-item.ts new file mode 100644 index 0000000000..763292a8a6 --- /dev/null +++ b/packages/medusa/src/services/line-item.ts @@ -0,0 +1,322 @@ +import { MedusaError } from "medusa-core-utils" +import { BaseService } from "medusa-interfaces" +import { EntityManager } from "typeorm" +import { LineItemRepository } from "../repositories/line-item" +import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line" +import { ProductService, RegionService, ProductVariantService } from "./index" +import { CartRepository } from "../repositories/cart" +import { LineItem } from "../models/line-item" +import LineItemAdjustmentService from "./line-item-adjustment" +import { Cart } from "../models/cart" +import { LineItemAdjustment } from "../models/line-item-adjustment" + +type InjectedDependencies = { + manager: EntityManager + lineItemRepository: typeof LineItemRepository + lineItemTaxLineRepository: typeof LineItemTaxLineRepository + cartRepository: typeof CartRepository + productVariantService: ProductVariantService + productService: ProductService + regionService: RegionService + lineItemAdjustmentService: LineItemAdjustmentService +} + +/** + * Provides layer to manipulate line items. + * @extends BaseService + */ +class LineItemService extends BaseService { + protected readonly manager_: EntityManager + protected readonly lineItemRepository_: typeof LineItemRepository + protected readonly itemTaxLineRepo_: typeof LineItemTaxLineRepository + protected readonly cartRepository_: typeof CartRepository + protected readonly productVariantService_: ProductVariantService + protected readonly productService_: ProductService + protected readonly regionService_: RegionService + protected readonly lineItemAdjustmentService_: LineItemAdjustmentService + + constructor({ + manager, + lineItemRepository, + lineItemTaxLineRepository, + productVariantService, + productService, + regionService, + cartRepository, + lineItemAdjustmentService + }: InjectedDependencies) { + super() + + this.manager_ = manager + this.lineItemRepository_ = lineItemRepository + this.itemTaxLineRepo_ = lineItemTaxLineRepository + this.productVariantService_ = productVariantService + this.productService_ = productService + this.regionService_ = regionService + this.cartRepository_ = cartRepository + this.lineItemAdjustmentService_ = lineItemAdjustmentService + } + + withTransaction(transactionManager: EntityManager): LineItemService { + if (!transactionManager) { + return this + } + + const cloned = new LineItemService({ + manager: transactionManager, + lineItemRepository: this.lineItemRepository_, + lineItemTaxLineRepository: this.itemTaxLineRepo_, + productVariantService: this.productVariantService_, + productService: this.productService_, + regionService: this.regionService_, + cartRepository: this.cartRepository_, + lineItemAdjustmentService: this.lineItemAdjustmentService_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned + } + + async list( + selector, + config = { skip: 0, take: 50, order: { created_at: "DESC" } } + ): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const lineItemRepo = transactionManager.getCustomRepository( + this.lineItemRepository_ + ) + const query = this.buildQuery_(selector, config) + return await lineItemRepo.find(query) + } + ) + } + + /** + * Retrieves a line item by its id. + * @param {string} id - the id of the line item to retrieve + * @param {object} config - the config to be used at query building + * @return {Promise} the line item + */ + async retrieve(id: string, config = {}): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const lineItemRepository = transactionManager.getCustomRepository( + this.lineItemRepository_ + ) + + const validatedId = this.validateId_(id) + const query = this.buildQuery_({ id: validatedId }, config) + + const lineItem = await lineItemRepository.findOne(query) + + if (!lineItem) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Line item with ${id} was not found` + ) + } + + return lineItem + } + ) + } + + /** + * Creates return line items for a given cart based on the return items in a + * return. + * @param {string} returnId - the id to generate return items from. + * @param {string} cartId - the cart to assign the return line items to. + * @return {Promise} the created line items + */ + async createReturnLines( + returnId: string, + cartId: string + ): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const lineItemRepo = transactionManager.getCustomRepository( + this.lineItemRepository_ + ) + + const itemTaxLineRepo = transactionManager.getCustomRepository( + this.itemTaxLineRepo_ + ) + + const returnLineItems = await lineItemRepo + .findByReturn(returnId) + .then((lineItems) => { + return lineItems.map((lineItem) => + lineItemRepo.create({ + cart_id: cartId, + thumbnail: lineItem.thumbnail, + is_return: true, + title: lineItem.title, + variant_id: lineItem.variant_id, + unit_price: -1 * lineItem.unit_price, + quantity: lineItem.return_item.quantity, + allow_discounts: lineItem.allow_discounts, + tax_lines: lineItem.tax_lines.map((taxLine) => { + return itemTaxLineRepo.create({ + name: taxLine.name, + code: taxLine.code, + rate: taxLine.rate, + metadata: taxLine.metadata, + }) + }), + metadata: lineItem.metadata, + adjustments: lineItem.adjustments.map((adjustment) => { + return { + amount: -1 * adjustment.amount, + description: adjustment.description, + discount_id: adjustment.discount_id, + metadata: adjustment.metadata, + } + }), + }) + ) + }) + + return await lineItemRepo.save(returnLineItems) + } + ) + } + + async generate( + variantId: string, + regionId: string, + quantity: number, + context: { + unit_price?: number + metadata?: Record + customer_id?: string + cart?: Cart + } = {} + ): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const [variant, region] = await Promise.all([ + this.productVariantService_ + .withTransaction(transactionManager) + .retrieve(variantId, { + relations: ["product"], + include_discount_prices: true, + }), + this.regionService_ + .withTransaction(transactionManager) + .retrieve(regionId), + ]) + + let unit_price = Number(context.unit_price) < 0 ? 0 : context.unit_price + let shouldMerge = false + + if (context.unit_price === undefined || context.unit_price === null) { + shouldMerge = true + unit_price = await this.productVariantService_ + .withTransaction(transactionManager) + .getRegionPrice(variant.id, { + regionId: region.id, + quantity: quantity, + customer_id: context?.customer_id, + include_discount_prices: true, + }) + } + + const rawLineItem: Partial = { + unit_price: unit_price as number, + title: variant.product.title, + description: variant.title, + thumbnail: variant.product.thumbnail, + variant_id: variant.id, + quantity: quantity || 1, + allow_discounts: variant.product.discountable, + is_giftcard: variant.product.is_giftcard, + metadata: context?.metadata || {}, + should_merge: shouldMerge + } + + const lineLitemRepo = transactionManager.getCustomRepository(this.lineItemRepository_) + const lineItem = lineLitemRepo.create(rawLineItem) + + if (context.cart) { + const adjustments = await this.lineItemAdjustmentService_ + .withTransaction(transactionManager) + .generateAdjustments(context.cart, lineItem, { variant }) + lineItem.adjustments = adjustments as unknown as LineItemAdjustment[] + } + + return lineItem + } + ) + } + + /** + * Create a line item + * @param {Partial} data - the line item object to create + * @return {Promise} the created line item + */ + async create(data: Partial): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const lineItemRepository = transactionManager.getCustomRepository( + this.lineItemRepository_ + ) + + const lineItem = lineItemRepository.create(data) + return await lineItemRepository.save(lineItem) + } + ) + } + + /** + * Updates a line item + * @param {string} id - the id of the line item to update + * @param {Partial} data - the properties to update on line item + * @return {Promise} the update line item + */ + async update(id: string, data: Partial): Promise { + const { metadata, ...rest } = data + + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const lineItemRepository = transactionManager.getCustomRepository( + this.lineItemRepository_ + ) + + const lineItem = await this.retrieve(id).then((lineItem) => { + const lineItemMetadata = metadata + ? this.setMetadata_(lineItem, metadata) + : lineItem.metadata + + return Object.assign(lineItem, { + ...rest, + metadata: lineItemMetadata, + }) + }) + return await lineItemRepository.save(lineItem) + } + ) + } + + /** + * Deletes a line item. + * @param {string} id - the id of the line item to delete + * @return {Promise} the result of the delete operation + */ + async delete(id: string): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const lineItemRepository = transactionManager.getCustomRepository( + this.lineItemRepository_ + ) + + return await lineItemRepository + .findOne({ where: { id } }) + .then((lineItem) => lineItem && lineItemRepository.remove(lineItem)) + } + ) + } +} + +export default LineItemService