diff --git a/client/modules/core/helpers/apps.js b/client/modules/core/helpers/apps.js index 72ab1829b47..94f34af1c73 100644 --- a/client/modules/core/helpers/apps.js +++ b/client/modules/core/helpers/apps.js @@ -3,7 +3,7 @@ import { Template } from "meteor/templating"; import { Meteor } from "meteor/meteor"; import { Roles } from "meteor/alanning:roles"; import { Reaction } from "/client/api"; -import { Packages } from "/lib/collections"; +import { Packages, Shops } from "/lib/collections"; /** @@ -49,6 +49,7 @@ export function Apps(optionHash) { let key; const reactionApps = []; let options = {}; + let shopType; // allow for object or option.hash if (optionHash) { @@ -64,6 +65,13 @@ export function Apps(optionHash) { options.shopId = Reaction.getShopId(); } + // Get the shop to determine shopType + const shop = Shops.findOne({ _id: options.shopId }); + if (shop) { + shopType = shop.shopType; + } + + // remove audience permissions for owner (still needed here for older/legacy calls) if (Reaction.hasOwnerAccess() && options.audience) { delete options.audience; @@ -130,6 +138,20 @@ export function Apps(optionHash) { delete itemFilter.audience; } + // Check that shopType matches showForShopType if option is present + if (item.showForShopTypes && + Array.isArray(item.showForShopTypes) && + item.showForShopTypes.indexOf(shopType) === -1) { + return false; + } + + // Check that shopType does not match hideForShopType if option is present + if (item.hideForShopTypes && + Array.isArray(item.hideForShopTypes) && + item.hideForShopTypes.indexOf(shopType) !== -1) { + return false; + } + return _.isMatch(item, itemFilter); }); diff --git a/client/modules/core/main.js b/client/modules/core/main.js index 949a0f111b7..8c467e5b5e8 100644 --- a/client/modules/core/main.js +++ b/client/modules/core/main.js @@ -30,13 +30,14 @@ export default { init() { Tracker.autorun(() => { - // marketplaceSettings come over on the PrimarySHopPackages subscription + // marketplaceSettings come over on the PrimaryShopPackages subscription if (this.Subscriptions.PrimaryShopPackages.ready()) { if (!this.marketplace._ready) { const marketplacePkgSettings = this.getMarketplaceSettings(); if (marketplacePkgSettings && marketplacePkgSettings.public) { - marketplacePkgSettings._ready = true; + this.marketplace._ready = true; this.marketplace = marketplacePkgSettings.public; + this.marketplace.enabled = true; } } } @@ -487,6 +488,11 @@ export default { return Packages.findOne(query); }, + getPackageSettingsWithOptions(options) { + const query = options; + return Packages.findOne(query); + }, + allowGuestCheckout() { let allowGuest = false; const settings = this.getShopSettings(); diff --git a/client/modules/core/subscriptions.js b/client/modules/core/subscriptions.js index b88325e2131..8bda12b640a 100644 --- a/client/modules/core/subscriptions.js +++ b/client/modules/core/subscriptions.js @@ -37,8 +37,14 @@ Subscriptions.PrimaryShop = Subscriptions.Manager.subscribe("PrimaryShop"); // Additional shop subscriptions Subscriptions.MerchantShops = Subscriptions.Manager.subscribe("MerchantShops"); -// Init Packages sub so we have a "ready" state +// This Packages subscription is used for the Active shop's packages +// // Init sub here so we have a "ready" state Subscriptions.Packages = Subscriptions.Manager.subscribe("Packages"); + +// This packages subscription is used for the Primary Shop's packages +// The Packages publication defaults to returning the primaryShopId's packages, +// so this subscription shouldn't ever need to be changed +// TODO: Consider how to handle routes for several shops which are all active at once Subscriptions.PrimaryShopPackages = Subscriptions.Manager.subscribe("Packages"); Subscriptions.Tags = Subscriptions.Manager.subscribe("Tags"); @@ -82,14 +88,9 @@ Tracker.autorun(() => { Tracker.autorun(() => { // Reload Packages sub if shopId changes - if (Reaction.getShopId()) { + // We have a persistent subscription to the primary shop's packages, + // so don't refresh sub if we're updating to primaryShopId sub + if (Reaction.getShopId() && Reaction.getShopId() !== Reaction.getPrimaryShopId()) { Subscriptions.Packages = Subscriptions.Manager.subscribe("Packages", Reaction.getShopId()); } }); - -Tracker.autorun(() => { - // Reload Packages sub if primaryShopId changes - if (Reaction.getPrimaryShopId()) { - Subscriptions.PrimaryShopPackages = Subscriptions.Manager.subscribe("Packages", Reaction.getPrimaryShopId()); - } -}); diff --git a/imports/plugins/core/checkout/client/methods/cart.js b/imports/plugins/core/checkout/client/methods/cart.js index e2f2b6d8c4b..23ce2b81d65 100644 --- a/imports/plugins/core/checkout/client/methods/cart.js +++ b/imports/plugins/core/checkout/client/methods/cart.js @@ -7,6 +7,9 @@ import { Cart } from "/lib/collections"; // Client Cart Methods // Stubs with matching server methods. Meteor.methods({ + // Not used for stripe connect integration + // Under consideration for deprecation and migrating other payment Packages + // to payments-stripe style methods "cart/submitPayment": function (paymentMethod) { check(paymentMethod, Reaction.Schemas.PaymentMethod); const checkoutCart = Cart.findOne({ diff --git a/imports/plugins/core/dashboard/client/templates/shop/settings/settings.html b/imports/plugins/core/dashboard/client/templates/shop/settings/settings.html index 0960df77fe9..40a50baa4f5 100644 --- a/imports/plugins/core/dashboard/client/templates/shop/settings/settings.html +++ b/imports/plugins/core/dashboard/client/templates/shop/settings/settings.html @@ -114,8 +114,8 @@
@@ -123,11 +123,13 @@ {{label}}
+ {{#if showAppSwitch}}
+ {{/if}}
-
+
{{> Template.dynamic template=template data=.}}
diff --git a/imports/plugins/core/dashboard/client/templates/shop/settings/settings.js b/imports/plugins/core/dashboard/client/templates/shop/settings/settings.js index 2fa16c5bc29..0abbe70ed27 100644 --- a/imports/plugins/core/dashboard/client/templates/shop/settings/settings.js +++ b/imports/plugins/core/dashboard/client/templates/shop/settings/settings.js @@ -153,6 +153,15 @@ Template.shopSettings.helpers({ _id: Reaction.getShopId() }).addressBook; return address[0]; + }, + showAppSwitch() { + if (Reaction.getMarketplaceSettings()) { + // if marketplace is enabled, only the primary shop can switch apps on and off. + return Reaction.getShopId() === Reaction.getPrimaryShopId(); + } + + // If marketplace is disabled, every shop can switch apps + return true; } }); diff --git a/imports/plugins/core/taxes/server/methods/methods.js b/imports/plugins/core/taxes/server/methods/methods.js index 0234b69a961..b1b81bab734 100644 --- a/imports/plugins/core/taxes/server/methods/methods.js +++ b/imports/plugins/core/taxes/server/methods/methods.js @@ -25,6 +25,28 @@ export const methods = { return Taxes.remove(taxId); }, + /** + * taxes/addRate + * @param {String} modifier update statement + * @param {String} docId tax docId + * @return {String} returns update/insert result + */ + "taxes/addRate": function (modifier, docId) { + check(modifier, Object); + check(docId, Match.OneOf(String, null, undefined)); + + // check permissions to add + if (!Reaction.hasPermission("taxes")) { + throw new Meteor.Error(403, "Access Denied"); + } + // if no doc, insert + if (!docId) { + return Taxes.insert(modifier); + } + // else update and return + return Taxes.update(docId, modifier); + }, + /** * taxes/setRate * update the cart without hooks @@ -47,25 +69,36 @@ export const methods = { }, /** - * taxes/addRate - * @param {String} modifier update statement - * @param {String} docId tax docId - * @return {String} returns update/insert result + * taxes/setRateByShopAndItem + * update the cart without hooks + * Options: + * taxRatesByShop - Object shopIds: taxRates + * itemsWithTax - items array with computed tax details + * cartTaxRate - tax rate for shop associated with cart.shopId + * cartTaxData - tax data for shop associated with cart.shopId + * @param {String} cartId cartId + * @param {Object} options - Options object + * @return {Number} returns update result */ - "taxes/addRate": function (modifier, docId) { - check(modifier, Object); - check(docId, Match.OneOf(String, null, undefined)); + "taxes/setRateByShopAndItem": function (cartId, options) { + check(cartId, String); + check(options, { + taxRatesByShop: Object, + itemsWithTax: [Object], + cartTaxRate: Number, + cartTaxData: Match.OneOf([Object], undefined, null) + }); - // check permissions to add - if (!Reaction.hasPermission("taxes")) { - throw new Meteor.Error(403, "Access Denied"); - } - // if no doc, insert - if (!docId) { - return Taxes.insert(modifier); - } - // else update and return - return Taxes.update(docId, modifier); + const { cartTaxData, cartTaxRate, itemsWithTax, taxRatesByShop } = options; + + return Cart.direct.update(cartId, { + $set: { + taxes: cartTaxData, + tax: cartTaxRate, + items: itemsWithTax, + taxRatesByShop: taxRatesByShop + } + }); }, /** @@ -76,14 +109,12 @@ export const methods = { "taxes/calculate": function (cartId) { check(cartId, String); const cartToCalc = Cart.findOne(cartId); - const shopId = cartToCalc.shopId; - let taxRate = 0; - // get all tax packages - // - // TODO FIND IN LAYOUT/REGISTRY - // + const cartShopId = cartToCalc.shopId; + let cartTaxRate = 0; + + // TODO: Calculate shipping taxes for regions that require it const pkg = Packages.findOne({ - shopId: shopId, + shopId: cartShopId, name: "reaction-taxes" }); // @@ -99,7 +130,8 @@ export const methods = { if (typeof cartToCalc.shipping !== "undefined" && typeof cartToCalc.items !== "undefined") { const shippingAddress = cartToCalc.shipping[0].address; - // + let totalTax = 0; + // custom rates that match shipping info // high chance this needs more review as // it's unlikely this matches all potential @@ -107,54 +139,77 @@ export const methods = { // match we're taking the first record, where the most // likely tax scenario is a postal code falling // back to a regional tax. - if (shippingAddress) { - let customTaxRate = 0; - let totalTax = 0; - // lookup custom tax rate - const addressTaxData = Taxes.find( - { - $and: [{ - $or: [{ - postal: shippingAddress.postal + // Get tax rates by shop + const taxDataByShop = cartToCalc.items.reduce((uniqueShopTaxRates, item) => { + // lookup custom tax rate for each shop once + if (!uniqueShopTaxRates[item.shopId]) { + uniqueShopTaxRates[item.shopId] = Taxes.findOne({ + $and: [{ + $or: [{ + postal: shippingAddress.postal + }, { + postal: { $exists: false }, + region: shippingAddress.region, + country: shippingAddress.country + }, { + postal: { $exists: false }, + region: { $exists: false }, + country: shippingAddress.country + }] }, { - postal: { $exists: false }, - region: shippingAddress.region, - country: shippingAddress.country - }, { - postal: { $exists: false }, - region: { $exists: false }, - country: shippingAddress.country + shopId: item.shopId }] - }, { - shopId: shopId - }] - }, { sort: { postal: -1 } } - ).fetch(); - - // return custom rates - // TODO break down the product origination, taxability - // by qty and an originating shop and inventory - // for location of each item in the cart. - if (addressTaxData.length > 0) { - customTaxRate = addressTaxData[0].rate; - } + }, { sort: { postal: -1 } }); + } + + return uniqueShopTaxRates; + }, {}); + + const taxRatesByShop = Object.keys(taxDataByShop).reduce((ratesByShop, shopId) => { + if (taxDataByShop[shopId]) { + ratesByShop[shopId] = taxDataByShop[shopId].rate / 100; + } + return ratesByShop; + }, {}); // calculate line item taxes - for (const items of cartToCalc.items) { - // only processs taxable products - if (items.variants.taxable === true) { - const subTotal = items.variants.price * items.quantity; - const tax = subTotal * (customTaxRate / 100); - totalTax += tax; + const itemsWithTax = cartToCalc.items.map((item) => { + // init rate to 0 + item.taxRate = 0; + item.taxData = undefined; + const shopTaxData = taxDataByShop[item.shopId]; + + // only process taxble products and skip if there is no shopTaxData + if (shopTaxData && item.variants.taxable === true) { + const shopTaxRate = shopTaxData.rate / 100; + + // If we have tax rates for this shop + if (shopTaxData && shopTaxRate) { + item.taxData = shopTaxData; + item.taxRate = shopTaxRate; + item.subtotal = item.variants.price * item.quantity; + item.tax = item.subtotal * item.taxRate; + } + totalTax += item.tax; } - } - // calculate overall cart rate + + // add the item to our new array + return item; + }); + if (totalTax > 0) { - taxRate = (totalTax / cartToCalc.cartSubTotal()); + cartTaxRate = totalTax / cartToCalc.cartSubTotal(); } - // store tax on cart - Meteor.call("taxes/setRate", cartToCalc._id, taxRate, addressTaxData); + + // Marketplace Compatible + Meteor.call("taxes/setRateByShopAndItem", cartToCalc._id, { + taxRatesByShop, + itemsWithTax, + cartTaxRate, + cartTaxData: undefined + // not setting cartTaxData here to disguise actual tax rate from client + }); } // end custom rates } // end shippingAddress calculation } else { @@ -162,7 +217,7 @@ export const methods = { // we're going to set an inital rate of 0 // all methods that trigger when taxes/calculate will // recalculate this rate as needed. - Meteor.call("taxes/setRate", cartToCalc._id, taxRate); + Meteor.call("taxes/setRate", cartToCalc._id, cartTaxRate); } } // end taxes/calculate }; diff --git a/imports/plugins/included/marketplace/client/index.js b/imports/plugins/included/marketplace/client/index.js index e3fcedd21d7..3bc69da6614 100644 --- a/imports/plugins/included/marketplace/client/index.js +++ b/imports/plugins/included/marketplace/client/index.js @@ -4,6 +4,9 @@ import "./templates/becomeSellerButton/becomeSellerButton.js"; import "./templates/dashboard/settings.html"; import "./templates/dashboard/settings.js"; +import "./templates/dashboard/merchant-settings.html"; +import "./templates/dashboard/merchant-settings.js"; + import "./templates/settings/sellerShopSettings.html"; import "./templates/settings/sellerShopSettings.js"; diff --git a/imports/plugins/included/marketplace/client/templates/dashboard/merchant-settings.html b/imports/plugins/included/marketplace/client/templates/dashboard/merchant-settings.html new file mode 100644 index 00000000000..7172d8b7641 --- /dev/null +++ b/imports/plugins/included/marketplace/client/templates/dashboard/merchant-settings.html @@ -0,0 +1,10 @@ + diff --git a/imports/plugins/included/marketplace/client/templates/dashboard/merchant-settings.js b/imports/plugins/included/marketplace/client/templates/dashboard/merchant-settings.js new file mode 100644 index 00000000000..fbcb9cc4c9e --- /dev/null +++ b/imports/plugins/included/marketplace/client/templates/dashboard/merchant-settings.js @@ -0,0 +1,9 @@ +import { Template } from "meteor/templating"; +Template.marketplaceMerchantSettings.helpers({ + shown(enabled) { + if (enabled !== true) { + return "hidden"; + } + return ""; + } +}); diff --git a/imports/plugins/included/marketplace/client/templates/stripeConnectSignupButton/stripeConnectSignupButton.html b/imports/plugins/included/marketplace/client/templates/stripeConnectSignupButton/stripeConnectSignupButton.html index 03e75e0ef6b..de281a09a47 100644 --- a/imports/plugins/included/marketplace/client/templates/stripeConnectSignupButton/stripeConnectSignupButton.html +++ b/imports/plugins/included/marketplace/client/templates/stripeConnectSignupButton/stripeConnectSignupButton.html @@ -1,9 +1,13 @@ diff --git a/imports/plugins/included/marketplace/client/templates/stripeConnectSignupButton/stripeConnectSignupButton.js b/imports/plugins/included/marketplace/client/templates/stripeConnectSignupButton/stripeConnectSignupButton.js index 2ecbf9ffde8..ae3235b3c69 100644 --- a/imports/plugins/included/marketplace/client/templates/stripeConnectSignupButton/stripeConnectSignupButton.js +++ b/imports/plugins/included/marketplace/client/templates/stripeConnectSignupButton/stripeConnectSignupButton.js @@ -1,48 +1,53 @@ -import { Meteor } from "meteor/meteor"; import { Template } from "meteor/templating"; import { Reaction } from "/lib/api"; +import { i18next } from "/client/api"; +import { Shops } from "/lib/collections"; -// TODO: This button should be a React component. +Template.stripeConnectSignupButton.events({ + "click [data-event-action='button-click-stripe-signup']": function () { + const shopId = Reaction.getShopId(); + const primaryShopId = Reaction.getPrimaryShopId(); + const primaryStripePackage = Reaction.getPackageSettingsWithOptions({ + shopId: primaryShopId, + name: "reaction-stripe", + enabled: true + }); -Template.stripeConnectSignupButton.onCreated(function () { - this.autorun(() => { - // TODO: this should probably be managed by a subscription? - // Seems inefficient to do it at the button component level - Meteor.subscribe("SellerShops"); - }); -}); + //eslint-disable-next-line + // debugger; + let clientId; -// Button -Template.stripeConnectSignupButton.helpers({ - /** - * Give it a size and style - * @return {String} The classes - */ - classes() { - const classes = [ - (this.type || "btn-info"), - (this.size || "") - ]; - - return classes.join(" "); - } -}); + if (primaryStripePackage && + primaryStripePackage.settings && + primaryStripePackage.settings.public && + typeof primaryStripePackage.settings.public.client_id === "string") { + // If the primaryshop has stripe enabled and set the client_id + clientId = primaryStripePackage.settings.public.client_id; + } else { + return Alerts.toast(`${i18next.t("admin.connect.stripeConnectNotEnabled")}`, "error"); + } -Template.stripeConnectSignupButton.events({ - "click [data-event-action='button-click-stripe-signup']": function () { - const sellerShop = Reaction.getSellerShop(); + const shop = Shops.findOne({ _id: shopId }); + + if (!shop.emails || !Array.isArray(shop.emails) || shop.emails.length === 0) { + return Alerts.toast(`${i18next.t("admin.connect.shopEmailNotConfigured")}`, "error"); + } + + if (!shop.addressBook || !Array.isArray(shop.addressBook) || shop.addressBook.length === 0) { + return Alerts.toast(`${i18next.t("admin.connect.shopAddressNotConfigured")}`, "error"); + } - const email = sellerShop.emails[0].address; - const country = sellerShop.addressBook[0].country; - const phoneNumber = sellerShop.addressBook[0].phone; - const businessName = sellerShop.addressBook[0].company; - const streetAddress = sellerShop.addressBook[0].address1; - const city = sellerShop.addressBook[0].city; - const state = sellerShop.addressBook[0].state; - const zip = sellerShop.addressBook[0].postal; + const email = shop.emails[0].address; + const country = shop.addressBook[0].country; + const phoneNumber = shop.addressBook[0].phone; + const businessName = shop.addressBook[0].company; + const streetAddress = shop.addressBook[0].address1; + const city = shop.addressBook[0].city; + const state = shop.addressBook[0].state; + const zip = shop.addressBook[0].postal; + const stripeConnectAuthorizeUrl = `https://connect.stripe.com/oauth/authorize?response_type=code&state=${shopId}&client_id=${clientId}&scope=read_write`; const autofillParams = `&stripe_user[email]=${email}&stripe_user[country]=${country}&stripe_user[phone_number]=${phoneNumber}&stripe_user[business_name]=${businessName}&stripe_user[street_address]=${streetAddress}&stripe_user[city]=${city}&stripe_user[state]=${state}&stripe_user[zip]=${zip}`; // eslint-disable-line max-len - // TODO: Should client_id be hardcoded in here? - window.location.href = "https://connect.stripe.com/oauth/authorize?response_type=code&client_id=ca_32D88BD1qLklliziD7gYQvctJIhWBSQ7&scope=read_write" + autofillParams; + window.open(stripeConnectAuthorizeUrl + autofillParams, "_blank"); } }); diff --git a/imports/plugins/included/marketplace/register.js b/imports/plugins/included/marketplace/register.js index 8e115cfd638..ac76e605788 100644 --- a/imports/plugins/included/marketplace/register.js +++ b/imports/plugins/included/marketplace/register.js @@ -29,11 +29,13 @@ Reaction.registerPackage({ "reaction-product-simple", "reaction-product-variant", "reaction-notification", + "reaction-marketplace", "reaction-analytics", "reaction-inventory", "reaction-sms", "reaction-social", - "reaction-stripe-connect", + "reaction-stripe", + "reaction-taxes", "discount-codes"] }, { shopType: "affiliate", @@ -64,7 +66,8 @@ Reaction.registerPackage({ icon: "fa fa-globe", provides: "shopSettings", container: "dashboard", - template: "marketplaceShopSettings" + template: "marketplaceShopSettings", + showForShopTypes: ["primary"] }, { route: "shop/:shopId", name: "shop", @@ -83,5 +86,13 @@ Reaction.registerPackage({ container: "dashboard", audience: ["seller"], priority: 1 + }, { + // This provides the settings container for marketplaceMerchantSettings + label: "My Shop Settings", + icon: "fa fa-briefcase", + provides: "shopSettings", + container: "dashboard", + template: "marketplaceMerchantSettings", + hideForShopTypes: ["primary"] }] }); diff --git a/imports/plugins/included/payments-stripe-connect/client/index.js b/imports/plugins/included/payments-stripe-connect/client/index.js deleted file mode 100644 index 151b499f63e..00000000000 --- a/imports/plugins/included/payments-stripe-connect/client/index.js +++ /dev/null @@ -1 +0,0 @@ -import "./settings/settings"; diff --git a/imports/plugins/included/payments-stripe-connect/client/settings/settings.html b/imports/plugins/included/payments-stripe-connect/client/settings/settings.html deleted file mode 100644 index b8f1e5756db..00000000000 --- a/imports/plugins/included/payments-stripe-connect/client/settings/settings.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - diff --git a/imports/plugins/included/payments-stripe-connect/client/settings/settings.js b/imports/plugins/included/payments-stripe-connect/client/settings/settings.js deleted file mode 100644 index f26bbf21a44..00000000000 --- a/imports/plugins/included/payments-stripe-connect/client/settings/settings.js +++ /dev/null @@ -1,45 +0,0 @@ -import { Template } from "meteor/templating"; -import { AutoForm } from "meteor/aldeed:autoform"; -import { Meteor } from "meteor/meteor"; -import { Reaction, i18next, Router } from "/client/api"; -import { Packages } from "/lib/collections"; -import { StripeConnectPackageConfig } from "../../lib/collections/schemas"; - -import "./settings.html"; - -Template.stripeConnectSettings.helpers({ - StripeConnectPackageConfig() { - return StripeConnectPackageConfig; - }, - packageData() { - return Packages.findOne({ - name: "reaction-stripe-connect", - shopId: Reaction.getShopId() - }); - } -}); - -AutoForm.hooks({ - "stripe-connect-update-form": { - onSuccess: function () { - return Alerts.toast(i18next.t("admin.settings.saveSuccess"), "success"); - }, - onError: function (error) { - return Alerts.toast(`${i18next.t("admin.settings.saveFailed")} ${error}`, "error"); - } - } -}); - -Template.stripeConnectRedirect.onCreated(function () { - // TODO: Verify that this works and define steps to reproduce. - // grab stripe connects oauth values and redirect the user - const authCode = Router.getQueryParam("code"); - - Meteor.call("stripeConnect/saveSellerParams", Reaction.getSellerShopId(), authCode, function (err) { - if (err) { - // TODO: i18n here - Alerts.toast("There was an error with saving your seller params from stripe."); - } - Reaction.Router.go("/"); - }); -}); diff --git a/imports/plugins/included/payments-stripe-connect/lib/collections/schemas/index.js b/imports/plugins/included/payments-stripe-connect/lib/collections/schemas/index.js deleted file mode 100644 index dc82024ed4b..00000000000 --- a/imports/plugins/included/payments-stripe-connect/lib/collections/schemas/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from "./stripe-connect"; diff --git a/imports/plugins/included/payments-stripe-connect/lib/collections/schemas/stripe-connect.js b/imports/plugins/included/payments-stripe-connect/lib/collections/schemas/stripe-connect.js deleted file mode 100644 index c00e1ed129c..00000000000 --- a/imports/plugins/included/payments-stripe-connect/lib/collections/schemas/stripe-connect.js +++ /dev/null @@ -1,24 +0,0 @@ -import { SimpleSchema } from "meteor/aldeed:simple-schema"; -import { PackageConfig } from "/lib/collections/schemas/registry"; - -export const StripeConnectPackageConfig = new SimpleSchema([ - PackageConfig, { - "settings.mode": { - type: Boolean, - defaultValue: false - }, - "settings.api_key": { - type: String, - label: "API Client ID" - }, - "settings.transactionFee.enabled": { - type: Boolean, - label: "Enable Fee" - }, - "settings.transactionFee.percentage": { - type: Number, - label: "Fee Percentage", - decimal: true - } - } -]); diff --git a/imports/plugins/included/payments-stripe-connect/register.js b/imports/plugins/included/payments-stripe-connect/register.js deleted file mode 100644 index 94321c78b39..00000000000 --- a/imports/plugins/included/payments-stripe-connect/register.js +++ /dev/null @@ -1,43 +0,0 @@ -/* eslint camelcase: 0 */ -import { Reaction } from "/server/api"; - -Reaction.registerPackage({ - label: "Stripe Connect", - name: "reaction-stripe-connect", - icon: "fa fa-cc-stripe", - autoEnable: true, - settings: { - "mode": false, - "api_key": "", - "reaction-stripe-connect": { - enabled: false - }, - "stripe-redirect-url": "stripe-connect-redirect", - "transactionFee": { - enabled: false, - percentage: 0 - } - }, - registry: [ - // Settings panel - { - label: "Stripe Connect", - provides: "paymentSettings", - container: "dashboard", - template: "stripeConnectSettings" - }, - - // Payment form for checkout - { - template: "stripePaymentForm", - provides: "paymentMethod", - icon: "fa fa-cc-stripe" - }, - - // Redirect for Stripe Connect Sign-In - { - template: "stripeConnectRedirect", - route: "/stripe-connect-redirect" - } - ] -}); diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/ar.json b/imports/plugins/included/payments-stripe-connect/server/i18n/ar.json deleted file mode 100644 index 2f457fd874d..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/ar.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Arabic", - "i18n": "ar", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "شريط الاتصال" - }, - "dashboard": { - "stripeConnectLabel": "شريط الاتصال", - "stripeConnectDescription": "المدفوعات ربط الشريط" - }, - "paymentSettings": { - "stripeConnectLabel": "شريط الاتصال", - "stripeConnectSettingsLabel": "شريط الاتصال", - "stripeConnectSettingsDescription": "ليس لديك معرف عميل واجهة برمجة التطبيقات للاتصال بالشريط؟", - "stripeConnectSettingsGetItHere": "أحضره هنا" - }, - "redirect": { - "stripeConnectWaitingNote": "سوف السيارات إعادة توجيه في بضع ثوان أو انقر هنا." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/bg.json b/imports/plugins/included/payments-stripe-connect/server/i18n/bg.json deleted file mode 100644 index 07a9a418e4f..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/bg.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Bulgarian", - "i18n": "bg", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Свържете лентата" - }, - "dashboard": { - "stripeConnectLabel": "Свържете лентата", - "stripeConnectDescription": "Свържете плащанията с лентата" - }, - "paymentSettings": { - "stripeConnectLabel": "Свържете лентата", - "stripeConnectSettingsLabel": "Свържете лентата", - "stripeConnectSettingsDescription": "Нямате клиентски идентификационен номер на клиентски интерфейс за API за Stripe Connect?", - "stripeConnectSettingsGetItHere": "Вземете го от тук" - }, - "redirect": { - "stripeConnectWaitingNote": "Ще се пренасочи автоматично след няколко секунди или щракнете тук." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/cs.json b/imports/plugins/included/payments-stripe-connect/server/i18n/cs.json deleted file mode 100644 index c9ed44f810c..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/cs.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Czech", - "i18n": "cs", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Stripe Connect" - }, - "dashboard": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectDescription": "Stripe Connect payments" - }, - "paymentSettings": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectSettingsLabel": "Stripe Connect", - "stripeConnectSettingsDescription": "Nemáte klientské ID rozhraní Stripe Connect API?", - "stripeConnectSettingsGetItHere": "Získat ji zde" - }, - "redirect": { - "stripeConnectWaitingNote": "Bude automatické přesměrování během několika vteřin nebo zde klikněte." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/de.json b/imports/plugins/included/payments-stripe-connect/server/i18n/de.json deleted file mode 100644 index 735ab8d7b2d..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/de.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "German", - "i18n": "de", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Stripe Connect" - }, - "dashboard": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectDescription": "Stripe Connect Zahlungen" - }, - "paymentSettings": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectSettingsLabel": "Stripe Connect", - "stripeConnectSettingsDescription": "Habe keine Stripe Connect API Client ID?", - "stripeConnectSettingsGetItHere": "Holen Sie sich hier" - }, - "redirect": { - "stripeConnectWaitingNote": "Wird automatisch in ein paar Sekunden umgeleitet oder hier klicken." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/el.json b/imports/plugins/included/payments-stripe-connect/server/i18n/el.json deleted file mode 100644 index 3e8bc929b9c..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/el.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Greek", - "i18n": "el", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Σύνδεση με ταινία" - }, - "dashboard": { - "stripeConnectLabel": "Σύνδεση με ταινία", - "stripeConnectDescription": "Συνδέσεις Stripe Connect" - }, - "paymentSettings": { - "stripeConnectLabel": "Σύνδεση με ταινία", - "stripeConnectSettingsLabel": "Σύνδεση με ταινία", - "stripeConnectSettingsDescription": "Δεν έχετε αναγνωριστικό πελάτη API Stripe Connect;", - "stripeConnectSettingsGetItHere": "Αποκτήστε το εδώ" - }, - "redirect": { - "stripeConnectWaitingNote": "Θα ανακατευθύνει αυτόματα σε μερικά δευτερόλεπτα ή κάντε κλικ εδώ." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/en.json b/imports/plugins/included/payments-stripe-connect/server/i18n/en.json deleted file mode 100644 index a8fa2e7708d..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/en.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "English", - "i18n": "en", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Stripe Connect" - }, - "dashboard": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectDescription": "Stripe Connect payments" - }, - "paymentSettings": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectSettingsLabel": "Stripe Connect", - "stripeConnectSettingsDescription": "Don't have a Stripe Connect API Client ID?", - "stripeConnectSettingsGetItHere": "Get it here" - }, - "redirect": { - "stripeConnectWaitingNote": "Will auto redirect in a couple seconds or click here." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/es.json b/imports/plugins/included/payments-stripe-connect/server/i18n/es.json deleted file mode 100644 index c3f7cecdaba..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/es.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Spanish", - "i18n": "es", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Conectarse a la banda" - }, - "dashboard": { - "stripeConnectLabel": "Conectarse a la banda", - "stripeConnectDescription": "Pagos de Stripe Connect" - }, - "paymentSettings": { - "stripeConnectLabel": "Conectarse a la banda", - "stripeConnectSettingsLabel": "Conectarse a la banda", - "stripeConnectSettingsDescription": "¿No tiene un ID de cliente de API de Stripe Connect?", - "stripeConnectSettingsGetItHere": "Consiguelo aqui" - }, - "redirect": { - "stripeConnectWaitingNote": "Se redirigirá automáticamente en un par de segundos o haga clic aquí." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/fr.json b/imports/plugins/included/payments-stripe-connect/server/i18n/fr.json deleted file mode 100644 index 8d3f96fc364..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/fr.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "French", - "i18n": "fr", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Stripe Connect" - }, - "dashboard": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectDescription": "Paiements Stripe Connect" - }, - "paymentSettings": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectSettingsLabel": "Stripe Connect", - "stripeConnectSettingsDescription": "Vous ne disposez pas d'une ID de client API Stripe Connect?", - "stripeConnectSettingsGetItHere": "Obtenez-le ici" - }, - "redirect": { - "stripeConnectWaitingNote": "Réoriente automatiquement en quelques secondes ou cliquez ici." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/he.json b/imports/plugins/included/payments-stripe-connect/server/i18n/he.json deleted file mode 100644 index 3b4885df2cc..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/he.json +++ /dev/null @@ -1,14 +0,0 @@ -[{ - "language": "Hebrew", - "i18n": "he", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "paymentSettings": { - "stripeConnectSettingsGetItHere": "קבל אותו/ה כאן" - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/hr.json b/imports/plugins/included/payments-stripe-connect/server/i18n/hr.json deleted file mode 100644 index dc61a90bf44..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/hr.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Croatian", - "i18n": "hr", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Stripe Connect" - }, - "dashboard": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectDescription": "Plaćanja za Stripe Connect" - }, - "paymentSettings": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectSettingsLabel": "Stripe Connect", - "stripeConnectSettingsDescription": "Nemate klijentski ID API-ja za Stripe Connect?", - "stripeConnectSettingsGetItHere": "Nabavite ga ovdje" - }, - "redirect": { - "stripeConnectWaitingNote": "Hoće li automatski preusmjeriti za nekoliko sekundi ili kliknite ovdje." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/hu.json b/imports/plugins/included/payments-stripe-connect/server/i18n/hu.json deleted file mode 100644 index 30db6036aeb..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/hu.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Hungarian", - "i18n": "hu", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Stripe Connect" - }, - "dashboard": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectDescription": "Stripe Connect fizetések" - }, - "paymentSettings": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectSettingsLabel": "Stripe Connect", - "stripeConnectSettingsDescription": "Nincsenek Stripe Connect API ügyfél-azonosítója?", - "stripeConnectSettingsGetItHere": "Szerezd meg itt" - }, - "redirect": { - "stripeConnectWaitingNote": "Néhány másodperc alatt automatikusan átirányít vagy kattintson ide." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/index.js b/imports/plugins/included/payments-stripe-connect/server/i18n/index.js deleted file mode 100644 index e937b0aa87d..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/index.js +++ /dev/null @@ -1,31 +0,0 @@ -import { loadTranslations } from "/server/startup/i18n"; - -import ar from "./ar.json"; -import bg from "./bg.json"; -import de from "./de.json"; -import el from "./el.json"; -import en from "./en.json"; -import es from "./es.json"; -import fr from "./fr.json"; -import he from "./he.json"; -import hr from "./hr.json"; -import it from "./it.json"; -import my from "./my.json"; -import nb from "./nb.json"; -import nl from "./nl.json"; -import pl from "./pl.json"; -import pt from "./pt.json"; -import ro from "./ro.json"; -import ru from "./ru.json"; -import sl from "./sl.json"; -import sv from "./sv.json"; -import tr from "./tr.json"; -import vi from "./vi.json"; -import zh from "./zh.json"; - -// -// we want all the files in individual -// imports for easier handling by -// automated translation software -// -loadTranslations([ar, bg, de, el, en, es, fr, he, hr, it, my, nb, nl, pl, pt, ro, ru, sl, sv, tr, vi, zh]); diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/it.json b/imports/plugins/included/payments-stripe-connect/server/i18n/it.json deleted file mode 100644 index efe693cb1aa..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/it.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Italian", - "i18n": "it", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Stripe Connect" - }, - "dashboard": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectDescription": "Pagamenti con Stripe Connect" - }, - "paymentSettings": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectSettingsLabel": "Stripe Connect", - "stripeConnectSettingsDescription": "Non hai un ID Client API API di Stripe?", - "stripeConnectSettingsGetItHere": "Prendilo qui" - }, - "redirect": { - "stripeConnectWaitingNote": "Verrà reindirizzato automaticamente in un paio di secondi o clicca qui." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/my.json b/imports/plugins/included/payments-stripe-connect/server/i18n/my.json deleted file mode 100644 index f87ee3453d5..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/my.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Burmese", - "i18n": "my", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "အစင်း Connect ကို" - }, - "dashboard": { - "stripeConnectLabel": "အစင်း Connect ကို", - "stripeConnectDescription": "အစင်း Connect ကိုငွေပေးချေမှု" - }, - "paymentSettings": { - "stripeConnectLabel": "အစင်း Connect ကို", - "stripeConnectSettingsLabel": "အစင်း Connect ကို", - "stripeConnectSettingsDescription": "တစ်ဦး Stripe ဟာ Connect ကို API ကိုလိုင်း ID ကိုရှိသည်မဟုတ်လော", - "stripeConnectSettingsGetItHere": "ဒီမှာရယူပါ" - }, - "redirect": { - "stripeConnectWaitingNote": "အော်တိုစုံတွဲတစ်တွဲစက္ကန့်အတွင်း redirect သို့မဟုတ်ဒီနေရာကိုနှိပ်ပါပါလိမ့်မယ်။" - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/nb.json b/imports/plugins/included/payments-stripe-connect/server/i18n/nb.json deleted file mode 100644 index e31fbc8436b..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/nb.json +++ /dev/null @@ -1,6 +0,0 @@ -[{ - "language": "Bokmål", - "i18n": "nb", - "ns": "reaction-stripe-connect", - "translation": { } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/nl.json b/imports/plugins/included/payments-stripe-connect/server/i18n/nl.json deleted file mode 100644 index 2d1065777f6..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/nl.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Dutch", - "i18n": "nl", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Stripe Connect" - }, - "dashboard": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectDescription": "Stripe Connect betalingen" - }, - "paymentSettings": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectSettingsLabel": "Stripe Connect", - "stripeConnectSettingsDescription": "Heeft u geen Strip-Connect API Client ID?", - "stripeConnectSettingsGetItHere": "Krijg het hier" - }, - "redirect": { - "stripeConnectWaitingNote": "Zet automatisch een paar seconden omverwijzen of klik hier." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/pl.json b/imports/plugins/included/payments-stripe-connect/server/i18n/pl.json deleted file mode 100644 index cf766c73a3d..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/pl.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Polish", - "i18n": "pl", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Stripe Connect" - }, - "dashboard": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectDescription": "Stripe Connect płatności" - }, - "paymentSettings": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectSettingsLabel": "Stripe Connect", - "stripeConnectSettingsDescription": "Nie masz identyfikatora klienta API programu Stripe Connect?", - "stripeConnectSettingsGetItHere": "Pobierz go tutaj" - }, - "redirect": { - "stripeConnectWaitingNote": "Czy przekieruje auto w ciągu kilku sekund lub kliknij tutaj." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/pt.json b/imports/plugins/included/payments-stripe-connect/server/i18n/pt.json deleted file mode 100644 index dc0de672744..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/pt.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Portuguese", - "i18n": "pt", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Stripe Connect" - }, - "dashboard": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectDescription": "Compensação dos pagamentos do Stripe Connect" - }, - "paymentSettings": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectSettingsLabel": "Stripe Connect", - "stripeConnectSettingsDescription": "Não tem uma identificação do cliente Stripe Connect API?", - "stripeConnectSettingsGetItHere": "Venha aqui" - }, - "redirect": { - "stripeConnectWaitingNote": "Redirecionará automaticamente em alguns segundos ou clique aqui." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/ro.json b/imports/plugins/included/payments-stripe-connect/server/i18n/ro.json deleted file mode 100644 index 5150a6be96a..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/ro.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Romanian", - "i18n": "ro", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Stripe Connect" - }, - "dashboard": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectDescription": "Conectați plățile Stripe" - }, - "paymentSettings": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectSettingsLabel": "Stripe Connect", - "stripeConnectSettingsDescription": "Nu aveți un ID de client API Stripe Connect?", - "stripeConnectSettingsGetItHere": "Adu-o aici" - }, - "redirect": { - "stripeConnectWaitingNote": "Va redirecționa automat în câteva secunde sau dați clic aici." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/ru.json b/imports/plugins/included/payments-stripe-connect/server/i18n/ru.json deleted file mode 100644 index 0ceaf136609..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/ru.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Russian", - "i18n": "ru", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Stripe Connect" - }, - "dashboard": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectDescription": "Платные платежи" - }, - "paymentSettings": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectSettingsLabel": "Stripe Connect", - "stripeConnectSettingsDescription": "У вас нет идентификатора клиента API Stripe Connect?", - "stripeConnectSettingsGetItHere": "Получи это здесь" - }, - "redirect": { - "stripeConnectWaitingNote": "Будет автоматически перенаправляться через пару секунд или нажмите здесь." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/sl.json b/imports/plugins/included/payments-stripe-connect/server/i18n/sl.json deleted file mode 100644 index 85246e2414b..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/sl.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Slovenian", - "i18n": "sl", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Stripe Connect" - }, - "dashboard": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectDescription": "Plačila Stripe Connect" - }, - "paymentSettings": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectSettingsLabel": "Stripe Connect", - "stripeConnectSettingsDescription": "Nimate ID-ja stranke API-ja Stripe Connect?", - "stripeConnectSettingsGetItHere": "Dobite ga tukaj" - }, - "redirect": { - "stripeConnectWaitingNote": "Ali se bo samodejno preusmerilo v nekaj sekundah ali kliknite tukaj." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/sv.json b/imports/plugins/included/payments-stripe-connect/server/i18n/sv.json deleted file mode 100644 index 16803baba2c..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/sv.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Swedish", - "i18n": "sv", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Stripe Connect" - }, - "dashboard": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectDescription": "Stripe Connect-betalningar" - }, - "paymentSettings": { - "stripeConnectLabel": "Stripe Connect", - "stripeConnectSettingsLabel": "Stripe Connect", - "stripeConnectSettingsDescription": "Har du inte ett Stripe Connect API Client ID?", - "stripeConnectSettingsGetItHere": "Få det här" - }, - "redirect": { - "stripeConnectWaitingNote": "Kommer automatiskt omdirigera om några sekunder eller klicka här." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/tr.json b/imports/plugins/included/payments-stripe-connect/server/i18n/tr.json deleted file mode 100644 index 9720b6e17e3..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/tr.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Turkish", - "i18n": "tr", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Şerit Bağlama" - }, - "dashboard": { - "stripeConnectLabel": "Şerit Bağlama", - "stripeConnectDescription": "Şerit Bağlantı ödemeleri" - }, - "paymentSettings": { - "stripeConnectLabel": "Şerit Bağlama", - "stripeConnectSettingsLabel": "Şerit Bağlama", - "stripeConnectSettingsDescription": "Bir Stripe Connect API İstemcisi Kimliği yok mu?", - "stripeConnectSettingsGetItHere": "Burada Get it" - }, - "redirect": { - "stripeConnectWaitingNote": "Otomatik birkaç saniye içinde yönlendirecektir veya buraya tıklayın." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/vi.json b/imports/plugins/included/payments-stripe-connect/server/i18n/vi.json deleted file mode 100644 index c1c97b36d9f..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/vi.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Vietnamese", - "i18n": "vi", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "Kết nối Sọc" - }, - "dashboard": { - "stripeConnectLabel": "Kết nối Sọc", - "stripeConnectDescription": "Thanh toán bằng Sọc nối" - }, - "paymentSettings": { - "stripeConnectLabel": "Kết nối Sọc", - "stripeConnectSettingsLabel": "Kết nối Sọc", - "stripeConnectSettingsDescription": "Bạn không có ID Khách hàng Kết nối API Sọc?", - "stripeConnectSettingsGetItHere": "Lấy nó ở đây" - }, - "redirect": { - "stripeConnectWaitingNote": "Sẽ tự động chuyển hướng trong vài giây hoặc nhấp vào đây." - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/i18n/zh.json b/imports/plugins/included/payments-stripe-connect/server/i18n/zh.json deleted file mode 100644 index 8e0e9174b16..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/i18n/zh.json +++ /dev/null @@ -1,27 +0,0 @@ -[{ - "language": "Chinese", - "i18n": "zh", - "ns": "reaction-stripe-connect", - "translation": { - "reaction-payments": { - "admin": { - "shortcut": { - "stripeConnectLabel": "条纹连接" - }, - "dashboard": { - "stripeConnectLabel": "条纹连接", - "stripeConnectDescription": "条纹连接付款" - }, - "paymentSettings": { - "stripeConnectLabel": "条纹连接", - "stripeConnectSettingsLabel": "条纹连接", - "stripeConnectSettingsDescription": "没有Stripe Connect API客户端ID?", - "stripeConnectSettingsGetItHere": "在这里获得" - }, - "redirect": { - "stripeConnectWaitingNote": "将在几秒钟内自动重定向或点击此处。" - } - } - } - } -}] diff --git a/imports/plugins/included/payments-stripe-connect/server/index.js b/imports/plugins/included/payments-stripe-connect/server/index.js deleted file mode 100644 index 3979f964b5a..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/index.js +++ /dev/null @@ -1 +0,0 @@ -import "./i18n"; diff --git a/imports/plugins/included/payments-stripe-connect/server/methods/methods.js b/imports/plugins/included/payments-stripe-connect/server/methods/methods.js deleted file mode 100644 index 4b26a3bf529..00000000000 --- a/imports/plugins/included/payments-stripe-connect/server/methods/methods.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Meteor } from "meteor/meteor"; -import { HTTP } from "meteor/http"; -import { check } from "meteor/check"; -import { Logger } from "/server/api"; -import { Shops, Packages } from "/lib/collections"; // TODO: Should this be SellerShops? - -Meteor.methods({ - // TODO: Review all of this code for functionality - // separate url into params - // save params into sellerShop collection - "stripeConnect/saveSellerParams": function (shopId, authCode) { - // add a robust check for stripe connect settings. - check(authCode, String); - let result; - const apiKey = Packages.findOne({ name: "reaction-stripe-connect" }).settings.api_key; - const stripeUrl = "https://connect.stripe.com/oauth/token"; - try { - result = HTTP.call("POST", stripeUrl, { - params: { - client_secret: apiKey, // eslint-disable-line camelcase - code: authCode, - grant_type: "authorization_code" // eslint-disable-line camelcase - } - }); - - // TODO: check result for correct data - Shops.update({ shopId }, { - $set: { stripeConnectSettings: result } - }); - } catch (error) { - Logger.error(error); - result = { error }; - } - return result; - } -}); diff --git a/imports/plugins/included/payments-stripe/client/checkout/stripe.js b/imports/plugins/included/payments-stripe/client/checkout/stripe.js index f251dc1b4f6..39b13503f29 100644 --- a/imports/plugins/included/payments-stripe/client/checkout/stripe.js +++ b/imports/plugins/included/payments-stripe/client/checkout/stripe.js @@ -4,13 +4,10 @@ import { Template } from "meteor/templating"; import { AutoForm } from "meteor/aldeed:autoform"; import { $ } from "meteor/jquery"; import { getCardType } from "/client/modules/core/helpers/globals"; -import { Reaction } from "/client/api"; -import { Cart, SellerShops, Packages } from "/lib/collections"; -import { Stripe } from "../../lib/api"; +import { Router } from "/client/api"; +import { Cart } from "/lib/collections"; import { StripePayment } from "../../lib/collections/schemas"; -import "./stripe.html"; - let submitting = false; // @@ -31,18 +28,44 @@ function hidePaymentAlert() { } function handleStripeSubmitError(error) { - // Match eror on card number. Not submitted to stripe + // Match error on card number. Not submitted to stripe if (error && error.reason && error.reason === "Match failed") { const message = "Your card number is invalid. Please check the number and try again"; return paymentAlert(message); } // this is a server message with a client-sanitized message - if (error && error.details) { - return paymentAlert(error.details); + if (error && error.message) { + return paymentAlert(error.message); } } +// Validation helpers +function luhnValid(x) { + return [...x].reverse().reduce((sum, c, i) => { + let d = parseInt(c, 10); + if (i % 2 !== 0) { d *= 2; } + if (d > 9) { d -= 9; } + return sum + d; + }, 0) % 10 === 0; +} + +function validCardNumber(x) { + return /^[0-9]{13,16}$/.test(x) && luhnValid(x); +} + +function validExpireMonth(x) { + return /^[0-9]{1,2}$/.test(x); +} + +function validExpireYear(x) { + return /^[0-9]{4}$/.test(x); +} + +function validCVV(x) { + return /^[0-9]{3,4}$/.test(x); +} + // // Template helpers // @@ -58,8 +81,37 @@ Template.stripePaymentForm.helpers({ AutoForm.addHooks("stripe-payment-form", { onSubmit(doc) { submitting = true; - const template = this.template; hidePaymentAlert(); + const template = this.template; + const cart = Cart.findOne({ userId: Meteor.userId() }); + + // validate card data + // also validated on server + if (!validCardNumber(doc.cardNumber)) { + submitting = false; + const error = { message: "Your card number is incorrect" }; + handleStripeSubmitError(error); + uiEnd(template, "Resubmit payment"); + return false; + } + + if (!validExpireMonth(doc.expireMonth) || !validExpireYear(doc.expireYear)) { + submitting = false; + const error = { message: "Your expiration date is incorrect" }; + handleStripeSubmitError(error); + uiEnd(template, "Resubmit payment"); + return false; + } + + if (!validCVV(doc.cvv)) { + submitting = false; + const error = { message: "Your cvv is incorrect" }; + handleStripeSubmitError(error); + uiEnd(template, "Resubmit payment"); + return false; + } + + const cardData = { name: doc.payerName, number: doc.cardNumber, @@ -68,66 +120,24 @@ AutoForm.addHooks("stripe-payment-form", { cvv2: doc.cvv, type: getCardType(doc.cardNumber) }; - const storedCard = cardData.type.charAt(0).toUpperCase() + cardData.type.slice(1) + " " + doc.cardNumber.slice(-4); - Stripe.authorize(cardData, { - total: Cart.findOne().cartTotal(), - currency: SellerShops.findOne().currency - // Commenting this out because it causes tests to fail and isn't fully implemented. - // shopId: SellerShops.findOne()._id // TODO: Implement Marketplace Payments - }, function (error, transaction) { - submitting = false; - if (error) { - handleStripeSubmitError(error); - uiEnd(template, "Resubmit payment"); - } else { - if (transaction.saved === true) { - const normalizedStatus = (function () { - switch (false) { - case !(!transaction.response.captured && !transaction.response.failure_code): - return "created"; - case !(transaction.response.captured === true && !transaction.response.failure_code): - return "settled"; - case !transaction.response.failure_code: - return "failed"; - default: - return "failed"; - } - })(); - const normalizedMode = (function () { - switch (false) { - case !(!transaction.response.captured && !transaction.response.failure_code): - return "authorize"; - case !transaction.response.captured: - return "capture"; - default: - return "capture"; - } - })(); - Meteor.subscribe("Packages", Reaction.getShopId()); - const packageData = Packages.findOne({ - name: "reaction-stripe", - shopId: Reaction.getShopId() - }); - - submitting = false; - const paymentMethod = { - processor: "Stripe", - storedCard: storedCard, - method: "credit", - paymentPackageId: packageData._id, - paymentSettingsKey: packageData.registry[0].settingsKey, - transactionId: transaction.response.id, - amount: transaction.response.amount * 0.01, - status: normalizedStatus, - mode: normalizedMode, - createdAt: new Date(transaction.response.created), - transactions: [] - }; - paymentMethod.transactions.push(transaction.response); - Meteor.call("cart/submitPayment", paymentMethod); - } else { - handleStripeSubmitError(transaction.error); + + // Use apply instead of call here to prevent the flash of "your cart is empty" + // that happens when we wait for the cart subscription to update before forwarding + Meteor.apply("stripe/payment/createCharges", ["authorize", cardData, cart._id], { + onResultReceived: (error, result) => { + submitting = false; + if (error) { + handleStripeSubmitError(error); uiEnd(template, "Resubmit payment"); + } else { + if (result.success) { + Router.go("cart/completed", {}, { + _id: cart._id + }); + } else { + handleStripeSubmitError(result.error); + uiEnd(template, "Resubmit payment"); + } } } }); diff --git a/imports/plugins/included/payments-stripe/client/index.js b/imports/plugins/included/payments-stripe/client/index.js index e5c611e8212..08e8b802404 100644 --- a/imports/plugins/included/payments-stripe/client/index.js +++ b/imports/plugins/included/payments-stripe/client/index.js @@ -1,2 +1,11 @@ -import "./checkout/stripe"; -import "./settings/stripe"; +import "./checkout/stripe.html"; +import "./checkout/stripe.js"; + +import "./settings/stripe.html"; +import "./settings/stripe.js"; + +import "./settings/merchant-settings.html"; +import "./settings/merchant-settings.js"; + +import "./settings/authorize.html"; +import "./settings/authorize.js"; diff --git a/imports/plugins/included/payments-stripe/client/settings/authorize.html b/imports/plugins/included/payments-stripe/client/settings/authorize.html new file mode 100644 index 00000000000..d20ca6b3048 --- /dev/null +++ b/imports/plugins/included/payments-stripe/client/settings/authorize.html @@ -0,0 +1,5 @@ + diff --git a/imports/plugins/included/payments-stripe/client/settings/authorize.js b/imports/plugins/included/payments-stripe/client/settings/authorize.js new file mode 100644 index 00000000000..294d0f767fc --- /dev/null +++ b/imports/plugins/included/payments-stripe/client/settings/authorize.js @@ -0,0 +1,31 @@ +import { Meteor } from "meteor/meteor"; +import { Template } from "meteor/templating"; +import { Reaction, Router, i18next } from "client/api"; +import { Components } from "@reactioncommerce/reaction-components"; + +Template.stripeConnectAuthorize.onCreated(function () { + const shopId = Reaction.Router.getQueryParam("state"); + const authCode = Reaction.Router.getQueryParam("code"); + const error = Reaction.Router.getQueryParam("error"); + + if (error) { + return Alerts.toast(`${i18next.t("admin.connect.shopUserDeniedRequest")}`, "error"); + } + + if (shopId && authCode) { + Meteor.call("stripe/connect/authorizeMerchant", shopId, authCode, (err) => { + if (error) { + Alerts.toast(`${i18next.t("admin.connect.stripeConnectAccountLinkFailure")} ${err}`, "error"); + } else { + Alerts.toast(`${i18next.t("admin.connect.stripeConnectAccountLinkSuccess")}`, "success"); + } + Router.go("/"); + }); + } +}); + +Template.stripeConnectAuthorize.helpers({ + loading() { + return Components.Loading; + } +}); diff --git a/imports/plugins/included/payments-stripe/client/settings/merchant-settings.html b/imports/plugins/included/payments-stripe/client/settings/merchant-settings.html new file mode 100644 index 00000000000..f57c5d71a99 --- /dev/null +++ b/imports/plugins/included/payments-stripe/client/settings/merchant-settings.html @@ -0,0 +1,12 @@ + + + diff --git a/imports/plugins/included/payments-stripe/client/settings/merchant-settings.js b/imports/plugins/included/payments-stripe/client/settings/merchant-settings.js new file mode 100644 index 00000000000..38fc93f5aef --- /dev/null +++ b/imports/plugins/included/payments-stripe/client/settings/merchant-settings.js @@ -0,0 +1,13 @@ +import { Template } from "meteor/templating"; + +import { Reaction } from "/client/api"; + +Template.stripeConnectMerchantSignup.helpers({ + stripeConnectIsConnected() { + const stripe = Reaction.getPackageSettingsWithOptions({ + shopId: Reaction.getShopId(), + name: "reaction-stripe" + }); + return stripe && stripe.settings && stripe.settings.connectAuth && stripe.settings.connectAuth.access_token; + } +}); diff --git a/imports/plugins/included/payments-stripe/client/settings/stripe.html b/imports/plugins/included/payments-stripe/client/settings/stripe.html index 3aecfe9a6ab..b54d2070e7a 100644 --- a/imports/plugins/included/payments-stripe/client/settings/stripe.html +++ b/imports/plugins/included/payments-stripe/client/settings/stripe.html @@ -1,7 +1,7 @@ @@ -28,9 +33,9 @@
{{#if packageData.settings.api_key}} - API Client ID: + API Secret Key: {{else}} - API Client ID: + API Secret Key: {{/if}}
@@ -58,3 +63,7 @@
+ + diff --git a/imports/plugins/included/payments-stripe/client/settings/stripe.js b/imports/plugins/included/payments-stripe/client/settings/stripe.js index a3be99041dd..cfbda7c46db 100644 --- a/imports/plugins/included/payments-stripe/client/settings/stripe.js +++ b/imports/plugins/included/payments-stripe/client/settings/stripe.js @@ -4,8 +4,6 @@ import { Reaction, i18next } from "/client/api"; import { Packages } from "/lib/collections"; import { StripePackageConfig } from "../../lib/collections/schemas"; -import "./stripe.html"; - Template.stripeSettings.helpers({ StripePackageConfig() { return StripePackageConfig; diff --git a/imports/plugins/included/payments-stripe/lib/api/index.js b/imports/plugins/included/payments-stripe/lib/api/index.js deleted file mode 100644 index c68b90e230e..00000000000 --- a/imports/plugins/included/payments-stripe/lib/api/index.js +++ /dev/null @@ -1 +0,0 @@ -export { Stripe } from "./stripe"; diff --git a/imports/plugins/included/payments-stripe/lib/api/stripe.js b/imports/plugins/included/payments-stripe/lib/api/stripe.js deleted file mode 100644 index b2529efe444..00000000000 --- a/imports/plugins/included/payments-stripe/lib/api/stripe.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Meteor } from "meteor/meteor"; - -export const Stripe = { - authorize: function (cardData, paymentInfo, callback) { - Meteor.call("stripeSubmit", "authorize", cardData, paymentInfo, (error, result) => { - callback(error, result); - }); - } -}; diff --git a/imports/plugins/included/payments-stripe/lib/collections/schemas/stripe.js b/imports/plugins/included/payments-stripe/lib/collections/schemas/stripe.js index 75a2210b6b9..5e87dbe07ee 100644 --- a/imports/plugins/included/payments-stripe/lib/collections/schemas/stripe.js +++ b/imports/plugins/included/payments-stripe/lib/collections/schemas/stripe.js @@ -7,6 +7,30 @@ import { PackageConfig } from "/lib/collections/schemas/registry"; * see: https://stripe.com/docs/api */ +const StripeConnectAuthorizationCredentials = new SimpleSchema({ + token_type: { // eslint-disable-line camelcase + type: String + }, + stripe_publishable_key: { // eslint-disable-line camelcase + type: String + }, + scope: { + type: String + }, + livemode: { + type: Boolean + }, + stripe_user_id: { // eslint-disable-line camelcase + type: String + }, + refresh_token: { // eslint-disable-line camelcase + type: String + }, + access_token: { // eslint-disable-line camelcase + type: String + } +}); + export const StripePackageConfig = new SimpleSchema([ PackageConfig, { "settings.mode": { @@ -15,7 +39,12 @@ export const StripePackageConfig = new SimpleSchema([ }, "settings.api_key": { type: String, - label: "API Client ID" + label: "API Secret Key" + }, + "settings.connectAuth": { + type: StripeConnectAuthorizationCredentials, + label: "Connect Authorization Credentials", + optional: true }, "settings.reaction-stripe.support": { type: Array, @@ -24,6 +53,13 @@ export const StripePackageConfig = new SimpleSchema([ "settings.reaction-stripe.support.$": { type: String, allowedValues: ["Authorize", "De-authorize", "Capture", "Refund"] + }, + + // Public Settings + "settings.public.client_id": { + type: String, + label: "Public Client ID", + optional: true } } ]); diff --git a/imports/plugins/included/payments-stripe/register.js b/imports/plugins/included/payments-stripe/register.js index cb213e2fb5f..d67a0231cf7 100644 --- a/imports/plugins/included/payments-stripe/register.js +++ b/imports/plugins/included/payments-stripe/register.js @@ -16,7 +16,11 @@ Reaction.registerPackage({ "Capture", "Refund" ] - } + }, + "public": { + client_id: "" + }, + "connectAuth": {} }, registry: [ // Settings panel @@ -24,7 +28,8 @@ Reaction.registerPackage({ label: "Stripe", provides: "paymentSettings", container: "dashboard", - template: "stripeSettings" + template: "stripeSettings", + hideForShopTypes: ["merchant", "affiliate"] }, // Payment form for checkout @@ -32,6 +37,22 @@ Reaction.registerPackage({ template: "stripePaymentForm", provides: "paymentMethod", icon: "fa fa-cc-stripe" + }, + + // Redirect for Stripe Connect Sign-In + { + route: "/stripe/connect/authorize", + template: "stripeConnectAuthorize" + }, + + // Payment Signup for Merchants + { + label: "Stripe Merchant Account", + icon: "fa fa-cc-stripe", + container: "dashboard", + provides: "marketplaceMerchantSettings", + template: "stripeConnectMerchantSignup", + hideForShopTypes: ["primary"] } ] }); diff --git a/imports/plugins/included/payments-stripe/server/i18n/en.json b/imports/plugins/included/payments-stripe/server/i18n/en.json index 26350359abf..b5544eb8972 100644 --- a/imports/plugins/included/payments-stripe/server/i18n/en.json +++ b/imports/plugins/included/payments-stripe/server/i18n/en.json @@ -16,7 +16,17 @@ "stripeLabel": "Stripe", "stripeSettingsLabel": "Stripe", "stripeSettingsDescription": "Don't have a Stripe API Client ID?", - "stripeSettingsGetItHere": "Get it here" + "stripeSettingsGetItHere": "Get it here", + "stripeClientIdWarning": "The Client ID is a PUBLIC field. Please ensure you're using the client_id found on the Stripe Connect settings page.", + "stripeClientIdLink": "Stripe Connect Settings" + }, + "connect": { + "stripeConnectNotEnabled": "Stripe Connect is not enabled for this application. Please contact the adminstrator.", + "shopEmailNotConfigured": "Please configure your shop email first.", + "shopAddressNotConfigured": "Please configure your shop address first" + }, + "redirect": { + "stripeConnectWaitingNote": "Will auto redirect in a couple seconds or click here." } } } diff --git a/imports/plugins/included/payments-stripe/server/index.js b/imports/plugins/included/payments-stripe/server/index.js index 8f5303791ae..8f056a015a2 100644 --- a/imports/plugins/included/payments-stripe/server/index.js +++ b/imports/plugins/included/payments-stripe/server/index.js @@ -1,4 +1,6 @@ import "./i18n"; import "./methods/stripe"; +import "./methods/stripe-connect"; +import "./startup/startup"; export * from "./methods/stripeapi"; diff --git a/imports/plugins/included/payments-stripe/server/methods/stripe-connect.js b/imports/plugins/included/payments-stripe/server/methods/stripe-connect.js new file mode 100644 index 00000000000..b1bb8b66a69 --- /dev/null +++ b/imports/plugins/included/payments-stripe/server/methods/stripe-connect.js @@ -0,0 +1,75 @@ +import { Meteor } from "meteor/meteor"; +import { HTTP } from "meteor/http"; +import { check } from "meteor/check"; +import { Reaction, Logger } from "/server/api"; +import { Packages } from "/lib/collections"; + +export const methods = { + // separate url into params + // save params into sellerShop collection + "stripe/connect/authorizeMerchant": function (shopId, authCode) { + check(shopId, String); + check(authCode, String); + + if (!Reaction.hasPermission(["owner", "admin", "reaction-stripe"], Meteor.userId(), shopId)) { + Logger.warn(`user: ${Meteor.userId()} attempted to authorize merchant account + for shopId ${shopId} but was denied access due to insufficient privileges.`); + throw new Meteor.Error("access-denied", "Access Denied"); + } + + let result; + const primaryShopId = Reaction.getPrimaryShopId(); + const stripePkg = Reaction.getPackageSettingsWithOptions({ + shopId: primaryShopId, + name: "reaction-stripe" + }); + + if (!stripePkg || !stripePkg.settings || !stripePkg.settings.api_key) { + throw new Meteor.Error("cannot-authorize", "Cannot authorize stripe connect merchant. Primary shop stripe must be configured."); + } + + const merchantStripePkg = Reaction.getPackageSettingsWithOptions({ + shopId: shopId, + name: "reaction-stripe" + }); + + if (merchantStripePkg && + merchantStripePkg.settings && + merchantStripePkg.settings.connectAuth && + typeof merchantStripePkg.settings.connectAuth.stripe_user_id === "string") { + return true; + } + + + const apiKey = stripePkg.settings.api_key; + const stripeAuthUrl = "https://connect.stripe.com/oauth/token"; + try { + result = HTTP.call("POST", stripeAuthUrl, { + params: { + client_secret: apiKey, // eslint-disable-line camelcase + code: authCode, + grant_type: "authorization_code" // eslint-disable-line camelcase + } + }); + + if (result.error) { + throw new Meteor.Error("There was a problem authorizing stripe connect", result.error, result.error_description); + } + + if (result && result.data) { + // Setup connectAuth settings for this merchant + Packages.update({ _id: merchantStripePkg._id }, { + $set: { + "settings.connectAuth": result.data + } + }); + } + } catch (error) { + Logger.error(error); + result = { error }; + } + return result; + } +}; + +Meteor.methods(methods); diff --git a/imports/plugins/included/payments-stripe/server/methods/stripe-payment-create-charges.app-test.js b/imports/plugins/included/payments-stripe/server/methods/stripe-payment-create-charges.app-test.js new file mode 100644 index 00000000000..c6b70b06e57 --- /dev/null +++ b/imports/plugins/included/payments-stripe/server/methods/stripe-payment-create-charges.app-test.js @@ -0,0 +1,502 @@ +/* eslint camelcase: 0 */ +import nock from "nock"; + +import { Meteor } from "meteor/meteor"; +import { expect } from "meteor/practicalmeteor:chai"; +import { sinon } from "meteor/practicalmeteor:sinon"; +import { Factory } from "meteor/dburles:factory"; +import { check, Match } from "meteor/check"; + +import { Reaction } from "/server/api"; +import { methods } from "./stripe.js"; + + +const testStripePkg = { + _id: "dLubvXeAciAY3ECD9", + name: "reaction-stripe", + shopId: "DFzmTo27eFS97tfSW", + enabled: true, + settings: { + "mode": false, + "api_key": "sk_test_testkey", + "reaction-stripe": { + enabled: false, + support: [ + "Authorize", + "Capture", + "Refund" + ] + }, + "public": { + client_id: "" + }, + "connectAuth": { + stripe_user_id: "ca_testconnectid" + }, + "client_id": "ca_testclientid" + } +}; + +const stripeCustomerResponse = { + id: "cus_testcust", + object: "customer", + account_balance: 0, + created: 1503200959, + currency: "usd", + default_source: null, + delinquent: false, + description: null, + discount: null, + email: null, + livemode: false, + metadata: { + }, + shipping: null, + sources: { + object: "list", + data: [ + + ], + has_more: false, + total_count: 0, + url: "/v1/customers/cus_testcust/sources" + }, + subscriptions: { + object: "list", + data: [], + has_more: false, + total_count: 0, + url: "/v1/customers/cus_testcust/subscriptions" + } +}; + +const stripeCustomerResponseWithSource = { + id: "card_testcard", + object: "card", + address_city: null, + address_country: null, + address_line1: null, + address_line1_check: null, + address_line2: null, + address_state: null, + address_zip: null, + address_zip_check: null, + brand: "Visa", + country: "US", + customer: "cus_testcust", + cvc_check: "pass", + dynamic_last4: null, + exp_month: 4, + exp_year: 2019, + fingerprint: "0tSZC0FAG4yYkbXM", + funding: "credit", + last4: "4242", + metadata: {}, + name: "Test User", + tokenization_method: null +}; + +// We'll need this when we test multiple charges +// const stripeTokenResponse = { +// id: "tok_1AskR8BXXkbZQs3xdsjQ9Fmp", +// object: "token", +// card: { +// id: "card_1AskR8BXXkbZQs3xpeBlqTiF", +// object: "card", +// address_city: null, +// address_country: null, +// address_line1: null, +// address_line1_check: null, +// address_line2: null, +// address_state: null, +// address_zip: null, +// address_zip_check: null, +// brand: "Visa", +// country: "US", +// cvc_check: null, +// dynamic_last4: null, +// exp_month: 8, +// exp_year: 2018, +// fingerprint: "sMf9T3BK8Si2Nqme", +// funding: "credit", +// last4: "4242", +// metadata: { +// }, +// name: null, +// tokenization_method: null +// }, +// client_ip: null, +// created: 1503200958, +// livemode: false, +// type: "card", +// used: false +// }; + +const stripeChargeResult = { + id: "ch_testcharge", + object: "charge", + amount: 2298, + amount_refunded: 0, + captured: false, + created: 1456110785, + currency: "usd", + refunded: false, + shipping: null, + source: { + id: "card_17hA8DBXXkbZQs3xclGesDrp", + object: "card", + address_city: null, + address_country: null, + address_line1: null, + address_line1_check: null, + address_line2: null, + address_state: null, + address_zip: null, + address_zip_check: null, + brand: "Visa", + country: "US", + customer: null, + cvc_check: "pass", + dynamic_last4: null, + exp_month: 3, + exp_year: 2019, + fingerprint: "sMf9T3BK8Si2Nqme", + funding: "credit", + last4: "4242", + metadata: {}, + name: "Test User", + tokenization_method: null + }, + statement_descriptor: null, + status: "succeeded" +}; + + +describe("stripe/payment/createCharges", function () { + let sandbox; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + // See below for deeper description of the nock lib used here. + // This method cleans up nocks that might have failed for any reason. + nock.cleanAll(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it("should call stripe/payment/createCharges with the proper parameters and create an order", function (done) { + this.timeout(10000); + // This is a pretty full payment => order integration test currently. + // This test should probably be split into multiple parts + // Each part should probably isolate downstream methods that get called + // such as copyCartToOrder, etc + const cart = Factory.create("cartToOrder"); + Factory.create("account", { + _id: cart.userId, + emails: [{ address: "test@example.com" }] + }); + + // Set Meteor userId to the cart userId + sandbox.stub(Meteor, "userId", function () { + return cart.userId; + }); + + sandbox.stub(Meteor.server.method_handlers, "cart/createCart", function () { + check(arguments, [Match.Any]); + }); + + sandbox.stub(Meteor.server.method_handlers, "orders/sendNotification", function () { + check(arguments, [Match.Any]); + }); + + // This stub causes the the charge to go through as the primary shop charge + // and skip the application_fee and customer tokenization that is required + // for charging multiple shops + sandbox.stub(Reaction, "getPrimaryShopId", function () { + return cart.shopId; + }); + + sandbox.stub(Reaction, "getPackageSettingsWithOptions", function () { + return testStripePkg; + }); + + const cardData = { + cvv2: "345", + expire_month: "4", + expire_year: "2022", + name: "Test User", + number: "4242424242424242", + type: "visa" + }; + + // create a charge result object that has the cart total in stripe format (cents) + const chargeResult = Object.assign({}, stripeChargeResult, { amount: cart.cartTotal() * 100 }); + + // Testing stripe using the npm Nock lib available here: + // NPM: https://www.npmjs.com/package/nock + // Docs / Github: https://github.com/node-nock/nock + + // The nock package permits mocking of HTTP calls and responses. + // Sinon struggles to mock the stripe node package well, this does a much + // better job. + // + // To extend or add additional tests, it's best to run + // nock.recorder.rec(); + // in your test and view the output. This will give you a basic nock structure + // that you can copy as the exact url and params are somewhat obscured by + // the stripe lib + // + // You can also append .log(console.log) to your nock chain to see which + // of your nocks are handling correctly. + // + // I'm leaving the commented `.log`s below as an example for how to build + // nock http mocks. + // + // This Stack Overflow answer was helpful to me when I was getting started with nock. + // https://stackoverflow.com/questions/22645216/stubbing-stripe-with-sinon-using-stub-yields/22662511#22662511 + + // Disable any HTTP connection during test + // nock.disableNetConnect(); + + // Stripe Create Customer Nock + nock("https://api.stripe.com:443", { encodedQueryParams: true }) + .post("/v1/customers", "email=test%40example.com") + .reply(200, stripeCustomerResponse); // .log(console.log); + + // Card data for adding a card source to a customer + const number = "source%5Bnumber%5D=4242424242424242"; + const name = "source%5Bname%5D=Test%20User"; + const cvc = "source%5Bcvc%5D=345"; + const expiry = "source%5Bexp_month%5D=4&source%5Bexp_year%5D=2022"; + const source = "source%5Bobject%5D=card"; + + // Stripe Add Source To Customer Nock + nock("https://api.stripe.com:443", { encodedQueryParams: true }) + .post(`/v1/customers/${stripeCustomerResponse.id}/sources`, `${number}&${name}&${cvc}&${expiry}&${source}`) + .reply(200, stripeCustomerResponseWithSource); // .log(console.log); + + // If we were testing a multi-shop order, we'd need to nock the tokens API + // and update our /v1/charges nock to use a source (token) instead of the + // customer=customerId as used in the charge nock below + // Stripe Token Nock + // nock("https://api.stripe.com:443", { encodedQueryParams: true }) + // .post("/v1/tokens", `customer=${stripeCustomerResponse.id}`) + // .reply(200, chargeResult).log(console.log); + + // Stripe Charge Nock + nock("https://api.stripe.com:443", { encodedQueryParams: true }) + .post("/v1/charges", `amount=${cart.cartTotal() * 100}&capture=false¤cy=USD&customer=${stripeCustomerResponse.id}`) + .reply(200, chargeResult); // .log(console.log); + + methods["stripe/payment/createCharges"]("authorize", cardData, cart._id).then((res) => { + const transactionIds = Object.keys(res.transactions); + const txId = transactionIds[0]; + expect(res.success).to.equal(true); + expect(res.transactions[txId].amount).to.equal(cart.cartTotal() * 100); + }).then(() => done(), done); + }); +}); + + +// TODO: Rebuild the tests below for the new Stripe integration +// describe("Stripe.authorize", function () { +// let sandbox; +// +// beforeEach(function () { +// sandbox = sinon.sandbox.create(); +// }); +// +// afterEach(function () { +// sandbox.restore(); +// }); +// +// it("should properly charge a card when using a currency besides USD", function (done) { +// const form = { +// cvv2: "345", +// expire_month: "4", +// expire_year: "2019", +// name: "Test User", +// number: "4242424242424242", +// type: "visa" +// }; +// const total = "22.98"; +// const currency = "EUR"; +// +// sandbox.stub(StripeApi.methods.createCharge, "call", function () { +// return stripeChargeResult; +// }); +// // spyOn(StripeApi.methods.createCharge, "call").and.returnValue(stripeChargeResult); +// let chargeResult = null; +// Stripe.authorize(form, { total: total, currency: currency }, function (error, result) { +// chargeResult = result; +// expect(chargeResult).to.not.be.undefined; +// expect(chargeResult.saved).to.be.true; +// expect(StripeApi.methods.createCharge.call).to.have.been.calledWith({ +// chargeObj: { +// amount: 2298, +// currency: "EUR", +// card: { +// number: "4242424242424242", +// name: "Test User", +// cvc: "345", +// exp_month: "4", +// exp_year: "2019" +// }, capture: false +// } +// }); +// done(); +// }); +// }); +// }); +// +// describe("Stripe.authorize", function () { +// let sandbox; +// +// beforeEach(function () { +// sandbox = sinon.sandbox.create(); +// }); +// +// afterEach(function () { +// sandbox.restore(); +// }); +// +// it("should return saved = false when card is declined", function (done) { +// const form = { +// cvv2: "345", +// expire_month: "4", +// expire_year: "2019", +// name: "Test User", +// number: "4000000000000002", +// type: "visa" +// }; +// const total = "22.98"; +// const currency = "EUR"; +// +// const stripeDeclineResult = +// { +// result: null, +// error: { +// type: "StripeCardError", +// rawType: "card_error", +// code: "card_declined", +// param: undefined, +// message: "Your card was declined.", +// detail: undefined, +// raw: { +// message: "Your card was declined.", +// type: "card_error", +// code: "card_declined", +// charge: "ch_17hXeXBXXkbZQs3x3lpNoH9l", +// statusCode: 402, +// requestId: "req_7xSZItk9XdVUIJ" +// }, +// requestId: "req_7xSZItk9XdVUIJ", +// statusCode: 402 +// } +// }; +// sandbox.stub(StripeApi.methods.createCharge, "call", function () { +// return stripeDeclineResult; +// }); +// // spyOn(StripeApi.methods.createCharge, "call").and.returnValue(stripeDeclineResult); +// +// let chargeResult = null; +// Stripe.authorize(form, { total: total, currency: currency }, function (error, result) { +// chargeResult = result; +// +// expect(chargeResult).to.not.be.undefined; +// expect(chargeResult.saved).to.be.false; +// expect(chargeResult.error.message).to.equal("Your card was declined."); +// expect(StripeApi.methods.createCharge.call).to.have.been.calledWith({ +// chargeObj: { +// amount: 2298, +// currency: "EUR", +// card: { +// number: "4000000000000002", +// name: "Test User", +// cvc: "345", +// exp_month: "4", +// exp_year: "2019" +// }, capture: false +// } +// }); +// done(); +// }); +// }); +// }); +// +// describe("Stripe.authorize", function () { +// let sandbox; +// +// beforeEach(function () { +// sandbox = sinon.sandbox.create(); +// }); +// +// afterEach(function () { +// sandbox.restore(); +// }); +// +// it("should return saved = false when an expired card is returned", function (done) { +// // Note that this test number makes the Stripe API return this error, it is +// // not looking at the actual expiration date. +// const form = { +// cvv2: "345", +// expire_month: "4", +// expire_year: "2019", +// name: "Test User", +// number: "4000000000000069", +// type: "visa" +// }; +// const total = "22.98"; +// const currency = "USD"; +// +// const stripeExpiredCardResult = +// { +// result: null, +// error: { +// type: "StripeCardError", +// rawType: "card_error", +// code: "expired_card", +// param: "exp_month", +// message: "Your card has expired.", +// raw: { +// message: "Your card has expired.", +// type: "card_error", +// param: "exp_month", +// code: "expired_card", +// charge: "ch_17iBsDBXXkbZQs3xfZArVPEd", +// statusCode: 402, +// requestId: "req_7y88CojR2UJYOd" +// }, +// requestId: "req_7y88CojR2UJYOd", +// statusCode: 402 +// } +// }; +// sandbox.stub(StripeApi.methods.createCharge, "call", function () { +// return stripeExpiredCardResult; +// }); +// +// let chargeResult = null; +// Stripe.authorize(form, { total: total, currency: currency }, function (error, result) { +// chargeResult = result; +// expect(chargeResult).to.not.be.undefined; +// expect(chargeResult.saved).to.be.false; +// expect(chargeResult.error.message).to.equal("Your card has expired."); +// expect(StripeApi.methods.createCharge.call).to.have.been.calledWith({ +// chargeObj: { +// amount: 2298, +// currency: "USD", +// card: { +// number: "4000000000000069", +// name: "Test User", +// cvc: "345", +// exp_month: "4", +// exp_year: "2019" +// }, capture: false +// } +// }); +// done(); +// }); +// }); +// }); diff --git a/imports/plugins/included/payments-stripe/server/methods/stripe.js b/imports/plugins/included/payments-stripe/server/methods/stripe.js index 1374d10eb20..87866c660b9 100644 --- a/imports/plugins/included/payments-stripe/server/methods/stripe.js +++ b/imports/plugins/included/payments-stripe/server/methods/stripe.js @@ -1,44 +1,22 @@ import accounting from "accounting-js"; -/* eslint camelcase: 0 */ -// meteor modules +import stripeNpm from "stripe"; + import { Meteor } from "meteor/meteor"; -import { check, Match } from "meteor/check"; -// reaction modules +import { check } from "meteor/check"; +import { Random } from "meteor/random"; + import { Reaction, Logger } from "/server/api"; import { StripeApi } from "./stripeapi"; -function luhnValid(x) { - return [...x].reverse().reduce((sum, c, i) => { - let d = parseInt(c, 10); - if (i % 2 !== 0) { d *= 2; } - if (d > 9) { d -= 9; } - return sum + d; - }, 0) % 10 === 0; -} - -const ValidCardNumber = Match.Where(function (x) { - return /^[0-9]{13,16}$/.test(x) && luhnValid(x); -}); - -const ValidExpireMonth = Match.Where(function (x) { - return /^[0-9]{1,2}$/.test(x); -}); - -const ValidExpireYear = Match.Where(function (x) { - return /^[0-9]{4}$/.test(x); -}); - -const ValidCVV = Match.Where(function (x) { - return /^[0-9]{3,4}$/.test(x); -}); +import { Cart, Shops, Accounts, Packages } from "/lib/collections"; function parseCardData(data) { return { number: data.number, name: data.name, cvc: data.cvv2, - exp_month: data.expire_month, - exp_year: data.expire_year + exp_month: data.expire_month, // eslint-disable-line camelcase + exp_year: data.expire_year // eslint-disable-line camelcase }; } @@ -83,95 +61,287 @@ function stripeCaptureCharge(paymentMethod) { return result; } +/** + * normalizes the status of a transaction + * @method normalizeStatus + * @param {object} transaction - The transaction that we need to normalize + * @return {string} normalized status string - either failed, settled, or created + */ +function normalizeStatus(transaction) { + if (!transaction) { + throw new Meteor.Error("normalizeStatus requires a transaction"); + } + + // if this transaction failed, mode is "failed" + if (transaction.failure_code) { + return "failed"; + } -Meteor.methods({ - "stripeSubmit": function (transactionType, cardData, paymentData) { + // if this transaction was captured, status is "settled" + if (transaction.captured) { // Transaction was authorized but not captured + return "settled"; + } + + // Otherwise status is "created" + return "created"; +} + +/** + * normalizes the mode of a transaction + * @method normalizeMode + * @param {object} transaction The transaction that we need to normalize + * @return {string} normalized status string - either failed, capture, or authorize + */ +function normalizeMode(transaction) { + if (!transaction) { + throw new Meteor.Error("normalizeMode requires a transaction"); + } + + // if this transaction failed, mode is "failed" + if (transaction.failure_code) { + return "failed"; + } + + // If this transaction was captured, mode is "capture" + if (transaction.captured) { + return "capture"; + } + + // Anything else, mode is "authorize" + return "authorize"; +} + + +function buildPaymentMethods(options) { + const { cardData, cartItemsByShop, transactionsByShopId } = options; + if (!transactionsByShopId) { + throw new Meteor.Error("Creating a payment method log requries transaction data"); + } + + const shopIds = Object.keys(transactionsByShopId); + const storedCard = cardData.type.charAt(0).toUpperCase() + cardData.type.slice(1) + " " + cardData.number.slice(-4); + const packageData = Packages.findOne({ + name: "reaction-stripe", + shopId: Reaction.getPrimaryShopId() + }); + + const paymentMethods = []; + + + shopIds.forEach((shopId) => { + if (transactionsByShopId[shopId]) { + const cartItems = cartItemsByShop[shopId].map((item) => { + return { + _id: item._id, + productId: item.productId, + variantId: item.variants._id, + shopId: shopId, + quantity: item.quantity + }; + }); + + const paymentMethod = { + processor: "Stripe", + storedCard: storedCard, + method: "credit", + paymentPackageId: packageData._id, + // TODO: REVIEW WITH AARON - why is paymentSettings key important + // and why is it just defined on the client? + paymentSettingsKey: packageData.name.split("/").splice(-1)[0], + transactionId: transactionsByShopId[shopId].id, + amount: transactionsByShopId[shopId].amount * 0.01, + status: normalizeStatus(transactionsByShopId[shopId]), + mode: normalizeMode(transactionsByShopId[shopId]), + createdAt: new Date(transactionsByShopId[shopId].created), + transactions: [], + items: cartItems, + shopId: shopId + }; + paymentMethod.transactions.push(transactionsByShopId[shopId]); + paymentMethods.push(paymentMethod); + } + }); + + return paymentMethods; +} + +export const methods = { + "stripe/payment/createCharges": async function (transactionType, cardData, cartId) { check(transactionType, String); check(cardData, { name: String, - number: ValidCardNumber, - expire_month: ValidExpireMonth, - expire_year: ValidExpireYear, - cvv2: ValidCVV, + number: String, + expire_month: String, // eslint-disable-line camelcase + expire_year: String, // eslint-disable-line camelcase + cvv2: String, type: String }); - check(paymentData, { - total: String, - currency: String - // Commenting this out because it causes tests to fail and isn't fully implemented. - // shopId: String // TODO: Implement Marketplace Payment - perhaps including shopId + check(cartId, String); + + const primaryShopId = Reaction.getPrimaryShopId(); + + const stripePkg = Reaction.getPackageSettingsWithOptions({ + shopId: primaryShopId, + name: "reaction-stripe" }); - const chargeObj = { - amount: "", - currency: "", - card: {}, - capture: true - }; + const card = parseCardData(cardData); - // check if this is a seller shop for destination and transaction fee logic - // TODO: Add transaction fee to Stripe chargeObj when stripeConnect is in use. - - // Where is sellerShops coming from here? - // const sellerShop = sellerShops.findOne(paymentData.shopId); - // - // if (sellerShop && sellerShop.stripeConnectSettings) { - // const chargeObj = { - // chargeData: { - // amount: "", - // currency: "", - // transactionFee: 0, - // card: {}, - // capture: true - // }, - // stripe_account: sellerShop.stripeConnectSettings.stripe_user_id - // }; - // } else { - // const chargeObj = { - // amount: "", - // currency: "", - // card: {}, - // capture: true - // }; - // } - - if (transactionType === "authorize") { - chargeObj.capture = false; + if (!stripePkg || !stripePkg.settings || !stripePkg.settings.api_key) { + // Fail if we can't find a Stripe API key + throw new Meteor.Error("Attempted to create multiple stripe charges, but stripe was not configured properly."); } - chargeObj.card = parseCardData(cardData); - chargeObj.amount = formatForStripe(paymentData.total); - chargeObj.currency = paymentData.currency; - // TODO: Check for a transaction fee and apply - // const stripeConnectSettings = Reaction.getPackageSettings("reaction-stripe-connect").settings; - // if (sellerShop.stripeConnectSettings && stripeConnectSettings.transactionFee.enabled) { - // chargeObj.transactionFee = chargeObj.amount * stripeConnectSettings.transactionFee.percentage; - // } + const capture = transactionType === "capture"; - let result; - let chargeResult; + // Must have an email + const cart = Cart.findOne({ _id: cartId }); + const customerAccount = Accounts.findOne({ _id: cart.userId }); + let customerEmail; + + if (!customerAccount || !Array.isArray(customerAccount.emails)) { + // TODO: Is it okay to create random email here if anonymous? + Logger.Error("cart email missing!"); + throw new Meteor.Error("Email is required for marketplace checkouts."); + } + + const defaultEmail = customerAccount.emails.find((email) => email.provides === "default"); + if (defaultEmail) { + customerEmail = defaultEmail.address; + } else { + throw new Meteor.Error("Customer does not have default email"); + } + + // Initialize stripe api lib + const stripeApiKey = stripePkg.settings.api_key; + const stripe = stripeNpm(stripeApiKey); + + // get array of shopIds that exist in this cart + const shopIds = cart.items.reduce((uniqueShopIds, item) => { + if (uniqueShopIds.indexOf(item.shopId) === -1) { + uniqueShopIds.push(item.shopId); + } + return uniqueShopIds; + }, []); + + const transactionsByShopId = {}; + + // TODO: If there is only one transactionsByShopId and the shopId is primaryShopId - + // Create a standard charge and bypass creating a customer for this charge + const primaryShop = Shops.findOne({ _id: primaryShopId }); + const currency = primaryShop.currency; try { - chargeResult = StripeApi.methods.createCharge.call({ chargeObj }); - if (chargeResult && chargeResult.status && chargeResult.status === "succeeded") { - result = { - saved: true, - response: chargeResult + // Creates a customer object, adds a source via the card data + // and waits for the promise to resolve + const customer = Promise.await(stripe.customers.create({ + email: customerEmail + }).then(function (cust) { + const customerCard = stripe.customers.createSource(cust.id, { source: { ...card, object: "card" } }); + return customerCard; + })); + + // Get cart totals for each Shop + const cartTotals = cart.cartTotalByShop(); + + // Loop through all shopIds represented in cart + shopIds.forEach((shopId) => { + // TODO: If shopId is primaryShopId - create a non-connect charge with the + // stripe customer object + + const isPrimaryShop = shopId === primaryShopId; + + let merchantStripePkg; + // Initialize options - this is where idempotency_key + // and, if using connect, stripe_account go + const stripeOptions = {}; + const stripeCharge = { + amount: formatForStripe(cartTotals[shopId]), + capture: capture, + currency: currency + // TODO: add product metadata to stripe charge }; - } else { - Logger.error("Stripe Call succeeded but charge failed"); - result = { - saved: false, - error: chargeResult.error + + if (isPrimaryShop) { + // If this is the primary shop, we can make a direct charge to the + // customer object we just created. + stripeCharge.customer = customer.customer; + } else { + // If this is a merchant shop, we need to tokenize the customer + // and charge the token with the merchant id + merchantStripePkg = Reaction.getPackageSettingsWithOptions({ + shopId: shopId, + name: "reaction-stripe" + }); + + // If this merchant doesn't have stripe setup, fail. + // We should _never_ get to this point, because + // this will not roll back the entire transaction + if (!merchantStripePkg || + !merchantStripePkg.settings || + !merchantStripePkg.settings.connectAuth || + !merchantStripePkg.settings.connectAuth.stripe_user_id) { + throw new Meteor.Error(`Error processing payment for merchant with shopId ${shopId}`); + } + + // get stripe account for this shop + const stripeUserId = merchantStripePkg.settings.connectAuth.stripe_user_id; + stripeOptions.stripe_account = stripeUserId; // eslint-disable-line camelcase + + // Create token from our customer object to use with merchant shop + const token = Promise.await(stripe.tokens.create({ + customer: customer.customer + }, stripeOptions)); + + // TODO: Add description to charge in Stripe + stripeCharge.source = token.id; + + // Demo 20% application fee + stripeCharge.application_fee = formatForStripe(cartTotals[shopId] * 0.2); // eslint-disable-line camelcase + } + + // We should only do this once per shop per cart + stripeOptions.idempotency_key = `${shopId}${cart._id}${Random.id()}`; // eslint-disable-line camelcase + + // Create a charge with the options set above + const charge = Promise.await(stripe.charges.create(stripeCharge, stripeOptions)); + + transactionsByShopId[shopId] = charge; + }); + + + // get cartItemsByShop to build paymentMethods + const cartItemsByShop = cart.cartItemsByShop(); + + // Build paymentMethods from transactions, card data and cart items + const paymentMethods = buildPaymentMethods({ cardData, cartItemsByShop, transactionsByShopId }); + + // If successful, call cart/submitPayment and return success back to client. + Meteor.call("cart/submitPayment", paymentMethods); + return { success: true, transactions: transactionsByShopId }; + } catch (error) { + // If unsuccessful + // return failure back to client if error is a standard stripe card error + if (error.rawType === "card_error") { + return { + success: false, + error: { + message: error.message, + code: error.code, + type: error.type, + rawType: error.rawType, + detail: error.detail + } }; } - return result; - } catch (e) { - Logger.error(e); - throw new Meteor.Error("error", e.message); + // If we get an unexpected error, log and return a censored error message + Logger.error("Received unexpected error type: " + error.rawType); + Logger.error(error); + throw new Meteor.Error("Error creating multiple stripe charges", "An unexpected error occurred"); } }, + // TODO: Update this method to support connect captures /** * Capture a Stripe charge * @see https://stripe.com/docs/api#capture_charge @@ -196,6 +366,7 @@ Meteor.methods({ return stripeCaptureCharge(paymentMethod); }, + // TODO: Update this method to support connect /** * Issue a refund against a previously captured transaction * @see https://stripe.com/docs/api#refunds @@ -242,6 +413,7 @@ Meteor.methods({ return result; }, + // Update this method to support connect /** * List refunds * @param {Object} paymentMethod object @@ -268,4 +440,6 @@ Meteor.methods({ } return result; } -}); +}; + +Meteor.methods(methods); diff --git a/imports/plugins/included/payments-stripe/server/methods/stripeapi-methods-charge.app-test.js b/imports/plugins/included/payments-stripe/server/methods/stripeapi-methods-charge.app-test.js deleted file mode 100644 index 9f8d587d3f9..00000000000 --- a/imports/plugins/included/payments-stripe/server/methods/stripeapi-methods-charge.app-test.js +++ /dev/null @@ -1,280 +0,0 @@ -/* eslint camelcase: 0 */ -import { expect } from "meteor/practicalmeteor:chai"; -import { sinon } from "meteor/practicalmeteor:sinon"; -import { StripeApi } from "./stripeapi"; -import { Stripe } from "../../lib/api"; - -const stripeChargeResult = { - id: "ch_17hA8DBXXkbZQs3xENUmN9bZ", - object: "charge", - amount: 2298, - amount_refunded: 0, - captured: false, - created: 1456110785, - currency: "usd", - refunded: false, - shipping: null, - source: { - id: "card_17hA8DBXXkbZQs3xclGesDrp", - object: "card", - address_city: null, - address_country: null, - address_line1: null, - address_line1_check: null, - address_line2: null, - address_state: null, - address_zip: null, - address_zip_check: null, - brand: "Visa", - country: "US", - customer: null, - cvc_check: "pass", - dynamic_last4: null, - exp_month: 3, - exp_year: 2019, - fingerprint: "sMf9T3BK8Si2Nqme", - funding: "credit", - last4: "4242", - metadata: {}, - name: "Test User", - tokenization_method: null - }, - statement_descriptor: null, - status: "succeeded" -}; - - -describe("Stripe.authorize", function () { - let sandbox; - - beforeEach(function () { - sandbox = sinon.sandbox.create(); - }); - - afterEach(function () { - sandbox.restore(); - }); - - it("should call StripeApi.methods.createCharge with the proper parameters and return saved = true", function (done) { - sandbox.stub(StripeApi.methods.createCharge, "call", function () { - return stripeChargeResult; - }); - const cardData = { - cvv2: "345", - expire_month: "4", - expire_year: "2019", - name: "Test User", - number: "4242424242424242", - type: "visa" - }; - const total = "22.98"; - const currency = "USD"; - let chargeResult = null; - Stripe.authorize(cardData, { total: total, currency: currency }, function (error, result) { - chargeResult = result; - expect(chargeResult).to.not.be.undefined; - expect(chargeResult.saved).to.be.true; - done(); - }); - }); -}); - -describe("Stripe.authorize", function () { - let sandbox; - - beforeEach(function () { - sandbox = sinon.sandbox.create(); - }); - - afterEach(function () { - sandbox.restore(); - }); - - it("should properly charge a card when using a currency besides USD", function (done) { - const form = { - cvv2: "345", - expire_month: "4", - expire_year: "2019", - name: "Test User", - number: "4242424242424242", - type: "visa" - }; - const total = "22.98"; - const currency = "EUR"; - - sandbox.stub(StripeApi.methods.createCharge, "call", function () { - return stripeChargeResult; - }); - // spyOn(StripeApi.methods.createCharge, "call").and.returnValue(stripeChargeResult); - let chargeResult = null; - Stripe.authorize(form, { total: total, currency: currency }, function (error, result) { - chargeResult = result; - expect(chargeResult).to.not.be.undefined; - expect(chargeResult.saved).to.be.true; - expect(StripeApi.methods.createCharge.call).to.have.been.calledWith({ - chargeObj: { - amount: 2298, - currency: "EUR", - card: { - number: "4242424242424242", - name: "Test User", - cvc: "345", - exp_month: "4", - exp_year: "2019" - }, capture: false - } - }); - done(); - }); - }); -}); - -describe("Stripe.authorize", function () { - let sandbox; - - beforeEach(function () { - sandbox = sinon.sandbox.create(); - }); - - afterEach(function () { - sandbox.restore(); - }); - - it("should return saved = false when card is declined", function (done) { - const form = { - cvv2: "345", - expire_month: "4", - expire_year: "2019", - name: "Test User", - number: "4000000000000002", - type: "visa" - }; - const total = "22.98"; - const currency = "EUR"; - - const stripeDeclineResult = - { - result: null, - error: { - type: "StripeCardError", - rawType: "card_error", - code: "card_declined", - param: undefined, - message: "Your card was declined.", - detail: undefined, - raw: { - message: "Your card was declined.", - type: "card_error", - code: "card_declined", - charge: "ch_17hXeXBXXkbZQs3x3lpNoH9l", - statusCode: 402, - requestId: "req_7xSZItk9XdVUIJ" - }, - requestId: "req_7xSZItk9XdVUIJ", - statusCode: 402 - } - }; - sandbox.stub(StripeApi.methods.createCharge, "call", function () { - return stripeDeclineResult; - }); - // spyOn(StripeApi.methods.createCharge, "call").and.returnValue(stripeDeclineResult); - - let chargeResult = null; - Stripe.authorize(form, { total: total, currency: currency }, function (error, result) { - chargeResult = result; - - expect(chargeResult).to.not.be.undefined; - expect(chargeResult.saved).to.be.false; - expect(chargeResult.error.message).to.equal("Your card was declined."); - expect(StripeApi.methods.createCharge.call).to.have.been.calledWith({ - chargeObj: { - amount: 2298, - currency: "EUR", - card: { - number: "4000000000000002", - name: "Test User", - cvc: "345", - exp_month: "4", - exp_year: "2019" - }, capture: false - } - }); - done(); - }); - }); -}); - -describe("Stripe.authorize", function () { - let sandbox; - - beforeEach(function () { - sandbox = sinon.sandbox.create(); - }); - - afterEach(function () { - sandbox.restore(); - }); - - it("should return saved = false when an expired card is returned", function (done) { - // Note that this test number makes the Stripe API return this error, it is - // not looking at the actual expiration date. - const form = { - cvv2: "345", - expire_month: "4", - expire_year: "2019", - name: "Test User", - number: "4000000000000069", - type: "visa" - }; - const total = "22.98"; - const currency = "USD"; - - const stripeExpiredCardResult = - { - result: null, - error: { - type: "StripeCardError", - rawType: "card_error", - code: "expired_card", - param: "exp_month", - message: "Your card has expired.", - raw: { - message: "Your card has expired.", - type: "card_error", - param: "exp_month", - code: "expired_card", - charge: "ch_17iBsDBXXkbZQs3xfZArVPEd", - statusCode: 402, - requestId: "req_7y88CojR2UJYOd" - }, - requestId: "req_7y88CojR2UJYOd", - statusCode: 402 - } - }; - sandbox.stub(StripeApi.methods.createCharge, "call", function () { - return stripeExpiredCardResult; - }); - - let chargeResult = null; - Stripe.authorize(form, { total: total, currency: currency }, function (error, result) { - chargeResult = result; - expect(chargeResult).to.not.be.undefined; - expect(chargeResult.saved).to.be.false; - expect(chargeResult.error.message).to.equal("Your card has expired."); - expect(StripeApi.methods.createCharge.call).to.have.been.calledWith({ - chargeObj: { - amount: 2298, - currency: "USD", - card: { - number: "4000000000000069", - name: "Test User", - cvc: "345", - exp_month: "4", - exp_year: "2019" - }, capture: false - } - }); - done(); - }); - }); -}); diff --git a/imports/plugins/included/payments-stripe/server/methods/stripeapi.js b/imports/plugins/included/payments-stripe/server/methods/stripeapi.js index 7a0b3e42e2d..61c7c2995ab 100644 --- a/imports/plugins/included/payments-stripe/server/methods/stripeapi.js +++ b/imports/plugins/included/payments-stripe/server/methods/stripeapi.js @@ -47,15 +47,14 @@ StripeApi.methods.getApiKey = new ValidatedMethod({ name: "StripeApi.methods.getApiKey", validate: null, run() { - const settings = Reaction.getPackageSettings("reaction-stripe").settings; - // TODO: We should merge Stripe Connect plugin with reaction-stripe - // or fully separate them, but we shouldn't be checking package settings for a package - // we don't require. - const stripeConnectSettings = Reaction.getPackageSettings("reaction-stripe-connect").settings; - if (!settings.api_key && !stripeConnectSettings.api_key) { - throw new Meteor.Error("403", "Invalid Stripe Credentials"); + const stripePkg = Reaction.getPackageSettingsWithOptions({ + shopId: Reaction.getPrimaryShopId(), + name: "reaction-stripe" + }); + if (stripePkg || stripePkg.settings && stripePkg.settings.api_key) { + return stripePkg.settings.api_key; } - return stripeConnectSettings.api_key || settings.api_key; + throw new Meteor.Error("access-denied", "Invalid Stripe Credentials"); } }); diff --git a/imports/plugins/included/payments-stripe/server/startup/startup.js b/imports/plugins/included/payments-stripe/server/startup/startup.js new file mode 100644 index 00000000000..3fe1de96480 --- /dev/null +++ b/imports/plugins/included/payments-stripe/server/startup/startup.js @@ -0,0 +1,9 @@ +import { Reaction, Hooks } from "/server/api"; + +Hooks.Events.add("afterCoreInit", () => { + Reaction.addRolesToGroups({ + allShops: true, + groups: ["customer", "guest"], + roles: ["stripe/connect/authorize"] + }); +}); diff --git a/imports/plugins/included/taxes-taxcloud/server/hooks/hooks.js b/imports/plugins/included/taxes-taxcloud/server/hooks/hooks.js index d341613ae7c..62dfad4f84f 100644 --- a/imports/plugins/included/taxes-taxcloud/server/hooks/hooks.js +++ b/imports/plugins/included/taxes-taxcloud/server/hooks/hooks.js @@ -48,7 +48,7 @@ MethodHooks.after("taxes/calculate", function (options) { if (!apiKey || !apiLoginId) { Logger.warn("TaxCloud API Key is required."); } - if (typeof cartToCalc.shipping !== "undefined" && cartToCalc.items) { + if (Array.isArray(cartToCalc.shipping) && cartToCalc.shipping.length > 0 && cartToCalc.items) { const shippingAddress = cartToCalc.shipping[0].address; if (shippingAddress) { diff --git a/lib/collections/schemas/cart.js b/lib/collections/schemas/cart.js index b441ddcdd37..dcb08a080fb 100644 --- a/lib/collections/schemas/cart.js +++ b/lib/collections/schemas/cart.js @@ -62,6 +62,16 @@ export const CartItem = new SimpleSchema({ optional: true, blackbox: true }, + taxData: { + type: Object, + optional: true, + blackbox: true + }, + taxRate: { + type: Number, + decimal: true, + optional: true + }, shippingMethod: { // Shipping Method associated with this item type: Object, optional: true, @@ -133,6 +143,7 @@ export const Cart = new SimpleSchema({ optional: true, blackbox: true }, + // This is the taxRate tax: { type: Number, decimal: true, @@ -143,6 +154,11 @@ export const Cart = new SimpleSchema({ optional: true, blackbox: true }, + taxRatesByShop: { + type: Object, + optional: true, + blackbox: true + }, discount: { type: Number, decimal: true, diff --git a/lib/collections/schemas/payments.js b/lib/collections/schemas/payments.js index 2a89e97f505..f34265bbe75 100644 --- a/lib/collections/schemas/payments.js +++ b/lib/collections/schemas/payments.js @@ -3,10 +3,43 @@ import { schemaIdAutoValue } from "./helpers"; import { Address } from "./address"; import { Workflow } from "./workflow"; + /** - * PaymentMethod Schema + * Schema for items we're inserting into our Payments to keep track of what items + * were paid for with a given paymentMethod + * @type {SimpleSchema} */ +export const PaymentItem = new SimpleSchema({ + _id: { + type: String, + label: "Shipment Line Item", + optional: true, + autoValue: schemaIdAutoValue + }, + productId: { + type: String, + index: 1 + }, + shopId: { + type: String, + index: 1, + label: "Shipment Item ShopId", + optional: true + }, + quantity: { + label: "Quantity", + type: Number, + min: 0 + }, + variantId: { + type: String + } +}); + +/** + * PaymentMethod Schema + */ export const PaymentMethod = new SimpleSchema({ processor: { type: String @@ -79,6 +112,16 @@ export const PaymentMethod = new SimpleSchema({ type: [Object], optional: true, blackbox: true + }, + // TODO: Build a migration to add payment items to payment methods + items: { + type: [PaymentItem], + optional: true + }, + // TODO: Build migration to add shopIds to payment methods + shopId: { + type: String, + optional: true } }); @@ -158,6 +201,12 @@ export const Payment = new SimpleSchema({ currency: { type: Currency, optional: true + }, + // TODO: REVIEW Not sure if shopId should be optional on the Payment Schema + // TODO: If we make shopId on the payment schema required, we need to build this into the migration for old orders + shopId: { + type: String, + optional: true } }); diff --git a/lib/collections/schemas/registry.js b/lib/collections/schemas/registry.js index b1578baa286..bd6287e3b12 100644 --- a/lib/collections/schemas/registry.js +++ b/lib/collections/schemas/registry.js @@ -103,6 +103,16 @@ export const Registry = new SimpleSchema({ type: Object, optional: true, blackbox: true + }, + showForShopTypes: { + label: "Shop Types this plugin should show for", + type: [String], + optional: true + }, + hideForShopTypes: { + label: "Shop Types this plugin should not show for", + type: [String], + optional: true } }); diff --git a/lib/collections/transform/cart.js b/lib/collections/transform/cart.js index 6965a922d80..b2fce07d58c 100644 --- a/lib/collections/transform/cart.js +++ b/lib/collections/transform/cart.js @@ -6,17 +6,29 @@ import accounting from "accounting-js"; * @param {Array} items - cart.items array * @param {Array} prop - path to item property represented by array * @param {Array} [prop2] - path to another item property represented by array + * @param {String} [shopId] - shopId * @return {Number} - computations result */ -function getSummary(items, prop, prop2) { +function getSummary(items, prop, prop2, shopId) { try { if (Array.isArray(items)) { return items.reduce((sum, item) => { if (prop2) { + if (shopId) { + if (shopId === item.shopId) { + // if we're looking for a specific shop's items and this item does match + return sum + item[prop[0]] * (prop2.length === 1 ? item[prop2[0]] : + item[prop2[0]][prop2[1]]); + } + // If we're looking for a specific shop's items and this item doesn't match + return sum; + } + // No shopId param // S + a * b, where b could be b1 or b2 return sum + item[prop[0]] * (prop2.length === 1 ? item[prop2[0]] : item[prop2[0]][prop2[1]]); } + // No prop2 param // S + b, where b could be b1 or b2 return sum + (prop.length === 1 ? item[prop[0]] : item[prop[0]][prop[1]]); @@ -56,6 +68,23 @@ export const cartTransform = { const subTotal = getSummary(this.items, ["quantity"], ["variants", "price"]); return accounting.toFixed(subTotal, 2); }, + + /** + * Aggregates the subtotals by shopId + * @method cartSubTotalByShop + * @return {object} An Object with a key for each shopId in the cart where the value is the subtotal for that shop + */ + cartSubTotalByShop() { + return this.items.reduce((uniqueShopSubTotals, item) => { + if (!uniqueShopSubTotals[item.shopId]) { + const subTotal = getSummary(this.items, ["quantity"], ["variants", "price"], item.shopId); + uniqueShopSubTotals[item.shopId] = accounting.toFixed(subTotal, 2); + return uniqueShopSubTotals; + } + return uniqueShopSubTotals; + }, {}); + }, + cartTaxes() { // taxes are calculated in a Cart.after.update hooks // the tax value stored with the cart is the effective tax rate @@ -66,10 +95,32 @@ export const cartTransform = { const taxTotal = subTotal * tax; return accounting.toFixed(taxTotal, 2); }, + + /** + * Aggregates the taxes by shopId + * @method cartTaxesByShop + * @return {[type]} An object with a key for each shopId in the cart where the value is the tax total for that shop + */ + cartTaxesByShop() { + const subtotals = this.cartSubTotalByShop(); + const taxRates = this.taxRatesByShop; + + return Object.keys(subtotals).reduce((shopTaxTotals, shopId) => { + if (!shopTaxTotals[shopId]) { + const shopSubtotal = parseFloat(subtotals[shopId]); + const shopTaxRate = taxRates && taxRates[shopId] || 0; + const shopTaxTotal = shopSubtotal * shopTaxRate; + shopTaxTotals[shopId] = accounting.toFixed(shopTaxTotal, 2); + } + return shopTaxTotals; + }, {}); + }, + cartDiscounts() { const discount = this.discount || 0; return accounting.toFixed(discount, 2); }, + cartTotal() { const subTotal = parseFloat(this.cartSubTotal()); const shipping = parseFloat(this.cartShipping()); @@ -78,5 +129,43 @@ export const cartTransform = { const discountTotal = Math.max(0, subTotal - discount); const total = discountTotal + shipping + taxes; return accounting.toFixed(total, 2); + }, + + /** + * Aggregates the cart total by shopId + * @method cartTotalByShop + * @return {object} An object with a key for each shopId in the cart where the value is the total for that shop + */ + cartTotalByShop() { + const subtotals = this.cartSubTotalByShop(); + const taxes = this.cartTaxesByShop(); + const shipping = parseFloat(this.cartShipping()); + // no discounts right now because that will need to support multi-shop + + return Object.keys(subtotals).reduce((shopTotals, shopId) => { + if (!shopTotals[shopId]) { + const shopSubtotal = parseFloat(subtotals[shopId]); + const shopTaxes = parseFloat(taxes[shopId]); + const shopTotal = shopSubtotal + shopTaxes + shipping; + shopTotals[shopId] = accounting.toFixed(shopTotal, 2); + } + return shopTotals; + }, {}); + }, + + /** + * cart items organized by shopId + * @method cartItemsByShop + * @return {Object} Dict of shopIds with an array of items from that shop that are present in the cart + */ + cartItemsByShop() { + return this.items.reduce((itemsByShop, item) => { + if (!itemsByShop[item.shopId]) { + itemsByShop[item.shopId] = [item]; + } else { + itemsByShop[item.shopId].push(item); + } + return itemsByShop; + }, {}); } }; diff --git a/package.json b/package.json index abec2763ad3..dbc86e6e060 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "moment": "^2.18.1", "moment-timezone": "^0.5.13", "nexmo": "^2.0.2", + "nock": "^9.0.14", "node-geocoder": "^3.18.0", "node-loggly-bulk": "^2.0.0", "nodemailer-wellknown": "^0.2.3", diff --git a/server/api/core/core.js b/server/api/core/core.js index 6af45958cea..91bb7f66937 100644 --- a/server/api/core/core.js +++ b/server/api/core/core.js @@ -413,6 +413,17 @@ export default { return Packages.findOne({ name: name, shopId: this.getShopId() }) || null; }, + /** + * Takes options in the form of a query object. Returns a package that matches. + * @method getPackageSettingsWithOptions + * @param {object} options Options object, forms the query for Packages.findOne + * @return {object} Returns the first package found with the provided options + */ + getPackageSettingsWithOptions(options) { + const query = options; + return Packages.findOne(query); + }, + /** * getMarketplaceSettings finds the enabled `reaction-marketplace` package for * the primary shop and returns the settings @@ -509,6 +520,11 @@ export default { if (enabledPackages && Array.isArray(enabledPackages)) { if (enabledPackages.indexOf(pkg.name) === -1) { pkg.enabled = false; + } else { + // Enable "soft switch" for package. + if (pkg.settings && pkg.settings[packageName]) { + pkg.settings[packageName].enabled = true; + } } } Packages.insert(pkg); diff --git a/server/methods/core/cart.js b/server/methods/core/cart.js index 24f7091287a..268602d828c 100644 --- a/server/methods/core/cart.js +++ b/server/methods/core/cart.js @@ -553,7 +553,7 @@ Meteor.methods({ const cart = Collections.Cart.findOne(cartId); // security check - method can only be called on own cart - if (cart.userId !== this.userId) { + if (cart.userId !== Meteor.userId()) { throw new Meteor.Error(403, "Access Denied"); } @@ -1094,74 +1094,94 @@ Meteor.methods({ * @summary saves a submitted payment to cart, triggers workflow * and adds "paymentSubmitted" to cart workflow * Note: this method also has a client stub, that forwards to cartCompleted - * @param {Object} paymentMethod - paymentMethod object - * directly within this method, just throw down though hooks + * @param {Object|Array} paymentMethods - an array of paymentMethods or (deprecated) a single paymentMethod object * @return {String} returns update result */ - "cart/submitPayment": function (paymentMethod) { - check(paymentMethod, Reaction.Schemas.PaymentMethod); + "cart/submitPayment": function (paymentMethods) { + if (Array.isArray((paymentMethods))) { + check(paymentMethods, [Reaction.Schemas.PaymentMethod]); + } else { + check(paymentMethods, Reaction.Schemas.PaymentMethod); + } + - const checkoutCart = Collections.Cart.findOne({ + const cart = Collections.Cart.findOne({ userId: Meteor.userId() }); - const cart = _.clone(checkoutCart); const cartId = cart._id; - const invoice = { - shipping: cart.cartShipping(), - subtotal: cart.cartSubTotal(), - taxes: cart.cartTaxes(), - discounts: cart.cartDiscounts(), - total: cart.cartTotal() - }; + + const cartShipping = cart.cartShipping(); + const cartSubTotal = cart.cartSubTotal(); + const cartSubTotalByShop = cart.cartSubTotalByShop(); + const cartTaxes = cart.cartTaxes(); + const cartTaxesByShop = cart.cartTaxesByShop(); + const cartDiscounts = cart.cartDiscounts(); + const cartTotal = cart.cartTotal(); + const cartTotalByShop = cart.cartTotalByShop(); // we won't actually close the order at this stage. // we'll just update the workflow and billing data where // method-hooks can process the workflow update. - let selector; - let update; - // temp hack until we build out multiple billing handlers - // if we have an existing item update it, otherwise add to set. + const payments = []; + let paymentAddress; - // TODO: Marketplace Payments - Add support for multiple billing handlers here - if (cart.items) { - // TODO: Needs to be improved to consider which transaction goes with which item - // For now just attach the transaction to each item in the cart - const cartItemsWithPayment = cart.items.map(item => { - item.transaction = paymentMethod.transactions[paymentMethod.transactions.length - 1]; - return item; - }); - cart.items = cartItemsWithPayment; + // Find the payment address associated that the user input during the + // checkout process + if (Array.isArray(cart.billing) && cart.billing[0]) { + paymentAddress = cart.billing[0].address; } - if (cart.billing) { - selector = { - "_id": cartId, - "billing._id": cart.billing[0]._id - }; - update = { - $set: { - "billing.$.paymentMethod": paymentMethod, - "billing.$.invoice": invoice, - "items": cart.items - } - }; + // Payment plugins which have been updated for marketplace are passing an array as paymentMethods + if (Array.isArray(paymentMethods)) { + paymentMethods.forEach((paymentMethod) => { + const shopId = paymentMethod.shopId; + const invoice = { + shipping: parseFloat(cartShipping), + subtotal: parseFloat(cartSubTotalByShop[shopId]), + taxes: parseFloat(cartTaxesByShop[shopId]), + discounts: parseFloat(cartDiscounts), + total: parseFloat(cartTotalByShop[shopId]) + }; + + payments.push({ + paymentMethod: paymentMethod, + invoice: invoice, + address: paymentAddress, + shopId: shopId + }); + }); } else { - selector = { - _id: cartId - }; - update = { - $addToSet: { - "billing.paymentMethod": paymentMethod, - "billing.invoice": invoice - }, - $set: { - items: cart.items - } + // Legacy payment integration - transactions are not split by shop + // Create an invoice based on cart totals. + const invoice = { + shipping: cartShipping, + subtotal: cartSubTotal, + taxes: cartTaxes, + discounts: cartDiscounts, + total: cartTotal }; + + // Legacy payment plugins are passing in a single paymentMethod object + payments.push({ + paymentMethod: paymentMethods, + invoice: invoice, + address: paymentAddress, + shopId: Reaction.getPrimaryShopId() + }); } + const selector = { + _id: cartId + }; + + const update = { + $set: { + billing: payments + } + }; + try { Collections.Cart.update(selector, update); } catch (e) { diff --git a/server/methods/core/hooks/cart.js b/server/methods/core/hooks/cart.js index c1fe009b886..943c262bd26 100644 --- a/server/methods/core/hooks/cart.js +++ b/server/methods/core/hooks/cart.js @@ -6,6 +6,7 @@ import "../cart"; // // Meteor.after to call after MethodHooks.after("cart/submitPayment", function (options) { + // TODO: REVIEW WITH AARON - this is too late to fail. We need to copy cart to order either way at this point // if cart/submit had an error we won't copy cart to Order // and we'll throw an error. Logger.debug("MethodHooks after cart/submitPayment", options); @@ -37,6 +38,8 @@ MethodHooks.after("cart/submitPayment", function (options) { ); } } + } else { + throw new Meteor.Error("Error after submitting payment", options.error); } return result; }); diff --git a/server/methods/core/orders.js b/server/methods/core/orders.js index 5b8ab41f286..071cb8d55d4 100644 --- a/server/methods/core/orders.js +++ b/server/methods/core/orders.js @@ -412,7 +412,7 @@ export const methods = { check(order, Object); check(action, Match.OneOf(String, undefined)); - // REVIEW: SECURITY this only checks to see if a userId exists + // TODO: REVIEW: SECURITY this only checks to see if a userId exists if (!this.userId) { Logger.error("orders/sendNotification: Access denied"); throw new Meteor.Error("access-denied", "Access Denied"); @@ -434,12 +434,14 @@ export const methods = { } const billing = orderCreditMethod(order); - const refundResult = Meteor.call("orders/refunds/list", order); - let refundTotal = 0; - - _.each(refundResult, function (item) { - refundTotal += parseFloat(item.amount); - }); + // TODO: Update */refunds/list for marketplace + // const refundResult = Meteor.call("orders/refunds/list", order); + const refundTotal = 0; + + // TODO: We should use reduce here + // _.each(refundResult, function (item) { + // refundTotal += parseFloat(item.amount); + // }); // Get user currency formatting from shops collection, remove saved rate const userCurrencyFormatting = _.omit(shop.currencies[billing.currency.userCurrency], ["enabled", "rate"]); diff --git a/server/methods/core/shop.js b/server/methods/core/shop.js index 881506d25c5..0cb25b00fb6 100644 --- a/server/methods/core/shop.js +++ b/server/methods/core/shop.js @@ -32,6 +32,7 @@ Meteor.methods({ // this.unblock(); const count = Collections.Shops.find().count() || ""; const currentUser = Meteor.user(); + const currentAccount = Collections.Accounts.findOne({ _id: currentUser._id }); if (!currentUser) { throw new Meteor.Error("Unable to create shop with specified user"); @@ -48,7 +49,6 @@ Meteor.methods({ // identify a shop owner const userId = shopAdminUserId || currentUser._id; - const shopOwner = Meteor.users.findOne(userId); // ensure unique id and shop name shop._id = Random.id(); @@ -62,13 +62,20 @@ Meteor.methods({ // admin or marketplace needs to be on and guests allowed to create shops if (currentUser && Reaction.hasMarketplaceAccess("guest")) { // add user info for new shop - shop.emails = shopOwner.emails; - // TODO: Review source of default address for shop from user + shop.emails = currentUser.emails; + + // Reaction currently stores addressBook in Accounts collection not users - if (shopOwner.profile && shopOwner.profile.addressBook) { - shop.addressBook = [shopOwner.profile && shopOwner.profile.addressBook]; + if (currentAccount && currentAccount.addressBook && Array.isArray(currentAccount.addressBook)) { + shop.addressBook = currentAccount.addressBook; } + // TODO: SEUN REVIEW. Changed to above from below + // if (currentUser.profile && currentUser.profile.addressBook) { + // shop.addressBook = [currentUser.profile && currentUser.profile.addressBook]; + // } + + // clean up new shop delete shop.createdAt; delete shop.updatedAt; diff --git a/server/publications/collections/packages.js b/server/publications/collections/packages.js index 7a2f7de8b45..1cb492df5cc 100644 --- a/server/publications/collections/packages.js +++ b/server/publications/collections/packages.js @@ -97,7 +97,7 @@ Meteor.publish("Packages", function (shopId) { }; if (!shopId) { - myShopId = Reaction.getShopId(); + myShopId = Reaction.getPrimaryShopId(); } // we should always have a shop