+ {{#autoForm
+ meteormethod="shippo/updateApiKey"
+ schema=ShippoPackageConfig
+ doc=packageData
+ type="method-update"
+ id="shippo-update-form"
+ }}
+ {{> afQuickField name="settings.apiKey" class='form-control'}}
+ {{> shopSettingsSubmitButton}}
+ {{/autoForm}}
+ {{else}}
+
{{#autoForm
meteormethod="shippo/updateApiKey"
schema=ShippoPackageConfig
@@ -11,20 +21,17 @@
type="method-update"
id="shippo-update-form"
}}
-
-
- {{> afQuickField name="settings.apiKey"}}
-
-
-
- Save Changes
-
+ {{> afQuickField name="settings.apiKey" class='form-control'}}
+ {{> shopSettingsSubmitButton}}
{{/autoForm}}
-
-
- Fetch Carriers
-
+
+
+ Edit API Key
+
+
+
+ {{> shippoCarriers}}
-
+ {{/unless}}
diff --git a/imports/plugins/included/shippo/register.js b/imports/plugins/included/shippo/register.js
index 2842ad83751..63ddc68569f 100644
--- a/imports/plugins/included/shippo/register.js
+++ b/imports/plugins/included/shippo/register.js
@@ -7,7 +7,7 @@ Reaction.registerPackage({
autoEnable: true,
settings: {
shippo: {
- enabled: true
+ enabled: false
},
// todo: move all settings in shippo subfield
apiKey: "",
@@ -27,22 +27,23 @@ Reaction.registerPackage({
}, {
label: "Shippo",
icon: "fa fa-plane",
- route: "/dashboard/shippo",
- provides: "settings",
+ name: "shipping/settings/shippo",
+ provides: "shippingSettings",
container: "connection",
template: "shippoSettings"
}
- // WIP:
- // For now we use Flat Rate's checkout template( which inherits its methods from coreCheckoutShipping
- // to show all shipping methods in the same panel.
- // .If we are gonna proceed with different panel per provider, we need to enable the 'provides:"Shipping Method"',
- // alter coreCheckoutShipping checkout.js and inherit from there (or write specific logic) for a shippo's
- // checkout template.
- //
- // provides: "shippingMethod",
- // name: "shipping/methods/shippo",
- // template: "shippoCheckoutShipping"
- // Not needed at the time cause the coreCheckoutShipping is enough(inherited from Flatrate)
- //}
+// WIP:
+// TODO: Review custom shipping in checkout, are layout handling this requirement
+// For now we use Flat Rate's checkout template( which inherits its methods from coreCheckoutShipping
+// to show all shipping methods in the same panel.
+// .If we are gonna proceed with different panel per provider, we need to enable the 'provides:"Shipping Method"',
+// alter coreCheckoutShipping checkout.js and inherit from there (or write specific logic) for a shippo's
+// checkout template.
+//
+// provides: "shippingMethod",
+// name: "shipping/methods/shippo",
+// template: "shippoCheckoutShipping"
+// Not needed at the time cause the coreCheckoutShipping is enough(inherited from Flatrate)
+// }
]
});
diff --git a/imports/plugins/included/shippo/server/hooks/hooks.js b/imports/plugins/included/shippo/server/hooks/hooks.js
new file mode 100644
index 00000000000..d796f210843
--- /dev/null
+++ b/imports/plugins/included/shippo/server/hooks/hooks.js
@@ -0,0 +1,65 @@
+import { Meteor } from "meteor/meteor";
+import { Shipping, Packages } from "/lib/collections";
+import { Logger, Reaction, Hooks } from "/server/api";
+
+// callback ran on getShippingRates hook
+function getShippingRates(rates, cart) {
+ const shops = [];
+ const products = cart.items;
+
+ const { settings } = Packages.findOne({
+ name: "reaction-shippo",
+ shopId: Reaction.getShopId()
+ });
+
+ // must have cart items and package enabled to calculate shipping
+ if (!cart.items || settings.shippo.enabled !== true) {
+ return rates;
+ }
+
+ // default selector is current shop
+ let selector = {
+ "shopId": Reaction.getShopId(),
+ "provider.enabled": true
+ };
+
+ // create an array of shops, allowing
+ // the cart to have products from multiple shops
+ for (const product of products) {
+ if (product.shopId) {
+ shops.push(product.shopId);
+ }
+ }
+ // if we have multiple shops in cart
+ if ((shops !== null ? shops.length : void 0) > 0) {
+ selector = {
+ "shopId": {
+ $in: shops
+ },
+ "provider.enabled": true
+ };
+ }
+
+ const shippingCollection = Shipping.find(selector);
+ const shippoDocs = {};
+ if (shippingCollection) {
+ shippingCollection.forEach(function (doc) {
+ // If provider is from Shippo, put it in an object to get rates dynamically(shippoApi) for all of them after.
+ if (doc.provider.shippoProvider) {
+ shippoDocs[doc.provider.shippoProvider.carrierAccountId] = doc;
+ }
+ });
+
+ // Get shippingRates from Shippo
+ if (Object.keys(shippoDocs).length > 0) {
+ const shippoRates = Meteor.call("shippo/getShippingRatesForCart", cart._id, shippoDocs);
+ rates.push(...shippoRates);
+ }
+ }
+
+ Logger.debug("Shippo onGetShippingRates", rates);
+ return rates;
+}
+
+// run getShippingRates when the onGetShippingRates event runs
+Hooks.Events.add("onGetShippingRates", getShippingRates);
diff --git a/imports/plugins/included/shippo/server/i18n/en.json b/imports/plugins/included/shippo/server/i18n/en.json
index f73387951d7..833d502f572 100644
--- a/imports/plugins/included/shippo/server/i18n/en.json
+++ b/imports/plugins/included/shippo/server/i18n/en.json
@@ -8,6 +8,16 @@
"shippoLabel": "Shippo",
"shippoTitle": "Shippo",
"shippoDescription": "Shipping labels and package tracking"
+ },
+ "shippingGrid": {
+ "carrier": "Carrier",
+ "enabled": "Enabled"
+ },
+ "shippingSettings": {
+ "noCarriersFound": "No shipping carriers configured.",
+ "editApiKey": "Edit API Key",
+ "carrierSaved": "Carrier saved successfully.",
+ "carrierFailed": "Error updating carrier."
}
},
"shippo": {
diff --git a/imports/plugins/included/shippo/server/index.js b/imports/plugins/included/shippo/server/index.js
index 7c8a97225e1..b573d50cab5 100644
--- a/imports/plugins/included/shippo/server/index.js
+++ b/imports/plugins/included/shippo/server/index.js
@@ -1,3 +1,4 @@
import "./methods";
import "./jobs";
import "./i18n";
+import "./hooks/hooks";
diff --git a/imports/plugins/included/shippo/server/jobs/shippo.js b/imports/plugins/included/shippo/server/jobs/shippo.js
index 30e94ad38ec..74ba64b6d68 100644
--- a/imports/plugins/included/shippo/server/jobs/shippo.js
+++ b/imports/plugins/included/shippo/server/jobs/shippo.js
@@ -27,8 +27,8 @@ Hooks.Events.add("afterCoreInit", () => {
if (!config.shippo.enabled || !refreshPeriod) {
return;
}
-
- Logger.info(`Adding shippo/fetchTrackingStatusForOrders to JobControl. Refresh ${refreshPeriod}`);
+ // there might be some validity to this being Logger.info.
+ Logger.debug(`Adding shippo/fetchTrackingStatusForOrders to JobControl. Refresh ${refreshPeriod}`);
new Job(Jobs, "shippo/fetchTrackingStatusForOrdersJob", {})
.priority("normal")
.retry({
@@ -56,6 +56,7 @@ export default function () {
workTimeout: 180 * 1000
},
(job, callback) => {
+ // TODO review meteor runAsUser and add to project documentation
// As this is run by the Server and we don't have userId()/this.userId
// which "shippo/fetchTrackingStatusForOrders" need, we use dispatch:run-as-user
// An alternative way is https://forums.meteor.com/t/cant-set-logged-in-user-for-rest-calls/18656/3
@@ -64,8 +65,8 @@ export default function () {
if (error) {
job.done(error.toString(), { repeatId: true });
} else {
- const success = "Latest Shippo's Tracking Status of Orders fetched successfully.";
- Logger.info(success);
+ const success = "Shippo tracking status updated.";
+ Logger.debug(success);
job.done(success, { repeatId: true });
}
});
diff --git a/imports/plugins/included/shippo/server/lib/roles.js b/imports/plugins/included/shippo/server/lib/roles.js
new file mode 100644
index 00000000000..6ddb33fb351
--- /dev/null
+++ b/imports/plugins/included/shippo/server/lib/roles.js
@@ -0,0 +1 @@
+export const shippingRoles = ["admin", "owner", "shipping", "reaction-shipping-rates"];
diff --git a/imports/plugins/included/shippo/server/methods/carriers.js b/imports/plugins/included/shippo/server/methods/carriers.js
new file mode 100644
index 00000000000..d952c977058
--- /dev/null
+++ b/imports/plugins/included/shippo/server/methods/carriers.js
@@ -0,0 +1,31 @@
+import { Meteor } from "meteor/meteor";
+import { check } from "meteor/check";
+import { Shipping } from "/lib/collections";
+import { Reaction } from "/server/api";
+import { shippingRoles } from "../lib/roles";
+
+export const methods = {
+ /**
+ * shippo/carrier/update
+ * @summary update Shipping methods for a provider
+ * @param {String} provider provider object
+ * @return {Number} update result
+ */
+ "shippo/carrier/update": function (provider) {
+ check(provider, Object); // ShippingProvider
+ if (!Reaction.hasPermission(shippingRoles)) {
+ throw new Meteor.Error(403, "Access Denied");
+ }
+ const method = {};
+ method.provider = provider;
+ const flatten = require("flatten-obj")();
+ const update = flatten(method);
+ return Shipping.update({
+ "provider._id": provider._id
+ }, {
+ $set: update
+ });
+ }
+};
+
+Meteor.methods(methods);
diff --git a/imports/plugins/included/shippo/server/methods/index.js b/imports/plugins/included/shippo/server/methods/index.js
index d8ba8f4d4ca..95523e245b7 100644
--- a/imports/plugins/included/shippo/server/methods/index.js
+++ b/imports/plugins/included/shippo/server/methods/index.js
@@ -1 +1,2 @@
import "./shippo";
+import "./carriers";
diff --git a/imports/plugins/included/shippo/server/methods/shippo.js b/imports/plugins/included/shippo/server/methods/shippo.js
index 9810f375911..d06020d91ad 100644
--- a/imports/plugins/included/shippo/server/methods/shippo.js
+++ b/imports/plugins/included/shippo/server/methods/shippo.js
@@ -1,8 +1,11 @@
/* eslint camelcase: 0 */
+import { Meteor } from "meteor/meteor";
+import { check } from "meteor/check";
import { Reaction } from "/server/api";
import { Packages, Accounts, Shops, Shipping, Cart, Orders } from "/lib/collections";
import { ShippoPackageConfig } from "../../lib/collections/schemas";
import { ShippoApi } from "./shippoapi";
+import { shippingRoles } from "../lib/roles";
// Creates an address (for sender or recipient) suitable for Shippo Api Calls given
// a reaction address an email and a purpose("QUOTE"|"PURCHASE")
@@ -53,7 +56,7 @@ function ratesParser(shippoRates, shippoDocs) {
rate: rateAmount,
handling: 0,
carrier: rate.provider,
- shippoMethod: {
+ settings: {
// carrierAccount: rate.carrier_account,
rateId: rate.object_id,
serviceLevelToken: rate.servicelevel_token
@@ -69,7 +72,7 @@ function ratesParser(shippoRates, shippoDocs) {
// Filters the carrier list and gets and parses only the ones that are activated in the Shippo Account
function filterActiveCarriers(carrierList) {
- let activeCarriers = [];
+ const activeCarriers = [];
if (carrierList.results && carrierList.count) {
carrierList.results.forEach(carrier => {
if (carrier.active) {
@@ -155,8 +158,8 @@ function updateShippoProviders(activeCarriers, shopId = Reaction.getShopId()) {
// Ids of Shippo Carriers that exist currently as docs in Shipping Collection
const currentCarriersIds = currentShippoProviders.map(doc => doc.provider.shippoProvider.carrierAccountId);
- let newActiveCarriers = [];
- let unchangedActiveCarriersIds = [];
+ const newActiveCarriers = [];
+ const unchangedActiveCarriersIds = [];
activeCarriers.forEach(carrier => {
const carrierId = carrier.carrierAccountId;
if (!currentCarriersIds.includes(carrierId)) {
@@ -177,8 +180,7 @@ function updateShippoProviders(activeCarriers, shopId = Reaction.getShopId()) {
return true;
}
-Meteor.methods({
-
+export const methods = {
/**
* Updates the Api key(Live/Test Token) used for connection with the Shippo account.
* Also inserts(and deletes if already exist) docs in the Shipping collection each of the
@@ -197,7 +199,7 @@ Meteor.methods({
// Make sure user has proper rights to this package
const { shopId } = Packages.findOne({ _id },
{ field: { shopId: 1 } });
- if (shopId && Roles.userIsInRole(this.userId, ["admin", "owner"], shopId)) {
+ if (shopId && Roles.userIsInRole(this.userId, shippingRoles, shopId)) {
// If user wants to delete existing key
if (modifier.hasOwnProperty("$unset")) {
const customModifier = { $set: { "settings.apiKey": null } };
@@ -238,7 +240,7 @@ Meteor.methods({
"shippo/fetchProviders"() {
const shopId = Reaction.getShopId();
- if (Roles.userIsInRole(this.userId, ["admin", "owner"], shopId)) {
+ if (Roles.userIsInRole(this.userId, shippingRoles, shopId)) {
const apiKey = getApiKey(shopId);
if (!apiKey) {
return false;
@@ -337,7 +339,6 @@ Meteor.methods({
check(shippoDocs, Object);
const cart = Cart.findOne(cartId);
if (cart && cart.userId === this.userId) { // confirm user has the right
- let shippoAddressFrom;
let shippoAddressTo;
let shippoParcel;
const purpose = "PURCHASE";
@@ -357,8 +358,8 @@ Meteor.methods({
if (!apiKey) {
return [];
}
-
- shippoAddressFrom = createShippoAddress(shop.addressBook[0], shop.emails[0].address, purpose);
+ // TODO create a shipping address book record for shop.
+ const shippoAddressFrom = createShippoAddress(shop.addressBook[0], shop.emails[0].address, purpose);
// product in the cart has to have parcel property with the dimensions
if (cart.items && cart.items[0] && cart.items[0].parcel) {
const unitOfMeasure = shop && shop.unitsOfMeasure && shop.unitsOfMeasure[0].uom || "KG";
@@ -376,7 +377,15 @@ Meteor.methods({
});
// check that there is address available in cart
if (cart.shipping && cart.shipping[0] && cart.shipping[0].address) {
- shippoAddressTo = createShippoAddress(cart.shipping[0].address, buyer.emails[0].address, purpose);
+ // TODO take a more elegant approach to guest checkout -> no email address
+ // add Logger.trace if this smells
+ let email = shop.emails[0].address || "noreply@localhost";
+ if (buyer.emails.length > 0) {
+ if (buyer.emails[0].address) {
+ email = buyer.emails[0].address;
+ }
+ }
+ shippoAddressTo = createShippoAddress(cart.shipping[0].address, email, purpose);
} else {
return [];
}
@@ -408,17 +417,17 @@ Meteor.methods({
check(orderId, String);
const order = Orders.findOne(orderId);
// Make sure user has permissions in the shop's order
- if (Roles.userIsInRole(this.userId, ["admin", "owner"], order.shopId)) {
+ if (Roles.userIsInRole(this.userId, shippingRoles, order.shopId)) {
// Here we done it for the first/unique Shipment only . in the near future it will be done for multiple ones
- if (order.shipping[0].shipmentMethod.shippoMethod &&
- order.shipping[0].shipmentMethod.shippoMethod.rateId) {
+ if (order.shipping[0].shipmentMethod.settings &&
+ order.shipping[0].shipmentMethod.settings.rateId) {
const apiKey = getApiKey(order.shopId);
// If for a weird reason Shop hasn't a Shippo Api key anymore you have to throw an error
// cause the Shippo label purchasing is not gonna happen.
if (!apiKey) {
throw new Meteor.Error("403", "Invalid Shippo Credentials");
}
- const rateId = order.shipping[0].shipmentMethod.shippoMethod.rateId;
+ const rateId = order.shipping[0].shipmentMethod.settings.rateId;
// make the actual purchase
const transaction = ShippoApi.methods.createTransaction.call({ rateId, apiKey });
@@ -438,4 +447,6 @@ Meteor.methods({
return false;
}
-});
+};
+
+Meteor.methods(methods);
diff --git a/lib/collections/schemas/shipping.js b/lib/collections/schemas/shipping.js
index 6f892da35d5..fe174932647 100644
--- a/lib/collections/schemas/shipping.js
+++ b/lib/collections/schemas/shipping.js
@@ -7,6 +7,7 @@ import { Workflow } from "./workflow";
/**
* ShippoShippingMethod Schema
+ * TODO move shippo related schema to shippo module
* This will only exist in ShippingMethods Inside Cart/Order and not DB shipping Collection
* as Shippo Methods are Dynamic.
*/
@@ -42,7 +43,8 @@ export const ShippingMethod = new SimpleSchema({
},
"group": {
type: String,
- label: "Group"
+ label: "Group",
+ allowedValues: ["Ground", "Priority", "One Day", "Free"]
},
"cost": {
type: Number,
@@ -67,7 +69,7 @@ export const ShippingMethod = new SimpleSchema({
"enabled": {
type: Boolean,
label: "Enabled",
- defaultValue: true
+ defaultValue: false
},
"validRanges": {
type: Array,
@@ -123,7 +125,7 @@ export const ShippingMethod = new SimpleSchema({
type: String, // Alternatively we can make an extra Schema:ShipmentMethod, that inherits
optional: true // ShippingMethod and add the optional carrier field
},
- "shippoMethod": {
+ "settings": {
type: ShippoShippingMethod,
optional: true
}
@@ -324,9 +326,16 @@ export const ShippoShippingProvider = new SimpleSchema({
*/
export const ShippingProvider = new SimpleSchema({
+ _id: {
+ type: String,
+ label: "Provider Id",
+ optional: true,
+ autoValue: schemaIdAutoValue
+ },
name: {
type: String,
- label: "Service Code"
+ label: "Service Code",
+ optional: true
},
label: {
type: String,
diff --git a/package.json b/package.json
index ce551870978..571c1c15c94 100644
--- a/package.json
+++ b/package.json
@@ -19,96 +19,97 @@
"url": "https://github.com/reactioncommerce/reaction/issues"
},
"dependencies": {
- "@reactioncommerce/authorize-net": "^1.0.7",
+ "@reactioncommerce/authorize-net": "^1.0.8",
"accounting-js": "^1.1.1",
- "autoprefixer": "^6.5.3",
- "autosize": "^3.0.19",
+ "autoprefixer": "^6.7.1",
+ "autosize": "^3.0.20",
"avalara-taxrates": "^1.0.1",
- "babel-runtime": "^6.18.0",
+ "babel-runtime": "^6.22.0",
"bcrypt": "^1.0.2",
"bootstrap": "^3.3.7",
- "braintree": "^1.41.0",
- "bunyan": "^1.8.1",
+ "braintree": "^1.47.0",
+ "bunyan": "^1.8.5",
"bunyan-format": "^0.2.1",
- "bunyan-loggly": "^1.1.0",
+ "bunyan-loggly": "^1.2.0",
"classnames": "^2.2.5",
"country-data": "^0.0.31",
"css-annotation": "^0.6.2",
"deep-diff": "^0.3.4",
"dnd-core": "^2.0.2",
"faker": "^3.1.0",
- "fibers": "^1.0.14",
- "font-awesome": "^4.6.3",
- "griddle-react": "^0.7.0",
- "handlebars": "^4.0.5",
- "i18next": "^4.1.0",
- "i18next-browser-languagedetector": "^1.0.0",
+ "fibers": "^1.0.15",
+ "flatten-obj": "^3.1.0",
+ "font-awesome": "^4.7.0",
+ "griddle-react": "^0.7.1",
+ "handlebars": "^4.0.6",
+ "i18next": "^6.0.3",
+ "i18next-browser-languagedetector": "^1.0.1",
"i18next-localstorage-cache": "^0.3.0",
"i18next-sprintf-postprocessor": "^0.2.2",
"immutable": "^3.8.1",
"jquery": "^3.1.1",
- "jquery-i18next": "^1.1.0",
+ "jquery-i18next": "^1.2.0",
"later": "^1.2.0",
- "lodash": "^4.17.2",
+ "lodash": "^4.17.4",
"lodash.pick": "^4.4.0",
- "meteor-node-stubs": "^0.2.3",
- "moment": "^2.17.0",
- "moment-timezone": "^0.5.9",
- "nexmo": "^1.1.0",
- "node-geocoder": "^3.15.0",
- "nodemailer": "^2.6.4",
- "nodemailer-wellknown": "^0.2.0",
+ "meteor-node-stubs": "^0.2.5",
+ "moment": "^2.17.1",
+ "moment-timezone": "^0.5.11",
+ "nexmo": "^1.1.2",
+ "node-geocoder": "^3.16.0",
+ "nodemailer": "^2.7.2",
+ "nodemailer-wellknown": "^0.2.1",
"npm-shrinkwrap": "^6.0.2",
- "paypal-rest-sdk": "^1.6.9",
- "postcss": "^5.2.5",
+ "paypal-rest-sdk": "^1.7.1",
+ "postcss": "^5.2.11",
"postcss-js": "^0.2.0",
- "prerender-node": "^2.6.0",
+ "prerender-node": "^2.7.0",
"radium": "^0.18.1",
- "react": "^15.3.2",
- "react-addons-create-fragment": "^15.3.2",
- "react-addons-pure-render-mixin": "^15.3.2",
- "react-autosuggest": "^7.0.1",
- "react-bootstrap": "^0.30.5",
- "react-color": "^2.3.2",
+ "react": "^15.4.2",
+ "react-addons-create-fragment": "^15.4.2",
+ "react-addons-pure-render-mixin": "^15.4.2",
+ "react-autosuggest": "^8.0.0",
+ "react-bootstrap": "^0.30.7",
+ "react-color": "^2.11.1",
"react-dnd": "^2.1.4",
"react-dnd-html5-backend": "^2.1.2",
- "react-dom": "^15.3.2",
- "react-dropzone": "^3.6.0",
- "react-helmet": "^3.1.0",
+ "react-dom": "^15.4.2",
+ "react-dropzone": "^3.9.2",
+ "react-helmet": "^4.0.0",
"react-komposer": "^2.0.0",
"react-nouislider": "^1.14.2",
- "react-onclickoutside": "^5.7.1",
+ "react-onclickoutside": "^5.8.4",
"react-select": "^1.0.0-rc.2",
"react-simple-di": "^1.2.0",
"react-taco-table": "^0.5.0",
- "react-tether": "^0.5.2",
+ "react-tether": "^0.5.5",
"react-textarea-autosize": "^4.0.5",
- "shippo": "^1.1.3",
+ "shippo": "^1.2.0",
"sortablejs": "^1.5.0-rc1",
- "stripe": "^4.11.0",
- "sweetalert2": "^6.1.0",
- "swiper": "^3.3.1",
+ "stripe": "^4.15.0",
+ "sweetalert2": "^6.3.2",
+ "swiper": "^3.4.1",
"tether-drop": "^1.4.2",
"tether-tooltip": "^1.2.0",
- "transliteration": "^1.1.9",
+ "transliteration": "^1.2.3",
"twilio": "^2.11.1",
"url": "^0.11.0",
- "velocity-animate": "^1.3.1",
- "velocity-react": "^1.1.11"
+ "velocity-animate": "^1.4.0",
+ "velocity-react": "^1.2.1"
},
"devDependencies": {
- "babel-eslint": "^7.0.0",
- "babel-plugin-lodash": "^3.2.9",
- "babel-preset-stage-2": "^6.17.0",
- "browserstack-local": "^1.0.0",
+ "babel-eslint": "^7.1.1",
+ "babel-plugin-lodash": "^3.2.11",
+ "babel-preset-stage-2": "^6.22.0",
+ "browserstack-local": "^1.3.0",
"chai": "^3.5.0",
- "eslint": "^3.7.1",
- "eslint-plugin-react": "^6.3.0",
- "js-yaml": "^3.6.1",
- "react-addons-test-utils": "^15.3.2",
- "wdio-allure-reporter": "^0.1.1",
- "wdio-mocha-framework": "^0.5.4",
- "webdriverio": "^4.2.16"
+ "eslint": "^3.14.1",
+ "eslint-plugin-react": "^6.9.0",
+ "js-yaml": "^3.7.0",
+ "react-addons-test-utils": "^15.4.2",
+ "wdio-allure-reporter": "^0.1.2",
+ "wdio-mocha-framework": "^0.5.8",
+ "webdriverio": "^4.6.2"
},
"postcss": {
"plugins": {
diff --git a/private/data/Shipping.json b/private/data/Shipping.json
index 1f40d8f817f..a3498100d9f 100644
--- a/private/data/Shipping.json
+++ b/private/data/Shipping.json
@@ -4,7 +4,6 @@
"name": "Free",
"label": "Free Shipping",
"group": "Ground",
- "enabled": true,
"rate": 0,
"validLocales": [{
"deliveryBegin": 2,
@@ -17,7 +16,6 @@
"name": "Standard",
"label": "Standard",
"group": "Ground",
- "enabled": true,
"rate": 2.99,
"validLocales": [{
"deliveryBegin": 2,
@@ -27,7 +25,6 @@
"name": "Priority",
"label": "Priority",
"group": "Priority",
- "enabled": true,
"rate": 6.99,
"validLocales": [{
"deliveryBegin": 1,
@@ -35,8 +32,7 @@
}]
}],
"provider": {
- "name": "Flat Rate",
- "label": "Flat Rate",
- "enabled": true
+ "name": "flatRates",
+ "label": "Flat Rate"
}
}]
diff --git a/private/data/i18n/en.json b/private/data/i18n/en.json
index 6b715ce0c77..18170900002 100644
--- a/private/data/i18n/en.json
+++ b/private/data/i18n/en.json
@@ -576,68 +576,6 @@
"checkoutReview": {
"review": "Review"
},
- "checkoutShipping": {
- "selectShippingOption": "Select shipping option",
- "noShippingMethods": "No shipping methods are configured.",
- "configureNow": "Configure now.",
- "shipping": "Shipping"
- },
- "shipping": {
- "addShippingProvider": "Add shipping provider",
- "editShippingProvider": "Edit shipping provider",
- "addShippingMethod": "Add shipping method",
- "editShippingMethod": "Edit shipping method",
- "deleteShippingMethod": "Delete shipping method",
- "noSettingsForThisView": "No settings for this view",
- "noShippingMethods": "No shipping methods are configured.",
- "removeShippingMethodConfirm": "Are you sure you want to delete {{method}}?",
- "removeShippingMethodTitle": "Remove Shipping Method",
- "shippingMethodDeleted": "This shipping method has been deleted.",
- "removeShippingProvider": "Remove Shipping Provider",
- "removeShippingProviderConfirm": "Are you sure you want to delete {{provider}}?",
- "shippingProviderSaved": "Shipping provider saved.",
- "shippingProviderUpdated": "Shipping provider data updated.",
- "shippingMethodRateAdded": "Shipping method rate added.",
- "shippingMethodRateUpdated": "Shipping method rate updated.",
- "name": "Name",
- "label": "Label",
- "group": "Group",
- "cost": "Cost",
- "handling": "Handling",
- "rate": "Rate",
- "enabled": "Enabled",
- "disabled": "Disabled",
- "addRate": "Add rate",
- "updateRate": "Update {{name}} rate",
- "addNewCondition": "Add new condition",
- "deleteCondition": "Delete condition",
- "provider": {
- "name": "Service Code",
- "label": "Public Label",
- "enabled": "Enabled"
- }
- },
- "shippingMethod": {
- "name": "Method Name",
- "label": "Public Label",
- "group": "Group",
- "cost": "Cost",
- "handling": "Handling",
- "rate": "Rate",
- "enabled": "Enabled",
- "matchingCartRanges": "Matching Cart Ranges",
- "validRanges": {
- "begin": "Begin",
- "end": "End"
- },
- "matchingLocales": "Matching Locales",
- "validLocales": {
- "origination": "From",
- "destination": "To",
- "deliveryBegin": "Shipping Est.",
- "deliveryEnd": "Delivery Est."
- }
- },
"uom": {
"OZ": "Ounces",
"LB": "Pounds",
diff --git a/server/methods/core/orders.js b/server/methods/core/orders.js
index 129fd9b973f..6f6dadea068 100644
--- a/server/methods/core/orders.js
+++ b/server/methods/core/orders.js
@@ -843,11 +843,9 @@ export const methods = {
});
// Temporarily(?) put here the Shippo's method/label purchasing.After a succesfull capture fund
- if (order.shipping[0].shipmentMethod.shippoMethod) {
+ if (order.shipping[0].shipmentMethod.settings) {
Meteor.call("shippo/confirmShippingMethodForOrder", orderId);
}
-
-
} else {
if (result && result.error) {
Logger.fatal("Failed to capture transaction.", order, paymentMethod.transactionId, result.error);
diff --git a/server/methods/core/shipping.js b/server/methods/core/shipping.js
index 511633e544d..7310d7364f2 100644
--- a/server/methods/core/shipping.js
+++ b/server/methods/core/shipping.js
@@ -1,15 +1,14 @@
-import _ from "lodash";
import { Meteor } from "meteor/meteor";
import { check } from "meteor/check";
-import { Cart, Shipping } from "/lib/collections";
-import { Logger, Reaction } from "/server/api";
+import { Cart } from "/lib/collections";
+import { Logger, Hooks } from "/server/api";
import { Cart as CartSchema } from "/lib/collections/schemas";
/*
* Reaction Shipping Methods
* methods typically used for checkout (shipping, taxes, etc)
*/
-Meteor.methods({
+export const methods = {
/**
* shipping/updateShipmentQuotes
* @summary gets shipping rates and updates the users cart methods
@@ -24,17 +23,15 @@ Meteor.methods({
check(cartId, String);
this.unblock();
const cart = Cart.findOne(cartId);
+
+ check(cart, CartSchema);
+
if (cart) {
const rates = Meteor.call("shipping/getShippingRates", cart);
- // no rates found
- if (!rates) {
- return [];
- }
let selector;
let update;
- // temp hack until we build out multiple shipment handlers
- // if we have an existing item update it, otherwise add to set.
- if (cart.shipping && rates.length > 0) {
+ // temp hack until we build out multiple shipment handlers if we have an existing item update it, otherwise add to set.
+ if (cart.shipping) {
selector = {
"_id": cartId,
"shipping._id": cart.shipping[0]._id
@@ -57,15 +54,13 @@ Meteor.methods({
};
}
// add quotes to the cart
- if (rates.length > 0) {
- Cart.update(selector, update, function (error) {
- if (error) {
- Logger.warn(`Error adding rates to cart ${cartId}`, error);
- return;
- }
- Logger.debug(`Success adding rates to cart ${cartId}`, rates);
- });
- }
+ Cart.update(selector, update, function (error) {
+ if (error) {
+ Logger.warn(`Error adding rates to cart ${cartId}`, error);
+ return;
+ }
+ Logger.debug(`Success adding rates to cart ${cartId}`, rates);
+ });
}
},
@@ -78,77 +73,16 @@ Meteor.methods({
"shipping/getShippingRates": function (cart) {
check(cart, CartSchema);
const rates = [];
- const shops = [];
- const products = cart.items;
- // default selector is current shop
- let selector = {
- "shopId": Reaction.getShopId(),
- "provider.enabled": true
- };
- // must have products to calculate shipping
+ // must have items to calculate shipping
if (!cart.items) {
return [];
}
- // create an array of shops, allowing
- // the cart to have products from multiple shops
- for (const product of products) {
- if (product.shopId) {
- shops.push(product.shopId);
- }
- }
- // if we have multiple shops in cart
- if ((shops !== null ? shops.length : void 0) > 0) {
- selector = {
- "shopId": {
- $in: shops
- },
- "provider.enabled": true
- };
- }
-
- const shippingCollection = Shipping.find(selector);
- let shippoDocs = {};
-
- shippingCollection.forEach(function (doc) {
- const _results = [];
- // If provider is from Shippo, put it in an object to get rates dynamically(shippoApi) for all of them after.
- if (doc.provider.shippoProvider) {
- shippoDocs[doc.provider.shippoProvider.carrierAccountId] = doc;
- } else {
- for (const method of doc.methods) {
- if (!method.enabled) {
- continue;
- }
- if (!method.rate) {
- method.rate = 0;
- }
- if (!method.handling) {
- method.handling = 0;
- }
- // Store shipping provider here in order to have it available in shipmentMethod
- // for cart and order usage
- if (!method.carrier) {
- method.carrier = doc.provider.label;
- }
- const rate = method.rate + method.handling;
- _results.push(
- rates.push({
- carrier: doc.provider.label,
- method: method,
- rate: rate,
- shopId: doc.shopId
- })
- );
- }
- return _results;
- }
- });
- // Get shippingRates from Shippo
- if (!_.isEmpty(shippoDocs)) {
- const shippoRates = Meteor.call("shippo/getShippingRatesForCart", cart._id, shippoDocs);
- rates.push(...shippoRates);
- }
+ // hooks for other shipping rate events
+ // all callbacks should return rates
+ Hooks.Events.run("onGetShippingRates", rates, cart);
Logger.debug("getShippingRates returning rates", rates);
return rates;
}
-});
+};
+
+Meteor.methods(methods);
diff --git a/server/publications/collections/products.js b/server/publications/collections/products.js
index 48dbcf2197b..324e9990844 100644
--- a/server/publications/collections/products.js
+++ b/server/publications/collections/products.js
@@ -1,5 +1,5 @@
import { Products, Revisions } from "/lib/collections";
-import { Reaction } from "/server/api";
+import { Reaction, Logger } from "/server/api";
import { RevisionApi } from "/imports/plugins/core/revisions/lib/api/revisions";
//
@@ -69,11 +69,19 @@ const filters = new SimpleSchema({
*/
Meteor.publish("Products", function (productScrollLimit = 24, productFilters, sort = {}) {
check(productScrollLimit, Number);
- check(productFilters, Match.OneOf(undefined, filters));
+ check(productFilters, Match.OneOf(undefined, Object));
check(sort, Match.OneOf(undefined, Object));
+ // if there are filter/params that don't match the schema
+ // validate, catch except but return no results
+ try {
+ check(productFilters, Match.OneOf(undefined, filters));
+ } catch (e) {
+ Logger.debug(e, "Invalid Product Filters");
+ return this.ready();
+ }
+ // ensure that we've got a shop instance
const shop = Reaction.getCurrentShop();
-
if (typeof shop !== "object") {
return this.ready();
}
diff --git a/server/publications/collections/shipping.js b/server/publications/collections/shipping.js
index bcead6dd656..7606a159652 100644
--- a/server/publications/collections/shipping.js
+++ b/server/publications/collections/shipping.js
@@ -1,16 +1,32 @@
+import { Meteor } from "meteor/meteor";
+import { Match, check } from "meteor/check";
import { Shipping } from "/lib/collections";
import { Reaction } from "/server/api";
-
+import { Counts } from "meteor/tmeasday:publish-counts";
/**
* shipping
*/
-Meteor.publish("Shipping", function () {
+Meteor.publish("Shipping", function (query, options) {
+ check(query, Match.Optional(Object));
+ check(options, Match.Optional(Object));
+
const shopId = Reaction.getShopId();
if (!shopId) {
return this.ready();
}
- return Shipping.find({
- shopId: shopId
- });
+ const select = query || {};
+ select.shopId = shopId;
+
+ // appends a count to the collection
+ // we're doing this for use with griddleTable
+ Counts.publish(this, "shipping-count", Shipping.find(
+ select,
+ options
+ ));
+
+ return Shipping.find(
+ select,
+ options
+ );
});