From 1d939342b448953bd99e9b4fc46c37181875d679 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Wed, 5 Feb 2020 15:14:19 +0100 Subject: [PATCH] Creates prices modifiers in productVariant --- .../src/models/__mocks__/product-variant.js | 50 +++++ .../medusa/src/models/schemas/money-amount.js | 1 + .../medusa/src/services/__mocks__/region.js | 24 ++- .../medusa/src/services/__tests__/cart.js | 47 ++++ .../src/services/__tests__/product-variant.js | 200 ++++++++++++++++++ packages/medusa/src/services/cart.js | 66 +++++- .../medusa/src/services/product-variant.js | 166 ++++++++++++++- 7 files changed, 547 insertions(+), 7 deletions(-) diff --git a/packages/medusa/src/models/__mocks__/product-variant.js b/packages/medusa/src/models/__mocks__/product-variant.js index 58685d2834..f6ce87bcc3 100644 --- a/packages/medusa/src/models/__mocks__/product-variant.js +++ b/packages/medusa/src/models/__mocks__/product-variant.js @@ -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) }), } diff --git a/packages/medusa/src/models/schemas/money-amount.js b/packages/medusa/src/models/schemas/money-amount.js index dd5564a238..5912fa60b1 100644 --- a/packages/medusa/src/models/schemas/money-amount.js +++ b/packages/medusa/src/models/schemas/money-amount.js @@ -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 }, }) diff --git a/packages/medusa/src/services/__mocks__/region.js b/packages/medusa/src/services/__mocks__/region.js index b9e67c937f..1d356e346e 100644 --- a/packages/medusa/src/services/__mocks__/region.js +++ b/packages/medusa/src/services/__mocks__/region.js @@ -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, + ]) }), } diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index 7c2384e677..1143837252 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -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", diff --git a/packages/medusa/src/services/__tests__/product-variant.js b/packages/medusa/src/services/__tests__/product-variant.js index 73f5976933..2f1bef9f00 100644 --- a/packages/medusa/src/services/__tests__/product-variant.js +++ b/packages/medusa/src/services/__tests__/product-variant.js @@ -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, + }, + ], + }, + } + ) + }) + }) }) diff --git a/packages/medusa/src/services/cart.js b/packages/medusa/src/services/cart.js index f0b9ab3c40..8bbae4da5d 100644 --- a/packages/medusa/src/services/cart.js +++ b/packages/medusa/src/services/cart.js @@ -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 diff --git a/packages/medusa/src/services/product-variant.js b/packages/medusa/src/services/product-variant.js index 92534fba23..767a9989b9 100644 --- a/packages/medusa/src/services/product-variant.js +++ b/packages/medusa/src/services/product-variant.js @@ -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