feat(medusa): Convert ShippingProfileService to TypeScript (#1963)
This commit is contained in:
1 parent
40ae53567a
commit
152934f8b0
8 files changed
+519
-512
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Convert ShippingProfileService to TypeScript
|
||||||
@@ -31,7 +31,7 @@ export default (app) => {
|
|||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultAdminShippingProfilesFields = [
|
export const defaultAdminShippingProfilesFields: (keyof ShippingProfile)[] = [
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"type",
|
"type",
|
||||||
@@ -43,10 +43,8 @@ export const defaultAdminShippingProfilesFields = [
|
|||||||
|
|
||||||
export type AdminDeleteShippingProfileRes = DeleteResponse
|
export type AdminDeleteShippingProfileRes = DeleteResponse
|
||||||
|
|
||||||
export const defaultAdminShippingProfilesRelations = [
|
export const defaultAdminShippingProfilesRelations: (keyof ShippingProfile)[] =
|
||||||
"products",
|
["products", "shipping_options"]
|
||||||
"shipping_options",
|
|
||||||
]
|
|
||||||
|
|
||||||
export type AdminShippingProfilesRes = {
|
export type AdminShippingProfilesRes = {
|
||||||
shipping_profile: ShippingProfile
|
shipping_profile: ShippingProfile
|
||||||
|
|||||||
@@ -29,21 +29,21 @@ describe("ShippingProfileService", () => {
|
|||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
const profRepo = MockRepository({
|
const profRepo = MockRepository({
|
||||||
findOne: q => {
|
findOne: (q) => {
|
||||||
return Promise.resolve({ id: q.where.id })
|
return Promise.resolve({ id: q.where.id })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const productService = {
|
const productService = {
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
withTransaction: function() {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const shippingOptionService = {
|
const shippingOptionService = {
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
withTransaction: function() {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -98,7 +98,7 @@ describe("ShippingProfileService", () => {
|
|||||||
|
|
||||||
describe("delete", () => {
|
describe("delete", () => {
|
||||||
const profRepo = MockRepository({
|
const profRepo = MockRepository({
|
||||||
findOne: q => {
|
findOne: (q) => {
|
||||||
return Promise.resolve({ id: q.where.id })
|
return Promise.resolve({ id: q.where.id })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -126,7 +126,7 @@ describe("ShippingProfileService", () => {
|
|||||||
|
|
||||||
const productService = {
|
const productService = {
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
withTransaction: function() {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@ describe("ShippingProfileService", () => {
|
|||||||
|
|
||||||
describe("fetchCartOptions", () => {
|
describe("fetchCartOptions", () => {
|
||||||
const profRepo = MockRepository({
|
const profRepo = MockRepository({
|
||||||
find: q => {
|
find: (q) => {
|
||||||
switch (q.where.id) {
|
switch (q.where.id) {
|
||||||
default:
|
default:
|
||||||
return Promise.resolve([
|
return Promise.resolve([
|
||||||
@@ -188,8 +188,8 @@ describe("ShippingProfileService", () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
validateCartOption: jest.fn().mockImplementation(s => s),
|
validateCartOption: jest.fn().mockImplementation((s) => s),
|
||||||
withTransaction: function() {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -301,7 +301,7 @@ describe("ShippingProfileService", () => {
|
|||||||
|
|
||||||
const shippingOptionService = {
|
const shippingOptionService = {
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
withTransaction: function() {
|
withTransaction: function () {
|
||||||
return this
|
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
|
* Utility type used to remove some optional attributes (coming from K) from a type T
|
||||||
*/
|
*/
|
||||||
export type WithRequiredProperty<T, K extends keyof T> = T &
|
export type WithRequiredProperty<T, K extends keyof T> = T & {
|
||||||
{
|
// -? removes 'optional' from a property
|
||||||
// -? removes 'optional' from a property
|
[Property in K]-?: T[Property]
|
||||||
[Property in K]-?: T[Property]
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export type PartialPick<T, K extends keyof T> = {
|
export type PartialPick<T, K extends keyof T> = {
|
||||||
[P in K]?: T[P]
|
[P in K]?: T[P]
|
||||||
@@ -80,10 +79,9 @@ export interface FindConfig<Entity> {
|
|||||||
|
|
||||||
export interface CustomFindOptions<TModel, InKeys extends keyof TModel> {
|
export interface CustomFindOptions<TModel, InKeys extends keyof TModel> {
|
||||||
select?: FindManyOptions<TModel>["select"]
|
select?: FindManyOptions<TModel>["select"]
|
||||||
where?: FindManyOptions<TModel>["where"] &
|
where?: FindManyOptions<TModel>["where"] & {
|
||||||
{
|
[P in InKeys]?: TModel[P][]
|
||||||
[P in InKeys]?: TModel[P][]
|
}
|
||||||
}
|
|
||||||
order?: OrderByCondition
|
order?: OrderByCondition
|
||||||
skip?: number
|
skip?: number
|
||||||
take?: number
|
take?: number
|
||||||
|
|||||||
@@ -69,5 +69,6 @@ export type UpdateShippingOptionInput = {
|
|||||||
requirements?: ShippingOptionRequirement[]
|
requirements?: ShippingOptionRequirement[]
|
||||||
region_id?: string
|
region_id?: string
|
||||||
provider_id?: string
|
provider_id?: string
|
||||||
|
profile_id?: string
|
||||||
data?: 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