diff --git a/bin/setup b/bin/setup index 689d5725bb5..24d18630b54 100755 --- a/bin/setup +++ b/bin/setup @@ -2,29 +2,37 @@ __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" env_file=${__dir}/../.env -env_example_file=${__dir}/../.env.example -function main { +function main() { set -e add_new_env_vars } -function add_new_env_vars { +function add_new_env_vars() { # create .env and set perms if it does not exist - [ ! -f "${env_file}" ] && { touch "${env_file}" ; chmod 0600 "${env_file}" ; } + [[ ! -f "${env_file}" ]] && { + touch "${env_file}" + chmod 0600 "${env_file}" + } - export IFS=$'\n' - for var in $(cat "${env_example_file}"); do - key="${var%%=*}" # get var key - var=$(eval echo "$var") # generate dynamic values + find . imports/plugins/custom -name .env.example -type f -print0 | + xargs -0 grep -Ehv '^\s*#' | + { + while IFS= read -r var; do + if [[ -z "${var}" ]]; then + continue + fi + key="${var%%=*}" # get var key + var=$(eval echo "$var") # generate dynamic values - # If .env doesn't contain this env key, add it - if ! $(grep -qLE "^$key=" "${env_file}"); then - echo "Adding $key to .env" - echo "$var" >> "${env_file}" - fi - done + # If .env doesn't contain this env key, add it + if ! grep -qLE "^$key=" "${env_file}"; then + echo "Adding $key to .env" + echo "$var" >>"${env_file}" + fi + done + } } main diff --git a/imports/plugins/core/cart/server/no-meteor/mutations/addCartItems.js b/imports/plugins/core/cart/server/no-meteor/mutations/addCartItems.js index 791aba3b352..bb8c26c53b6 100644 --- a/imports/plugins/core/cart/server/no-meteor/mutations/addCartItems.js +++ b/imports/plugins/core/cart/server/no-meteor/mutations/addCartItems.js @@ -43,7 +43,7 @@ export default async function addCartItems(context, input, options = {}) { incorrectPriceFailures, minOrderQuantityFailures, updatedItemList - } = await addCartItemsUtil(collections, cart.items, items, { skipPriceCheck: options.skipPriceCheck }); + } = await addCartItemsUtil(context, cart.items, items, { skipPriceCheck: options.skipPriceCheck }); const updatedAt = new Date(); diff --git a/imports/plugins/core/cart/server/no-meteor/mutations/createCart.js b/imports/plugins/core/cart/server/no-meteor/mutations/createCart.js index b1b5271a15b..f6c6a709ee9 100644 --- a/imports/plugins/core/cart/server/no-meteor/mutations/createCart.js +++ b/imports/plugins/core/cart/server/no-meteor/mutations/createCart.js @@ -42,7 +42,7 @@ export default async function createCart(context, input) { incorrectPriceFailures, minOrderQuantityFailures, updatedItemList - } = await addCartItems(collections, [], items); + } = await addCartItems(context, [], items); // If all input items were invalid, don't create a cart if (!updatedItemList.length && shouldCreateWithoutItems !== true) { diff --git a/imports/plugins/core/cart/server/no-meteor/mutations/reconcileCarts.js b/imports/plugins/core/cart/server/no-meteor/mutations/reconcileCarts.js index 865e531152a..82805625898 100644 --- a/imports/plugins/core/cart/server/no-meteor/mutations/reconcileCarts.js +++ b/imports/plugins/core/cart/server/no-meteor/mutations/reconcileCarts.js @@ -67,7 +67,7 @@ export default async function reconcileCarts(context, input) { case "merge": return { - cart: await reconcileCartsMerge({ accountCart, accountCartSelector, anonymousCart, anonymousCartSelector, collections, userId }) + cart: await reconcileCartsMerge({ accountCart, accountCartSelector, anonymousCart, anonymousCartSelector, context, userId }) }; default: diff --git a/imports/plugins/core/cart/server/no-meteor/mutations/reconcileCartsMerge.js b/imports/plugins/core/cart/server/no-meteor/mutations/reconcileCartsMerge.js index beeacd89bab..6eefa16be51 100644 --- a/imports/plugins/core/cart/server/no-meteor/mutations/reconcileCartsMerge.js +++ b/imports/plugins/core/cart/server/no-meteor/mutations/reconcileCartsMerge.js @@ -10,7 +10,7 @@ import addCartItems from "../util/addCartItems"; * @param {Object} accountCartSelector The MongoDB selector for the account cart * @param {Object} anonymousCart The anonymous cart document * @param {Object} anonymousCartSelector The MongoDB selector for the anonymous cart - * @param {Object} collections A map of MongoDB collection instances + * @param {Object} context App context * @return {Object} The updated account cart */ export default async function reconcileCartsMerge({ @@ -18,9 +18,10 @@ export default async function reconcileCartsMerge({ accountCartSelector, anonymousCart, anonymousCartSelector, - collections, + context, userId }) { + const { collections } = context; const { Cart } = collections; // Convert item schema to input item schema @@ -35,7 +36,7 @@ export default async function reconcileCartsMerge({ })); // Merge the item lists - const { updatedItemList: items } = await addCartItems(collections, accountCart.items, itemsInput, { + const { updatedItemList: items } = await addCartItems(context, accountCart.items, itemsInput, { skipPriceCheck: true }); diff --git a/imports/plugins/core/cart/server/no-meteor/mutations/reconcileCartsMerge.test.js b/imports/plugins/core/cart/server/no-meteor/mutations/reconcileCartsMerge.test.js index b91f5f0e8a6..55a630020c3 100644 --- a/imports/plugins/core/cart/server/no-meteor/mutations/reconcileCartsMerge.test.js +++ b/imports/plugins/core/cart/server/no-meteor/mutations/reconcileCartsMerge.test.js @@ -75,7 +75,7 @@ test("merges anonymous cart items into account cart items, deletes anonymous car items }, anonymousCartSelector, - collections + context: mockContext }); expect(Cart.deleteOne).toHaveBeenCalledWith(anonymousCartSelector); @@ -104,7 +104,7 @@ test("throws if deleteOne fails", async () => { items }, anonymousCartSelector, - collections + context: mockContext }); return expect(promise).rejects.toThrowErrorMatchingSnapshot(); @@ -120,7 +120,7 @@ test("throws if updateOne fails", async () => { items }, anonymousCartSelector, - collections + context: mockContext }); return expect(promise).rejects.toThrowErrorMatchingSnapshot(); diff --git a/imports/plugins/core/cart/server/no-meteor/startup.js b/imports/plugins/core/cart/server/no-meteor/startup.js index 5fd905d4c54..5384137dc6b 100644 --- a/imports/plugins/core/cart/server/no-meteor/startup.js +++ b/imports/plugins/core/cart/server/no-meteor/startup.js @@ -5,31 +5,33 @@ const AFTER_CATALOG_UPDATE_EMITTED_BY_NAME = "CART_CORE_PLUGIN_AFTER_CATALOG_UPD /** * @param {Object[]} catalogProductVariants The `product.variants` array from a catalog item - * @returns {Object} Map of variant IDs to updated pricing objects + * @returns {Object[]} All variants and their options flattened in one array */ -function getVariantPricingMap(catalogProductVariants) { - const variantPricingMap = {}; +function getFlatVariantsAndOptions(catalogProductVariants) { + const variants = []; catalogProductVariants.forEach((variant) => { - variantPricingMap[variant.variantId] = variant.pricing; + variants.push(variant); if (variant.options) { variant.options.forEach((option) => { - variantPricingMap[option.variantId] = option.pricing; + variants.push(option); }); } }); - return variantPricingMap; + return variants; } /** - * @param {Object} appEvents App event emitter * @param {Object} Cart Cart collection - * @param {Object} pricing Potentially updated pricing map for the variant - * @param {String} variantId The ID of the variant to update for + * @param {Object} context App context + * @param {String} variant The catalog product variant or option * @returns {Promise} Promise that resolves with null */ -async function updateAllCartsForVariant({ appEvents, Cart, pricing, variantId }) { +async function updateAllCartsForVariant({ Cart, context, variant }) { + const { appEvents, queries } = context; + const { variantId } = variant; + // Do find + update because we need the `cart.currencyCode` to figure out pricing // and we need current quantity to recalculate `subtotal` for each item. // It should be fine to load all results into an array because even for large shops, @@ -41,7 +43,7 @@ async function updateAllCartsForVariant({ appEvents, Cart, pricing, variantId }) }).toArray(); await Promise.all(carts.map(async (cart) => { - const prices = pricing[cart.currencyCode]; + const prices = await queries.getVariantPrice(context, variant, cart.currencyCode); if (!prices) return; const { didUpdate, updatedItems } = updateCartItemsForVariantPriceChange(cart.items, variantId, prices); @@ -98,14 +100,10 @@ export default function startup(context) { appEvents.on("afterPublishProductToCatalog", async ({ catalogProduct }) => { const { variants } = catalogProduct; - // Build a map of variant IDs to their potentially-changed prices - const variantPricingMap = getVariantPricingMap(variants); - const variantIds = Object.keys(variantPricingMap); + const variantsAndOptions = getFlatVariantsAndOptions(variants); // Update all cart items that are linked with the updated variants - await Promise.all(variantIds.map(async (variantId) => { - const pricing = variantPricingMap[variantId]; - return updateAllCartsForVariant({ appEvents, Cart, pricing, variantId }); - })); + await Promise.all(variantsAndOptions.map(async (variant) => + updateAllCartsForVariant({ Cart, context, variant }))); }); } diff --git a/imports/plugins/core/cart/server/no-meteor/util/addCartItems.js b/imports/plugins/core/cart/server/no-meteor/util/addCartItems.js index 5ac3ad0d350..8bf775d7bbc 100644 --- a/imports/plugins/core/cart/server/no-meteor/util/addCartItems.js +++ b/imports/plugins/core/cart/server/no-meteor/util/addCartItems.js @@ -27,7 +27,7 @@ const inputItemSchema = new SimpleSchema({ /** * @summary Given a list of current cart items and a list of items a shopper wants * to add, validate available quantities and return the full merged list. - * @param {Object} collections - Map of raw MongoDB collections + * @param {Object} context - App context * @param {Object[]} currentItems - Array of current items in CartItem schema * @param {Object[]} inputItems - Array of items to add in CartItemInput schema * @param {Object} [options] - Options @@ -35,7 +35,9 @@ const inputItemSchema = new SimpleSchema({ * Skipping this is not recommended for new code. * @return {Object} Object with `incorrectPriceFailures` and `minOrderQuantityFailures` and `updatedItemList` props */ -export default async function addCartItems(collections, currentItems, inputItems, options = {}) { +export default async function addCartItems(context, currentItems, inputItems, options = {}) { + const { collections, queries } = context; + inputItemSchema.validate(inputItems); const incorrectPriceFailures = []; @@ -57,7 +59,7 @@ export default async function addCartItems(collections, currentItems, inputItems variant: chosenVariant } = await findProductAndVariant(collections, productId, productVariantId); - const variantPriceInfo = chosenVariant.pricing[price.currencyCode]; + const variantPriceInfo = await queries.getVariantPrice(context, chosenVariant, price.currencyCode); if (!variantPriceInfo) { throw new ReactionError("invalid-param", `This product variant does not have a price for ${price.currencyCode}`); } diff --git a/imports/plugins/included/simple-pricing/README.md b/imports/plugins/included/simple-pricing/README.md new file mode 100644 index 00000000000..eafcf855270 --- /dev/null +++ b/imports/plugins/included/simple-pricing/README.md @@ -0,0 +1,46 @@ +# Simple Pricing + +Pricing data falls under it's own domain within the Reaction Commerce system, however it currently needs to be intertwined and available from other system domains (i.e., Catalog, Cart, Checkout, Orders). To give us more flexibility in pricing data management we've begun to move pricing get/set functions into this `simple-pricing` plugin and calling these functions from the `context.queries` object from within their respective functions. Now we can fully replace the pricing management system without modification to core by creating a custom plugin that replaces the `simple-pricing` queries. + +**Example** + +``` js +// old addCartItems() funcitons + +... +const { + catalogProduct, + parentVariant, + variant: chosenVariant + } = await findProductAndVariant(collections, productId, productVariantId); + + const variantPriceInfo = chosenVariant.pricing[providedPrice.currencyCode]; + if (!variantPriceInfo) { + throw new ReactionError("invalid-param", `This product variant does not have a price for ${price.currencyCode}`); + } +... + +// new addCartItems() function with simple-pricing +const { + catalogProduct, + parentVariant, + variant: chosenVariant + } = await findProductAndVariant(collections, productId, productVariantId); + + const variantPriceInfo = await queries.getVariantPrice(context, chosenVariant, currencyCode); + if (!variantPriceInfo) { + throw new ReactionError("invalid-param", `This product variant does not have a price for ${price.currencyCode}`); + } +``` + +## Queries + +### Price Queries +**getVariantPrice** +This query is used to get a selected product's real price. + +**getCurrentCatalogPriceForProductConfiguration** +This query is used to verify a product's price is correct before we process the order. + +### Catalog Price Queries +TBD diff --git a/imports/plugins/included/simple-pricing/register.js b/imports/plugins/included/simple-pricing/register.js new file mode 100644 index 00000000000..c91efec2d74 --- /dev/null +++ b/imports/plugins/included/simple-pricing/register.js @@ -0,0 +1,23 @@ +import Reaction from "/imports/plugins/core/core/server/Reaction"; +import queries from "./server/no-meteor/queries"; +import startup from "./server/no-meteor/startup"; + +/** + * Simple Pricing plugin + * Isolates the get/set of pricing data to this plugin. + */ + +Reaction.registerPackage({ + label: "Pricing", + name: "reaction-pricing", + icon: "fa fa-dollar-sign", + autoEnable: true, + functionsByType: { + startup: [startup] + }, + graphQL: {}, + queries, + settings: { + name: "Pricing" + } +}); diff --git a/imports/plugins/included/simple-pricing/server/no-meteor/queries/getCurrentCatalogPriceForProductConfiguration.js b/imports/plugins/included/simple-pricing/server/no-meteor/queries/getCurrentCatalogPriceForProductConfiguration.js new file mode 100644 index 00000000000..75738078d54 --- /dev/null +++ b/imports/plugins/included/simple-pricing/server/no-meteor/queries/getCurrentCatalogPriceForProductConfiguration.js @@ -0,0 +1,26 @@ +import findProductAndVariant from "/imports/plugins/core/catalog/server/no-meteor/utils/findProductAndVariant"; + +/** + * @summary Returns the current price in the Catalog for the given product configuration + * @param {Object} productConfiguration The ProductConfiguration object + * @param {String} currencyCode The currency code + * @param {Object} collections Map of MongoDB collections + * @returns {Object} Object with `price` as the current price in the Catalog for the given product configuration. + * Also returns catalogProduct and catalogProductVariant docs in case you need them. + */ +export default async function getCurrentCatalogPriceForProductConfiguration(productConfiguration, currencyCode, collections) { + const { productId, productVariantId } = productConfiguration; + const { + catalogProduct, + variant: catalogProductVariant + } = await findProductAndVariant(collections, productId, productVariantId); + + const variantPriceInfo = (catalogProductVariant.pricing && catalogProductVariant.pricing[currencyCode]) || {}; + const price = variantPriceInfo.price || catalogProductVariant.price; + + return { + catalogProduct, + catalogProductVariant, + price + }; +} diff --git a/imports/plugins/included/simple-pricing/server/no-meteor/queries/getVariantPrice.js b/imports/plugins/included/simple-pricing/server/no-meteor/queries/getVariantPrice.js new file mode 100644 index 00000000000..e615cfe6365 --- /dev/null +++ b/imports/plugins/included/simple-pricing/server/no-meteor/queries/getVariantPrice.js @@ -0,0 +1,11 @@ +/** + * @method getVariantPrice + * @summary This method returns the applicable price and currency code for a selected product. + * @param {Object} context - App context + * @param {Object} catalogVariant - A selected product variant. + * @param {Object} currencyCode - The currency code in which to get price + * @return {Object} - A cart item price value. + */ +export default function getVariantPrice(context, catalogVariant, currencyCode) { + return catalogVariant.pricing[currencyCode]; +} diff --git a/imports/plugins/included/simple-pricing/server/no-meteor/queries/index.js b/imports/plugins/included/simple-pricing/server/no-meteor/queries/index.js new file mode 100644 index 00000000000..c94fc10ff52 --- /dev/null +++ b/imports/plugins/included/simple-pricing/server/no-meteor/queries/index.js @@ -0,0 +1,7 @@ +import getVariantPrice from "./getVariantPrice"; +import getCurrentCatalogPriceForProductConfiguration from "./getCurrentCatalogPriceForProductConfiguration"; + +export default { + getVariantPrice, + getCurrentCatalogPriceForProductConfiguration +}; diff --git a/imports/plugins/included/simple-pricing/server/no-meteor/startup.js b/imports/plugins/included/simple-pricing/server/no-meteor/startup.js new file mode 100644 index 00000000000..dbba193edb3 --- /dev/null +++ b/imports/plugins/included/simple-pricing/server/no-meteor/startup.js @@ -0,0 +1,8 @@ +/** + * + * @method startup + * @summary Simple pricing startup function. + * @param {Object} context - App context. + * @return {undefin} - void, no return. + */ +export default function startup() {}