From 630bf3abc666ed880056ec5550d6a06d0bec3af5 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Wed, 29 Jul 2020 11:51:59 +0200 Subject: [PATCH] Adds gift card support --- .../src/api/routes/hooks/shipping.js | 19 +- .../src/services/klarna-provider.js | 56 ++++-- .../src/services/sendgrid.js | 17 +- .../src/subscribers/order.js | 4 + packages/medusa/package.json | 3 +- .../routes/admin/products/create-product.js | 3 +- .../routes/admin/products/create-variant.js | 1 + .../api/routes/admin/products/get-product.js | 1 + .../routes/admin/products/list-products.js | 1 + .../routes/admin/products/update-product.js | 7 + .../routes/admin/products/update-variant.js | 1 + .../create-shipping-option.js | 13 +- packages/medusa/src/models/__mocks__/cart.js | 89 +++++++++ .../medusa/src/models/__mocks__/discount.js | 2 +- .../src/models/__mocks__/shipping-profile.js | 12 +- .../medusa/src/models/schemas/discount.js | 1 + .../medusa/src/models/schemas/line-item.js | 1 + .../medusa/src/services/__mocks__/discount.js | 5 + .../src/services/__mocks__/shipping-option.js | 1 + .../services/__mocks__/shipping-profile.js | 2 +- .../medusa/src/services/__tests__/cart.js | 52 ++++- .../medusa/src/services/__tests__/discount.js | 29 +++ .../medusa/src/services/__tests__/order.js | 119 ++++++++++- .../services/__tests__/shipping-profile.js | 31 +++ packages/medusa/src/services/cart.js | 188 +++++++++++++----- packages/medusa/src/services/discount.js | 41 +++- packages/medusa/src/services/line-item.js | 1 + packages/medusa/src/services/order.js | 45 ++++- packages/medusa/src/services/product.js | 9 +- .../medusa/src/services/shipping-profile.js | 16 +- packages/medusa/src/services/totals.js | 25 +-- packages/medusa/src/subscribers/order.js | 36 ++++ packages/medusa/yarn.lock | 19 ++ 33 files changed, 722 insertions(+), 128 deletions(-) diff --git a/packages/medusa-payment-klarna/src/api/routes/hooks/shipping.js b/packages/medusa-payment-klarna/src/api/routes/hooks/shipping.js index 6e91ab6422..633c8390d2 100644 --- a/packages/medusa-payment-klarna/src/api/routes/hooks/shipping.js +++ b/packages/medusa-payment-klarna/src/api/routes/hooks/shipping.js @@ -10,15 +10,18 @@ export default async (req, res) => { const cart = await cartService.retrieve(merchant_data) const shippingOptions = await shippingProfileService.fetchCartOptions(cart) - const option = shippingOptions.find(({ _id }) => _id.equals(selected_shipping_option.id)) + const ids = selected_shipping_option.id.split(".") + await Promise.all(ids.map(async id => { + const option = shippingOptions.find(({ _id }) => _id.equals(id)) - if (option) { - const newCart = await cartService.addShippingMethod(cart._id, option._id, option.data) - const order = await klarnaProviderService.cartToKlarnaOrder(newCart) - res.json(order) - } else { - res.sendStatus(400) - } + if (option) { + await cartService.addShippingMethod(cart._id, option._id, option.data) + } + })) + + const newCart = await cartService.retrieve(cart._id) + const order = await klarnaProviderService.cartToKlarnaOrder(newCart) + res.json(order) } catch (error) { throw error } diff --git a/packages/medusa-payment-klarna/src/services/klarna-provider.js b/packages/medusa-payment-klarna/src/services/klarna-provider.js index bb1aa4a44e..546da2a2e3 100644 --- a/packages/medusa-payment-klarna/src/services/klarna-provider.js +++ b/packages/medusa-payment-klarna/src/services/klarna-provider.js @@ -26,7 +26,7 @@ class KlarnaProviderService extends PaymentService { this.klarnaOrderManagementUrl_ = "/ordermanagement/v1/orders" this.backendUrl_ = - process.env.BACKEND_URL || "https://7e9a5bc2a2eb.ngrok.io" + process.env.BACKEND_URL || "https://c8e1abe7d8b3.ngrok.io" this.totalsService_ = totalsService @@ -73,10 +73,14 @@ class KlarnaProviderService extends PaymentService { }) if (cart.shipping_methods.length) { - const shippingMethod = cart.shipping_methods[0] - const price = shippingMethod.price + const { name, price } = cart.shipping_methods.reduce((acc, next) => { + acc.name = [...acc.name, next.name] + acc.price += next.price + return acc + }, { name: [], price: 0 }) + order_lines.push({ - name: `${shippingMethod.name}`, + name: name.join(" + "), quantity: 1, type: "shipping_fee", unit_price: price * (1 + taxRate) * 100, @@ -167,18 +171,42 @@ class KlarnaProviderService extends PaymentService { } } - // If the cart does have shipping methods, set the selected shipping method - order.shipping_options = shippingOptions.map((so) => ({ - id: so._id, - name: so.name, - price: so.price * (1 + tax_rate) * 100, - tax_amount: so.price * tax_rate * 100, - tax_rate: tax_rate * 10000, - preselected: shippingOptions.length === 1 - })) + const partitioned = shippingOptions.reduce((acc, next) => { + if (acc[next.profile_id]) { + acc[next.profile_id] = [...acc[next.profile_id], next] + } else { + acc[next.profile_id] = [next] + } + return acc + }, {}) + + let f = (a, b) => [].concat(...a.map(a => b.map(b => [].concat(a, b)))) + let cartesian = (a, b, ...c) => b ? cartesian(f(a, b), ...c) : a + + const methods = Object.keys(partitioned).map(k => partitioned[k]) + const combinations = cartesian(...methods) + + + order.shipping_options = combinations.map((combination) => { + combination = Array.isArray(combination) ? combination : [combination] + const details = combination.reduce((acc, next) => { + acc.id = [...acc.id, next._id] + acc.name = [...acc.name, next.name] + acc.price += next.price + return acc + }, { id: [], name: [], price: 0 }) + + return { + id: details.id.join("."), + name: details.name.join(" + "), + price: details.price * (1 + tax_rate) * 100, + tax_amount: details.price * tax_rate * 100, + tax_rate: tax_rate * 10000, + preselected: combinations.length === 1 + } + }) } - console.log(order) return order } diff --git a/packages/medusa-plugin-sendgrid/src/services/sendgrid.js b/packages/medusa-plugin-sendgrid/src/services/sendgrid.js index d50d4a4397..c58379798d 100644 --- a/packages/medusa-plugin-sendgrid/src/services/sendgrid.js +++ b/packages/medusa-plugin-sendgrid/src/services/sendgrid.js @@ -33,6 +33,9 @@ class SendGridService extends BaseService { async transactionalEmail(event, data) { let templateId switch (event) { + case "order.gift_card_created": + templateId = this.options_.gift_card_created_template + break case "order.placed": templateId = this.options_.order_placed_template break @@ -55,12 +58,14 @@ class SendGridService extends BaseService { return } try { - return SendGrid.send({ - template_id: templateId, - from: this.options_.from, - to: data.email, - dynamic_template_data: data, - }) + if (templateId) { + return SendGrid.send({ + template_id: templateId, + from: this.options_.from, + to: data.email, + dynamic_template_data: data, + }) + } } catch (error) { throw error } diff --git a/packages/medusa-plugin-sendgrid/src/subscribers/order.js b/packages/medusa-plugin-sendgrid/src/subscribers/order.js index af82482b43..f345b6e192 100644 --- a/packages/medusa-plugin-sendgrid/src/subscribers/order.js +++ b/packages/medusa-plugin-sendgrid/src/subscribers/order.js @@ -4,6 +4,10 @@ class OrderSubscriber { this.eventBus_ = eventBusService + this.eventBus_.subscribe("order.gift_card_created", async (order) => { + await this.sendgridService_.transactionalEmail("order.gift_card_created", order) + }) + this.eventBus_.subscribe("order.placed", async (order) => { await this.sendgridService_.transactionalEmail("order.placed", order) }) diff --git a/packages/medusa/package.json b/packages/medusa/package.json index 9a91c5a7d9..53872feb28 100644 --- a/packages/medusa/package.json +++ b/packages/medusa/package.json @@ -67,7 +67,8 @@ "passport-http-bearer": "^1.0.1", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", + "randomatic": "^3.1.1", "winston": "^3.2.1" }, "gitHead": "35e0930650d5f4aedf2610749cd131ae8b7e17cc" -} \ No newline at end of file +} diff --git a/packages/medusa/src/api/routes/admin/products/create-product.js b/packages/medusa/src/api/routes/admin/products/create-product.js index df1e31f1cb..dcb92c2720 100644 --- a/packages/medusa/src/api/routes/admin/products/create-product.js +++ b/packages/medusa/src/api/routes/admin/products/create-product.js @@ -3,7 +3,7 @@ import { MedusaError, Validator } from "medusa-core-utils" export default async (req, res) => { const schema = Validator.object().keys({ title: Validator.string().required(), - description: Validator.string(), + description: Validator.string().allow(""), tags: Validator.string(), is_giftcard: Validator.boolean().default(false), options: Validator.array().items({ @@ -15,6 +15,7 @@ export default async (req, res) => { title: Validator.string().required(), sku: Validator.string(), ean: Validator.string(), + barcode: Validator.string(), prices: Validator.array() .items({ currency_code: Validator.string().required(), diff --git a/packages/medusa/src/api/routes/admin/products/create-variant.js b/packages/medusa/src/api/routes/admin/products/create-variant.js index eb7b744206..2507a064b3 100644 --- a/packages/medusa/src/api/routes/admin/products/create-variant.js +++ b/packages/medusa/src/api/routes/admin/products/create-variant.js @@ -36,6 +36,7 @@ export default async (req, res) => { [ "title", "description", + "is_giftcard", "tags", "thumbnail", "handle", diff --git a/packages/medusa/src/api/routes/admin/products/get-product.js b/packages/medusa/src/api/routes/admin/products/get-product.js index 3bee285762..4356933146 100644 --- a/packages/medusa/src/api/routes/admin/products/get-product.js +++ b/packages/medusa/src/api/routes/admin/products/get-product.js @@ -9,6 +9,7 @@ export default async (req, res) => { [ "title", "description", + "is_giftcard", "tags", "thumbnail", "handle", diff --git a/packages/medusa/src/api/routes/admin/products/list-products.js b/packages/medusa/src/api/routes/admin/products/list-products.js index 3114333737..47f0f46395 100644 --- a/packages/medusa/src/api/routes/admin/products/list-products.js +++ b/packages/medusa/src/api/routes/admin/products/list-products.js @@ -10,6 +10,7 @@ export default async (req, res) => { [ "title", "description", + "is_giftcard", "tags", "thumbnail", "handle", diff --git a/packages/medusa/src/api/routes/admin/products/update-product.js b/packages/medusa/src/api/routes/admin/products/update-product.js index f4526c4afd..f3c79f2af3 100644 --- a/packages/medusa/src/api/routes/admin/products/update-product.js +++ b/packages/medusa/src/api/routes/admin/products/update-product.js @@ -18,6 +18,13 @@ export default async (req, res) => { title: Validator.string().optional(), sku: Validator.string().optional(), ean: Validator.string().optional(), + published: Validator.boolean(), + image: Validator.string() + .allow("") + .optional(), + barcode: Validator.string() + .allow("") + .optional(), prices: Validator.array().items( Validator.object() .keys({ diff --git a/packages/medusa/src/api/routes/admin/products/update-variant.js b/packages/medusa/src/api/routes/admin/products/update-variant.js index 92796fe84b..6c9aee0f85 100644 --- a/packages/medusa/src/api/routes/admin/products/update-variant.js +++ b/packages/medusa/src/api/routes/admin/products/update-variant.js @@ -87,6 +87,7 @@ export default async (req, res) => { "options", "thumbnail", "variants", + "is_giftcard", "published", ], ["variants"] diff --git a/packages/medusa/src/api/routes/admin/shipping-options/create-shipping-option.js b/packages/medusa/src/api/routes/admin/shipping-options/create-shipping-option.js index 1704ddb1d8..1261c6afbb 100644 --- a/packages/medusa/src/api/routes/admin/shipping-options/create-shipping-option.js +++ b/packages/medusa/src/api/routes/admin/shipping-options/create-shipping-option.js @@ -33,13 +33,14 @@ export default async (req, res) => { const shippingProfileService = req.scope.resolve("shippingProfileService") // Add to default shipping profile - const { _id } = await shippingProfileService.retrieveDefault() + if (!value.profile_id) { + const { _id } = await shippingProfileService.retrieveDefault() + value.profile_id = _id + } - const data = await optionService.create({ - ...value, - profile_id: _id, - }) - await shippingProfileService.addShippingOption(_id, data._id) + const data = await optionService.create(value) + + await shippingProfileService.addShippingOption(value.profile_id, data._id) res.status(200).json({ shipping_option: data }) } catch (err) { diff --git a/packages/medusa/src/models/__mocks__/cart.js b/packages/medusa/src/models/__mocks__/cart.js index 238953e88d..30e2e9c75e 100644 --- a/packages/medusa/src/models/__mocks__/cart.js +++ b/packages/medusa/src/models/__mocks__/cart.js @@ -41,6 +41,7 @@ export const carts = { cartWithPaySessionsDifRegion: { _id: IdMap.getId("cartWithPaySessionsDifRegion"), region_id: IdMap.getId("region-france"), + total: 1, items: [ { _id: IdMap.getId("existingLine"), @@ -81,6 +82,7 @@ export const carts = { }, cartWithPaySessions: { _id: IdMap.getId("cartWithPaySessions"), + total: 1, region_id: IdMap.getId("testRegion"), shipping_methods: [], items: [ @@ -123,6 +125,7 @@ export const carts = { }, cartWithLine: { _id: IdMap.getId("cartWithLine"), + total: 1, title: "test", region_id: IdMap.getId("testRegion"), items: [ @@ -166,6 +169,92 @@ export const carts = { discounts: [], customer_id: "", }, + withGiftCard: { + _id: IdMap.getId("withGiftCard"), + region_id: IdMap.getId("region-france"), + items: [ + { + _id: IdMap.getId("existingLine"), + title: "merge line", + description: "This is a new line", + is_giftcard: false, + 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, + }, + { + _id: IdMap.getId("giftline"), + title: "GiftCard", + description: "Gift card line", + thumbnail: "test-img-yeah.com/thumb", + metadata: { + name: "Test Name", + }, + is_giftcard: true, + content: { + unit_price: 100, + variant: { + _id: IdMap.getId("giftCardVar"), + }, + product: { + _id: IdMap.getId("giftCardProd"), + }, + quantity: 1, + }, + quantity: 1, + }, + ], + email: "test", + payment_sessions: [ + { + provider_id: "default_provider", + data: { + money_id: "success", + }, + }, + ], + payment_method: { + provider_id: "default_provider", + data: { + money_id: "success", + }, + }, + shipping_methods: [ + { + provider_id: "gls", + data: { + yes: "sir", + }, + }, + ], + shipping_address: { + first_name: "hi", + last_name: "you", + country_code: "DK", + city: "of lights", + address_1: "You bet street", + postal_code: "4242", + }, + billing_address: { + first_name: "hi", + last_name: "you", + country_code: "DK", + city: "of lights", + address_1: "You bet street", + postal_code: "4242", + }, + discounts: [], + customer_id: "", + }, completeCart: { _id: IdMap.getId("complete-cart"), region_id: IdMap.getId("region-france"), diff --git a/packages/medusa/src/models/__mocks__/discount.js b/packages/medusa/src/models/__mocks__/discount.js index 37f2aef200..23594351d4 100644 --- a/packages/medusa/src/models/__mocks__/discount.js +++ b/packages/medusa/src/models/__mocks__/discount.js @@ -122,7 +122,7 @@ export const discounts = { } export const DiscountModelMock = { - create: jest.fn().mockReturnValue(Promise.resolve()), + create: jest.fn().mockImplementation(data => Promise.resolve(data)), updateOne: jest.fn().mockImplementation((query, update) => { return Promise.resolve() }), diff --git a/packages/medusa/src/models/__mocks__/shipping-profile.js b/packages/medusa/src/models/__mocks__/shipping-profile.js index 42b1b2fa80..1f7837a5fd 100644 --- a/packages/medusa/src/models/__mocks__/shipping-profile.js +++ b/packages/medusa/src/models/__mocks__/shipping-profile.js @@ -11,7 +11,13 @@ export const profiles = { _id: IdMap.getId("profile1"), name: "Profile One", products: [IdMap.getId("product1")], - shipping_options: [IdMap.getId("shipping1")], + shipping_options: [IdMap.getId("shipping_1")], + }, + profile2: { + _id: IdMap.getId("profile2"), + name: "Profile two", + products: [IdMap.getId("product2")], + shipping_options: [IdMap.getId("shipping_2")], }, } @@ -21,6 +27,10 @@ export const ShippingProfileModelMock = { return Promise.resolve() }), find: jest.fn().mockImplementation(query => { + if (query.products && query.products.$in) { + return Promise.resolve([profiles.profile1, profiles.profile2]) + } + return Promise.resolve([]) }), deleteOne: jest.fn().mockReturnValue(Promise.resolve()), diff --git a/packages/medusa/src/models/schemas/discount.js b/packages/medusa/src/models/schemas/discount.js index 3ccf038215..c0fc8b96e2 100644 --- a/packages/medusa/src/models/schemas/discount.js +++ b/packages/medusa/src/models/schemas/discount.js @@ -3,6 +3,7 @@ import mongoose from "mongoose" import DiscountRule from "./discount-rule" export default new mongoose.Schema({ + is_giftcard: { type: Boolean }, code: { type: String }, discount_rule: { type: DiscountRule }, usage_count: { type: Number }, diff --git a/packages/medusa/src/models/schemas/line-item.js b/packages/medusa/src/models/schemas/line-item.js index 1f3d1009e9..aafcc942d8 100644 --- a/packages/medusa/src/models/schemas/line-item.js +++ b/packages/medusa/src/models/schemas/line-item.js @@ -8,6 +8,7 @@ export default new mongoose.Schema({ description: { type: String }, thumbnail: { type: String }, is_giftcard: { type: Boolean, default: false }, + has_shipping: { type: Boolean, default: false }, // mongoose doesn't allow multi-type validation but this field allows both // an object containing: diff --git a/packages/medusa/src/services/__mocks__/discount.js b/packages/medusa/src/services/__mocks__/discount.js index e287a99393..0c351ff4b8 100644 --- a/packages/medusa/src/services/__mocks__/discount.js +++ b/packages/medusa/src/services/__mocks__/discount.js @@ -46,6 +46,11 @@ export const DiscountServiceMock = { removeRegion: jest.fn().mockReturnValue(Promise.resolve()), addValidVariant: jest.fn().mockReturnValue(Promise.resolve()), removeValidVariant: jest.fn().mockReturnValue(Promise.resolve()), + generateGiftCard: jest.fn().mockReturnValue( + Promise.resolve({ + _id: IdMap.getId("gift_card_id"), + }) + ), } const mock = jest.fn().mockImplementation(() => { diff --git a/packages/medusa/src/services/__mocks__/shipping-option.js b/packages/medusa/src/services/__mocks__/shipping-option.js index 9ee657d057..97b4e32d90 100644 --- a/packages/medusa/src/services/__mocks__/shipping-option.js +++ b/packages/medusa/src/services/__mocks__/shipping-option.js @@ -110,6 +110,7 @@ export const ShippingOptionServiceMock = { _id: IdMap.getId("fail"), }) } + return Promise.resolve({ _id: methodId }) }), delete: jest.fn().mockReturnValue(Promise.resolve()), } diff --git a/packages/medusa/src/services/__mocks__/shipping-profile.js b/packages/medusa/src/services/__mocks__/shipping-profile.js index 6e49c1dd81..97028bf88c 100644 --- a/packages/medusa/src/services/__mocks__/shipping-profile.js +++ b/packages/medusa/src/services/__mocks__/shipping-profile.js @@ -29,7 +29,7 @@ export const ShippingProfileServiceMock = { if (data === IdMap.getId("profile1")) { return Promise.resolve(profiles.other) } - return Promise.resolve() + return Promise.resolve(profiles.default) }), retrieveGiftCardDefault: jest.fn().mockImplementation(data => { return Promise.resolve({ _id: IdMap.getId("giftCardProfile") }) diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index 2d538ae06f..a1d79e7f63 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -10,6 +10,7 @@ import { RegionServiceMock } from "../__mocks__/region" import { EventBusServiceMock } from "../__mocks__/event-bus" import { CustomerServiceMock } from "../__mocks__/customer" import { ShippingOptionServiceMock } from "../__mocks__/shipping-option" +import { TotalsServiceMock } from "../__mocks__/totals" import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile" import { CartModelMock, carts } from "../../models/__mocks__/cart" import { LineItemServiceMock } from "../__mocks__/line-item" @@ -193,7 +194,12 @@ describe("CartService", () => { _id: IdMap.getId("emptyCart"), }, { - $push: { items: lineItem }, + $push: { + items: { + ...lineItem, + has_shipping: false, + }, + }, } ) }) @@ -225,7 +231,10 @@ describe("CartService", () => { "items._id": IdMap.getId("existingLine"), }, { - $set: { "items.$.quantity": 20 }, + $set: { + "items.$.quantity": 20, + "items.$.has_shipping": false, + }, } ) }) @@ -267,7 +276,12 @@ describe("CartService", () => { _id: IdMap.getId("cartWithLine"), }, { - $push: { items: lineItem }, + $push: { + items: { + ...lineItem, + has_shipping: false, + }, + }, } ) }) @@ -390,10 +404,9 @@ describe("CartService", () => { expect(CartModelMock.updateOne).toHaveBeenCalledWith( { _id: IdMap.getId("cartWithLine"), - "items._id": IdMap.getId("existingLine"), }, { - $set: { "items.$.quantity": 9 }, + $pull: { items: { _id: IdMap.getId("existingLine") } }, } ) }) @@ -905,6 +918,7 @@ describe("CartService", () => { cartModel: CartModelMock, regionService: RegionServiceMock, paymentProviderService: PaymentProviderServiceMock, + totalsService: TotalsServiceMock, eventBusService: EventBusServiceMock, }) @@ -1183,6 +1197,26 @@ describe("CartService", () => { }, { $set: { + items: [ + { + _id: IdMap.getId("existingLine"), + title: "merge line", + description: "This is a new line", + has_shipping: true, + 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, + }, + ], shipping_methods: [ { _id: IdMap.getId("freeShipping"), @@ -1221,6 +1255,10 @@ describe("CartService", () => { }, { $set: { + items: carts.frCart.items.map(i => ({ + ...i, + has_shipping: false, + })), shipping_methods: [ { _id: IdMap.getId("freeShipping"), @@ -1261,6 +1299,10 @@ describe("CartService", () => { }, { $set: { + items: carts.frCart.items.map(i => ({ + ...i, + has_shipping: false, + })), shipping_methods: [ { _id: IdMap.getId("freeShipping"), diff --git a/packages/medusa/src/services/__tests__/discount.js b/packages/medusa/src/services/__tests__/discount.js index b3f43fdff5..1de10e3cf0 100644 --- a/packages/medusa/src/services/__tests__/discount.js +++ b/packages/medusa/src/services/__tests__/discount.js @@ -6,6 +6,7 @@ import { } from "../../models/__mocks__/dynamic-discount-code" import { IdMap } from "medusa-test-utils" import { ProductVariantServiceMock } from "../__mocks__/product-variant" +import { EventBusServiceMock } from "../__mocks__/event-bus" import { RegionServiceMock } from "../__mocks__/region" describe("DiscountService", () => { @@ -274,4 +275,32 @@ describe("DiscountService", () => { ) }) }) + + describe("generateGiftCard", () => { + const discountService = new DiscountService({ + discountModel: DiscountModelMock, + regionService: RegionServiceMock, + eventBusService: EventBusServiceMock, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("calls model layer create", async () => { + await discountService.generateGiftCard(100, IdMap.getId("testRegion")) + + expect(DiscountModelMock.create).toHaveBeenCalledTimes(1) + expect(DiscountModelMock.create).toHaveBeenCalledWith({ + code: expect.stringMatching(/(([A-Z0-9]){4}(-?)){4}/), + is_giftcard: true, + discount_rule: { + type: "fixed", + allocation: "total", + value: 100, + }, + regions: [IdMap.getId("testRegion")], + }) + }) + }) }) diff --git a/packages/medusa/src/services/__tests__/order.js b/packages/medusa/src/services/__tests__/order.js index 8ee3ac0fcd..feb9f78b7c 100644 --- a/packages/medusa/src/services/__tests__/order.js +++ b/packages/medusa/src/services/__tests__/order.js @@ -3,6 +3,7 @@ import { OrderModelMock, orders } from "../../models/__mocks__/order" import { carts } from "../../models/__mocks__/cart" import OrderService from "../order" import { PaymentProviderServiceMock } from "../__mocks__/payment-provider" +import { DiscountServiceMock } from "../__mocks__/discount" import { FulfillmentProviderServiceMock } from "../__mocks__/fulfillment-provider" import { ShippingProfileServiceMock } from "../__mocks__/shipping-profile" import { TotalsServiceMock } from "../__mocks__/totals" @@ -36,6 +37,7 @@ describe("OrderService", () => { const orderService = new OrderService({ orderModel: OrderModelMock, paymentProviderService: PaymentProviderServiceMock, + discountService: DiscountServiceMock, regionService: RegionServiceMock, eventBusService: EventBusServiceMock, }) @@ -60,6 +62,83 @@ describe("OrderService", () => { session: expect.anything(), }) }) + + it("creates cart with gift card", async () => { + await orderService.createFromCart(carts.withGiftCard) + + const order = { + ...carts.withGiftCard, + items: [ + { + _id: IdMap.getId("existingLine"), + title: "merge line", + description: "This is a new line", + is_giftcard: false, + 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, + }, + { + _id: IdMap.getId("giftline"), + title: "GiftCard", + description: "Gift card line", + thumbnail: "test-img-yeah.com/thumb", + metadata: { + giftcard: IdMap.getId("gift_card_id"), + name: "Test Name", + }, + is_giftcard: true, + content: { + unit_price: 100, + variant: { + _id: IdMap.getId("giftCardVar"), + }, + product: { + _id: IdMap.getId("giftCardProd"), + }, + quantity: 1, + }, + quantity: 1, + }, + ], + currency_code: "eur", + cart_id: carts.withGiftCard._id, + } + + delete order._id + delete order.payment_sessions + + expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(2) + expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + "order.gift_card_created", + { + currency_code: "eur", + tax_rate: 0.25, + email: "test", + giftcard: expect.any(Object), + } + ) + + expect(DiscountServiceMock.generateGiftCard).toHaveBeenCalledTimes(1) + expect(DiscountServiceMock.generateGiftCard).toHaveBeenCalledWith( + 100, + IdMap.getId("region-france") + ) + + expect(OrderModelMock.create).toHaveBeenCalledTimes(1) + expect(OrderModelMock.create).toHaveBeenCalledWith([order], { + session: expect.anything(), + }) + }) }) describe("retrieve", () => { @@ -297,11 +376,9 @@ describe("OrderService", () => { }) it("throws if payment is already processed", async () => { - try { - await orderService.capturePayment(IdMap.getId("payed-order")) - } catch (error) { - expect(error.message).toEqual("Payment already captured") - } + await expect( + orderService.capturePayment(IdMap.getId("payed-order")) + ).rejects.toThrow("Payment already captured") }) }) @@ -324,16 +401,36 @@ describe("OrderService", () => { expect(OrderModelMock.updateOne).toHaveBeenCalledTimes(1) expect(OrderModelMock.updateOne).toHaveBeenCalledWith( { _id: IdMap.getId("test-order") }, - { $set: { fulfillment_status: "fulfilled" } } + { + $set: { + fulfillment_status: "fulfilled", + shipping_methods: [ + { + _id: IdMap.getId("expensiveShipping"), + items: [], + name: "Expensive Shipping", + price: 100, + profile_id: IdMap.getId("default"), + provider_id: "default_provider", + }, + { + _id: IdMap.getId("freeShipping"), + items: [], + name: "Free Shipping", + price: 10, + profile_id: IdMap.getId("profile1"), + provider_id: "default_provider", + }, + ], + }, + } ) }) it("throws if payment is already processed", async () => { - try { - await orderService.createFulfillment(IdMap.getId("fulfilled-order")) - } catch (error) { - expect(error.message).toEqual("Order is already fulfilled") - } + await expect( + orderService.createFulfillment(IdMap.getId("fulfilled-order")) + ).rejects.toThrow("Order is already fulfilled") }) }) diff --git a/packages/medusa/src/services/__tests__/shipping-profile.js b/packages/medusa/src/services/__tests__/shipping-profile.js index 9a49f349bd..fc7a986db4 100644 --- a/packages/medusa/src/services/__tests__/shipping-profile.js +++ b/packages/medusa/src/services/__tests__/shipping-profile.js @@ -353,6 +353,37 @@ describe("ShippingProfileService", () => { }) }) + describe("fetchCartOptions", () => { + const profileService = new ShippingProfileService({ + shippingProfileModel: ShippingProfileModelMock, + shippingOptionService: ShippingOptionServiceMock, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("fetches correct options", async () => { + await profileService.fetchCartOptions({ + items: [ + { + content: { product: { _id: IdMap.getId("product_1") } }, + }, + { + content: { product: { _id: IdMap.getId("product_2") } }, + }, + ], + }) + + expect(ShippingProfileModelMock.find).toBeCalledTimes(1) + expect(ShippingProfileModelMock.find).toBeCalledWith({ + products: { $in: [IdMap.getId("product_1"), IdMap.getId("product_2")] }, + }) + + expect(ShippingOptionServiceMock.validateCartOption).toBeCalledTimes(2) + }) + }) + describe("addShippingOption", () => { const profileService = new ShippingProfileService({ shippingProfileModel: ShippingProfileModelMock, diff --git a/packages/medusa/src/services/cart.js b/packages/medusa/src/services/cart.js index dcc33b3de2..dbefa68c33 100644 --- a/packages/medusa/src/services/cart.js +++ b/packages/medusa/src/services/cart.js @@ -249,6 +249,23 @@ class CartService extends BaseService { return c } + /** + * Returns an array of product ids in a line item. + * @param {LineItem} item - the line item to fetch products from + * @return {[string]} an array of product ids + */ + getItemProducts_(item) { + // Find all the products in the line item + const products = [] + if (Array.isArray(item.content)) { + item.content.forEach(c => products.push(`${c.product._id}`)) + } else { + products.push(`${item.content.product._id}`) + } + + return products + } + /** * Removes a line item from the cart. * @param {string} cartId - the id of the cart that we will remove from @@ -258,33 +275,42 @@ class CartService extends BaseService { 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 + const update = { + $pull: { items: { _id: itemToRemove._id } }, + } - return this.cartModel_ - .updateOne( - { - _id: cartId, - "items._id": itemToRemove._id, - }, - { - $set: { - "items.$.quantity": newQuantity, - }, + // Remove shipping methods if they are not needed + if (cart.shipping_methods && cart.shipping_methods.length) { + const filteredItems = cart.items.filter(i => !i._id.equals(lineItemId)) + + let newShippingMethods = await Promise.all( + cart.shipping_methods.map(async m => { + const profile = await this.shippingProfileService_.retrieve( + m.profile_id + ) + const hasItem = filteredItems.find(item => { + const products = this.getItemProducts_(item) + return products.some(p => profile.products.includes(p)) + }) + + if (hasItem) { + return m } - ) - .then(result => { - // Notify subscribers - this.eventBus_.emit(CartService.Events.UPDATED, result) - return result + + return null }) + ) + newShippingMethods = newShippingMethods.filter(n => !!n) + + if (newShippingMethods.length !== cart.shipping_methods.length) { + update.$set = { + shipping_methods: newShippingMethods, + } + } } return this.cartModel_ @@ -292,11 +318,7 @@ class CartService extends BaseService { { _id: cartId, }, - { - $pull: { - items: { _id: itemToRemove._id }, - }, - } + update ) .then(result => { // Notify subscribers @@ -305,6 +327,34 @@ class CartService extends BaseService { }) } + /** + * Checks if a given line item has a shipping method that can fulfill it. + * Returns true if all products in the cart can be fulfilled with the current + * shipping methods. + * @param {Cart} cart - the cart + * @param {LineItem} lineItem - the line item + * @return {boolean} + */ + async validateLineItemShipping_(shippingMethods, lineItem) { + if (shippingMethods && shippingMethods.length) { + const profiles = await Promise.all( + shippingMethods.map(m => + this.shippingProfileService_.retrieve(m.profile_id) + ) + ) + + const products = this.getItemProducts_(lineItem) + + // Check if there is a shipping method for each product + const hasShipping = products.map( + p => !!profiles.find(profile => profile.products.includes(p)) + ) + return hasShipping.every(b => b) + } + + return false + } + /** * Adds a line item to the cart. * @param {string} cartId - the id of the cart that we will add to @@ -318,6 +368,11 @@ class CartService extends BaseService { this.lineItemService_.isEqual(line, validatedLineItem) ) + const hasShipping = await this.validateLineItemShipping_( + cart.shipping_methods, + 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) { @@ -345,6 +400,7 @@ class CartService extends BaseService { { $set: { "items.$.quantity": newQuantity, + "items.$.has_shipping": hasShipping, }, } ) @@ -375,7 +431,12 @@ class CartService extends BaseService { _id: cartId, }, { - $push: { items: validatedLineItem }, + $push: { + items: { + ...validatedLineItem, + has_shipping: hasShipping, + }, + }, } ) .then(result => { @@ -808,6 +869,25 @@ class CartService extends BaseService { const cart = await this.retrieve(cartId) const region = await this.regionService_.retrieve(cart.region_id) + const total = await this.totalsService_.getTotal(cart) + + if (total === 0) { + return this.cartModel_ + .updateOne( + { + _id: cart._id, + }, + { + $set: { payment_sessions: [] }, + } + ) + .then(result => { + // Notify subscribers + this.eventBus_.emit(CartService.Events.UPDATED, result) + return result + }) + } + // If there are existing payment sessions ensure that these are up to date let sessions = [] if (cart.payment_sessions && cart.payment_sessions.length) { @@ -817,10 +897,19 @@ class CartService extends BaseService { return null } - const data = await this.paymentProviderService_.updateSession( - pSession, - cart - ) + let data + try { + data = await this.paymentProviderService_.updateSession( + pSession, + cart + ) + } catch (err) { + data = await this.paymentProviderService_.createSession( + pSession.provider_id, + cart + ) + } + return { provider_id: pSession.provider_id, data, @@ -981,13 +1070,27 @@ class CartService extends BaseService { newMethods.push(option) } + const newItems = await Promise.all( + cart.items.map(async item => { + const hasShipping = await this.validateLineItemShipping_( + newMethods, + item + ) + + return { + ...item, + has_shipping: hasShipping, + } + }) + ) + return this.cartModel_ .updateOne( { _id: cart._id, }, { - $set: { shipping_methods: newMethods }, + $set: { shipping_methods: newMethods, items: newItems }, } ) .then(result => { @@ -1045,20 +1148,15 @@ class CartService extends BaseService { 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, - // } - // }) - // ) - //} + if (cart.discounts && cart.discounts.length) { + const newDiscounts = cart.discounts.map(d => { + if (d.regions.includes(regionId)) { + return d + } + }) + + update.discounts = newDiscounts.filter(d => !!d) + } // Payment methods are region specific so the user needs to find a // new payment method diff --git a/packages/medusa/src/services/discount.js b/packages/medusa/src/services/discount.js index 5f1b98f6d1..c0241d53e7 100644 --- a/packages/medusa/src/services/discount.js +++ b/packages/medusa/src/services/discount.js @@ -1,6 +1,7 @@ +import _ from "lodash" +import randomize from "randomatic" import { BaseService } from "medusa-interfaces" import { Validator, MedusaError } from "medusa-core-utils" -import _ from "lodash" /** * Provides layer to manipulate discounts. @@ -13,6 +14,7 @@ class DiscountService extends BaseService { totalsService, productVariantService, regionService, + eventBusService, }) { super() @@ -30,6 +32,9 @@ class DiscountService extends BaseService { /** @private @const {RegionService} */ this.regionService_ = regionService + + /** @private @const {EventBus} */ + this.eventBus_ = eventBusService } /** @@ -60,7 +65,7 @@ class DiscountService extends BaseService { description: Validator.string(), type: Validator.string().required(), value: Validator.number() - .positive() + .min(0) .required(), allocation: Validator.string().required(), valid_for: Validator.array().items(Validator.string()), @@ -210,6 +215,38 @@ class DiscountService extends BaseService { ) } + /** + * Generates a gift card with the specified value which is valid in the + * specified region. + * @param {number} value - the value that the gift card represents + * @param {string} regionId - the id of the region in which the gift card can + * be used + * @return {Discount} the newly created gift card + */ + async generateGiftCard(value, regionId) { + const region = await this.regionService_.retrieve(regionId) + + const code = [ + randomize("A0", 4), + randomize("A0", 4), + randomize("A0", 4), + randomize("A0", 4), + ].join("-") + + const discountRule = this.validateDiscountRule_({ + type: "fixed", + allocation: "total", + value, + }) + + return this.discountModel_.create({ + code, + discount_rule: discountRule, + is_giftcard: true, + regions: [region._id], + }) + } + /** * Creates a dynamic code for a discount id. * @param {string} discountId - the id of the discount to create a code for diff --git a/packages/medusa/src/services/line-item.js b/packages/medusa/src/services/line-item.js index d9f6317a9e..823f9b59cb 100644 --- a/packages/medusa/src/services/line-item.js +++ b/packages/medusa/src/services/line-item.js @@ -37,6 +37,7 @@ class LineItemService extends BaseService { const lineItemSchema = Validator.object({ title: Validator.string().required(), + is_giftcard: Validator.bool().optional(), description: Validator.string() .allow("") .optional(), diff --git a/packages/medusa/src/services/order.js b/packages/medusa/src/services/order.js index b065ba8b6c..d1f385e8e7 100644 --- a/packages/medusa/src/services/order.js +++ b/packages/medusa/src/services/order.js @@ -4,6 +4,7 @@ import { BaseService } from "medusa-interfaces" class OrderService extends BaseService { static Events = { + GIFT_CARD_CREATED: "order.gift_card_created", PLACED: "order.placed", UPDATED: "order.updated", CANCELLED: "order.cancelled", @@ -14,6 +15,7 @@ class OrderService extends BaseService { orderModel, paymentProviderService, shippingProfileService, + discountService, fulfillmentProviderService, lineItemService, totalsService, @@ -43,6 +45,9 @@ class OrderService extends BaseService { /** @private @const {RegionService} */ this.regionService_ = regionService + /** @private @const {DiscountService} */ + this.discountService_ = discountService + /** @private @const {EventBus} */ this.eventBus_ = eventBusService } @@ -306,6 +311,33 @@ class OrderService extends BaseService { paymentSession.data ) + // Generate gift cards if in cart + const items = await Promise.all( + cart.items.map(async i => { + if (i.is_giftcard) { + const giftcard = await this.discountService_ + .generateGiftCard(i.content.unit_price, region._id) + .then(result => { + this.eventBus_.emit(OrderService.Events.GIFT_CARD_CREATED, { + currency_code: region.currency_code, + tax_rate: region.tax_rate, + giftcard: result, + email: cart.email, + }) + return result + }) + return { + ...i, + metadata: { + ...i.metadata, + giftcard: giftcard._id, + }, + } + } + return i + }) + ) + const o = { payment_method: { provider_id: paymentSession.provider_id, @@ -313,7 +345,7 @@ class OrderService extends BaseService { }, discounts: cart.discounts, shipping_methods: cart.shipping_methods, - items: cart.items, + items, shipping_address: cart.shipping_address, billing_address: cart.shipping_address, region_id: cart.region_id, @@ -329,7 +361,7 @@ class OrderService extends BaseService { // Emit and return this.eventBus_.emit(OrderService.Events.PLACED, orderDocument[0]) - return orderDocument[0].toObject() + return orderDocument[0] }) .then(() => this.orderModel_.findOne({ cart_id: cart._id })) } @@ -541,14 +573,17 @@ class OrderService extends BaseService { } // partition order items to their dedicated shipping method - order.shipping_methods = await this.partitionItems_(shipping_methods, items) + updateFields.shipping_methods = await this.partitionItems_( + shipping_methods, + items + ) await Promise.all( - order.shipping_methods.map(method => { + updateFields.shipping_methods.map(method => { const provider = this.fulfillmentProviderService_.retrieveProvider( method.provider_id ) - provider.createOrder(method.data, method.items) + return provider.createOrder(method.data, method.items) }) ) diff --git a/packages/medusa/src/services/product.js b/packages/medusa/src/services/product.js index 1bd5729fb6..a91ed67639 100644 --- a/packages/medusa/src/services/product.js +++ b/packages/medusa/src/services/product.js @@ -143,7 +143,14 @@ class ProductService extends BaseService { } if (update.variants) { - update.variants = await Promise.all( + const existingVariants = await this.retrieveVariants(validatedId) + for (const existing of existingVariants) { + if (!update.variants.find(v => v._id && existing._id.equals(v._id))) { + await this.deleteVariant(productId, existing._id) + } + } + + await Promise.all( update.variants.map(async variant => { if (variant._id) { if (variant.prices && variant.prices.length) { diff --git a/packages/medusa/src/services/shipping-profile.js b/packages/medusa/src/services/shipping-profile.js index 52cc5ba5e8..ec63af4698 100644 --- a/packages/medusa/src/services/shipping-profile.js +++ b/packages/medusa/src/services/shipping-profile.js @@ -101,7 +101,7 @@ class ShippingProfileService extends BaseService { * Retrieves the default gift card profile * @return the shipping profile for gift cards */ - async retrieveGiftCardProfile() { + async retrieveGiftCardDefault() { return await this.profileModel_ .findOne({ name: "default_gift_card_profile" }) .catch(err => { @@ -115,7 +115,7 @@ class ShippingProfileService extends BaseService { * @return {Promise} the shipping profile */ async createGiftCardDefault() { - const profile = await this.retrieveGiftCardProfile() + const profile = await this.retrieveGiftCardDefault() if (!profile) { return this.profileModel_.create({ name: "default_gift_card_profile" }) } @@ -388,13 +388,21 @@ class ShippingProfileService extends BaseService { ) const options = await Promise.all( - optionIds.map(oId => { - return this.shippingOptionService_ + optionIds.map(async oId => { + const option = await this.shippingOptionService_ .validateCartOption(oId, cart) .catch(err => { // If validation failed we skip the option return null }) + + if (option) { + return { + ...option, + profile: profiles.find(p => p._id.equals(option.profile_id)), + } + } + return null }) ) diff --git a/packages/medusa/src/services/totals.js b/packages/medusa/src/services/totals.js index 483333a807..09289df137 100644 --- a/packages/medusa/src/services/totals.js +++ b/packages/medusa/src/services/totals.js @@ -292,36 +292,29 @@ class TotalsService extends BaseService { } const { type, allocation, value } = discount.discount_rule + let toReturn = 0 if (type === "percentage" && allocation === "total") { - return (subtotal / 100) * value - } - - if (type === "percentage" && allocation === "item") { + toReturn = (subtotal / 100) * value + } else if (type === "percentage" && allocation === "item") { const itemPercentageDiscounts = await this.getAllocationItemDiscounts( discount, cart, "percentage" ) - const totalDiscount = _.sumBy(itemPercentageDiscounts, d => d.amount) - return totalDiscount - } - - if (type === "fixed" && allocation === "total") { - return value - } - - if (type === "fixed" && allocation === "item") { + toReturn = _.sumBy(itemPercentageDiscounts, d => d.amount) + } else if (type === "fixed" && allocation === "total") { + toReturn = value + } else if (type === "fixed" && allocation === "item") { const itemFixedDiscounts = await this.getAllocationItemDiscounts( discount, cart, "fixed" ) - const totalDiscount = _.sumBy(itemFixedDiscounts, d => d.amount) - return totalDiscount + toReturn = _.sumBy(itemFixedDiscounts, d => d.amount) } - return 0 + return Math.min(subtotal, toReturn) } } diff --git a/packages/medusa/src/subscribers/order.js b/packages/medusa/src/subscribers/order.js index f9689029b3..81f8f708c6 100644 --- a/packages/medusa/src/subscribers/order.js +++ b/packages/medusa/src/subscribers/order.js @@ -4,9 +4,17 @@ class OrderSubscriber { cartService, customerService, eventBusService, + discountService, + totalsService, }) { + this.totalsService_ = totalsService + this.paymentProviderService_ = paymentProviderService + this.customerService_ = customerService + + this.discountService_ = discountService + this.cartService_ = cartService this.eventBus_ = eventBusService @@ -33,6 +41,34 @@ class OrderSubscriber { this.eventBus_.subscribe("order.placed", async order => { await this.cartService_.delete(order.cart_id) }) + + this.eventBus_.subscribe("order.placed", this.handleDiscounts) + } + + handleDiscounts = async order => { + await Promise.all( + order.discounts.map(async d => { + const subtotal = await this.totalsService_.getSubtotal(order) + if (d.is_giftcard) { + const discountRule = { + ...d.discount_rule, + value: Math.max(0, d.discount_rule.value - subtotal), + } + + delete discountRule._id + + return this.discountService_.update(d._id, { + discount_rule: discountRule, + usage_count: d.usage_count + 1, + disabled: discountRule.value === 0, + }) + } else { + return this.discountService_.update(d._id, { + usage_count: d.usage_count + 1, + }) + } + }) + ) } } diff --git a/packages/medusa/yarn.lock b/packages/medusa/yarn.lock index 20f2789ac0..d8c0b200f7 100644 --- a/packages/medusa/yarn.lock +++ b/packages/medusa/yarn.lock @@ -3665,6 +3665,11 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" +is-number@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" + integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -4553,6 +4558,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +math-random@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" + integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -5501,6 +5511,15 @@ random-bytes@~1.0.0: resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs= +randomatic@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" + integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw== + dependencies: + is-number "^4.0.0" + kind-of "^6.0.0" + math-random "^1.0.1" + range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"