diff --git a/packages/medusa/src/services/__mocks__/customer.js b/packages/medusa/src/services/__mocks__/customer.js index d324cb8968..067c0d97dc 100644 --- a/packages/medusa/src/services/__mocks__/customer.js +++ b/packages/medusa/src/services/__mocks__/customer.js @@ -2,6 +2,9 @@ import { IdMap } from "medusa-test-utils" import Scrypt from "scrypt-kdf" export const CustomerServiceMock = { + withTransaction: function () { + return this + }, create: jest.fn().mockImplementation((data) => { return Promise.resolve({ ...data, id: IdMap.getId("lebron") }) }), diff --git a/packages/medusa/src/services/__mocks__/user.js b/packages/medusa/src/services/__mocks__/user.js index 968e714551..192fb3d9fb 100644 --- a/packages/medusa/src/services/__mocks__/user.js +++ b/packages/medusa/src/services/__mocks__/user.js @@ -26,6 +26,9 @@ export const users = { } export const UserServiceMock = { + withTransaction: function () { + return this + }, create: jest.fn().mockImplementation(data => { if (data.email === "oliver@test.dk") { return Promise.resolve(users.testUser) diff --git a/packages/medusa/src/services/__tests__/auth.js b/packages/medusa/src/services/__tests__/auth.js index a6d1b6f212..5b39e33a37 100644 --- a/packages/medusa/src/services/__tests__/auth.js +++ b/packages/medusa/src/services/__tests__/auth.js @@ -1,11 +1,18 @@ import AuthService from "../auth" +import { MockManager } from "medusa-test-utils" import { users, UserServiceMock } from "../__mocks__/user" -import { customers, CustomerServiceMock } from "../__mocks__/customer" +import { CustomerServiceMock } from "../__mocks__/customer" + +const managerMock = MockManager describe("AuthService", () => { + const authService = new AuthService({ + manager: managerMock, + userService: UserServiceMock, + customerService: CustomerServiceMock + }) + describe("authenticate", () => { - let authService - authService = new AuthService({ userService: UserServiceMock }) beforeEach(() => { jest.clearAllMocks() }) @@ -33,8 +40,6 @@ describe("AuthService", () => { }) describe("authenticateCustomer", () => { - let authService - authService = new AuthService({ customerService: CustomerServiceMock }) beforeEach(() => { jest.clearAllMocks() }) @@ -62,8 +67,6 @@ describe("AuthService", () => { }) describe("authenticateAPIToken", () => { - let authService - authService = new AuthService({ userService: UserServiceMock }) beforeEach(() => { jest.clearAllMocks() }) diff --git a/packages/medusa/src/services/__tests__/discount.js b/packages/medusa/src/services/__tests__/discount.js index 733439d430..e598c5d5ad 100644 --- a/packages/medusa/src/services/__tests__/discount.js +++ b/packages/medusa/src/services/__tests__/discount.js @@ -761,7 +761,9 @@ describe("DiscountService", () => { let discountService beforeEach(async () => { - discountService = new DiscountService({}) + discountService = new DiscountService({ + manager: MockManager + }) const hasReachedLimitMock = jest.fn().mockImplementation(() => false) const isDisabledMock = jest.fn().mockImplementation(() => false) const isValidForRegionMock = jest @@ -1064,7 +1066,9 @@ describe("DiscountService", () => { } }) - const discountService = new DiscountService({}) + const discountService = new DiscountService({ + manager: MockManager + }) discountService.retrieve = retrieveMock beforeEach(() => { diff --git a/packages/medusa/src/services/auth.ts b/packages/medusa/src/services/auth.ts index c573edb378..492ef5d18a 100644 --- a/packages/medusa/src/services/auth.ts +++ b/packages/medusa/src/services/auth.ts @@ -1,21 +1,32 @@ import Scrypt from "scrypt-kdf" -import { BaseService } from "medusa-interfaces" import { AuthenticateResult } from "../types/auth" -import { User } from "../models/user" -import { Customer } from "../models/customer" +import { User, Customer } from "../models" +import { TransactionBaseService } from "../interfaces" +import UserService from "./user" +import CustomerService from "./customer" +import { EntityManager } from "typeorm" + +type InjectedDependencies = { + manager: EntityManager + userService: UserService + customerService: CustomerService +} /** * Can authenticate a user based on email password combination * @extends BaseService */ -class AuthService extends BaseService { - constructor({ userService, customerService }) { - super() +class AuthService extends TransactionBaseService { + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + protected readonly userService_: UserService + protected readonly customerService_: CustomerService - /** @private @const {UserService} */ + constructor({ manager, userService, customerService }: InjectedDependencies) { + super({ manager, userService, customerService }) + + this.manager_ = manager this.userService_ = userService - - /** @private @const {CustomerService} */ this.customerService_ = customerService } @@ -25,7 +36,10 @@ class AuthService extends BaseService { * @param {string} hash - the hash to compare against * @return {bool} the result of the comparison */ - async comparePassword_(password: string, hash: string): Promise { + protected async comparePassword_( + password: string, + hash: string + ): Promise { const buf = Buffer.from(hash, "base64") return Scrypt.verify(buf, password) } @@ -39,30 +53,36 @@ class AuthService extends BaseService { * error: a string with the error message */ async authenticateAPIToken(token: string): Promise { - if (process.env.NODE_ENV === "development") { + return await this.atomicPhase_(async (transactionManager) => { + if (process.env.NODE_ENV?.startsWith("dev")) { + try { + const user: User = await this.userService_ + .withTransaction(transactionManager) + .retrieve(token) + return { + success: true, + user, + } + } catch (error) { + // ignore + } + } + try { - const user: User = await this.userService_.retrieve(token) + const user: User = await this.userService_ + .withTransaction(transactionManager) + .retrieveByApiToken(token) return { success: true, user, } } catch (error) { - // ignore + return { + success: false, + error: "Invalid API Token", + } } - } - - try { - const user: User = await this.userService_.retrieveByApiToken(token) - return { - success: true, - user, - } - } catch (error) { - return { - success: false, - error: "Invalid API Token", - } - } + }) } /** @@ -79,35 +99,39 @@ class AuthService extends BaseService { email: string, password: string ): Promise { - try { - const userPasswordHash: User = await this.userService_.retrieveByEmail( - email, - { - select: ["password_hash"], - } - ) + return await this.atomicPhase_(async (transactionManager) => { + try { + const userPasswordHash: User = await this.userService_ + .withTransaction(transactionManager) + .retrieveByEmail(email, { + select: ["password_hash"], + }) - const passwordsMatch = await this.comparePassword_( - password, - userPasswordHash.password_hash - ) + const passwordsMatch = await this.comparePassword_( + password, + userPasswordHash.password_hash + ) - if (passwordsMatch) { - const user = await this.userService_.retrieveByEmail(email) + if (passwordsMatch) { + const user = await this.userService_ + .withTransaction(transactionManager) + .retrieveByEmail(email) - return { - success: true, - user: user, + return { + success: true, + user: user, + } } + } catch (error) { + console.log("error ->", error) + // ignore } - } catch (error) { - // ignore - } - return { - success: false, - error: "Invalid email or password", - } + return { + success: false, + error: "Invalid email or password", + } + }) } /** @@ -124,32 +148,39 @@ class AuthService extends BaseService { email: string, password: string ): Promise { - try { - const customerPasswordHash: Customer = - await this.customerService_.retrieveByEmail(email, { - select: ["password_hash"], - }) - if (customerPasswordHash.password_hash) { - const passwordsMatch = await this.comparePassword_( - password, - customerPasswordHash.password_hash - ) + return await this.atomicPhase_(async (transactionManager) => { + try { + const customerPasswordHash: Customer = await this.customerService_ + .withTransaction(transactionManager) + .retrieveByEmail(email, { + select: ["password_hash"], + }) + if (customerPasswordHash.password_hash) { + const passwordsMatch = await this.comparePassword_( + password, + customerPasswordHash.password_hash + ) - if (passwordsMatch) { - const customer = await this.customerService_.retrieveByEmail(email) - return { - success: true, - customer, + if (passwordsMatch) { + const customer = await this.customerService_ + .withTransaction(transactionManager) + .retrieveByEmail(email) + + return { + success: true, + customer, + } } } + } catch (error) { + // ignore } - } catch (error) { - // ignore - } - return { - success: false, - error: "Invalid email or password", - } + + return { + success: false, + error: "Invalid email or password", + } + }) } } diff --git a/packages/medusa/src/services/discount-condition.ts b/packages/medusa/src/services/discount-condition.ts index 49d2eab603..b6217e892b 100644 --- a/packages/medusa/src/services/discount-condition.ts +++ b/packages/medusa/src/services/discount-condition.ts @@ -1,47 +1,51 @@ import { MedusaError } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" import { EntityManager } from "typeorm" import { EventBusService } from "." -import { DiscountCondition, DiscountConditionType } from "../models" +import { + DiscountCondition, + DiscountConditionCustomerGroup, + DiscountConditionProduct, + DiscountConditionProductCollection, + DiscountConditionProductTag, + DiscountConditionProductType, + DiscountConditionType, +} from "../models" import { DiscountConditionRepository } from "../repositories/discount-condition" import { FindConfig } from "../types/common" import { UpsertDiscountConditionInput } from "../types/discount" import { PostgresError } from "../utils/exception-formatter" +import { TransactionBaseService } from "../interfaces" +import { buildQuery } from "../utils" + +type InjectedDependencies = { + manager: EntityManager + discountConditionRepository: typeof DiscountConditionRepository + eventBusService: EventBusService +} /** * Provides layer to manipulate discount conditions. * @implements {BaseService} */ -class DiscountConditionService extends BaseService { - protected readonly manager_: EntityManager +class DiscountConditionService extends TransactionBaseService { protected readonly discountConditionRepository_: typeof DiscountConditionRepository protected readonly eventBus_: EventBusService - protected transactionManager_?: EntityManager - constructor({ manager, discountConditionRepository, eventBusService }) { - super() + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + + constructor({ + manager, + discountConditionRepository, + eventBusService, + }: InjectedDependencies) { + super({ manager, discountConditionRepository, eventBusService }) this.manager_ = manager this.discountConditionRepository_ = discountConditionRepository this.eventBus_ = eventBusService } - withTransaction(transactionManager: EntityManager): DiscountConditionService { - if (!transactionManager) { - return this - } - - const cloned = new DiscountConditionService({ - manager: transactionManager, - discountConditionRepository: this.discountConditionRepository_, - eventBusService: this.eventBus_, - }) - - cloned.transactionManager_ = transactionManager - - return cloned - } - async retrieve( conditionId: string, config?: FindConfig @@ -51,7 +55,7 @@ class DiscountConditionService extends BaseService { this.discountConditionRepository_ ) - const query = this.buildQuery_({ id: conditionId }, config) + const query = buildQuery({ id: conditionId }, config) const condition = await conditionRepo.findOne(query) @@ -103,7 +107,17 @@ class DiscountConditionService extends BaseService { } } - async upsertCondition(data: UpsertDiscountConditionInput): Promise { + async upsertCondition( + data: UpsertDiscountConditionInput + ): Promise< + ( + | DiscountConditionProduct + | DiscountConditionProductType + | DiscountConditionProductCollection + | DiscountConditionProductTag + | DiscountConditionCustomerGroup + )[] + > { let resolvedConditionType return await this.atomicPhase_( @@ -164,7 +178,7 @@ class DiscountConditionService extends BaseService { ) } - async delete(discountConditionId: string): Promise { + async delete(discountConditionId: string): Promise { return await this.atomicPhase_(async (manager: EntityManager) => { const conditionRepo = manager.getCustomRepository( this.discountConditionRepository_ diff --git a/packages/medusa/src/services/discount.ts b/packages/medusa/src/services/discount.ts index 61906f66ee..0fde9be4fe 100644 --- a/packages/medusa/src/services/discount.ts +++ b/packages/medusa/src/services/discount.ts @@ -1,7 +1,6 @@ import { parse, toSeconds } from "iso8601-duration" import { isEmpty, omit } from "lodash" import { MedusaError } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" import { Brackets, DeepPartial, @@ -15,19 +14,17 @@ import { RegionService, TotalsService, } from "." -import { Cart } from "../models/cart" -import { Discount } from "../models/discount" +import { Cart, Discount, LineItem, Region } from "../models" import { AllocationType as DiscountAllocation, DiscountRule, DiscountRuleType, } from "../models/discount-rule" -import { LineItem } from "../models/line-item" import { DiscountRepository } from "../repositories/discount" import { DiscountConditionRepository } from "../repositories/discount-condition" import { DiscountRuleRepository } from "../repositories/discount-rule" import { GiftCardRepository } from "../repositories/gift-card" -import { FindConfig } from "../types/common" +import { FindConfig, Selector } from "../types/common" import { CreateDiscountInput, CreateDiscountRuleInput, @@ -39,22 +36,28 @@ import { import { isFuture, isPast } from "../utils/date-helpers" import { formatException } from "../utils/exception-formatter" import DiscountConditionService from "./discount-condition" +import CustomerService from "./customer" +import { TransactionBaseService } from "../interfaces" +import { buildQuery, setMetadata } from "../utils" /** * Provides layer to manipulate discounts. * @implements {BaseService} */ -class DiscountService extends BaseService { - private manager_: EntityManager - private discountRepository_: typeof DiscountRepository - private discountRuleRepository_: typeof DiscountRuleRepository - private giftCardRepository_: typeof GiftCardRepository - private discountConditionRepository_: typeof DiscountConditionRepository - private discountConditionService_: DiscountConditionService - private totalsService_: TotalsService - private productService_: ProductService - private regionService_: RegionService - private eventBus_: EventBusService +class DiscountService extends TransactionBaseService { + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + + protected readonly discountRepository_: typeof DiscountRepository + protected readonly customerService_: CustomerService + protected readonly discountRuleRepository_: typeof DiscountRuleRepository + protected readonly giftCardRepository_: typeof GiftCardRepository + protected readonly discountConditionRepository_: typeof DiscountConditionRepository + protected readonly discountConditionService_: DiscountConditionService + protected readonly totalsService_: TotalsService + protected readonly productService_: ProductService + protected readonly regionService_: RegionService + protected readonly eventBus_: EventBusService constructor({ manager, @@ -69,67 +72,22 @@ class DiscountService extends BaseService { customerService, eventBusService, }) { - super() + // eslint-disable-next-line prefer-rest-params + super(arguments[0]) - /** @private @const {EntityManager} */ this.manager_ = manager - - /** @private @const {DiscountRepository} */ this.discountRepository_ = discountRepository - - /** @private @const {DiscountRuleRepository} */ this.discountRuleRepository_ = discountRuleRepository - - /** @private @const {GiftCardRepository} */ this.giftCardRepository_ = giftCardRepository - - /** @private @const {DiscountConditionRepository} */ this.discountConditionRepository_ = discountConditionRepository - - /** @private @const {DiscountConditionRepository} */ this.discountConditionService_ = discountConditionService - - /** @private @const {TotalsService} */ this.totalsService_ = totalsService - - /** @private @const {ProductService} */ this.productService_ = productService - - /** @private @const {RegionService} */ this.regionService_ = regionService - - /** @private @const {CustomerService} */ this.customerService_ = customerService - - /** @private @const {EventBus} */ this.eventBus_ = eventBusService } - withTransaction(transactionManager: EntityManager): DiscountService { - if (!transactionManager) { - return this - } - - const cloned = new DiscountService({ - manager: transactionManager, - discountRepository: this.discountRepository_, - discountRuleRepository: this.discountRuleRepository_, - giftCardRepository: this.giftCardRepository_, - discountConditionRepository: this.discountConditionRepository_, - discountConditionService: this.discountConditionService_, - totalsService: this.totalsService_, - productService: this.productService_, - regionService: this.regionService_, - customerService: this.customerService_, - eventBusService: this.eventBus_, - }) - - cloned.transactionManager_ = transactionManager - cloned.manager_ = transactionManager - - return cloned - } - /** * Creates a discount rule with provided data given that the data is validated. * @param {DiscountRule} discountRule - the discount rule to create @@ -157,12 +115,14 @@ class DiscountService extends BaseService { selector: FilterableDiscountProps = {}, config: FindConfig = { relations: [], skip: 0, take: 10 } ): Promise { - const discountRepo = this.manager_.getCustomRepository( - this.discountRepository_ - ) + return await this.atomicPhase_(async (transactionManager) => { + const discountRepo = transactionManager.getCustomRepository( + this.discountRepository_ + ) - const query = this.buildQuery_(selector, config) - return discountRepo.find(query) + const query = buildQuery(selector as Selector, config) + return await discountRepo.find(query) + }) } /** @@ -178,37 +138,39 @@ class DiscountService extends BaseService { order: { created_at: "DESC" }, } ): Promise<[Discount[], number]> { - const discountRepo = this.manager_.getCustomRepository( - this.discountRepository_ - ) + return await this.atomicPhase_(async (transactionManager) => { + const discountRepo = transactionManager.getCustomRepository( + this.discountRepository_ + ) - 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.code - - query.where = (qb: SelectQueryBuilder): void => { - qb.where(where) - - qb.andWhere( - new Brackets((qb) => { - qb.where({ code: ILike(`%${q}%`) }) - }) - ) + let q + if ("q" in selector) { + q = selector.q + delete selector.q } - } - const [discounts, count] = await discountRepo.findAndCount(query) + const query = buildQuery(selector as Selector, config) - return [discounts, count] + if (q) { + const where = query.where + + delete where.code + + query.where = (qb: SelectQueryBuilder): void => { + qb.where(where) + + qb.andWhere( + new Brackets((qb) => { + qb.where({ code: ILike(`%${q}%`) }) + }) + ) + } + } + + const [discounts, count] = await discountRepo.findAndCount(query) + + return [discounts, count] + }) } /** @@ -218,7 +180,7 @@ class DiscountService extends BaseService { * @return {Promise} the result of the create operation */ async create(discount: CreateDiscountInput): Promise { - return this.atomicPhase_(async (manager: EntityManager) => { + return await this.atomicPhase_(async (manager: EntityManager) => { const discountRepo = manager.getCustomRepository(this.discountRepository_) const ruleRepo = manager.getCustomRepository(this.discountRuleRepository_) @@ -240,11 +202,11 @@ class DiscountService extends BaseService { } try { if (discount.regions) { - discount.regions = await Promise.all( + discount.regions = (await Promise.all( discount.regions.map((regionId) => this.regionService_.withTransaction(manager).retrieve(regionId) ) - ) + )) as Region[] } const discountRule = ruleRepo.create(validatedRule) @@ -286,22 +248,23 @@ class DiscountService extends BaseService { discountId: string, config: FindConfig = {} ): Promise { - const discountRepo = this.manager_.getCustomRepository( - this.discountRepository_ - ) - - const validatedId = this.validateId_(discountId) - const query = this.buildQuery_({ id: validatedId }, config) - const discount = await discountRepo.findOne(query) - - if (!discount) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Discount with ${discountId} was not found` + return await this.atomicPhase_(async (transactionManager) => { + const discountRepo = transactionManager.getCustomRepository( + this.discountRepository_ ) - } - return discount + const query = buildQuery({ id: discountId }, config) + const discount = await discountRepo.findOne(query) + + if (!discount) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Discount with ${discountId} was not found` + ) + } + + return discount + }) } /** @@ -314,29 +277,28 @@ class DiscountService extends BaseService { discountCode: string, config: FindConfig = {} ): Promise { - const discountRepo = this.manager_.getCustomRepository( - this.discountRepository_ - ) + return await this.atomicPhase_(async (transactionManager) => { + const discountRepo = transactionManager.getCustomRepository( + this.discountRepository_ + ) - let query = this.buildQuery_( - { code: discountCode, is_dynamic: false }, - config - ) - let discount = await discountRepo.findOne(query) - - if (!discount) { - query = this.buildQuery_({ code: discountCode, is_dynamic: true }, config) - discount = await discountRepo.findOne(query) + let query = buildQuery({ code: discountCode, is_dynamic: false }, config) + let discount = await discountRepo.findOne(query) if (!discount) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Discount with code ${discountCode} was not found` - ) - } - } + query = buildQuery({ code: discountCode, is_dynamic: true }, config) + discount = await discountRepo.findOne(query) - return discount + if (!discount) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Discount with code ${discountCode} was not found` + ) + } + } + + return discount + }) } /** @@ -349,7 +311,7 @@ class DiscountService extends BaseService { discountId: string, update: UpdateDiscountInput ): Promise { - return this.atomicPhase_(async (manager) => { + return await this.atomicPhase_(async (manager) => { const discountRepo: DiscountRepository = manager.getCustomRepository( this.discountRepository_ ) @@ -365,7 +327,7 @@ class DiscountService extends BaseService { const ruleToUpdate = omit(update.rule, "conditions") if (!isEmpty(ruleToUpdate)) { - update.rule = ruleToUpdate + update.rule = ruleToUpdate as UpdateDiscountRuleInput } const { rule, metadata, regions, ...rest } = update @@ -403,7 +365,7 @@ class DiscountService extends BaseService { } if (metadata) { - discount.metadata = await this.setMetadata_(discount.id, metadata) + discount.metadata = await setMetadata(discount, metadata) } if (rule) { @@ -416,12 +378,10 @@ class DiscountService extends BaseService { }) } - const updatedRule = ruleRepo.create({ + discount.rule = ruleRepo.create({ ...discount.rule, ...ruleUpdate, - }) - - discount.rule = updatedRule + } as DiscountRule) } for (const key of Object.keys(rest).filter( @@ -432,8 +392,7 @@ class DiscountService extends BaseService { discount.code = discount.code.toUpperCase() - const updated = await discountRepo.save(discount) - return updated + return await discountRepo.save(discount) }) } @@ -447,7 +406,7 @@ class DiscountService extends BaseService { discountId: string, data: CreateDynamicDiscountInput ): Promise { - return this.atomicPhase_(async (manager) => { + return await this.atomicPhase_(async (manager) => { const discountRepo = manager.getCustomRepository(this.discountRepository_) const discount = await this.retrieve(discountId) @@ -483,9 +442,8 @@ class DiscountService extends BaseService { ) toCreate.ends_at = lastValidDate } - const created = await discountRepo.create(toCreate) - const result = await discountRepo.save(created) - return result + const created: Discount = discountRepo.create(toCreate) + return await discountRepo.save(created) }) } @@ -496,19 +454,17 @@ class DiscountService extends BaseService { * @return {Promise} the newly created dynamic code */ async deleteDynamicCode(discountId: string, code: string): Promise { - return this.atomicPhase_(async (manager) => { + return await this.atomicPhase_(async (manager) => { const discountRepo = manager.getCustomRepository(this.discountRepository_) const discount = await discountRepo.findOne({ where: { parent_discount_id: discountId, code }, }) if (!discount) { - return Promise.resolve() + return } await discountRepo.softRemove(discount) - - return Promise.resolve() }) } @@ -519,7 +475,7 @@ class DiscountService extends BaseService { * @return {Promise} the result of the update operation */ async addRegion(discountId: string, regionId: string): Promise { - return this.atomicPhase_(async (manager) => { + return await this.atomicPhase_(async (manager) => { const discountRepo = manager.getCustomRepository(this.discountRepository_) const discount = await this.retrieve(discountId, { @@ -543,8 +499,7 @@ class DiscountService extends BaseService { discount.regions = [...discount.regions, region] - const updated = await discountRepo.save(discount) - return updated + return await discountRepo.save(discount) }) } @@ -555,7 +510,7 @@ class DiscountService extends BaseService { * @return {Promise} the result of the update operation */ async removeRegion(discountId: string, regionId: string): Promise { - return this.atomicPhase_(async (manager) => { + return await this.atomicPhase_(async (manager) => { const discountRepo = manager.getCustomRepository(this.discountRepository_) const discount = await this.retrieve(discountId, { @@ -570,8 +525,7 @@ class DiscountService extends BaseService { discount.regions = discount.regions.filter((r) => r.id !== regionId) - const updated = await discountRepo.save(discount) - return updated + return await discountRepo.save(discount) }) } @@ -581,18 +535,16 @@ class DiscountService extends BaseService { * @return {Promise} the result of the delete operation */ async delete(discountId: string): Promise { - return this.atomicPhase_(async (manager) => { + return await this.atomicPhase_(async (manager) => { const discountRepo = manager.getCustomRepository(this.discountRepository_) const discount = await discountRepo.findOne({ where: { id: discountId } }) if (!discount) { - return Promise.resolve() + return } await discountRepo.softRemove(discount) - - return Promise.resolve() }) } @@ -600,7 +552,7 @@ class DiscountService extends BaseService { discountRuleId: string, productId: string | undefined ): Promise { - return this.atomicPhase_(async (manager) => { + return await this.atomicPhase_(async (manager) => { const discountConditionRepo: DiscountConditionRepository = manager.getCustomRepository(this.discountConditionRepository_) @@ -626,98 +578,102 @@ class DiscountService extends BaseService { lineItem: LineItem, cart: Cart ): Promise { - let adjustment = 0 + return await this.atomicPhase_(async () => { + let adjustment = 0 - if (!lineItem.allow_discounts) { - return adjustment - } + if (!lineItem.allow_discounts) { + return adjustment + } - const discount = await this.retrieve(discountId, { relations: ["rule"] }) + const discount = await this.retrieve(discountId, { relations: ["rule"] }) - const { type, value, allocation } = discount.rule + const { type, value, allocation } = discount.rule - const fullItemPrice = lineItem.unit_price * lineItem.quantity + const fullItemPrice = lineItem.unit_price * lineItem.quantity - if (type === DiscountRuleType.PERCENTAGE) { - adjustment = Math.round((fullItemPrice / 100) * value) - } else if ( - type === DiscountRuleType.FIXED && - allocation === DiscountAllocation.TOTAL - ) { - // when a fixed discount should be applied to the total, - // we create line adjustments for each item with an amount - // relative to the subtotal - const subtotal = this.totalsService_.getSubtotal(cart, { - excludeNonDiscounts: true, - }) - const nominator = Math.min(value, subtotal) - const itemRelativeToSubtotal = lineItem.unit_price / subtotal - const totalItemPercentage = itemRelativeToSubtotal * lineItem.quantity - adjustment = Math.round(nominator * totalItemPercentage) - } else { - adjustment = value * lineItem.quantity - } - // if the amount of the discount exceeds the total price of the item, - // we return the total item price, else the fixed amount - return adjustment >= fullItemPrice ? fullItemPrice : adjustment + if (type === DiscountRuleType.PERCENTAGE) { + adjustment = Math.round((fullItemPrice / 100) * value) + } else if ( + type === DiscountRuleType.FIXED && + allocation === DiscountAllocation.TOTAL + ) { + // when a fixed discount should be applied to the total, + // we create line adjustments for each item with an amount + // relative to the subtotal + const subtotal = this.totalsService_.getSubtotal(cart, { + excludeNonDiscounts: true, + }) + const nominator = Math.min(value, subtotal) + const itemRelativeToSubtotal = lineItem.unit_price / subtotal + const totalItemPercentage = itemRelativeToSubtotal * lineItem.quantity + adjustment = Math.round(nominator * totalItemPercentage) + } else { + adjustment = value * lineItem.quantity + } + // if the amount of the discount exceeds the total price of the item, + // we return the total item price, else the fixed amount + return adjustment >= fullItemPrice ? fullItemPrice : adjustment + }) } async validateDiscountForCartOrThrow( cart: Cart, discount: Discount ): Promise { - if (this.hasReachedLimit(discount)) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Discount has been used maximum allowed times" - ) - } - - if (this.hasNotStarted(discount)) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Discount is not valid yet" - ) - } - - if (this.hasExpired(discount)) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Discount is expired" - ) - } - - if (this.isDisabled(discount)) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "The discount code is disabled" - ) - } - - const isValidForRegion = await this.isValidForRegion( - discount, - cart.region_id - ) - if (!isValidForRegion) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "The discount is not available in current region" - ) - } - - if (cart.customer_id) { - const canApplyForCustomer = await this.canApplyForCustomer( - discount.rule.id, - cart.customer_id - ) - - if (!canApplyForCustomer) { + return await this.atomicPhase_(async () => { + if (this.hasReachedLimit(discount)) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, - "Discount is not valid for customer" + "Discount has been used maximum allowed times" ) } - } + + if (this.hasNotStarted(discount)) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Discount is not valid yet" + ) + } + + if (this.hasExpired(discount)) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Discount is expired" + ) + } + + if (this.isDisabled(discount)) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "The discount code is disabled" + ) + } + + const isValidForRegion = await this.isValidForRegion( + discount, + cart.region_id + ) + if (!isValidForRegion) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "The discount is not available in current region" + ) + } + + if (cart.customer_id) { + const canApplyForCustomer = await this.canApplyForCustomer( + discount.rule.id, + cart.customer_id + ) + + if (!canApplyForCustomer) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Discount is not valid for customer" + ) + } + } + }) } hasReachedLimit(discount: Discount): boolean { @@ -746,24 +702,26 @@ class DiscountService extends BaseService { discount: Discount, region_id: string ): Promise { - let regions = discount.regions + return await this.atomicPhase_(async () => { + let regions = discount.regions - if (discount.parent_discount_id) { - const parent = await this.retrieve(discount.parent_discount_id, { - relations: ["rule", "regions"], - }) + if (discount.parent_discount_id) { + const parent = await this.retrieve(discount.parent_discount_id, { + relations: ["rule", "regions"], + }) - regions = parent.regions - } + regions = parent.regions + } - return regions.find(({ id }) => id === region_id) !== undefined + return regions.find(({ id }) => id === region_id) !== undefined + }) } async canApplyForCustomer( discountRuleId: string, customerId: string | undefined ): Promise { - return this.atomicPhase_(async (manager) => { + return await this.atomicPhase_(async (manager) => { const discountConditionRepo: DiscountConditionRepository = manager.getCustomRepository(this.discountConditionRepository_) diff --git a/packages/medusa/src/services/user.ts b/packages/medusa/src/services/user.ts index 733934eca1..c306e5f7d3 100644 --- a/packages/medusa/src/services/user.ts +++ b/packages/medusa/src/services/user.ts @@ -1,9 +1,8 @@ import jwt from "jsonwebtoken" -import { MedusaError, Validator } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" import Scrypt from "scrypt-kdf" +import { MedusaError, Validator } from "medusa-core-utils" import { EntityManager } from "typeorm" -import { User } from "../models/user" +import { User } from "../models" import { UserRepository } from "../repositories/user" import { FindConfig } from "../types/common" import { @@ -12,6 +11,8 @@ import { UpdateUserInput, } from "../types/user" import EventBusService from "./event-bus" +import { TransactionBaseService } from "../interfaces" +import { buildQuery, setMetadata } from "../utils" type UserServiceProps = { userRepository: typeof UserRepository @@ -23,45 +24,24 @@ type UserServiceProps = { * Provides layer to manipulate users. * @extends BaseService */ -class UserService extends BaseService { +class UserService extends TransactionBaseService { static Events = { PASSWORD_RESET: "user.password_reset", } - private userRepository_: typeof UserRepository - private eventBus_: EventBusService - private manager_: EntityManager - private transactionManager_: EntityManager + protected manager_: EntityManager + protected transactionManager_: EntityManager + protected readonly userRepository_: typeof UserRepository + protected readonly eventBus_: EventBusService constructor({ userRepository, eventBusService, manager }: UserServiceProps) { - super() + super({ userRepository, eventBusService, manager }) - /** @private @const {UserRepository} */ this.userRepository_ = userRepository - - /** @private @const {EventBus} */ this.eventBus_ = eventBusService - - /** @private @const {EntityManager} */ this.manager_ = manager } - withTransaction(transactionManager: EntityManager): UserService { - if (!transactionManager) { - return this - } - - const cloned = new UserService({ - manager: transactionManager, - userRepository: this.userRepository_, - eventBusService: this.eventBus_, - }) - - cloned.transactionManager_ = transactionManager - - return cloned - } - /** * Used to validate user email. * @param {string} email - email to validate @@ -86,8 +66,12 @@ class UserService extends BaseService { * @return {Promise} the result of the find operation */ async list(selector: FilterableUserProps, config = {}): Promise { - const userRepo = this.manager_.getCustomRepository(this.userRepository_) - return userRepo.find(this.buildQuery_(selector, config)) + return await this.atomicPhase_(async (transactionManager) => { + const userRepo = transactionManager.getCustomRepository( + this.userRepository_ + ) + return await userRepo.find(buildQuery(selector, config)) + }) } /** @@ -98,20 +82,23 @@ class UserService extends BaseService { * @return {Promise} the user document. */ async retrieve(userId: string, config: FindConfig = {}): Promise { - const userRepo = this.manager_.getCustomRepository(this.userRepository_) - const validatedId = this.validateId_(userId) - const query = this.buildQuery_({ id: validatedId }, config) - - const user = await userRepo.findOne(query) - - if (!user) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `User with id: ${userId} was not found` + return await this.atomicPhase_(async (transactionManager) => { + const userRepo = transactionManager.getCustomRepository( + this.userRepository_ ) - } + const query = buildQuery({ id: userId }, config) - return user + const user = await userRepo.findOne(query) + + if (!user) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `User with id: ${userId} was not found` + ) + } + + return user + }) } /** @@ -125,21 +112,25 @@ class UserService extends BaseService { apiToken: string, relations: string[] = [] ): Promise { - const userRepo = this.manager_.getCustomRepository(this.userRepository_) - - const user = await userRepo.findOne({ - where: { api_token: apiToken }, - relations, - }) - - if (!user) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `User with api token: ${apiToken} was not found` + return await this.atomicPhase_(async (transactionManager) => { + const userRepo = transactionManager.getCustomRepository( + this.userRepository_ ) - } - return user + const user = await userRepo.findOne({ + where: { api_token: apiToken }, + relations, + }) + + if (!user) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `User with api token: ${apiToken} was not found` + ) + } + + return user + }) } /** @@ -153,19 +144,23 @@ class UserService extends BaseService { email: string, config: FindConfig = {} ): Promise { - const userRepo = this.manager_.getCustomRepository(this.userRepository_) - - const query = this.buildQuery_({ email: email.toLowerCase() }, config) - const user = await userRepo.findOne(query) - - if (!user) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `User with email: ${email} was not found` + return await this.atomicPhase_(async (transactionManager) => { + const userRepo = transactionManager.getCustomRepository( + this.userRepository_ ) - } - return user + const query = buildQuery({ email: email.toLowerCase() }, config) + const user = await userRepo.findOne(query) + + if (!user) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `User with email: ${email} was not found` + ) + } + + return user + }) } /** @@ -186,7 +181,7 @@ class UserService extends BaseService { * @return {Promise} the result of create */ async create(user: CreateUserInput, password: string): Promise { - return this.atomicPhase_(async (manager: EntityManager) => { + return await this.atomicPhase_(async (manager: EntityManager) => { const userRepo = manager.getCustomRepository(this.userRepository_) const createData = { ...user } as CreateUserInput & { @@ -203,7 +198,7 @@ class UserService extends BaseService { const created = userRepo.create(createData) - return userRepo.save(created) + return await userRepo.save(created) }) } @@ -214,11 +209,10 @@ class UserService extends BaseService { * @return {Promise} the result of create */ async update(userId: string, update: UpdateUserInput): Promise { - return this.atomicPhase_(async (manager: EntityManager) => { + return await this.atomicPhase_(async (manager: EntityManager) => { const userRepo = manager.getCustomRepository(this.userRepository_) - const validatedId = this.validateId_(userId) - const user = await this.retrieve(validatedId) + const user = await this.retrieve(userId) const { email, password_hash, metadata, ...rest } = update @@ -237,14 +231,14 @@ class UserService extends BaseService { } if (metadata) { - user.metadata = this.setMetadata_(user, metadata) + user.metadata = setMetadata(user, metadata) } for (const [key, value] of Object.entries(rest)) { user[key as keyof User] = value } - return userRepo.save(user) + return await userRepo.save(user) }) } @@ -254,8 +248,8 @@ class UserService extends BaseService { * castable as an ObjectId * @return {Promise} the result of the delete operation. */ - async delete(userId: string): Promise { - return this.atomicPhase_(async (manager: EntityManager) => { + async delete(userId: string): Promise { + return await this.atomicPhase_(async (manager: EntityManager) => { const userRepo = manager.getCustomRepository(this.userRepository_) // Should not fail, if user does not exist, since delete is idempotent @@ -280,7 +274,7 @@ class UserService extends BaseService { * @return {Promise} the result of the update operation */ async setPassword_(userId: string, password: string): Promise { - return this.atomicPhase_(async (manager: EntityManager) => { + return await this.atomicPhase_(async (manager: EntityManager) => { const userRepo = manager.getCustomRepository(this.userRepository_) const user = await this.retrieve(userId) @@ -295,7 +289,7 @@ class UserService extends BaseService { user.password_hash = hashedPassword - return userRepo.save(user) + return await userRepo.save(user) }) } @@ -309,20 +303,25 @@ class UserService extends BaseService { * @return {string} the generated JSON web token */ async generateResetPasswordToken(userId: string): Promise { - const user = await this.retrieve(userId, { - select: ["id", "email", "password_hash"], - }) - const secret = user.password_hash - const expiry = Math.floor(Date.now() / 1000) + 60 * 15 - const payload = { user_id: user.id, email: user.email, exp: expiry } - const token = jwt.sign(payload, secret) + return await this.atomicPhase_(async (transactionManager) => { + const user = await this.retrieve(userId, { + select: ["id", "email", "password_hash"], + }) + const secret = user.password_hash + const expiry = Math.floor(Date.now() / 1000) + 60 * 15 + const payload = { user_id: user.id, email: user.email, exp: expiry } + const token = jwt.sign(payload, secret) - // Notify subscribers - this.eventBus_.emit(UserService.Events.PASSWORD_RESET, { - email: user.email, - token, + // Notify subscribers + await this.eventBus_ + .withTransaction(transactionManager) + .emit(UserService.Events.PASSWORD_RESET, { + email: user.email, + token, + }) + + return token }) - return token } } diff --git a/packages/medusa/src/types/discount.ts b/packages/medusa/src/types/discount.ts index f835e0ff1d..7883fd6273 100644 --- a/packages/medusa/src/types/discount.ts +++ b/packages/medusa/src/types/discount.ts @@ -11,6 +11,7 @@ import { import { DiscountConditionOperator } from "../models/discount-condition" import { AllocationType, DiscountRuleType } from "../models/discount-rule" import { ExactlyOne } from "./validators/exactly-one" +import { Region } from "../models" export type QuerySelector = { q?: string @@ -132,7 +133,7 @@ export type CreateDiscountInput = { ends_at?: Date valid_duration?: string usage_limit?: number - regions?: string[] + regions?: string[] | Region[] metadata?: Record }