Creates prices modifiers in productVariant

This commit is contained in:
Sebastian Rindom
2020-02-05 15:14:19 +01:00
parent cb727c8689
commit 1d939342b4
7 changed files with 547 additions and 7 deletions

View File

@@ -55,6 +55,56 @@ export const ProductVariantModelMock = {
manage_inventory: true,
})
}
if (query._id === IdMap.getId("no-prices")) {
return Promise.resolve({
_id: IdMap.getId("no-prices"),
title: "No Prices",
prices: [],
})
}
if (query._id === IdMap.getId("eur-prices")) {
return Promise.resolve({
_id: IdMap.getId("eur-prices"),
title: "eur Prices",
prices: [
{
currency_code: "eur",
amount: 1000,
},
{
region_id: IdMap.getId("region-france"),
currency_code: "eur",
amount: 950,
},
],
})
}
if (query._id === IdMap.getId("france-prices")) {
return Promise.resolve({
_id: IdMap.getId("france-prices"),
title: "France Prices",
prices: [
{
currency_code: "eur",
amount: 1000,
},
{
region_id: IdMap.getId("region-france"),
currency_code: "eur",
amount: 950,
},
{
region_id: IdMap.getId("region-us"),
currency_code: "usd",
amount: 1200,
},
],
})
}
return Promise.resolve(undefined)
}),
}

View File

@@ -5,6 +5,7 @@
import mongoose from "mongoose"
export default new mongoose.Schema({
region_id: { type: String },
currency_code: { type: String, required: true },
amount: { type: Number, required: true, min: 0 },
})

View File

@@ -10,6 +10,18 @@ export const regions = {
shipping_providers: ["test_shipper"],
currency_code: "usd",
},
regionFrance: {
_id: IdMap.getId("region-france"),
name: "France",
countries: ["FR"],
currency_code: "eur",
},
regionUs: {
_id: IdMap.getId("region-us"),
name: "USA",
countries: ["US"],
currency_code: "usd",
},
}
export const RegionServiceMock = {
@@ -17,10 +29,20 @@ export const RegionServiceMock = {
if (regionId === IdMap.getId("testRegion")) {
return Promise.resolve(regions.testRegion)
}
if (regionId === IdMap.getId("region-france")) {
return Promise.resolve(regions.regionFrance)
}
if (regionId === IdMap.getId("region-us")) {
return Promise.resolve(regions.regionUs)
}
return Promise.resolve(undefined)
}),
list: jest.fn().mockImplementation(data => {
return Promise.resolve([regions.testRegion])
return Promise.resolve([
regions.testRegion,
regions.regionFrance,
regions.regionUs,
])
}),
}

View File

@@ -102,6 +102,53 @@ describe("CartService", () => {
)
})
it("successfully defaults quantity of content to 1", async () => {
const lineItem = {
title: "New Line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
content: {
unit_price: 123,
variant: {
_id: IdMap.getId("can-cover"),
},
product: {
_id: IdMap.getId("product"),
},
},
quantity: 10,
}
await cartService.addLineItem(IdMap.getId("emptyCart"), lineItem)
expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(CartModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("emptyCart"),
},
{
$push: {
items: {
title: "New Line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
content: {
unit_price: 123,
variant: {
_id: IdMap.getId("can-cover"),
},
product: {
_id: IdMap.getId("product"),
},
quantity: 1,
},
quantity: 10,
},
},
}
)
})
it("successfully merges existing line item", async () => {
const lineItem = {
title: "merge line",

View File

@@ -3,6 +3,7 @@ import { IdMap } from "medusa-test-utils"
import ProductVariantService from "../product-variant"
import { ProductVariantModelMock } from "../../models/__mocks__/product-variant"
import { ProductServiceMock } from "../__mocks__/product"
import { RegionServiceMock } from "../__mocks__/region"
describe("ProductVariantService", () => {
describe("retrieve", () => {
@@ -459,4 +460,203 @@ describe("ProductVariantService", () => {
expect(res).toEqual(false)
})
})
describe("setCurrencyPrice", () => {
const productVariantService = new ProductVariantService({
productVariantModel: ProductVariantModelMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("creates a prices array if none exist", async () => {
await productVariantService.setCurrencyPrice(
IdMap.getId("no-prices"),
"usd",
100
)
expect(ProductVariantModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(ProductVariantModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("no-prices"),
},
{
$set: {
prices: [
{
currency_code: "usd",
amount: 100,
},
],
},
}
)
})
it("updates all eur prices", async () => {
await productVariantService.setCurrencyPrice(
IdMap.getId("eur-prices"),
"eur",
100
)
expect(ProductVariantModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(ProductVariantModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("eur-prices"),
},
{
$set: {
prices: [
{
currency_code: "eur",
amount: 100,
},
{
region_id: IdMap.getId("region-france"),
currency_code: "eur",
amount: 100,
},
],
},
}
)
})
it("creates usd prices", async () => {
await productVariantService.setCurrencyPrice(
IdMap.getId("eur-prices"),
"usd",
300
)
expect(ProductVariantModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(ProductVariantModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("eur-prices"),
},
{
$set: {
prices: [
{
currency_code: "eur",
amount: 1000,
},
{
region_id: IdMap.getId("region-france"),
currency_code: "eur",
amount: 950,
},
{
currency_code: "usd",
amount: 300,
},
],
},
}
)
})
})
describe("setRegionPrice", () => {
const productVariantService = new ProductVariantService({
productVariantModel: ProductVariantModelMock,
regionService: RegionServiceMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("creates a prices array if none exist", async () => {
await productVariantService.setCurrencyPrice(
IdMap.getId("no-prices"),
"usd",
100
)
expect(ProductVariantModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(ProductVariantModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("no-prices"),
},
{
$set: {
prices: [
{
currency_code: "usd",
amount: 100,
},
],
},
}
)
})
it("updates all eur prices", async () => {
await productVariantService.setCurrencyPrice(
IdMap.getId("eur-prices"),
"eur",
100
)
expect(ProductVariantModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(ProductVariantModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("eur-prices"),
},
{
$set: {
prices: [
{
currency_code: "eur",
amount: 100,
},
{
region_id: IdMap.getId("region-france"),
currency_code: "eur",
amount: 100,
},
],
},
}
)
})
it("creates usd prices", async () => {
await productVariantService.setCurrencyPrice(
IdMap.getId("eur-prices"),
"usd",
300
)
expect(ProductVariantModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(ProductVariantModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("eur-prices"),
},
{
$set: {
prices: [
{
currency_code: "eur",
amount: 1000,
},
{
region_id: IdMap.getId("region-france"),
currency_code: "eur",
amount: 950,
},
{
currency_code: "usd",
amount: 300,
},
],
},
}
)
})
})
})

View File

@@ -1,4 +1,3 @@
import mongoose from "mongoose"
import _ from "lodash"
import { Validator, MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
@@ -63,7 +62,8 @@ class CartService extends BaseService {
product: Validator.object().required(),
quantity: Validator.number()
.integer()
.min(1),
.min(1)
.default(1),
})
const lineItemSchema = Validator.object({
@@ -246,7 +246,15 @@ class CartService extends BaseService {
* @param {string} email - the email to add to cart
* @return {Promise} the result of the update operation
*/
updateEmail(cartId, email) {
async updateEmail(cartId, email) {
const cart = await this.retrieve(cartId)
if (!cart) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
"The cart was not found"
)
}
const schema = Validator.string()
.email()
.required()
@@ -268,7 +276,21 @@ class CartService extends BaseService {
)
}
updateBillingAddress(cartId, address) {
/**
* 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)
if (!cart) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
"The cart was not found"
)
}
const { value, error } = Validator.address().validate(address)
if (error) {
throw new MedusaError(
@@ -287,7 +309,21 @@ class CartService extends BaseService {
)
}
updateShippingAddress(cartId, address) {
/**
* 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)
if (!cart) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
"The cart was not found"
)
}
const { value, error } = Validator.address().validate(address)
if (error) {
throw new MedusaError(
@@ -306,6 +342,26 @@ class CartService extends BaseService {
)
}
/**
* 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
*/
setRegion(cartId, regionId) {
// Check if cart exists
// Check if region exists
//
// If the cart already has items, go through the items and update the prices
// based on new tax rate, new currency, region specific pricing.
//
// If addresses are set, clear the country code.
//
// If the cart has a shipping method, clear this.
//
// Update the region
}
/**
* Dedicated method to set metadata for a cart.
* To ensure that plugins does not overwrite each

View File

@@ -8,7 +8,12 @@ import { Validator, MedusaError } from "medusa-core-utils"
*/
class ProductVariantService extends BaseService {
/** @param { productVariantModel: (ProductVariantModel) } */
constructor({ productVariantModel, eventBusService, productService }) {
constructor({
productVariantModel,
eventBusService,
productService,
regionService,
}) {
super()
/** @private @const {ProductVariantModel} */
@@ -19,6 +24,9 @@ class ProductVariantService extends BaseService {
/** @private @const {ProductService} */
this.productService_ = productService
/** @private @const {RegionService} */
this.regionService_ = regionService
}
/**
@@ -94,6 +102,13 @@ class ProductVariantService extends BaseService {
update(variantId, update) {
const validatedId = this.validateId_(variantId)
if (update.prices) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Use setCurrencyPrices, setRegionPrices method to update prices field"
)
}
if (update.metadata) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
@@ -112,6 +127,155 @@ class ProductVariantService extends BaseService {
})
}
/**
* Sets the default price for the given currency.
* @param {string} variantId - the id of the variant to set prices for
* @param {string} currencyCode - the currency to set prices for
* @param {number} amount - the amount to set the price to
* @return {Promise} the result of the update operation
*/
async setCurrencyPrice(variantId, currencyCode, amount) {
const variant = await this.retrieve(variantId)
if (!variant) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Variant: ${variantId} was not found`
)
}
// If prices already exist we need to update all prices with the same currency
if (variant.prices.length) {
let foundDefault = false
const newPrices = variant.prices.map(moneyAmount => {
if (moneyAmount.currency_code === currencyCode) {
moneyAmount.amount = amount
if (!moneyAmount.region_id) {
foundDefault = true
}
}
return moneyAmount
})
// If there is no price entries for the currency we are updating we need
// to push it
if (!foundDefault) {
newPrices.push({
currency_code: currencyCode,
amount,
})
}
return this.productVariantModel_.updateOne(
{
_id: variant._id,
},
{
$set: {
prices: newPrices,
},
}
)
}
return this.productVariantModel_.updateOne(
{
_id: variant._id,
},
{
$set: {
prices: [
{
currency_code: currencyCode,
amount,
},
],
},
}
)
}
/**
* Sets the price of a specific region
* @param {string} variantId - the id of the variant to update
* @param {string} regionId - the id of the region to set price for
* @param {number} amount - the amount to set the price to
* @return {Promise} the result of the update operation
*/
async setRegionPrice(variantId, regionId, amount) {
const variant = await this.retrieve(variantId)
if (!variant) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Variant: ${variantId} was not found`
)
}
const region = await this.regionService_.retrieve(regionId)
if (!region) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Region: ${region} was not found`
)
}
// If prices already exist we need to update all prices with the same currency
if (variant.prices.length) {
let foundRegion = false
const newPrices = variant.prices.map(moneyAmount => {
if (moneyAmount.region_id === region._id) {
moneyAmount.amount = amount
foundRegion = true
}
return moneyAmount
})
// If the region doesn't exist in the prices we need to push it
if (!foundRegion) {
newPrices.push({
region_id: region._id,
currency_code: region.currency_code,
amount,
})
}
return this.productVariantModel_.updateOne(
{
_id: variant._id,
},
{
$set: {
prices: newPrices,
},
}
)
}
// Set the price both for default currency price and for the region
return this.productVariantModel_.updateOne(
{
_id: variant._id,
},
{
$set: {
prices: [
{
region_id: region._id,
currency_code: region.currency_code,
amount,
},
{
currency_code: region.currency_code,
amount,
},
],
},
}
)
}
/**
* Adds option value to a varaint.
* Fails when product with variant does not exists or