|
|
|
@@ -0,0 +1,698 @@
|
|
|
|
|
import { MedusaError } from "medusa-core-utils"
|
|
|
|
|
import { DeepPartial, EntityManager } from "typeorm"
|
|
|
|
|
import { TransactionBaseService } from "../interfaces"
|
|
|
|
|
import {
|
|
|
|
|
Cart,
|
|
|
|
|
Order,
|
|
|
|
|
ShippingMethod,
|
|
|
|
|
ShippingOption,
|
|
|
|
|
ShippingOptionPriceType,
|
|
|
|
|
ShippingOptionRequirement,
|
|
|
|
|
} from "../models"
|
|
|
|
|
import { ShippingMethodRepository } from "../repositories/shipping-method"
|
|
|
|
|
import { ShippingOptionRepository } from "../repositories/shipping-option"
|
|
|
|
|
import { ShippingOptionRequirementRepository } from "../repositories/shipping-option-requirement"
|
|
|
|
|
import { ExtendedFindConfig, FindConfig, Selector } from "../types/common"
|
|
|
|
|
import {
|
|
|
|
|
CreateShippingMethodDto,
|
|
|
|
|
ShippingMethodUpdate,
|
|
|
|
|
UpdateShippingOptionInput,
|
|
|
|
|
CreateShippingOptionInput,
|
|
|
|
|
} from "../types/shipping-options"
|
|
|
|
|
import { buildQuery, setMetadata } from "../utils"
|
|
|
|
|
import FulfillmentProviderService from "./fulfillment-provider"
|
|
|
|
|
import RegionService from "./region"
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Provides layer to manipulate profiles.
|
|
|
|
|
*/
|
|
|
|
|
class ShippingOptionService extends TransactionBaseService<ShippingOptionService> {
|
|
|
|
|
protected readonly providerService_: FulfillmentProviderService
|
|
|
|
|
protected readonly regionService_: RegionService
|
|
|
|
|
protected readonly requirementRepository_: typeof ShippingOptionRequirementRepository
|
|
|
|
|
protected readonly optionRepository_: typeof ShippingOptionRepository
|
|
|
|
|
protected readonly methodRepository_: typeof ShippingMethodRepository
|
|
|
|
|
|
|
|
|
|
protected manager_: EntityManager
|
|
|
|
|
protected transactionManager_: EntityManager | undefined
|
|
|
|
|
|
|
|
|
|
constructor({
|
|
|
|
|
manager,
|
|
|
|
|
shippingOptionRepository,
|
|
|
|
|
shippingOptionRequirementRepository,
|
|
|
|
|
shippingMethodRepository,
|
|
|
|
|
fulfillmentProviderService,
|
|
|
|
|
regionService,
|
|
|
|
|
}) {
|
|
|
|
|
// eslint-disable-next-line prefer-rest-params
|
|
|
|
|
super(arguments[0])
|
|
|
|
|
|
|
|
|
|
this.manager_ = manager
|
|
|
|
|
this.optionRepository_ = shippingOptionRepository
|
|
|
|
|
this.methodRepository_ = shippingMethodRepository
|
|
|
|
|
this.requirementRepository_ = shippingOptionRequirementRepository
|
|
|
|
|
this.providerService_ = fulfillmentProviderService
|
|
|
|
|
this.regionService_ = regionService
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validates a requirement
|
|
|
|
|
* @param {ShippingOptionRequirement} requirement - the requirement to validate
|
|
|
|
|
* @param {string} optionId - the id to validate the requirement
|
|
|
|
|
* @return {ShippingOptionRequirement} a validated shipping requirement
|
|
|
|
|
*/
|
|
|
|
|
async validateRequirement_(
|
|
|
|
|
requirement: ShippingOptionRequirement,
|
|
|
|
|
optionId: string | undefined = undefined
|
|
|
|
|
): Promise<ShippingOptionRequirement> {
|
|
|
|
|
return await this.atomicPhase_(async (manager) => {
|
|
|
|
|
if (!requirement.type) {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.INVALID_DATA,
|
|
|
|
|
"A Shipping Requirement must have a type field"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
requirement.type !== "min_subtotal" &&
|
|
|
|
|
requirement.type !== "max_subtotal"
|
|
|
|
|
) {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.INVALID_DATA,
|
|
|
|
|
"Requirement type must be one of min_subtotal, max_subtotal"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const reqRepo = manager.getCustomRepository(this.requirementRepository_)
|
|
|
|
|
|
|
|
|
|
const existingReq = await reqRepo.findOne({
|
|
|
|
|
where: { id: requirement.id },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!existingReq && requirement.id) {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.INVALID_DATA,
|
|
|
|
|
"ID does not exist"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If no option id is provided, we are currently in the process of creating
|
|
|
|
|
// a new shipping option. Therefore, simply return the requirement, such
|
|
|
|
|
// that the cascading will take care of the creation of the requirement.
|
|
|
|
|
if (!optionId) {
|
|
|
|
|
return requirement
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let req
|
|
|
|
|
if (existingReq) {
|
|
|
|
|
req = await reqRepo.save({
|
|
|
|
|
...existingReq,
|
|
|
|
|
...requirement,
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
const created = reqRepo.create({
|
|
|
|
|
...requirement,
|
|
|
|
|
shipping_option_id: optionId,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
req = await reqRepo.save(created)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return req
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Object} selector - the query object for find
|
|
|
|
|
* @param {object} config - config object
|
|
|
|
|
* @return {Promise} the result of the find operation
|
|
|
|
|
*/
|
|
|
|
|
async list(
|
|
|
|
|
selector: Selector<ShippingMethod>,
|
|
|
|
|
config: FindConfig<ShippingOption> = { skip: 0, take: 50 }
|
|
|
|
|
): Promise<ShippingOption[]> {
|
|
|
|
|
return await this.atomicPhase_(async (transactionManager) => {
|
|
|
|
|
const optRepo = transactionManager.getCustomRepository(
|
|
|
|
|
this.optionRepository_
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const query = buildQuery(selector, config)
|
|
|
|
|
return optRepo.find(query)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Object} selector - the query object for find
|
|
|
|
|
* @param {object} config - config object
|
|
|
|
|
* @return {Promise} the result of the find operation
|
|
|
|
|
*/
|
|
|
|
|
async listAndCount(
|
|
|
|
|
selector: Selector<ShippingMethod>,
|
|
|
|
|
config: FindConfig<ShippingOption> = { skip: 0, take: 50 }
|
|
|
|
|
): Promise<[ShippingOption[], number]> {
|
|
|
|
|
return await this.atomicPhase_(async (transactionManager) => {
|
|
|
|
|
const optRepo = transactionManager.getCustomRepository(
|
|
|
|
|
this.optionRepository_
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const query = buildQuery(selector, config)
|
|
|
|
|
return await optRepo.findAndCount(query)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gets a profile by id.
|
|
|
|
|
* Throws in case of DB Error and if profile was not found.
|
|
|
|
|
* @param {string} optionId - the id of the profile to get.
|
|
|
|
|
* @param {object} options - the options to get a profile
|
|
|
|
|
* @return {Promise<Product>} the profile document.
|
|
|
|
|
*/
|
|
|
|
|
async retrieve(
|
|
|
|
|
optionId,
|
|
|
|
|
options: { select?: (keyof ShippingOption)[]; relations?: string[] } = {}
|
|
|
|
|
): Promise<ShippingOption> {
|
|
|
|
|
return await this.atomicPhase_(async (transactionManager) => {
|
|
|
|
|
const soRepo: ShippingOptionRepository =
|
|
|
|
|
transactionManager.getCustomRepository(this.optionRepository_)
|
|
|
|
|
|
|
|
|
|
const query: ExtendedFindConfig<ShippingOption> = {
|
|
|
|
|
where: { id: optionId },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (options.select) {
|
|
|
|
|
query.select = options.select
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (options.relations) {
|
|
|
|
|
query.relations = options.relations
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const option = await soRepo.findOne(query)
|
|
|
|
|
|
|
|
|
|
if (!option) {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.NOT_FOUND,
|
|
|
|
|
`Shipping Option with ${optionId} was not found`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return option
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Updates a shipping method's associations. Useful when a cart is completed
|
|
|
|
|
* and its methods should be copied to an order/swap entity.
|
|
|
|
|
* @param {string} id - the id of the shipping method to update
|
|
|
|
|
* @param {object} update - the values to update the method with
|
|
|
|
|
* @return {Promise<ShippingMethod>} the resulting shipping method
|
|
|
|
|
*/
|
|
|
|
|
async updateShippingMethod(
|
|
|
|
|
id: string,
|
|
|
|
|
update: ShippingMethodUpdate
|
|
|
|
|
): Promise<ShippingMethod | undefined> {
|
|
|
|
|
return await this.atomicPhase_(async (manager) => {
|
|
|
|
|
const methodRepo: ShippingMethodRepository = manager.getCustomRepository(
|
|
|
|
|
this.methodRepository_
|
|
|
|
|
)
|
|
|
|
|
const method = await methodRepo.findOne({ where: { id } })
|
|
|
|
|
|
|
|
|
|
if (!method) {
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const key of Object.keys(update).filter(
|
|
|
|
|
(k) => typeof update[k] !== `undefined`
|
|
|
|
|
)) {
|
|
|
|
|
method[key] = update[key]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return methodRepo.save(method)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Removes a given shipping method
|
|
|
|
|
* @param {ShippingMethod | Array<ShippingMethod>} shippingMethods - the shipping method to remove
|
|
|
|
|
* @returns removed shipping methods
|
|
|
|
|
*/
|
|
|
|
|
async deleteShippingMethods(
|
|
|
|
|
shippingMethods: ShippingMethod | ShippingMethod[]
|
|
|
|
|
): Promise<ShippingMethod[]> {
|
|
|
|
|
const removeEntities: ShippingMethod[] = Array.isArray(shippingMethods)
|
|
|
|
|
? shippingMethods
|
|
|
|
|
: [shippingMethods]
|
|
|
|
|
|
|
|
|
|
return await this.atomicPhase_(async (manager) => {
|
|
|
|
|
const methodRepo = manager.getCustomRepository(this.methodRepository_)
|
|
|
|
|
return await methodRepo.remove(removeEntities)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a shipping method for a given cart.
|
|
|
|
|
* @param {string} optionId - the id of the option to use for the method.
|
|
|
|
|
* @param {object} data - the optional provider data to use.
|
|
|
|
|
* @param {object} config - the cart to create the shipping method for.
|
|
|
|
|
* @return {ShippingMethod} the resulting shipping method.
|
|
|
|
|
*/
|
|
|
|
|
async createShippingMethod(
|
|
|
|
|
optionId: string,
|
|
|
|
|
data: object,
|
|
|
|
|
config: CreateShippingMethodDto
|
|
|
|
|
): Promise<ShippingMethod> {
|
|
|
|
|
return await this.atomicPhase_(async (manager) => {
|
|
|
|
|
const option = await this.retrieve(optionId, {
|
|
|
|
|
relations: ["requirements"],
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const methodRepo = manager.getCustomRepository(this.methodRepository_)
|
|
|
|
|
|
|
|
|
|
if (typeof config.cart !== "undefined") {
|
|
|
|
|
this.validateCartOption(option, config.cart)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const validatedData = await this.providerService_.validateFulfillmentData(
|
|
|
|
|
option,
|
|
|
|
|
data,
|
|
|
|
|
config.cart || {}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
let methodPrice
|
|
|
|
|
if (typeof config.price === "number") {
|
|
|
|
|
methodPrice = config.price
|
|
|
|
|
} else {
|
|
|
|
|
methodPrice = await this.getPrice_(option, validatedData, config.cart)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const toCreate: Partial<ShippingMethod> = {
|
|
|
|
|
shipping_option_id: option.id,
|
|
|
|
|
data: validatedData,
|
|
|
|
|
price: methodPrice,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (config.order) {
|
|
|
|
|
toCreate.order_id = config.order.id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (config.cart) {
|
|
|
|
|
toCreate.cart_id = config.cart.id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (config.cart_id) {
|
|
|
|
|
toCreate.cart_id = config.cart_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (config.return_id) {
|
|
|
|
|
toCreate.return_id = config.return_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (config.order_id) {
|
|
|
|
|
toCreate.order_id = config.order_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (config.claim_order_id) {
|
|
|
|
|
toCreate.claim_order_id = config.claim_order_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const method = await methodRepo.create(toCreate)
|
|
|
|
|
|
|
|
|
|
const created = await methodRepo.save(method)
|
|
|
|
|
|
|
|
|
|
return methodRepo.findOne({
|
|
|
|
|
where: { id: created.id },
|
|
|
|
|
relations: ["shipping_option"],
|
|
|
|
|
}) as unknown as ShippingMethod
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Checks if a given option id is a valid option for a cart. If it is the
|
|
|
|
|
* option is returned with the correct price. Throws when region_ids do not
|
|
|
|
|
* match, or when the shipping option requirements are not satisfied.
|
|
|
|
|
* @param {object} option - the option object to check
|
|
|
|
|
* @param {Cart} cart - the cart object to check against
|
|
|
|
|
* @return {ShippingOption} the validated shipping option
|
|
|
|
|
*/
|
|
|
|
|
validateCartOption(
|
|
|
|
|
option: ShippingOption,
|
|
|
|
|
cart: Cart
|
|
|
|
|
): ShippingOption | null {
|
|
|
|
|
if (option.is_return) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cart.region_id !== option.region_id) {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.INVALID_DATA,
|
|
|
|
|
"The shipping option is not available in the cart's region"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const subtotal = cart.subtotal as number
|
|
|
|
|
|
|
|
|
|
const requirementResults: boolean[] = option.requirements.map(
|
|
|
|
|
(requirement) => {
|
|
|
|
|
switch (requirement.type) {
|
|
|
|
|
case "max_subtotal":
|
|
|
|
|
return requirement.amount > subtotal
|
|
|
|
|
case "min_subtotal":
|
|
|
|
|
return requirement.amount <= subtotal
|
|
|
|
|
default:
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (!requirementResults.every(Boolean)) {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.NOT_ALLOWED,
|
|
|
|
|
"The Cart does not satisfy the shipping option's requirements"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return option
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a new shipping option. Used both for outbound and inbound shipping
|
|
|
|
|
* options. The difference is registered by the `is_return` field which
|
|
|
|
|
* defaults to false.
|
|
|
|
|
* @param {ShippingOption} data - the data to create shipping options
|
|
|
|
|
* @return {Promise<ShippingOption>} the result of the create operation
|
|
|
|
|
*/
|
|
|
|
|
async create(data: CreateShippingOptionInput): Promise<ShippingOption> {
|
|
|
|
|
return this.atomicPhase_(async (manager) => {
|
|
|
|
|
const optionRepo = manager.getCustomRepository(this.optionRepository_)
|
|
|
|
|
const option = await optionRepo.create(
|
|
|
|
|
data as DeepPartial<ShippingOption>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const region = await this.regionService_
|
|
|
|
|
.withTransaction(manager)
|
|
|
|
|
.retrieve(option.region_id, {
|
|
|
|
|
relations: ["fulfillment_providers"],
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!region.fulfillment_providers.find(
|
|
|
|
|
({ id }) => id === option.provider_id
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.INVALID_DATA,
|
|
|
|
|
"The fulfillment provider is not available in the provided region"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
option.price_type = await this.validatePriceType_(data.price_type, option)
|
|
|
|
|
option.amount =
|
|
|
|
|
data.price_type === "calculated" ? null : data.amount ?? null
|
|
|
|
|
|
|
|
|
|
const isValid = await this.providerService_.validateOption(option)
|
|
|
|
|
|
|
|
|
|
if (!isValid) {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.INVALID_DATA,
|
|
|
|
|
"The fulfillment provider cannot validate the shipping option"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof data.requirements !== "undefined") {
|
|
|
|
|
const acc: ShippingOptionRequirement[] = []
|
|
|
|
|
for (const r of data.requirements) {
|
|
|
|
|
const validated = await this.validateRequirement_(r)
|
|
|
|
|
|
|
|
|
|
if (acc.find((raw) => raw.type === validated.type)) {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.INVALID_DATA,
|
|
|
|
|
"Only one requirement of each type is allowed"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
acc.find(
|
|
|
|
|
(raw) =>
|
|
|
|
|
(raw.type === "max_subtotal" &&
|
|
|
|
|
validated.amount > raw.amount) ||
|
|
|
|
|
(raw.type === "min_subtotal" && validated.amount < raw.amount)
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.INVALID_DATA,
|
|
|
|
|
"Max. subtotal must be greater than Min. subtotal"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
acc.push(validated)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await optionRepo.save(option)
|
|
|
|
|
return result
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validates a shipping option price
|
|
|
|
|
* @param {ShippingOptionPriceType} priceType - the price to validate
|
|
|
|
|
* @param {ShippingOption} option - the option to validate against
|
|
|
|
|
* @return {Promise<ShippingOptionPriceType>} the validated price
|
|
|
|
|
*/
|
|
|
|
|
async validatePriceType_(
|
|
|
|
|
priceType: ShippingOptionPriceType,
|
|
|
|
|
option: ShippingOption
|
|
|
|
|
): Promise<ShippingOptionPriceType> {
|
|
|
|
|
if (
|
|
|
|
|
!priceType ||
|
|
|
|
|
(priceType !== "flat_rate" && priceType !== "calculated")
|
|
|
|
|
) {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.INVALID_DATA,
|
|
|
|
|
"The price must be of type flat_rate or calculated"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (priceType === "calculated") {
|
|
|
|
|
const canCalculate = await this.providerService_.canCalculate(option)
|
|
|
|
|
if (!canCalculate) {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.INVALID_DATA,
|
|
|
|
|
"The fulfillment provider cannot calculate prices for this option"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return priceType
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Updates a profile. Metadata updates and product updates should use
|
|
|
|
|
* dedicated methods, e.g. `setMetadata`, etc. The function
|
|
|
|
|
* will throw errors if metadata or product updates are attempted.
|
|
|
|
|
* @param {string} optionId - the id of the option. Must be a string that
|
|
|
|
|
* can be casted to an ObjectId
|
|
|
|
|
* @param {object} update - an object with the update values.
|
|
|
|
|
* @return {Promise} resolves to the update result.
|
|
|
|
|
*/
|
|
|
|
|
async update(
|
|
|
|
|
optionId: string,
|
|
|
|
|
update: UpdateShippingOptionInput
|
|
|
|
|
): Promise<ShippingOption> {
|
|
|
|
|
return this.atomicPhase_(async (manager) => {
|
|
|
|
|
const option = await this.retrieve(optionId, {
|
|
|
|
|
relations: ["requirements"],
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (typeof update.metadata !== "undefined") {
|
|
|
|
|
option.metadata = await setMetadata(option, update.metadata)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (update.region_id || update.provider_id || update.data) {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.NOT_ALLOWED,
|
|
|
|
|
"Region and Provider cannot be updated after creation"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof update.is_return !== "undefined") {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.NOT_ALLOWED,
|
|
|
|
|
"is_return cannot be changed after creation"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof update.requirements !== "undefined") {
|
|
|
|
|
const acc: ShippingOptionRequirement[] = []
|
|
|
|
|
for (const r of update.requirements) {
|
|
|
|
|
const validated = await this.validateRequirement_(r, optionId)
|
|
|
|
|
|
|
|
|
|
if (acc.find((raw) => raw.type === validated.type)) {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.INVALID_DATA,
|
|
|
|
|
"Only one requirement of each type is allowed"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
acc.find(
|
|
|
|
|
(raw) =>
|
|
|
|
|
(raw.type === "max_subtotal" &&
|
|
|
|
|
validated.amount > raw.amount) ||
|
|
|
|
|
(raw.type === "min_subtotal" && validated.amount < raw.amount)
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.INVALID_DATA,
|
|
|
|
|
"Max. subtotal must be greater than Min. subtotal"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
acc.push(validated)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (option.requirements) {
|
|
|
|
|
const accReqs = acc.map((a) => a.id)
|
|
|
|
|
const toRemove = option.requirements.filter(
|
|
|
|
|
(r) => !accReqs.includes(r.id)
|
|
|
|
|
)
|
|
|
|
|
await Promise.all(
|
|
|
|
|
toRemove.map(async (req) => {
|
|
|
|
|
await this.removeRequirement(req.id)
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
option.requirements = acc
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof update.price_type !== "undefined") {
|
|
|
|
|
option.price_type = await this.validatePriceType_(
|
|
|
|
|
update.price_type,
|
|
|
|
|
option
|
|
|
|
|
)
|
|
|
|
|
if (update.price_type === "calculated") {
|
|
|
|
|
option.amount = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
typeof update.amount !== "undefined" &&
|
|
|
|
|
option.price_type !== "calculated"
|
|
|
|
|
) {
|
|
|
|
|
option.amount = update.amount
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof update.name !== "undefined") {
|
|
|
|
|
option.name = update.name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof update.admin_only !== "undefined") {
|
|
|
|
|
option.admin_only = update.admin_only
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const optionRepo = manager.getCustomRepository(this.optionRepository_)
|
|
|
|
|
return await optionRepo.save(option)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Deletes a profile with a given profile id.
|
|
|
|
|
* @param {string} optionId - the id of the profile to delete. Must be
|
|
|
|
|
* castable as an ObjectId
|
|
|
|
|
* @return {Promise} the result of the delete operation.
|
|
|
|
|
*/
|
|
|
|
|
async delete(optionId: string): Promise<ShippingOption | void> {
|
|
|
|
|
return await this.atomicPhase_(async (manager) => {
|
|
|
|
|
try {
|
|
|
|
|
const option = await this.retrieve(optionId)
|
|
|
|
|
|
|
|
|
|
const optionRepo = manager.getCustomRepository(this.optionRepository_)
|
|
|
|
|
|
|
|
|
|
return optionRepo.softRemove(option)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Delete is idempotent, but we return a promise to allow then-chaining
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Adds a requirement to a shipping option. Only 1 requirement of each type
|
|
|
|
|
* is allowed.
|
|
|
|
|
* @param {string} optionId - the option to add the requirement to.
|
|
|
|
|
* @param {ShippingOptionRequirement} requirement - the requirement for the option.
|
|
|
|
|
* @return {Promise} the result of update
|
|
|
|
|
*/
|
|
|
|
|
async addRequirement(
|
|
|
|
|
optionId: string,
|
|
|
|
|
requirement: ShippingOptionRequirement
|
|
|
|
|
): Promise<ShippingOption> {
|
|
|
|
|
return this.atomicPhase_(async (manager) => {
|
|
|
|
|
const option = await this.retrieve(optionId, {
|
|
|
|
|
relations: ["requirements"],
|
|
|
|
|
})
|
|
|
|
|
const validatedReq = await this.validateRequirement_(requirement)
|
|
|
|
|
|
|
|
|
|
if (option.requirements.find((r) => r.type === validatedReq.type)) {
|
|
|
|
|
throw new MedusaError(
|
|
|
|
|
MedusaError.Types.DUPLICATE_ERROR,
|
|
|
|
|
`A requirement with type: ${validatedReq.type} already exists`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
option.requirements.push(validatedReq)
|
|
|
|
|
|
|
|
|
|
const optionRepo = manager.getCustomRepository(this.optionRepository_)
|
|
|
|
|
return optionRepo.save(option)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Removes a requirement from a shipping option
|
|
|
|
|
* @param {string} requirementId - the id of the requirement to remove
|
|
|
|
|
* @return {Promise} the result of update
|
|
|
|
|
*/
|
|
|
|
|
async removeRequirement(
|
|
|
|
|
requirementId
|
|
|
|
|
): Promise<ShippingOptionRequirement | void> {
|
|
|
|
|
return await this.atomicPhase_(async (manager) => {
|
|
|
|
|
const reqRepo: ShippingOptionRequirementRepository =
|
|
|
|
|
manager.getCustomRepository(this.requirementRepository_)
|
|
|
|
|
|
|
|
|
|
const requirement = await reqRepo.findOne({
|
|
|
|
|
where: { id: requirementId },
|
|
|
|
|
})
|
|
|
|
|
// Delete is idempotent, but we return a promise to allow then-chaining
|
|
|
|
|
if (typeof requirement === "undefined") {
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await reqRepo.softRemove(requirement)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the amount to be paid for a shipping method. Will ask the
|
|
|
|
|
* fulfillment provider to calculate the price if the shipping option has the
|
|
|
|
|
* price type "calculated".
|
|
|
|
|
* @param {ShippingOption} option - the shipping option to retrieve the price
|
|
|
|
|
* for.
|
|
|
|
|
* @param {ShippingData} data - the shipping data to retrieve the price.
|
|
|
|
|
* @param {Cart | Order} cart - the context in which the price should be
|
|
|
|
|
* retrieved.
|
|
|
|
|
* @return {Promise<Number>} the price of the shipping option.
|
|
|
|
|
*/
|
|
|
|
|
async getPrice_(
|
|
|
|
|
option: ShippingOption,
|
|
|
|
|
data: object,
|
|
|
|
|
cart: Cart | Order | undefined
|
|
|
|
|
): Promise<number> {
|
|
|
|
|
if (option.price_type === "calculated") {
|
|
|
|
|
return this.providerService_.calculatePrice(option, data, cart)
|
|
|
|
|
}
|
|
|
|
|
return option.amount as number
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default ShippingOptionService
|