Files
medusa-store/packages/medusa/src/services/cart.js
Sebastian Rindom ec6a538877 All tests passing
2020-07-15 17:47:43 +02:00

1142 lines
32 KiB
JavaScript

import _ from "lodash"
import { Validator, MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
/**
* Provides layer to manipulate carts.
* @implements BaseService
*/
class CartService extends BaseService {
static Events = {
CUSTOMER_UPDATED: "cart.customer_updated",
CREATED: "cart.created",
UPDATED: "cart.updated",
}
constructor({
cartModel,
eventBusService,
paymentProviderService,
productService,
productVariantService,
regionService,
lineItemService,
shippingOptionService,
shippingProfileService,
customerService,
discountService,
totalsService,
}) {
super()
/** @private @const {CartModel} */
this.cartModel_ = cartModel
/** @private @const {EventBus} */
this.eventBus_ = eventBusService
/** @private @const {ProductVariantService} */
this.productVariantService_ = productVariantService
/** @private @const {ProductService} */
this.productService_ = productService
/** @private @const {RegionService} */
this.regionService_ = regionService
/** @private @const {LineItemService} */
this.lineItemService_ = lineItemService
/** @private @const {PaymentProviderService} */
this.paymentProviderService_ = paymentProviderService
/** @private @const {ShippingProfileService} */
this.shippingProfileService_ = shippingProfileService
/** @private @const {CustomerService} */
this.customerService_ = customerService
/** @private @const {ShippingOptionService} */
this.shippingOptionService_ = shippingOptionService
/** @private @const {DiscountService} */
this.discountService_ = discountService
/** @private @const {DiscountService} */
this.totalsService_ = totalsService
}
/**
* Used to validate cart ids. Throws an error if the cast fails
* @param {string} rawId - the raw cart id to validate.
* @return {string} the validated id
*/
validateId_(rawId) {
const schema = Validator.objectId()
const { value, error } = schema.validate(rawId.toString())
if (error) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"The cartId could not be casted to an ObjectId"
)
}
return value
}
/**
* Contents of a line item
* @typedef {(object | array)} LineItemContent
* @property {number} unit_price - the price of the content
* @property {object} variant - the product variant of the content
* @property {object} product - the product of the content
* @property {number} quantity - the quantity of the content
*/
/**
* A collection of contents grouped in the same line item
* @typedef {LineItemContent[]} LineItemContentArray
*/
/**
* Confirms if the contents of a line item is covered by the inventory.
* To be covered a variant must either not have its inventory managed or it
* must allow backorders or it must have enough inventory to cover the request.
* If the content is made up of multiple variants it will return true if all
* variants can be covered. If the content consists of a single variant it will
* return true if the variant is covered.
* @param {(LineItemContent | LineItemContentArray)} - the content of the line
* item
* @param {number} - the quantity of the line item
* @return {boolean} true if the inventory covers the line item.
*/
async confirmInventory_(content, lineQuantity) {
if (Array.isArray(content)) {
const coverage = await Promise.all(
content.map(({ variant, quantity }) => {
return this.productVariantService_.canCoverQuantity(
variant._id,
lineQuantity * quantity
)
})
)
return coverage.every(c => c)
}
const { variant, quantity } = content
return this.productVariantService_.canCoverQuantity(
variant._id,
lineQuantity * quantity
)
}
/**
* Transforms some line item content to have unit_prices corresponding to a
* given region's pricing scheme.
* @param {(LineItemContent | LineItemContentArray)} - the content of the line
* item
* @param {string} regionId - the id of the region whose price we should
* update to
* @return {(LineItemContent | LineItemContentArray)} true if the inventory
* covers the line item.
*/
async updateContentPrice_(content, regionId) {
if (Array.isArray(content)) {
return await Promise.all(
content.map(async c => {
const unitPrice = await this.productVariantService_.getRegionPrice(
c.variant._id,
regionId
)
c.unit_price = unitPrice
return c
})
)
}
const unitPrice = await this.productVariantService_.getRegionPrice(
content.variant._id,
regionId
)
content.unit_price = unitPrice
return content
}
/**
* @param {Object} selector - the query object for find
* @return {Promise} the result of the find operation
*/
list(selector) {
return this.cartModel_.find(selector)
}
/**
* Gets a cart by id.
* @param {string} cartId - the id of the cart to get.
* @return {Promise<Cart>} the cart document.
*/
async retrieve(cartId) {
const validatedId = this.validateId_(cartId)
const cart = await this.cartModel_
.findOne({ _id: validatedId })
.catch(err => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
if (!cart) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Cart with ${cartId} was not found`
)
}
return cart
}
/**
* Creates a cart.
* @param {Object} data - the data to create the cart with
* @return {Promise} the result of the create operation
*/
async create(data) {
const { region_id } = data
if (!region_id) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`A region_id must be provided when creating a cart`
)
}
const region = await this.regionService_.retrieve(region_id)
if (region.countries.length === 1) {
// Preselect the country if the region only has 1
data.shipping_address = {
country_code: region.countries[0],
}
}
return this.cartModel_
.create({
...data,
region_id: region._id,
})
.then(result => {
this.eventBus_.emit(CartService.Events.CREATED, result)
return result
})
.catch(err => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
}
/**
* Decorates a cart.
* @param {Cart} cart - the cart to decorate.
* @param {string[]} fields - the fields to include.
* @param {string[]} expandFields - fields to expand.
* @return {Cart} return the decorated cart.
*/
async decorate(cart, fields, expandFields = []) {
const c = cart
c.shipping_total = await this.totalsService_.getShippingTotal(cart)
c.discount_total = await this.totalsService_.getDiscountTotal(cart)
c.tax_total = await this.totalsService_.getTaxTotal(cart)
c.subtotal = await this.totalsService_.getSubtotal(cart)
c.total = await this.totalsService_.getTotal(cart)
if (expandFields.includes("region")) {
c.region = await this.regionService_.retrieve(cart.region_id)
}
return c
}
/**
* Removes a line item from the cart.
* @param {string} cartId - the id of the cart that we will remove from
* @param {LineItem} lineItemId - the line item to remove.
* @retur {Promise} the result of the update operation
*/
async removeLineItem(cartId, lineItemId) {
const cart = await this.retrieve(cartId)
const itemToRemove = cart.items.find(line => line._id.equals(lineItemId))
if (!itemToRemove) {
return Promise.resolve(cart)
}
// If cart has more than one of those line items, we update the quantity
// instead of removing it
if (itemToRemove.quantity > 1) {
const newQuantity = itemToRemove.quantity - 1
return this.cartModel_
.updateOne(
{
_id: cartId,
"items._id": itemToRemove._id,
},
{
$set: {
"items.$.quantity": newQuantity,
},
}
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
}
return this.cartModel_
.updateOne(
{
_id: cartId,
},
{
$pull: {
items: { _id: itemToRemove._id },
},
}
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
}
/**
* Adds a line item to the cart.
* @param {string} cartId - the id of the cart that we will add to
* @param {LineItem} lineItem - the line item to add.
* @retur {Promise} the result of the update operation
*/
async addLineItem(cartId, lineItem) {
const validatedLineItem = this.lineItemService_.validate(lineItem)
const cart = await this.retrieve(cartId)
const currentItem = cart.items.find(line =>
this.lineItemService_.isEqual(line, validatedLineItem)
)
// If content matches one of the line items currently in the cart we can
// simply update the quantity of the existing line item
if (currentItem) {
const newQuantity = currentItem.quantity + validatedLineItem.quantity
// Confirm inventory
const hasInventory = await this.confirmInventory_(
validatedLineItem.content,
newQuantity
)
if (!hasInventory) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Inventory doesn't cover the desired quantity"
)
}
return this.cartModel_
.updateOne(
{
_id: cartId,
"items._id": currentItem._id,
},
{
$set: {
"items.$.quantity": newQuantity,
},
}
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
}
// Confirm inventory
const hasInventory = await this.confirmInventory_(
validatedLineItem.content,
validatedLineItem.quantity
)
if (!hasInventory) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Inventory doesn't cover the desired quantity"
)
}
// The line we are adding doesn't already exist so it is safe to push
return this.cartModel_
.updateOne(
{
_id: cartId,
},
{
$push: { items: validatedLineItem },
}
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
}
/**
* Updates a cart's existing line item.
* @param {string} cartId - the id of the cart to update
* @param {string} lineItemId - the id of the line item to update.
* @param {LineItem} lineItem - the line item to update. Must include an _id
* field.
* @return {Promise} the result of the update operation
*/
async updateLineItem(cartId, lineItemId, lineItem) {
const cart = await this.retrieve(cartId)
const validatedLineItem = this.lineItemService_.validate(lineItem)
if (!lineItemId) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Line Item must have an _id corresponding to an existing line item id"
)
}
// Ensure that the line item exists in the cart
const lineItemExists = cart.items.find(i => i._id.equals(lineItemId))
if (!lineItemExists) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"A line item with the provided id doesn't exist in the cart"
)
}
// Ensure that inventory covers the request
const hasInventory = await this.confirmInventory_(
validatedLineItem.content,
validatedLineItem.quantity
)
if (!hasInventory) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Inventory doesn't cover the desired quantity"
)
}
// Update the line item
return this.cartModel_
.updateOne(
{
_id: cartId,
"items._id": lineItemId,
},
{
$set: {
"items.$": validatedLineItem,
},
}
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
}
/**
* Sets the customer id of a cart
* @param {string} cartId - the id of the cart to add email to
* @param {string} customerId - the customer to add to cart
* @return {Promise} the result of the update operation
*/
async updateCustomerId(cartId, customerId) {
const cart = await this.retrieve(cartId)
const schema = Validator.objectId().required()
const { value, error } = schema.validate(customerId.toString())
if (error) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The customerId is not valid"
)
}
return this.cartModel_
.updateOne(
{
_id: cart._id,
},
{
$set: { customer_id: value },
}
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.CUSTOMER_UPDATED, result)
return result
})
}
/**
* Sets the email of a cart
* @param {string} cartId - the id of the cart to add email to
* @param {string} email - the email to add to cart
* @return {Promise} the result of the update operation
*/
async updateEmail(cartId, email) {
const cart = await this.retrieve(cartId)
const schema = Validator.string()
.email()
.required()
const { value, error } = schema.validate(email)
if (error) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The email is not valid"
)
}
let customer = await this.customerService_
.retrieveByEmail(value)
.catch(err => undefined)
if (!customer) {
customer = await this.customerService_.create({ email })
}
const customerChanged = !customer._id.equals(cart.customer_id)
return this.cartModel_
.updateOne(
{
_id: cart._id,
},
{
$set: {
email: value,
customer_id: customer._id,
},
}
)
.then(result => {
// Notify subscribers
if (customerChanged) {
this.eventBus_.emit(CartService.Events.CUSTOMER_UPDATED, result)
}
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
}
/**
* Updates the cart's billing address.
* @param {string} cartId - the id of the cart to update
* @param {object} address - the value to set the billing address to
* @return {Promise} the result of the update operation
*/
async updateBillingAddress(cartId, address) {
const cart = await this.retrieve(cartId)
const { value, error } = Validator.address().validate(address)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.message)
}
address.country_code = address.country_code.toUpperCase()
return this.cartModel_
.updateOne(
{
_id: cart._id,
},
{
$set: { billing_address: value },
}
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
}
/**
* Updates the cart's shipping address.
* @param {string} cartId - the id of the cart to update
* @param {object} address - the value to set the shipping address to
* @return {Promise} the result of the update operation
*/
async updateShippingAddress(cartId, address) {
const cart = await this.retrieve(cartId)
const { value, error } = Validator.address().validate(address)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.message)
}
address.country_code = address.country_code.toUpperCase()
const region = await this.regionService_.retrieve(cart.region_id)
if (!region.countries.includes(address.country_code.toUpperCase())) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Shipping country must be in the cart region"
)
}
return this.cartModel_
.updateOne(
{
_id: cartId,
},
{
$set: { shipping_address: value },
}
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
}
/**
* Updates the cart's discounts.
* If discount besides free shipping is already applied, this
* will be overwritten
* Throws if discount regions does not include the cart region
* @param {string} cartId - the id of the cart to update
* @param {string} discountCode - the discount code
* @return {Promise} the result of the update operation
*/
async applyDiscount(cartId, discountCode) {
const cart = await this.retrieve(cartId)
const discount = await this.discountService_.retrieveByCode(discountCode)
if (!discount.regions.includes(cart.region_id)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The discount is not available in current region"
)
}
// if discount is already there, we simply resolve
if (cart.discounts.includes(discount._id)) {
return Promise.resolve()
}
// find the current discounts (if there)
// partition them into shipping and other
const [shippingDisc, otherDisc] = _.partition(
cart.discounts,
d => d.discount_rule.type === "free_shipping"
)
// if no shipping exists and the one to apply is shipping, we simply add it
// else we remove the current shipping and add the other one
if (
shippingDisc.length === 0 &&
discount.discount_rule.type === "free_shipping"
) {
return this.cartModel_
.updateOne(
{
_id: cart._id,
},
{
$push: { discounts: discount },
}
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
} else if (
shippingDisc.length > 0 &&
discount.discount_rule.type === "free_shipping"
) {
return this.cartModel_
.updateOne(
{
_id: cart._id,
},
{
$pull: { discounts: { _id: shippingDisc[0]._id } },
$push: { discounts: discount },
}
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
}
// replace the current discount if there, else add the new one
if (otherDisc.length === 0) {
return this.cartModel_
.updateOne(
{
_id: cart._id,
},
{
$push: { discounts: discount },
}
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
} else {
return this.cartModel_
.updateOne(
{
_id: cart._id,
},
{
$pull: { discounts: { _id: otherDisc[0]._id } },
$push: { discounts: discount },
}
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
}
}
async removeDiscount(cartId, discountCode) {
const cart = await this.retrieve(cartId)
return this.cartModel_.updateOne(
{ _id: cart._id },
{
$pull: { discounts: { code: discountCode } },
}
)
}
/**
* A payment method represents a way for the customer to pay. The payment
* method will typically come from one of the payment sessions.
* @typedef {object} PaymentMethod
* @property {string} provider_id - the identifier of the payment method's
* provider
* @property {object} data - the data associated with the payment method
*/
/**
* Retrieves an open payment session from the list of payment sessions
* stored in the cart. If none is an INVALID_DATA error is thrown.
* @param {string} cartId - the id of the cart to retrieve the session from
* @param {string} providerId - the id of the provider the session belongs to
* @return {PaymentMethod} the session
*/
async retrievePaymentSession(cartId, providerId) {
const cart = await this.retrieve(cartId)
const session = cart.payment_sessions.find(
({ provider_id }) => provider_id === providerId
)
if (!session) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The provider_id did not match any open payment sessions`
)
}
return session
}
/**
* Sets a payment method for a cart.
* @param {string} cartId - the id of the cart to add payment method to
* @param {PaymentMethod} paymentMethod - the method to be set to the cart
* @returns {Promise} result of update operation
*/
async setPaymentMethod(cartId, paymentMethod) {
const cart = await this.retrieve(cartId)
const region = await this.regionService_.retrieve(cart.region_id)
// The region must have the provider id in its providers array
if (
!(
region.payment_providers.length &&
region.payment_providers.includes(paymentMethod.provider_id)
)
) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`The payment method is not available in this region`
)
}
// At this point we can register the payment method.
return this.cartModel_
.updateOne(
{
_id: cart._id,
},
{
$set: { payment_method: paymentMethod },
}
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
}
/**
* Creates, updates and sets payment sessions associated with the cart. The
* first time the method is called payment sessions will be created for each
* provider. Additional calls will ensure that payment sessions have correct
* amounts, currencies, etc. as well as make sure to filter payment sessions
* that are not available for the cart's region.
* @param {string} cartId - the id of the cart to set payment session for
* @returns {Promise} the result of the update operation.
*/
async setPaymentSessions(cartId) {
const cart = await this.retrieve(cartId)
const region = await this.regionService_.retrieve(cart.region_id)
// If there are existing payment sessions ensure that these are up to date
let sessions = []
if (cart.payment_sessions && cart.payment_sessions.length) {
sessions = await Promise.all(
cart.payment_sessions.map(async pSession => {
if (!region.payment_providers.includes(pSession.provider_id)) {
return null
}
const data = await this.paymentProviderService_.updateSession(
pSession,
cart
)
return {
provider_id: pSession.provider_id,
data,
}
})
)
}
// Filter all null sessions
sessions = sessions.filter(s => !!s)
// For all the payment providers in the region make sure to either skip them
// if they already exist or create them if they don't yet exist.
let newSessions = await Promise.all(
region.payment_providers.map(async pId => {
if (sessions.find(s => s.provider_id === pId)) {
return null
}
const data = await this.paymentProviderService_.createSession(pId, cart)
return {
provider_id: pId,
data,
}
})
)
// Filter null sessions
newSessions = newSessions.filter(s => !!s)
// Update the payment sessions with the concatenated array of updated and
// newly created payment sessions
return this.cartModel_
.updateOne(
{
_id: cart._id,
},
{
$set: { payment_sessions: sessions.concat(newSessions) },
}
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
}
async deletePaymentSession(cartId, providerId) {
const cart = await this.retrieve(cartId)
if (cart.payment_sessions) {
const session = cart.payment_sessions.find(
s => s.provider_id === providerId
)
if (session) {
// Delete the session with the provider
await this.paymentProviderService_.deleteSession(session)
const selector = {
$pull: { payment_sessions: { provider_id: providerId } },
}
if (
cart.payment_method &&
cart.payment_method.provider_id === providerId
) {
selector["$set"] = { payment_method: null }
}
return this.cartModel_
.updateOne({ _id: cart._id }, selector)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
}
}
return cart
}
async updatePaymentSession(cartId, providerId, session) {
const cart = await this.retrieve(cartId)
return this.cartModel_
.updateOne(
{
_id: cart._id,
"payment_sessions.provider_id": providerId,
},
{
$set: { "payment_sessions.$": session },
}
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
}
/**
* Adds the shipping method to the list of shipping methods associated with
* the cart. Shipping Methods are the ways that an order is shipped, whereas a
* Shipping Option is a possible way to ship an order. Shipping Methods may
* also have additional details in the data field such as an id for a package
* shop.
* @param {string} cartId - the id of the cart to add shipping method to
* @param {string} optionId - id of shipping option to add as valid method
* @param {Object} data - the fulmillment data for the method
* @return {Promise} the result of the update operation
*/
async addShippingMethod(cartId, optionId, data) {
const cart = await this.retrieve(cartId)
const { shipping_methods } = cart
const option = await this.shippingOptionService_.validateCartOption(
optionId,
cart
)
option.data = await this.shippingOptionService_.validateFulfillmentData(
optionId,
data,
cart
)
const profile = await this.shippingProfileService_.list({
shipping_options: option._id,
})
if (profile.length !== 1) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Shipping Method must belong to a shipping profile"
)
}
option.profile_id = profile[0]._id
// Go through all existing selected shipping methods and update the one
// that has the same profile as the selected shipping method.
let exists = false
const newMethods = shipping_methods.map(sm => {
if (option.profile_id.equals(sm.profile_id)) {
exists = true
return option
}
return sm
})
// If none of the selected methods are for the same profile as the new
// shipping method the exists flag will be false. Therefore we push the new
// method.
if (!exists) {
newMethods.push(option)
}
return this.cartModel_
.updateOne(
{
_id: cart._id,
},
{
$set: { shipping_methods: newMethods },
}
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
}
/**
* Set's the region of a cart.
* @param {string} cartId - the id of the cart to set region on
* @param {string} regionId - the id of the region to set the cart to
* @return {Promise} the result of the update operation
*/
async setRegion(cartId, regionId) {
const cart = await this.retrieve(cartId)
const region = await this.regionService_.retrieve(regionId)
let update = {
region_id: region._id,
}
// If the cart contains items we want to change the unit_price field of each
// item to correspond to the price given in the region
if (cart.items.length) {
const newItems = await Promise.all(
cart.items.map(async lineItem => {
try {
lineItem.content = await this.updateContentPrice_(
lineItem.content,
region._id
)
} catch (err) {
return null
}
return lineItem
})
)
update.items = newItems.filter(i => !!i)
}
// If the country code of a shipping address is set we need to clear it
let shippingAddress = cart.shipping_address
if (!_.isEmpty(shippingAddress) && shippingAddress.country_code) {
shippingAddress.country_code =
region.countries.length === 1 ? region.countries[0] : ""
update.shipping_address = shippingAddress
}
// Shipping methods are determined by region so the user needs to find a
// new shipping method
if (cart.shipping_methods && cart.shipping_methods.length) {
update.shipping_methods = []
}
//if (cart.items.length && cart.payment_sessions.length) {
// update.payment_sessions = await Promise.all(
// region.payment_providers.map(async pId => {
// const data = await this.paymentProviderService_.createSession(pId, {
// ...cart,
// ...update,
// })
// return {
// provider_id: pId,
// data,
// }
// })
// )
//}
// Payment methods are region specific so the user needs to find a
// new payment method
if (!_.isEmpty(cart.payment_method)) {
update.payment_method = undefined
}
if (cart.payment_sessions && cart.payment_sessions.length) {
update.payment_sessions = []
}
return this.cartModel_
.updateOne({ _id: cart._id }, { $set: update })
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
}
async delete(cartId) {
const cart = await this.retrieve(cartId)
return this.cartModel_.deleteOne({ _id: cart._id })
}
/**
* Dedicated method to set metadata for a cart.
* To ensure that plugins does not overwrite each
* others metadata fields, setMetadata is provided.
* @param {string} cartId - the cart to apply metadata to.
* @param {string} key - key for metadata field
* @param {string} value - value for metadata field.
* @return {Promise} resolves to the updated result.
*/
async setMetadata(cartId, key, value) {
const validatedId = this.validateId_(cartId)
if (typeof key !== "string") {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Key type is invalid. Metadata keys must be strings"
)
}
const keyPath = `metadata.${key}`
return this.cartModel_
.updateOne({ _id: validatedId }, { $set: { [keyPath]: value } })
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
.catch(err => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
}
/**
* Dedicated method to delete metadata for a cart.
* @param {string} cartId - the cart to delete metadata from.
* @param {string} key - key for metadata field
* @return {Promise} resolves to the updated result.
*/
async deleteMetadata(cartId, key) {
const validatedId = this.validateId_(cartId)
if (typeof key !== "string") {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Key type is invalid. Metadata keys must be strings"
)
}
const keyPath = `metadata.${key}`
return this.cartModel_
.updateOne({ _id: validatedId }, { $unset: { [keyPath]: "" } })
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
.catch(err => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
}
}
export default CartService