refactor(medusa): cleanup, fix, migrate baseService and therefore fix errors (#1701)

* refactor(medusa): cleanup, fix, migrate baseService and therefore fix errors for ts version > 4.5

* test(medusa): Fix discount tests

* fix(medusa): build

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