feat(medusa): Convert ShippingProfileService to TypeScript (#1963)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
Convert ShippingProfileService to TypeScript
|
||||
@@ -31,7 +31,7 @@ export default (app) => {
|
||||
return app
|
||||
}
|
||||
|
||||
export const defaultAdminShippingProfilesFields = [
|
||||
export const defaultAdminShippingProfilesFields: (keyof ShippingProfile)[] = [
|
||||
"id",
|
||||
"name",
|
||||
"type",
|
||||
@@ -43,10 +43,8 @@ export const defaultAdminShippingProfilesFields = [
|
||||
|
||||
export type AdminDeleteShippingProfileRes = DeleteResponse
|
||||
|
||||
export const defaultAdminShippingProfilesRelations = [
|
||||
"products",
|
||||
"shipping_options",
|
||||
]
|
||||
export const defaultAdminShippingProfilesRelations: (keyof ShippingProfile)[] =
|
||||
["products", "shipping_options"]
|
||||
|
||||
export type AdminShippingProfilesRes = {
|
||||
shipping_profile: ShippingProfile
|
||||
|
||||
@@ -29,21 +29,21 @@ describe("ShippingProfileService", () => {
|
||||
|
||||
describe("update", () => {
|
||||
const profRepo = MockRepository({
|
||||
findOne: q => {
|
||||
findOne: (q) => {
|
||||
return Promise.resolve({ id: q.where.id })
|
||||
},
|
||||
})
|
||||
|
||||
const productService = {
|
||||
update: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const shippingOptionService = {
|
||||
update: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -98,7 +98,7 @@ describe("ShippingProfileService", () => {
|
||||
|
||||
describe("delete", () => {
|
||||
const profRepo = MockRepository({
|
||||
findOne: q => {
|
||||
findOne: (q) => {
|
||||
return Promise.resolve({ id: q.where.id })
|
||||
},
|
||||
})
|
||||
@@ -126,7 +126,7 @@ describe("ShippingProfileService", () => {
|
||||
|
||||
const productService = {
|
||||
update: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -156,7 +156,7 @@ describe("ShippingProfileService", () => {
|
||||
|
||||
describe("fetchCartOptions", () => {
|
||||
const profRepo = MockRepository({
|
||||
find: q => {
|
||||
find: (q) => {
|
||||
switch (q.where.id) {
|
||||
default:
|
||||
return Promise.resolve([
|
||||
@@ -188,8 +188,8 @@ describe("ShippingProfileService", () => {
|
||||
},
|
||||
])
|
||||
}),
|
||||
validateCartOption: jest.fn().mockImplementation(s => s),
|
||||
withTransaction: function() {
|
||||
validateCartOption: jest.fn().mockImplementation((s) => s),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -301,7 +301,7 @@ describe("ShippingProfileService", () => {
|
||||
|
||||
const shippingOptionService = {
|
||||
update: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,489 +0,0 @@
|
||||
import _ from "lodash"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { Any } from "typeorm"
|
||||
|
||||
/**
|
||||
* Provides layer to manipulate profiles.
|
||||
* @class
|
||||
* @implements {BaseService}
|
||||
*/
|
||||
class ShippingProfileService extends BaseService {
|
||||
constructor({
|
||||
manager,
|
||||
shippingProfileRepository,
|
||||
productService,
|
||||
productRepository,
|
||||
shippingOptionService,
|
||||
customShippingOptionService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
/** @private @const {EntityManager} */
|
||||
this.manager_ = manager
|
||||
|
||||
/** @private @const {ShippingProfileRepository} */
|
||||
this.shippingProfileRepository_ = shippingProfileRepository
|
||||
|
||||
/** @private @const {ProductService} */
|
||||
this.productService_ = productService
|
||||
|
||||
/** @private @const {ProductReppsitory} */
|
||||
this.productRepository_ = productRepository
|
||||
|
||||
/** @private @const {ShippingOptionService} */
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
|
||||
/** @private @const {CustomShippingOptionService} */
|
||||
this.customShippingOptionService_ = customShippingOptionService
|
||||
}
|
||||
|
||||
withTransaction(transactionManager) {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new ShippingProfileService({
|
||||
manager: transactionManager,
|
||||
shippingProfileRepository: this.shippingProfileRepository_,
|
||||
productService: this.productService_,
|
||||
shippingOptionService: this.shippingOptionService_,
|
||||
customShippingOptionService: this.customShippingOptionService_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} selector - the query object for find
|
||||
* @param {Object} config - the config object for find
|
||||
* @return {Promise} the result of the find operation
|
||||
*/
|
||||
async list(selector = {}, config = { relations: [], skip: 0, take: 10 }) {
|
||||
const shippingProfileRepo = this.manager_.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
const query = this.buildQuery_(selector, config)
|
||||
return shippingProfileRepo.find(query)
|
||||
}
|
||||
|
||||
async fetchOptionsByProductIds(productIds, filter) {
|
||||
const products = await this.productService_.list(
|
||||
{
|
||||
id: Any(productIds),
|
||||
},
|
||||
{
|
||||
relations: [
|
||||
"profile",
|
||||
"profile.shipping_options",
|
||||
"profile.shipping_options.requirements",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
const profiles = products.map((p) => p.profile)
|
||||
|
||||
const optionIds = profiles.reduce(
|
||||
(acc, next) => acc.concat(next.shipping_options),
|
||||
[]
|
||||
)
|
||||
|
||||
const options = await Promise.all(
|
||||
optionIds.map(async (option) => {
|
||||
let canSend = true
|
||||
if (filter.region_id) {
|
||||
if (filter.region_id !== option.region_id) {
|
||||
canSend = false
|
||||
}
|
||||
}
|
||||
|
||||
if (option.deleted_at !== null) {
|
||||
canSend = false
|
||||
}
|
||||
|
||||
return canSend ? option : null
|
||||
})
|
||||
)
|
||||
|
||||
return options.filter((o) => !!o)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a profile by id.
|
||||
* Throws in case of DB Error and if profile was not found.
|
||||
* @param {string} profileId - the id of the profile to get.
|
||||
* @param {Object} options - options opf the query.
|
||||
* @return {Promise<Product>} the profile document.
|
||||
*/
|
||||
async retrieve(profileId, options = {}) {
|
||||
const profileRepository = this.manager_.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
const validatedId = this.validateId_(profileId)
|
||||
|
||||
const query = {
|
||||
where: { id: validatedId },
|
||||
}
|
||||
|
||||
if (options.select) {
|
||||
query.select = options.select
|
||||
}
|
||||
|
||||
if (options.relations) {
|
||||
query.relations = options.relations
|
||||
}
|
||||
|
||||
const profile = await profileRepository.findOne(query)
|
||||
|
||||
if (!profile) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Profile with id: ${profileId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return profile
|
||||
}
|
||||
|
||||
async retrieveDefault() {
|
||||
const profileRepository = this.manager_.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
const profile = await profileRepository.findOne({
|
||||
where: { type: "default" },
|
||||
})
|
||||
|
||||
return profile
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default shipping profile, if this does not already exist.
|
||||
* @return {Promise<ShippingProfile>} the shipping profile
|
||||
*/
|
||||
async createDefault() {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
let profile = await this.retrieveDefault()
|
||||
|
||||
if (!profile) {
|
||||
const profileRepository = manager.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
const p = await profileRepository.create({
|
||||
type: "default",
|
||||
name: "Default Shipping Profile",
|
||||
})
|
||||
|
||||
profile = await profileRepository.save(p)
|
||||
}
|
||||
|
||||
return profile
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default gift card profile
|
||||
* @return {Object} the shipping profile for gift cards
|
||||
*/
|
||||
async retrieveGiftCardDefault() {
|
||||
const profileRepository = this.manager_.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
const giftCardProfile = await profileRepository.findOne({
|
||||
where: { type: "gift_card" },
|
||||
})
|
||||
|
||||
return giftCardProfile
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default shipping profile, for gift cards if unless it already
|
||||
* exists.
|
||||
* @return {Promise<ShippingProfile>} the shipping profile
|
||||
*/
|
||||
async createGiftCardDefault() {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
let profile = await this.retrieveGiftCardDefault()
|
||||
|
||||
if (!profile) {
|
||||
const profileRepository = manager.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
const p = await profileRepository.create({
|
||||
type: "gift_card",
|
||||
name: "Gift Card Profile",
|
||||
})
|
||||
|
||||
profile = await profileRepository.save(p)
|
||||
}
|
||||
|
||||
return profile
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new shipping profile.
|
||||
* @param {ShippingProfile} profile - the shipping profile to create from
|
||||
* @return {Promise} the result of the create operation
|
||||
*/
|
||||
async create(profile) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const profileRepository = manager.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
if (profile.products || profile.shipping_options) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Please add products and shipping_options after creating Shipping Profiles"
|
||||
)
|
||||
}
|
||||
|
||||
const created = profileRepository.create(profile)
|
||||
const result = await profileRepository.save(created)
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a profile. Metadata updates and product updates should use
|
||||
* dedicated methods, e.g. `setMetadata`, `addProduct`, etc. The function
|
||||
* will throw errors if metadata or product updates are attempted.
|
||||
* @param {string} profileId - the id of the profile. 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(profileId, update) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const profileRepository = manager.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
const profile = await this.retrieve(profileId, {
|
||||
relations: [
|
||||
"products",
|
||||
"products.profile",
|
||||
"shipping_options",
|
||||
"shipping_options.profile",
|
||||
],
|
||||
})
|
||||
|
||||
const { metadata, products, shipping_options, ...rest } = update
|
||||
|
||||
if (metadata) {
|
||||
profile.metadata = this.setMetadata_(profile, metadata)
|
||||
}
|
||||
|
||||
if (products) {
|
||||
for (const pId of products) {
|
||||
await this.productService_.withTransaction(manager).update(pId, {
|
||||
profile_id: profile.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (shipping_options) {
|
||||
for (const oId of shipping_options) {
|
||||
await this.shippingOptionService_
|
||||
.withTransaction(manager)
|
||||
.update(oId, {
|
||||
profile_id: profile.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(rest)) {
|
||||
profile[key] = value
|
||||
}
|
||||
|
||||
const result = await profileRepository.save(profile)
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a profile with a given profile id.
|
||||
* @param {string} profileId - the id of the profile to delete. Must be
|
||||
* castable as an ObjectId
|
||||
* @return {Promise} the result of the delete operation.
|
||||
*/
|
||||
async delete(profileId) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const profileRepo = manager.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
// Should not fail, if profile does not exist, since delete is idempotent
|
||||
const profile = await profileRepo.findOne({ where: { id: profileId } })
|
||||
|
||||
if (!profile) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
await profileRepo.softRemove(profile)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a product to a profile. The method is idempotent, so multiple calls
|
||||
* with the same product variant will have the same result.
|
||||
* @param {string} profileId - the profile to add the product to.
|
||||
* @param {string} productId - the product to add.
|
||||
* @return {Promise} the result of update
|
||||
*/
|
||||
async addProduct(profileId, productId) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
await this.productService_
|
||||
.withTransaction(manager)
|
||||
.update(productId, { profile_id: profileId })
|
||||
|
||||
const updated = await this.retrieve(profileId)
|
||||
return updated
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a shipping option to the profile. The shipping option can be used to
|
||||
* fulfill the products in the products field.
|
||||
* @param {string} profileId - the profile to apply the shipping option to
|
||||
* @param {string} optionId - the option to add to the profile
|
||||
* @return {Promise} the result of the model update operation
|
||||
*/
|
||||
async addShippingOption(profileId, optionId) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
await this.shippingOptionService_
|
||||
.withTransaction(manager)
|
||||
.update(optionId, { profile_id: profileId })
|
||||
|
||||
const updated = await this.retrieve(profileId)
|
||||
return updated
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorates a profile.
|
||||
* @param {Profile} profile - the profile to decorate.
|
||||
* @param {string[]} fields - the fields to include.
|
||||
* @param {string[]} expandFields - fields to expand.
|
||||
* @return {Profile} return the decorated profile.
|
||||
*/
|
||||
async decorate(profile, fields, expandFields = []) {
|
||||
const requiredFields = ["_id", "metadata"]
|
||||
const decorated = _.pick(profile, fields.concat(requiredFields))
|
||||
|
||||
if (expandFields.includes("products") && profile.products) {
|
||||
decorated.products = await Promise.all(
|
||||
profile.products.map((pId) => this.productService_.retrieve(pId))
|
||||
)
|
||||
}
|
||||
|
||||
if (expandFields.includes("shipping_options") && profile.shipping_options) {
|
||||
decorated.shipping_options = await Promise.all(
|
||||
profile.shipping_options.map((oId) =>
|
||||
this.shippingOptionService_.retrieve(oId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const final = await this.runDecorators_(decorated)
|
||||
return final
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all the productIds in the cart.
|
||||
* @param {Cart} cart - the cart to extract products from
|
||||
* @return {[string]} a list of product ids
|
||||
*/
|
||||
getProfilesInCart_(cart) {
|
||||
return cart.items.reduce((acc, next) => {
|
||||
// We may have line items that are not associated with a product
|
||||
if (next.variant && next.variant.product) {
|
||||
if (!acc.includes(next.variant.product.profile_id)) {
|
||||
acc.push(next.variant.product.profile_id)
|
||||
}
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all the shipping profiles that cover the products in a cart, and
|
||||
* validates all options that are available for the cart.
|
||||
* @param {Cart} cart - the cart object to find shipping options for
|
||||
* @return {Promise<[ShippingOption]>} a list of the available shipping options
|
||||
*/
|
||||
async fetchCartOptions(cart) {
|
||||
const profileIds = this.getProfilesInCart_(cart)
|
||||
|
||||
const selector = {
|
||||
profile_id: profileIds,
|
||||
admin_only: false,
|
||||
}
|
||||
|
||||
const customShippingOptions = await this.customShippingOptionService_
|
||||
.withTransaction(this.manager_)
|
||||
.list(
|
||||
{
|
||||
cart_id: cart.id,
|
||||
},
|
||||
{ select: ["id", "shipping_option_id", "price"] }
|
||||
)
|
||||
|
||||
const hasCustomShippingOptions = customShippingOptions?.length
|
||||
// if there are custom shipping options associated with the cart, use those
|
||||
if (hasCustomShippingOptions) {
|
||||
selector.id = customShippingOptions.map((cso) => cso.shipping_option_id)
|
||||
}
|
||||
|
||||
const rawOpts = await this.shippingOptionService_
|
||||
.withTransaction(this.manager_)
|
||||
.list(selector, {
|
||||
relations: ["requirements", "profile"],
|
||||
})
|
||||
|
||||
// if there are custom shipping options associated with the cart, return cart shipping options with custom price
|
||||
if (hasCustomShippingOptions) {
|
||||
return rawOpts.map((so) => {
|
||||
const customOption = customShippingOptions.find(
|
||||
(cso) => cso.shipping_option_id === so.id
|
||||
)
|
||||
|
||||
return {
|
||||
...so,
|
||||
amount: customOption?.price,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const options = await Promise.all(
|
||||
rawOpts.map(async (so) => {
|
||||
try {
|
||||
const option = await this.shippingOptionService_
|
||||
.withTransaction(this.manager_)
|
||||
.validateCartOption(so, cart)
|
||||
if (option) {
|
||||
return option
|
||||
}
|
||||
return null
|
||||
} catch (err) {
|
||||
// if validateCartOption fails it means the option is not valid
|
||||
return null
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return options.filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
export default ShippingProfileService
|
||||
@@ -0,0 +1,481 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import {
|
||||
Cart,
|
||||
ShippingOption,
|
||||
ShippingProfile,
|
||||
ShippingProfileType,
|
||||
} from "../models"
|
||||
import { ProductRepository } from "../repositories/product"
|
||||
import { ShippingProfileRepository } from "../repositories/shipping-profile"
|
||||
import { FindConfig, Selector } from "../types/common"
|
||||
import {
|
||||
CreateShippingProfile,
|
||||
UpdateShippingProfile,
|
||||
} from "../types/shipping-profile"
|
||||
import { buildQuery, setMetadata } from "../utils"
|
||||
import CustomShippingOptionService from "./custom-shipping-option"
|
||||
import ProductService from "./product"
|
||||
import ShippingOptionService from "./shipping-option"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
productService: ProductService
|
||||
shippingOptionService: ShippingOptionService
|
||||
customShippingOptionService: CustomShippingOptionService
|
||||
shippingProfileRepository: typeof ShippingProfileRepository
|
||||
productRepository: typeof ProductRepository
|
||||
}
|
||||
/**
|
||||
* Provides layer to manipulate profiles.
|
||||
* @constructor
|
||||
* @implements {BaseService}
|
||||
*/
|
||||
class ShippingProfileService extends TransactionBaseService<ShippingProfileService> {
|
||||
protected readonly productService_: ProductService
|
||||
protected readonly shippingOptionService_: ShippingOptionService
|
||||
protected readonly customShippingOptionService_: CustomShippingOptionService
|
||||
protected readonly shippingProfileRepository_: typeof ShippingProfileRepository
|
||||
protected readonly productRepository_: typeof ProductRepository
|
||||
|
||||
protected manager_: EntityManager
|
||||
protected transactionManager_: EntityManager | undefined
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
shippingProfileRepository,
|
||||
productService,
|
||||
productRepository,
|
||||
shippingOptionService,
|
||||
customShippingOptionService,
|
||||
}: InjectedDependencies) {
|
||||
super({
|
||||
manager,
|
||||
shippingProfileRepository,
|
||||
productService,
|
||||
productRepository,
|
||||
shippingOptionService,
|
||||
customShippingOptionService,
|
||||
})
|
||||
|
||||
this.manager_ = manager
|
||||
this.shippingProfileRepository_ = shippingProfileRepository
|
||||
this.productService_ = productService
|
||||
this.productRepository_ = productRepository
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
this.customShippingOptionService_ = customShippingOptionService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param selector - the query object for find
|
||||
* @param config - the config object for find
|
||||
* @return the result of the find operation
|
||||
*/
|
||||
async list(
|
||||
selector: Selector<ShippingProfile> = {},
|
||||
config: FindConfig<ShippingProfile> = { relations: [], skip: 0, take: 10 }
|
||||
): Promise<ShippingProfile[]> {
|
||||
const shippingProfileRepo = this.manager_.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
const query = buildQuery(selector, config)
|
||||
return shippingProfileRepo.find(query)
|
||||
}
|
||||
|
||||
async fetchOptionsByProductIds(
|
||||
productIds: string[],
|
||||
filter: Selector<ShippingOption>
|
||||
): Promise<ShippingOption[]> {
|
||||
const products = await this.productService_.list(
|
||||
{
|
||||
id: productIds,
|
||||
},
|
||||
{
|
||||
relations: [
|
||||
"profile",
|
||||
"profile.shipping_options",
|
||||
"profile.shipping_options.requirements",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
const profiles = products.map((p) => p.profile)
|
||||
|
||||
const shippingOptions = profiles.reduce(
|
||||
(acc: ShippingOption[], next: ShippingProfile) =>
|
||||
acc.concat(next.shipping_options),
|
||||
[]
|
||||
)
|
||||
|
||||
const options = await Promise.all(
|
||||
shippingOptions.map(async (option) => {
|
||||
let canSend = true
|
||||
if (filter.region_id) {
|
||||
if (filter.region_id !== option.region_id) {
|
||||
canSend = false
|
||||
}
|
||||
}
|
||||
|
||||
if (option.deleted_at !== null) {
|
||||
canSend = false
|
||||
}
|
||||
|
||||
return canSend ? option : null
|
||||
})
|
||||
)
|
||||
|
||||
return options.filter(Boolean) as ShippingOption[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a profile by id.
|
||||
* Throws in case of DB Error and if profile was not found.
|
||||
* @param profileId - the id of the profile to get.
|
||||
* @param options - options opf the query.
|
||||
* @return {Promise<Product>} the profile document.
|
||||
*/
|
||||
async retrieve(
|
||||
profileId: string,
|
||||
options: FindConfig<ShippingProfile> = {}
|
||||
): Promise<ShippingProfile> {
|
||||
const profileRepository = this.manager_.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
const query = buildQuery({ id: profileId }, options)
|
||||
|
||||
const profile = await profileRepository.findOne(query)
|
||||
|
||||
if (!profile) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Profile with id: ${profileId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return profile
|
||||
}
|
||||
|
||||
async retrieveDefault(): Promise<ShippingProfile | undefined> {
|
||||
const profileRepository = this.manager_.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
const profile = await profileRepository.findOne({
|
||||
where: { type: "default" },
|
||||
})
|
||||
|
||||
return profile
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default shipping profile, if this does not already exist.
|
||||
* @return {Promise<ShippingProfile>} the shipping profile
|
||||
*/
|
||||
async createDefault(): Promise<ShippingProfile> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
let profile = await this.retrieveDefault()
|
||||
|
||||
if (!profile) {
|
||||
const profileRepository = manager.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
const toCreate = {
|
||||
type: ShippingProfileType.DEFAULT,
|
||||
name: "Default Shipping Profile",
|
||||
}
|
||||
|
||||
const created = await profileRepository.create(toCreate)
|
||||
|
||||
profile = await profileRepository.save(created)
|
||||
}
|
||||
|
||||
return profile
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default gift card profile
|
||||
* @return the shipping profile for gift cards
|
||||
*/
|
||||
async retrieveGiftCardDefault(): Promise<ShippingProfile | undefined> {
|
||||
const profileRepository = this.manager_.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
const giftCardProfile = await profileRepository.findOne({
|
||||
where: { type: "gift_card" },
|
||||
})
|
||||
|
||||
return giftCardProfile
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default shipping profile, for gift cards if unless it already
|
||||
* exists.
|
||||
* @return the shipping profile
|
||||
*/
|
||||
async createGiftCardDefault(): Promise<ShippingProfile> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
let profile = await this.retrieveGiftCardDefault()
|
||||
|
||||
if (!profile) {
|
||||
const profileRepository = manager.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
const created = await profileRepository.create({
|
||||
type: ShippingProfileType.GIFT_CARD,
|
||||
name: "Gift Card Profile",
|
||||
})
|
||||
|
||||
profile = await profileRepository.save(created)
|
||||
}
|
||||
|
||||
return profile
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new shipping profile.
|
||||
* @param profile - the shipping profile to create from
|
||||
* @return the result of the create operation
|
||||
*/
|
||||
async create(profile: CreateShippingProfile): Promise<ShippingProfile> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const profileRepository = manager.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
if (profile["products"] || profile["shipping_options"]) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Please add products and shipping_options after creating Shipping Profiles"
|
||||
)
|
||||
}
|
||||
|
||||
const created = profileRepository.create(profile)
|
||||
const result = await profileRepository.save(created)
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a profile. Metadata updates and product updates should use
|
||||
* dedicated methods, e.g. `setMetadata`, `addProduct`, etc. The function
|
||||
* will throw errors if metadata or product updates are attempted.
|
||||
* @param profileId - the id of the profile. Must be a string that
|
||||
* can be casted to an ObjectId
|
||||
* @param update - an object with the update values.
|
||||
* @return resolves to the update result.
|
||||
*/
|
||||
async update(
|
||||
profileId: string,
|
||||
update: UpdateShippingProfile
|
||||
): Promise<ShippingProfile> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const profileRepository = manager.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
const profile = await this.retrieve(profileId, {
|
||||
relations: [
|
||||
"products",
|
||||
"products.profile",
|
||||
"shipping_options",
|
||||
"shipping_options.profile",
|
||||
],
|
||||
})
|
||||
|
||||
const { metadata, products, shipping_options, ...rest } = update
|
||||
|
||||
if (metadata) {
|
||||
profile.metadata = setMetadata(profile, metadata)
|
||||
}
|
||||
|
||||
if (products) {
|
||||
for (const pId of products) {
|
||||
await this.productService_.withTransaction(manager).update(pId, {
|
||||
profile_id: profile.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (shipping_options) {
|
||||
for (const oId of shipping_options) {
|
||||
await this.shippingOptionService_
|
||||
.withTransaction(manager)
|
||||
.update(oId, {
|
||||
profile_id: profile.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(rest)) {
|
||||
profile[key] = value
|
||||
}
|
||||
|
||||
return await profileRepository.save(profile)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a profile with a given profile id.
|
||||
* @param profileId - the id of the profile to delete. Must be
|
||||
* castable as an ObjectId
|
||||
* @return the result of the delete operation.
|
||||
*/
|
||||
async delete(profileId: string): Promise<void> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const profileRepo = manager.getCustomRepository(
|
||||
this.shippingProfileRepository_
|
||||
)
|
||||
|
||||
// Should not fail, if profile does not exist, since delete is idempotent
|
||||
const profile = await profileRepo.findOne({ where: { id: profileId } })
|
||||
|
||||
if (!profile) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
await profileRepo.softRemove(profile)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a product to a profile. The method is idempotent, so multiple calls
|
||||
* with the same product variant will have the same result.
|
||||
* @param profileId - the profile to add the product to.
|
||||
* @param productId - the product to add.
|
||||
* @return the result of update
|
||||
*/
|
||||
async addProduct(
|
||||
profileId: string,
|
||||
productId: string
|
||||
): Promise<ShippingProfile> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
await this.productService_
|
||||
.withTransaction(manager)
|
||||
.update(productId, { profile_id: profileId })
|
||||
|
||||
return await this.retrieve(profileId)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a shipping option to the profile. The shipping option can be used to
|
||||
* fulfill the products in the products field.
|
||||
* @param profileId - the profile to apply the shipping option to
|
||||
* @param optionId - the option to add to the profile
|
||||
* @return the result of the model update operation
|
||||
*/
|
||||
async addShippingOption(
|
||||
profileId: string,
|
||||
optionId: string
|
||||
): Promise<ShippingProfile> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
await this.shippingOptionService_
|
||||
.withTransaction(manager)
|
||||
.update(optionId, { profile_id: profileId })
|
||||
|
||||
const updated = await this.retrieve(profileId)
|
||||
return updated
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all the shipping profiles that cover the products in a cart, and
|
||||
* validates all options that are available for the cart.
|
||||
* @param cart - the cart object to find shipping options for
|
||||
* @return a list of the available shipping options
|
||||
*/
|
||||
async fetchCartOptions(cart): Promise<ShippingOption[]> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const profileIds = this.getProfilesInCart(cart)
|
||||
|
||||
const selector: Selector<ShippingOption> = {
|
||||
profile_id: profileIds,
|
||||
admin_only: false,
|
||||
}
|
||||
|
||||
const customShippingOptions = await this.customShippingOptionService_
|
||||
.withTransaction(manager)
|
||||
.list(
|
||||
{
|
||||
cart_id: cart.id,
|
||||
},
|
||||
{ select: ["id", "shipping_option_id", "price"] }
|
||||
)
|
||||
|
||||
const hasCustomShippingOptions = customShippingOptions?.length
|
||||
// if there are custom shipping options associated with the cart, use those
|
||||
if (hasCustomShippingOptions) {
|
||||
selector.id = customShippingOptions.map((cso) => cso.shipping_option_id)
|
||||
}
|
||||
|
||||
const rawOpts = await this.shippingOptionService_
|
||||
.withTransaction(manager)
|
||||
.list(selector, {
|
||||
relations: ["requirements", "profile"],
|
||||
})
|
||||
|
||||
// if there are custom shipping options associated with the cart, return cart shipping options with custom price
|
||||
if (hasCustomShippingOptions) {
|
||||
return rawOpts.map((so) => {
|
||||
const customOption = customShippingOptions.find(
|
||||
(cso) => cso.shipping_option_id === so.id
|
||||
)
|
||||
|
||||
return {
|
||||
...so,
|
||||
amount: customOption?.price,
|
||||
}
|
||||
}) as ShippingOption[]
|
||||
}
|
||||
|
||||
const options = await Promise.all(
|
||||
rawOpts.map(async (so) => {
|
||||
try {
|
||||
const option = await this.shippingOptionService_
|
||||
.withTransaction(manager)
|
||||
.validateCartOption(so, cart)
|
||||
if (option) {
|
||||
return option
|
||||
}
|
||||
return null
|
||||
} catch (err) {
|
||||
// if validateCartOption fails it means the option is not valid
|
||||
return null
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return options.filter(Boolean) as ShippingOption[]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all the productIds in the cart.
|
||||
* @param cart - the cart to extract products from
|
||||
* @return a list of product ids
|
||||
*/
|
||||
protected getProfilesInCart(cart: Cart): string[] {
|
||||
return cart.items.reduce((acc, next) => {
|
||||
// We may have line items that are not associated with a product
|
||||
if (next.variant && next.variant.product) {
|
||||
if (!acc.includes(next.variant.product.profile_id)) {
|
||||
acc.push(next.variant.product.profile_id)
|
||||
}
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [] as string[])
|
||||
}
|
||||
}
|
||||
|
||||
export default ShippingProfileService
|
||||
@@ -20,11 +20,10 @@ import { ClassConstructor } from "./global"
|
||||
/**
|
||||
* Utility type used to remove some optional attributes (coming from K) from a type T
|
||||
*/
|
||||
export type WithRequiredProperty<T, K extends keyof T> = T &
|
||||
{
|
||||
// -? removes 'optional' from a property
|
||||
[Property in K]-?: T[Property]
|
||||
}
|
||||
export type WithRequiredProperty<T, K extends keyof T> = T & {
|
||||
// -? removes 'optional' from a property
|
||||
[Property in K]-?: T[Property]
|
||||
}
|
||||
|
||||
export type PartialPick<T, K extends keyof T> = {
|
||||
[P in K]?: T[P]
|
||||
@@ -80,10 +79,9 @@ export interface FindConfig<Entity> {
|
||||
|
||||
export interface CustomFindOptions<TModel, InKeys extends keyof TModel> {
|
||||
select?: FindManyOptions<TModel>["select"]
|
||||
where?: FindManyOptions<TModel>["where"] &
|
||||
{
|
||||
[P in InKeys]?: TModel[P][]
|
||||
}
|
||||
where?: FindManyOptions<TModel>["where"] & {
|
||||
[P in InKeys]?: TModel[P][]
|
||||
}
|
||||
order?: OrderByCondition
|
||||
skip?: number
|
||||
take?: number
|
||||
|
||||
@@ -69,5 +69,6 @@ export type UpdateShippingOptionInput = {
|
||||
requirements?: ShippingOptionRequirement[]
|
||||
region_id?: string
|
||||
provider_id?: string
|
||||
profile_id?: string
|
||||
data?: string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Product, ShippingOption, ShippingProfileType } from "../models"
|
||||
|
||||
export type CreateShippingProfile = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type UpdateShippingProfile = {
|
||||
name?: string
|
||||
metadata?: Record<string, unknown>
|
||||
type?: ShippingProfileType
|
||||
products?: string[]
|
||||
shipping_options?: string[]
|
||||
}
|
||||
Reference in New Issue
Block a user