diff --git a/integration-tests/api/__tests__/admin/draft-order.js b/integration-tests/api/__tests__/admin/draft-order.js index c010598e82..272635d149 100644 --- a/integration-tests/api/__tests__/admin/draft-order.js +++ b/integration-tests/api/__tests__/admin/draft-order.js @@ -353,6 +353,84 @@ describe("/admin/draft-orders", () => { ) }) + it("creates a draft order with discount and free shipping along the line item", async () => { + const api = useApi() + + const payload = { + email: "oli@test.dk", + shipping_address: "oli-shipping", + discounts: [{ code: "TEST" }, { code: "free-shipping"}], + items: [ + { + variant_id: "test-variant", + quantity: 2, + metadata: {}, + }, + ], + region_id: "test-region", + customer_id: "oli-test", + shipping_methods: [ + { + option_id: "test-option", + }, + ], + } + + const response = await api + .post("/admin/draft-orders", payload, { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + const created = await api + .get(`/admin/draft-orders/${response.data.draft_order.id}`, { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + const draftOrder = created.data.draft_order + const lineItemId = draftOrder.cart.items[0].id + + expect(response.status).toEqual(200) + expect(draftOrder.cart.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + variant_id: "test-variant", + unit_price: 8000, + quantity: 2, + adjustments: expect.arrayContaining([ + expect.objectContaining({ + item_id: lineItemId, + amount: 1600, + description: "discount", + discount_id: "test-discount", + }), + ]), + }), + ]) + ) + + // Check that discounts are applied + expect(draftOrder.cart.discounts).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "TEST", + }), + expect.objectContaining({ + code: "free-shipping", + }) + ]) + ) + }) + it("creates a draft order with created shipping address", async () => { const api = useApi() diff --git a/integration-tests/api/helpers/draft-order-seeder.js b/integration-tests/api/helpers/draft-order-seeder.js index 19b63b0d7c..e865f51eb4 100644 --- a/integration-tests/api/helpers/draft-order-seeder.js +++ b/integration-tests/api/helpers/draft-order-seeder.js @@ -119,7 +119,15 @@ module.exports = async (connection, data = {}) => { type: "percentage", }) - const d = manager.create(Discount, { + await manager.insert(DiscountRule, { + id: "free-shipping-rule", + description: "Free shipping rule", + type: "free_shipping", + value: 100, + allocation: "total", + }) + + const testDiscount = manager.create(Discount, { id: "test-discount", code: "TEST", is_dynamic: false, @@ -127,7 +135,15 @@ module.exports = async (connection, data = {}) => { rule_id: "discount_rule_id", }) - d.regions = [ + const freeShippingDiscount = manager.create(Discount, { + id: "free-shipping-discount", + code: "free-shipping", + is_dynamic: false, + is_disabled: false, + rule_id: "free-shipping-rule", + }) + + testDiscount.regions = [ { id: "test-region", name: "Test Region", @@ -136,7 +152,17 @@ module.exports = async (connection, data = {}) => { }, ] - await manager.save(d) + freeShippingDiscount.regions = [ + { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }, + ] + + await manager.save(testDiscount) + await manager.save(freeShippingDiscount) await manager.query( `UPDATE "country" SET region_id='test-region' WHERE iso_2 = 'us'` diff --git a/packages/medusa/src/api/routes/admin/draft-orders/index.ts b/packages/medusa/src/api/routes/admin/draft-orders/index.ts index 5edffd00b2..d41b187a17 100644 --- a/packages/medusa/src/api/routes/admin/draft-orders/index.ts +++ b/packages/medusa/src/api/routes/admin/draft-orders/index.ts @@ -77,7 +77,7 @@ export const defaultAdminDraftOrdersCartFields: (keyof Cart)[] = [ "total", ] -export const defaultAdminDraftOrdersFields = [ +export const defaultAdminDraftOrdersFields: (keyof DraftOrder)[] = [ "id", "status", "display_id", diff --git a/packages/medusa/src/api/routes/admin/draft-orders/list-draft-orders.ts b/packages/medusa/src/api/routes/admin/draft-orders/list-draft-orders.ts index 808d5ed8d2..0981168086 100644 --- a/packages/medusa/src/api/routes/admin/draft-orders/list-draft-orders.ts +++ b/packages/medusa/src/api/routes/admin/draft-orders/list-draft-orders.ts @@ -7,6 +7,8 @@ import { IsNumber, IsOptional, IsString } from "class-validator" import { validator } from "../../../../utils/validator" import { Type } from "class-transformer" import { DraftOrderListSelector } from "../../../../types/draft-orders" +import { FindConfig } from "../../../../types/common" +import { DraftOrder } from "../../../../models" /** * @oas [get] /draft-orders * operationId: "GetDraftOrders" @@ -38,11 +40,11 @@ export default async (req, res) => { selector.q = validated.q } - const listConfig = { + const listConfig: FindConfig = { select: defaultAdminDraftOrdersFields, relations: defaultAdminDraftOrdersRelations, - skip: validated.offset, - take: validated.limit, + skip: validated.offset ?? 0, + take: validated.limit ?? 50, order: { created_at: "DESC" }, } diff --git a/packages/medusa/src/models/draft-order.ts b/packages/medusa/src/models/draft-order.ts index 00eb84df91..54c44f42b9 100644 --- a/packages/medusa/src/models/draft-order.ts +++ b/packages/medusa/src/models/draft-order.ts @@ -18,7 +18,7 @@ import { Cart } from "./cart" import { Order } from "./order" import { generateEntityId } from "../utils/generate-entity-id" -enum DraftOrderStatus { +export enum DraftOrderStatus { OPEN = "open", COMPLETED = "completed", } diff --git a/packages/medusa/src/services/__tests__/draft-order.js b/packages/medusa/src/services/__tests__/draft-order.js index 0b0bb5471d..1847f5379b 100644 --- a/packages/medusa/src/services/__tests__/draft-order.js +++ b/packages/medusa/src/services/__tests__/draft-order.js @@ -1,4 +1,3 @@ -import _ from "lodash" import { MockRepository, MockManager } from "medusa-test-utils" import { EventBusServiceMock } from "../__mocks__/event-bus" import DraftOrderService from "../draft-order" diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index bd2797fa8d..32774ae0c1 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -15,12 +15,7 @@ import { CartRepository } from "../repositories/cart" import { LineItemRepository } from "../repositories/line-item" import { PaymentSessionRepository } from "../repositories/payment-session" import { ShippingMethodRepository } from "../repositories/shipping-method" -import { - CartCreateProps, - CartUpdateProps, - FilterableCartProps, - LineItemUpdate, -} from "../types/cart" +import { CartCreateProps, CartUpdateProps, FilterableCartProps, LineItemUpdate } from "../types/cart" import { AddressPayload, FindConfig, TotalField } from "../types/common" import { buildQuery, setMetadata, validateId } from "../utils" import CustomShippingOptionService from "./custom-shipping-option" @@ -38,6 +33,7 @@ import RegionService from "./region" import ShippingOptionService from "./shipping-option" import TaxProviderService from "./tax-provider" import TotalsService from "./totals" +import { DiscountRuleType } from "../models" type InjectedDependencies = { manager: EntityManager @@ -1071,7 +1067,7 @@ class CartService extends TransactionBaseService { let sawNotShipping = false const newDiscounts = toParse.map((discountToParse) => { switch (discountToParse.rule?.type) { - case "free_shipping": + case DiscountRuleType.FREE_SHIPPING: if (discountToParse.rule.type === rule.type) { return discount } diff --git a/packages/medusa/src/services/draft-order.js b/packages/medusa/src/services/draft-order.js deleted file mode 100644 index 4da9958b58..0000000000 --- a/packages/medusa/src/services/draft-order.js +++ /dev/null @@ -1,390 +0,0 @@ -import { BaseService } from "medusa-interfaces" -import { MedusaError } from "medusa-core-utils" -import { Brackets } from "typeorm" - -/** - * Handles draft orders - * @implements {BaseService} - */ -class DraftOrderService extends BaseService { - static Events = { - CREATED: "draft_order.created", - UPDATED: "draft_order.updated", - } - - constructor({ - manager, - draftOrderRepository, - paymentRepository, - orderRepository, - eventBusService, - cartService, - lineItemService, - productVariantService, - shippingOptionService, - }) { - super() - - /** @private @const {EntityManager} */ - this.manager_ = manager - - /** @private @const {DraftOrderRepository} */ - this.draftOrderRepository_ = draftOrderRepository - - /** @private @const {PaymentRepository} */ - this.paymentRepository_ = paymentRepository - - /** @private @const {OrderRepository} */ - this.orderRepository_ = orderRepository - - /** @private @const {LineItemService} */ - this.lineItemService_ = lineItemService - - /** @private @const {CartService} */ - this.cartService_ = cartService - - /** @private @const {ProductVariantService} */ - this.productVariantService_ = productVariantService - - /** @private @const {ShippingOptionService} */ - this.shippingOptionService_ = shippingOptionService - - /** @private @const {EventBusService} */ - this.eventBus_ = eventBusService - } - - withTransaction(transactionManager) { - if (!transactionManager) { - return this - } - - const cloned = new DraftOrderService({ - manager: transactionManager, - draftOrderRepository: this.draftOrderRepository_, - paymentRepository: this.paymentRepository_, - orderRepository: this.orderRepository_, - lineItemService: this.lineItemService_, - cartService: this.cartService_, - productVariantService: this.productVariantService_, - shippingOptionService: this.shippingOptionService_, - eventBusService: this.eventBus_, - }) - - cloned.transactionManager_ = transactionManager - - return cloned - } - - /** - * Retrieves a draft order with the given id. - * @param {string} id - id of the draft order to retrieve - * @param {object} config - query object for findOne - * @return {Promise} the draft order - */ - async retrieve(id, config = {}) { - const draftOrderRepo = this.manager_.getCustomRepository( - this.draftOrderRepository_ - ) - - const validatedId = this.validateId_(id) - - const query = this.buildQuery_({ id: validatedId }, config) - - const draftOrder = await draftOrderRepo.findOne(query) - - if (!draftOrder) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Draft order with ${id} was not found` - ) - } - - return draftOrder - } - - /** - * Retrieves a draft order based on its associated cart id - * @param {string} cartId - cart id that the draft orders's cart has - * @param {object} config - query object for findOne - * @return {Promise} the draft order - */ - async retrieveByCartId(cartId, config = {}) { - const draftOrderRepo = this.manager_.getCustomRepository( - this.draftOrderRepository_ - ) - - const query = this.buildQuery_({ cart_id: cartId }, config) - - const draftOrder = await draftOrderRepo.findOne(query) - - if (!draftOrder) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Draft order was not found` - ) - } - - return draftOrder - } - - /** - * Deletes draft order idempotently. - * @param {string} draftOrderId - id of draft order to delete - * @return {Promise} empty promise - */ - async delete(draftOrderId) { - return this.atomicPhase_(async (manager) => { - const draftOrderRepo = manager.getCustomRepository( - this.draftOrderRepository_ - ) - - const draftOrder = await draftOrderRepo.findOne({ - where: { id: draftOrderId }, - }) - - if (!draftOrder) { - return Promise.resolve() - } - - await draftOrderRepo.remove(draftOrder) - - return Promise.resolve() - }) - } - - /** - * Lists draft orders alongside the count - * @param {object} selector - query selector to filter draft orders - * @param {object} config - query config - * @return {Promise} draft orders - */ - async listAndCount( - selector, - config = { skip: 0, take: 50, order: { created_at: "DESC" } } - ) { - const draftOrderRepository = this.manager_.getCustomRepository( - this.draftOrderRepository_ - ) - - let q - if ("q" in selector) { - q = selector.q - delete selector.q - } - - const query = this.buildQuery_(selector, config) - - if (q) { - const where = query.where - - delete where.display_id - - query.join = { - alias: "draft_order", - innerJoin: { - cart: "draft_order.cart", - }, - } - - query.where = (qb) => { - qb.where(where) - - qb.andWhere( - new Brackets((qb) => { - qb.where(`cart.email ILIKE :q`, { - q: `%${q}%`, - }).orWhere(`draft_order.display_id::varchar(255) ILIKE :dId`, { - dId: `${q}`, - }) - }) - ) - } - } - - const [draftOrders, count] = await draftOrderRepository.findAndCount(query) - - return [draftOrders, count] - } - - /** - * Lists draft orders - * @param {Object} selector - query object for find - * @param {Object} config - configurable attributes for find - * @return {Promise} list of draft orders - */ - async list( - selector, - config = { skip: 0, take: 50, order: { created_at: "DESC" } } - ) { - const draftOrderRepo = this.manager_.getCustomRepository( - this.draftOrderRepository_ - ) - - const query = this.buildQuery_(selector, config) - - return draftOrderRepo.find(query) - } - - /** - * Creates a draft order. - * @param {object} data - data to create draft order from - * @return {Promise} the created draft order - */ - async create(data) { - return this.atomicPhase_(async (manager) => { - const draftOrderRepo = manager.getCustomRepository( - this.draftOrderRepository_ - ) - - if (!data.region_id) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `region_id is required to create a draft order` - ) - } - - if (!data.items || !data.items.length) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Items are required to create a draft order` - ) - } - - const { - shipping_methods, - discounts, - no_notification_order, - items, - ...rest - } = data - - if (discounts) { - for (const { code } of discounts) { - rest.discounts = [] - await this.cartService_ - .withTransaction(manager) - .applyDiscount(rest, code) - } - } - - const createdCart = await this.cartService_ - .withTransaction(manager) - .create({ type: "draft_order", ...rest }) - - const draftOrder = draftOrderRepo.create({ - cart_id: createdCart.id, - no_notification_order, - }) - const result = await draftOrderRepo.save(draftOrder) - - await this.eventBus_ - .withTransaction(manager) - .emit(DraftOrderService.Events.CREATED, { - id: result.id, - }) - - for (const item of items) { - if (item.variant_id) { - const line = await this.lineItemService_ - .withTransaction(manager) - .generate(item.variant_id, data.region_id, item.quantity, { - metadata: item?.metadata || {}, - unit_price: item.unit_price, - cart: createdCart, - }) - - await this.lineItemService_.withTransaction(manager).create({ - cart_id: createdCart.id, - ...line, - }) - } else { - let price - if (typeof item.unit_price === `undefined` || item.unit_price < 0) { - price = 0 - } else { - price = item.unit_price - } - - // custom line items can be added to a draft order - await this.lineItemService_.withTransaction(manager).create({ - cart_id: createdCart.id, - has_shipping: true, - title: item.title || "Custom item", - allow_discounts: false, - unit_price: price, - quantity: item.quantity, - }) - } - } - - for (const method of shipping_methods) { - await this.cartService_ - .withTransaction(manager) - .addShippingMethod(createdCart.id, method.option_id, method.data) - } - - return result - }) - } - - /** - * Registers a draft order as completed, when an order has been completed. - * @param {string} doId - id of draft order to complete - * @param {string} orderId - id of order completed from draft order cart - * @return {Promise} the created order - */ - async registerCartCompletion(doId, orderId) { - return this.atomicPhase_(async (manager) => { - const draftOrderRepo = manager.getCustomRepository( - this.draftOrderRepository_ - ) - const draftOrder = await this.retrieve(doId) - - draftOrder.status = "completed" - draftOrder.completed_at = new Date() - draftOrder.order_id = orderId - - await draftOrderRepo.save(draftOrder) - }) - } - - /** - * Updates a draft order with the given data - * @param {String} doId - id of the draft order - * @param {DraftOrder} data - values to update the order with - * @return {Promise} the updated draft order - */ - async update(doId, data) { - return this.atomicPhase_(async (manager) => { - const doRepo = manager.getCustomRepository(this.draftOrderRepository_) - const draftOrder = await this.retrieve(doId) - let touched = false - - if (draftOrder.status === "completed") { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Can't update a draft order which is complete" - ) - } - - if (data.no_notification_order !== undefined) { - touched = true - draftOrder.no_notification_order = data.no_notification_order - } - - if (touched) { - doRepo.save(draftOrder) - - await this.eventBus_ - .withTransaction(manager) - .emit(DraftOrderService.Events.UPDATED, { - id: draftOrder.id, - }) - } - - return draftOrder - }) - } -} - -export default DraftOrderService diff --git a/packages/medusa/src/services/draft-order.ts b/packages/medusa/src/services/draft-order.ts new file mode 100644 index 0000000000..b787135e62 --- /dev/null +++ b/packages/medusa/src/services/draft-order.ts @@ -0,0 +1,432 @@ +import { MedusaError } from "medusa-core-utils" +import { Brackets, EntityManager, FindManyOptions, UpdateResult } from "typeorm" +import { DraftOrderRepository } from "../repositories/draft-order" +import { PaymentRepository } from "../repositories/payment" +import EventBusService from "./event-bus" +import CartService from "./cart" +import LineItemService from "./line-item" +import { OrderRepository } from "../repositories/order" +import ProductVariantService from "./product-variant" +import ShippingOptionService from "./shipping-option" +import { DraftOrder, DraftOrderStatus, Cart, CartType } from "../models" +import { AdminPostDraftOrdersReq } from "../api/routes/admin/draft-orders" +import { TransactionBaseService } from "../interfaces" +import { ExtendedFindConfig, FindConfig } from "../types/common" +import { buildQuery } from "../utils" + +type InjectedDependencies = { + manager: EntityManager + draftOrderRepository: typeof DraftOrderRepository + paymentRepository: typeof PaymentRepository + orderRepository: typeof OrderRepository + eventBusService: EventBusService + cartService: CartService + lineItemService: LineItemService + productVariantService: ProductVariantService + shippingOptionService: ShippingOptionService +} + +/** + * Handles draft orders + * @implements {BaseService} + */ +class DraftOrderService extends TransactionBaseService { + static readonly Events = { + CREATED: "draft_order.created", + UPDATED: "draft_order.updated", + } + + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + + protected readonly draftOrderRepository_: typeof DraftOrderRepository + protected readonly paymentRepository_: typeof PaymentRepository + protected readonly orderRepository_: typeof OrderRepository + protected readonly eventBus_: EventBusService + protected readonly cartService_: CartService + protected readonly lineItemService_: LineItemService + protected readonly productVariantService_: ProductVariantService + protected readonly shippingOptionService_: ShippingOptionService + + constructor({ + manager, + draftOrderRepository, + paymentRepository, + orderRepository, + eventBusService, + cartService, + lineItemService, + productVariantService, + shippingOptionService, + }: InjectedDependencies) { + super({ + manager, + draftOrderRepository, + paymentRepository, + orderRepository, + eventBusService, + cartService, + lineItemService, + productVariantService, + shippingOptionService, + }) + + this.manager_ = manager + this.draftOrderRepository_ = draftOrderRepository + this.paymentRepository_ = paymentRepository + this.orderRepository_ = orderRepository + this.lineItemService_ = lineItemService + this.cartService_ = cartService + this.productVariantService_ = productVariantService + this.shippingOptionService_ = shippingOptionService + this.eventBus_ = eventBusService + } + + /** + * Retrieves a draft order with the given id. + * @param id - id of the draft order to retrieve + * @param config - query object for findOne + * @return the draft order + */ + async retrieve( + id: string, + config: FindConfig = {} + ): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const draftOrderRepo = transactionManager.getCustomRepository( + this.draftOrderRepository_ + ) + + const query = buildQuery({ id }, config) + const draftOrder = await draftOrderRepo.findOne(query) + if (!draftOrder) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Draft order with ${id} was not found` + ) + } + + return draftOrder + } + ) + } + + /** + * Retrieves a draft order based on its associated cart id + * @param cartId - cart id that the draft orders's cart has + * @param config - query object for findOne + * @return the draft order + */ + async retrieveByCartId( + cartId: string, + config: FindConfig = {} + ): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const draftOrderRepo = transactionManager.getCustomRepository( + this.draftOrderRepository_ + ) + + const query = buildQuery({ cart_id: cartId }, config) + const draftOrder = await draftOrderRepo.findOne(query) + if (!draftOrder) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Draft order was not found` + ) + } + + return draftOrder + } + ) + } + + /** + * Deletes draft order idempotently. + * @param {string} draftOrderId - id of draft order to delete + * @return {Promise} empty promise + */ + async delete(draftOrderId: string): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const draftOrderRepo = transactionManager.getCustomRepository( + this.draftOrderRepository_ + ) + const draftOrder = await draftOrderRepo.findOne({ + where: { id: draftOrderId }, + }) + + if (!draftOrder) { + return + } + return await draftOrderRepo.remove(draftOrder) + } + ) + } + + /** + * Lists draft orders alongside the count + * @param selector - query selector to filter draft orders + * @param config - query config + * @return draft orders + */ + async listAndCount( + selector, + config: FindConfig = { + skip: 0, + take: 50, + order: { created_at: "DESC" }, + } + ): Promise<[DraftOrder[], number]> { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const draftOrderRepository = transactionManager.getCustomRepository( + this.draftOrderRepository_ + ) + + const { q, ...restSelector } = selector + const query = buildQuery( + restSelector, + config + ) as FindManyOptions & ExtendedFindConfig + + if (q) { + const where = query.where + delete where?.display_id + + query.join = { + alias: "draft_order", + innerJoin: { + cart: "draft_order.cart", + }, + } + + query.where = (qb): void => { + qb.where(where) + + qb.andWhere( + new Brackets((qb) => { + qb.where(`cart.email ILIKE :q`, { + q: `%${q}%`, + }).orWhere(`draft_order.display_id::TEXT ILIKE :displayId`, { + displayId: `${q}`, + }) + }) + ) + } + } + + return await draftOrderRepository.findAndCount(query) + } + ) + } + + /** + * Lists draft orders + * @param selector - query object for find + * @param config - configurable attributes for find + * @return list of draft orders + */ + async list( + selector, + config: FindConfig = { + skip: 0, + take: 50, + order: { created_at: "DESC" }, + } + ): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const draftOrderRepo = transactionManager.getCustomRepository( + this.draftOrderRepository_ + ) + + const query = buildQuery(selector, config) + + return await draftOrderRepo.find(query) + } + ) + } + + /** + * Creates a draft order. + * @param data - data to create draft order from + * @return the created draft order + */ + async create(data: AdminPostDraftOrdersReq): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const draftOrderRepo = transactionManager.getCustomRepository( + this.draftOrderRepository_ + ) + + if (!data.region_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `region_id is required to create a draft order` + ) + } + + if (!data.items || !data.items.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Items are required to create a draft order` + ) + } + + const { shipping_methods, no_notification_order, items, ...rawCart } = + data + + if (rawCart.discounts) { + const { discounts } = rawCart + rawCart.discounts = [] + + for (const { code } of discounts) { + await this.cartService_ + .withTransaction(transactionManager) + .applyDiscount(rawCart as Cart, code) + } + } + + const createdCart = await this.cartService_ + .withTransaction(transactionManager) + .create({ type: CartType.DRAFT_ORDER, ...rawCart }) + + const draftOrder = draftOrderRepo.create({ + cart_id: createdCart.id, + no_notification_order, + }) + const result = await draftOrderRepo.save(draftOrder) + + await this.eventBus_ + .withTransaction(transactionManager) + .emit(DraftOrderService.Events.CREATED, { + id: result.id, + }) + + for (const item of items) { + if (item.variant_id) { + const line = await this.lineItemService_ + .withTransaction(transactionManager) + .generate(item.variant_id, data.region_id, item.quantity, { + metadata: item?.metadata || {}, + unit_price: item.unit_price, + cart: createdCart, + }) + + await this.lineItemService_ + .withTransaction(transactionManager) + .create({ + ...line, + cart_id: createdCart.id, + }) + } else { + let price + if (typeof item.unit_price === `undefined` || item.unit_price < 0) { + price = 0 + } else { + price = item.unit_price + } + + // custom line items can be added to a draft order + await this.lineItemService_ + .withTransaction(transactionManager) + .create({ + cart_id: createdCart.id, + has_shipping: true, + title: item.title || "Custom item", + allow_discounts: false, + unit_price: price, + quantity: item.quantity, + }) + } + } + + for (const method of shipping_methods) { + await this.cartService_ + .withTransaction(transactionManager) + .addShippingMethod(createdCart.id, method.option_id, method.data) + } + + return result + } + ) + } + + /** + * Registers a draft order as completed, when an order has been completed. + * @param draftOrderId - id of draft order to complete + * @param orderId - id of order completed from draft order cart + * @return the created order + */ + async registerCartCompletion( + draftOrderId: string, + orderId: string + ): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const draftOrderRepo = transactionManager.getCustomRepository( + this.draftOrderRepository_ + ) + return await draftOrderRepo.update( + { + id: draftOrderId, + }, + { + status: DraftOrderStatus.COMPLETED, + completed_at: new Date(), + order_id: orderId, + } + ) + } + ) + } + + /** + * Updates a draft order with the given data + * @param id - id of the draft order + * @param data - values to update the order with + * @return the updated draft order + */ + async update( + id: string, + data: { no_notification_order: boolean } + ): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const draftOrderRepo = transactionManager.getCustomRepository( + this.draftOrderRepository_ + ) + const draftOrder = await this.retrieve(id) + + if (draftOrder.status === DraftOrderStatus.COMPLETED) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Can't update a draft order which is complete" + ) + } + + let touched = false + if (data.no_notification_order !== undefined) { + touched = true + draftOrder.no_notification_order = data.no_notification_order + } + + if (touched) { + await draftOrderRepo.save(draftOrder) + + await this.eventBus_ + .withTransaction(transactionManager) + .emit(DraftOrderService.Events.UPDATED, { + id: draftOrder.id, + }) + } + + return draftOrder + } + ) + } +} + +export default DraftOrderService diff --git a/packages/medusa/tsconfig.json b/packages/medusa/tsconfig.json index d81ce7302b..6d92a61c7a 100644 --- a/packages/medusa/tsconfig.json +++ b/packages/medusa/tsconfig.json @@ -1,6 +1,9 @@ { "compilerOptions": { - "lib": ["es5", "es6"], + "lib": [ + "es5", + "es6" + ], "target": "es5", "outDir": "./dist", "esModuleInterop": true, @@ -18,7 +21,10 @@ "skipLibCheck": true, "downlevelIteration": true // to use ES5 specific tooling }, - "include": ["./src/**/*", "index.d.ts"], + "include": [ + "./src/**/*", + "index.d.ts" + ], "exclude": [ "./dist/**/*", "./src/**/__tests__",