Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
75f92f6
feat: add ordersByAccountId query
kieckhafer Feb 12, 2019
2a113ef
fix formatting of package.json
kieckhafer Feb 12, 2019
b64ab60
Merge remote-tracking branch 'origin/develop' into feat-kieckhafer-or…
kieckhafer Feb 12, 2019
8d77f77
add order status resolver and types
kieckhafer Feb 12, 2019
cd5c14d
add permissions check for multiple shopIds
kieckhafer Feb 13, 2019
6f86510
Merge remote-tracking branch 'origin/develop' into feat-kieckhafer-or…
kieckhafer Feb 13, 2019
d418cac
Merge remote-tracking branch 'origin/develop' into feat-kieckhafer-or…
kieckhafer Feb 13, 2019
34c857f
add shopsUserHasPermissionFor check
kieckhafer Feb 15, 2019
3595b72
change ordersByAccountId to paginated list
kieckhafer Feb 15, 2019
6a88627
add `shopUserHasPermissionsFor` permission check
kieckhafer Feb 15, 2019
18f59a4
add and update tests for new permissions check
kieckhafer Feb 15, 2019
e122131
update variables to fix lint issues
kieckhafer Feb 15, 2019
af1e5a8
alphabatize variables for easier searching
kieckhafer Feb 15, 2019
7c99274
add language to orderStatusLabels query
kieckhafer Feb 19, 2019
b2eb181
extend Shops schema to allow for orderStatusLabel translations
kieckhafer Feb 19, 2019
4a5f298
Merge remote-tracking branch 'origin/develop' into feat-kieckhafer-or…
kieckhafer Feb 20, 2019
f35ec0f
Merge remote-tracking branch 'origin/develop' into feat-kieckhafer-or…
kieckhafer Feb 20, 2019
e6d6a3b
add tracking number to fulfillment gropu data
kieckhafer Feb 20, 2019
3e81f65
add orderStatus variable to query to limit search resutls to certain …
kieckhafer Feb 21, 2019
836a402
make order status optional, always return all if no other statuses ar…
kieckhafer Feb 21, 2019
c4e3b09
add filter for canceled ordres to query
kieckhafer Feb 21, 2019
7f29d63
add orderSummary to order resolver
kieckhafer Feb 22, 2019
ef3c086
get currencyCode form order object instead of first fulfillmentGroup
kieckhafer Feb 25, 2019
6cc54fd
refactor the way Status is passed to the client
kieckhafer Feb 25, 2019
009bff0
use break instead of ifs for statuses
kieckhafer Feb 25, 2019
62586a0
Pass array of orderstatus into ordersByAccountId
kieckhafer Feb 26, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions imports/node-app/core/util/buildContext.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getHasPermissionFunctionForUser } from "/imports/plugins/core/accounts/server/no-meteor/hasPermission";
import { getShopsUserHasPermissionForFunctionForUser } from "/imports/plugins/core/accounts/server/no-meteor/shopsUserHasPermissionFor";
import getShopIdForContext from "/imports/plugins/core/accounts/server/no-meteor/getShopIdForContext";
import getRootUrl from "/imports/plugins/core/core/server/util/getRootUrl";
import getAbsoluteUrl from "/imports/plugins/core/core/server/util/getAbsoluteUrl";
Expand Down Expand Up @@ -38,6 +39,9 @@ export default async function buildContext(context, request = {}) {
// Add a curried hasPermission tied to the current user (or to no user)
context.userHasPermission = getHasPermissionFunctionForUser(context.user);

// Add array of all shopsIds user has permissions for
context.shopsUserHasPermissionFor = getShopsUserHasPermissionForFunctionForUser(context.user);

context.rootUrl = getRootUrl(request);
context.getAbsoluteUrl = (path) => getAbsoluteUrl(context.rootUrl, path);

Expand Down
12 changes: 7 additions & 5 deletions imports/node-app/core/util/buildContext.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ test("properly mutates the context object without user", async () => {
await buildContext(context, { user: undefined });
expect(context).toEqual({
collections: mockContext.collections,
getAbsoluteUrl: jasmine.any(Function),
queries: {
primaryShopId: jasmine.any(Function)
},
rootUrl: "http://localhost:3000/",
shopId: "PRIMARY_SHOP_ID",
shopsUserHasPermissionFor: jasmine.any(Function),
user: null,
userHasPermission: jasmine.any(Function),
userId: null,
rootUrl: "http://localhost:3000/",
getAbsoluteUrl: jasmine.any(Function)
userId: null
});
});

Expand All @@ -45,15 +46,16 @@ test("properly mutates the context object with user", async () => {
account: mockAccount,
accountId: mockAccount._id,
collections: mockContext.collections,
getAbsoluteUrl: jasmine.any(Function),
queries: {
primaryShopId: jasmine.any(Function)
},
shopId: "PRIMARY_SHOP_ID",
shopsUserHasPermissionFor: jasmine.any(Function),
user: fakeUser,
userHasPermission: jasmine.any(Function),
userId: fakeUser._id,
rootUrl: "https://localhost:3000/",
getAbsoluteUrl: jasmine.any(Function)
userId: fakeUser._id
});

// Make sure the hasPermission currying works with one arg
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { curryN } from "ramda";

/**
* @name shopsUserHasPermissionFor
* @method
* @memberof Accounts
* @param {Object} user - The user object, with `roles` property, to check.
* @param {String} permission - Permission to check for.
* @return {Array} Shop IDs user has provided permissions for
*/
export default function shopsUserHasPermissionFor(user, permission) {
if (!user || !user.roles || !permission) return [];

const { roles } = user;
const shopIds = [];

// `role` is a shopId, with an array of permissions attached to it.
// Get the key of each shopId, and check if the permission exists on that key
// If it does, then user has permission on this shop.
Object.keys(roles).forEach((role) => {
if (roles[role].includes(permission)) {
shopIds.push(role);
}
});

return shopIds;
}

export const getShopsUserHasPermissionForFunctionForUser = curryN(2, shopsUserHasPermissionFor);
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import shopsUserHasPermissionFor from "./shopsUserHasPermissionFor";

const user = {
roles: {
abc: [
"accounts",
"orders"
],
def: [
"navigation",
"orders"
]
}
};

test("returns blank array if no user", () => {
const result = shopsUserHasPermissionFor(null, "orders");
expect(result).toEqual([]);
});

test("returns blank array if no user.roles", () => {
const result = shopsUserHasPermissionFor({}, "orders");
expect(result).toEqual([]);
});

test("returns blank array if no permission", () => {
const result = shopsUserHasPermissionFor(user, null);
expect(result).toEqual([]);
});

test("returns an array of both shops for `orders` permission", () => {
const result = shopsUserHasPermissionFor(user, "orders");
expect(result).toEqual(["abc", "def"]);
});

test("returns an array of shop `abc` for `accounts` permission", () => {
const result = shopsUserHasPermissionFor(user, "accounts");
expect(result).toEqual(["abc"]);
});

test("returns an array of shop `def` for `navigation` permission", () => {
const result = shopsUserHasPermissionFor(user, "navigation");
expect(result).toEqual(["def"]);
});

test("returns a blank array for `dogs` permission", () => {
const result = shopsUserHasPermissionFor(user, "dogs");
expect(result).toEqual([]);
});
2 changes: 2 additions & 0 deletions imports/plugins/core/orders/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import "./templates/orders.js";
import "./containers/invoiceContainer";
import "./containers/orderSummaryContainer";

import "../lib/extendShopSchema";

registerOperatorRoute({
path: "/orders/:_id",
mainComponent: OrderDetail,
Expand Down
14 changes: 14 additions & 0 deletions imports/plugins/core/orders/lib/extendShopSchema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Shop } from "/imports/collections/schemas";

/**
* @name Shop
* @memberof Schemas
* @type {SimpleSchema}
* @property {Object} orderStatusLabels optional
*/
Shop.extend({
orderStatusLabels: {
type: Object,
blackbox: true
}
});
2 changes: 2 additions & 0 deletions imports/plugins/core/orders/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ import "./startup";
import "./i18n";
import methods from "./methods";

import "../lib/extendShopSchema";

Meteor.methods(methods);
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import orderById from "./orderById";
import orderByReferenceId from "./orderByReferenceId";
import ordersByAccountId from "./ordersByAccountId";

export default {
orderById,
orderByReferenceId
orderByReferenceId,
ordersByAccountId
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import ReactionError from "@reactioncommerce/reaction-error";

/**
* @name ordersByAccountId
* @method
* @memberof Order/NoMeteorQueries
* @summary Query the Orders collection for orders made by the provided accountId and (optionally) shopIds
* @param {Object} context - an object containing the per-request state
* @param {Object} params - request parameters
* @param {String} params.accountId - Account ID to search orders for
* @param {String} params.orderStatus - Workflow status to limit search results
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be more useful and not much extra work to do orderStatuses array rather than just one. Return orders with any of the statuses in the array. (So in UI the filter could be check boxes rather than a single status select.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. How do we want to go about the All selection then? Just have all the statuses checked by default?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rymorgan See above. What are your thoughts on using a multi-select instead of a single dropdown for the Order Status filter?

I don't think it looks that great, might get confusing for the user too.

I think @aldeed's idea of passing an array is best for the server side, but on the client maybe we have pre-set arrays that we pass (at least in our starterkit, other people can do what we want), and continue to use the single select. Thoughts?

account_profile___reaction

Copy link
Contributor

@rymorgan rymorgan Feb 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed @kieckhafer that a multi-select would likely be confusing to a user. I think it adds an extra bit of complexity to the user experience for not a huge benefit to the user. And actually a detriment to the user if they are confused by the UI. I don't remember seeing this type of behavior on brands that I visit which makes me think we'd be designing something almost all storefronts would eliminate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, too. I was thinking more of future operator UI needs.

* @param {String} params.shopIds - Shop IDs for the shops that owns the orders
* @return {Promise<Object>|undefined} - An Array of Order documents, if found
*/
export default async function ordersByAccountId(context, { accountId, orderStatus, shopIds } = {}) {
const { accountId: contextAccountId, collections, shopsUserHasPermissionFor, userHasPermission } = context;
const { Orders } = collections;

if (!accountId) {
throw new ReactionError("invalid-param", "You must provide accountId arguments");
}

let query = {
accountId
};

// If orderStatus array is provided, only return orders with statuses in Array
// Otherwise, return all orders
if (Array.isArray(orderStatus) && orderStatus.length > 0) {
query = {
"workflow.status": { $in: orderStatus },
...query
};
}

if (shopIds) query.shopId = { $in: shopIds };

if (accountId !== contextAccountId) {
// If an admin wants all orders for an account, we force it to be limited to the
// shops for which they're allowed to see orders.
if (!shopIds) {
const shopIdsUserHasPermissionFor = shopsUserHasPermissionFor("orders");
query.shopId = { $in: shopIdsUserHasPermissionFor };
} else {
shopIds.forEach((shopId) => {
if (!userHasPermission(["orders"], shopId)) {
throw new ReactionError("access-denied", "Access Denied");
}
});
}
}

return Orders.find(query);
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { encodeCartOpaqueId } from "@reactioncommerce/reaction-graphql-xforms/cart";
import { encodeOrderOpaqueId, xformOrderPayment } from "@reactioncommerce/reaction-graphql-xforms/order";
import { resolveAccountFromAccountId, resolveShopFromShopId } from "@reactioncommerce/reaction-graphql-utils";
import orderDisplayStatus from "./orderDisplayStatus";
import orderSummary from "./orderSummary";
import totalItemQuantity from "./totalItemQuantity";

export default {
_id: (node) => encodeOrderOpaqueId(node._id),
account: resolveAccountFromAccountId,
cartId: (node) => encodeCartOpaqueId(node._id),
displayStatus: (node, { language }, context) => orderDisplayStatus(context, node, language),
fulfillmentGroups: (node) => node.shipping || [],
notes: (node) => node.notes || [],
payments: (node) => (Array.isArray(node.payments) ? node.payments.map(xformOrderPayment) : null),
shop: resolveShopFromShopId,
status: (node) => node.workflow.status,
summary: (node, _, context) => orderSummary(context, node),
totalItemQuantity
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* @name "Order.orderDisplayStatus"
* @method
* @memberof Order/GraphQL
* @summary Displays a human readable status of order state
* @param {Object} context An object with request-specific state
* @param {Object} order - Result of the parent resolver, which is a Order object in GraphQL schema format
* @param {String} language Language to filter item content by
* @return {String} A string of the order status
*/
export default async function orderDisplayStatus(context, order, language) {
const { Shops } = context.collections;
const shop = await Shops.findOne({ _id: order.shopId });
const orderStatusLabels = shop && shop.orderStatusLabels;
const { workflow: { status } } = order;

// If translations are available in the `Shops` collection,
// and are available for this specific order status, get translations
if (orderStatusLabels && orderStatusLabels[status]) {
const orderStatusLabel = orderStatusLabels[status];
const translatedLabel = orderStatusLabel.find((label) => label.language === language);

// If translations are available in desired language, return them.
// Otherwise, return raw status
return (translatedLabel && translatedLabel.label) || status;
}

// If no translations are available in the `Shops` collection, use raw status data
return status;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { xformRateToRateObject } from "@reactioncommerce/reaction-graphql-xforms/core";

/**
* @name "Order.orderSummary"
* @method
* @memberof Order/GraphQL
* @summary Returns an aggregate of all fulfillmentGroup summaries to provide a single orderSummary
* @param {Object} context An object with request-specific state
* @param {Object} order - Result of the parent resolver, which is a Order object in GraphQL schema format
* @return {Object} An object containing order pricing information from all fulfillmentGroups
*/
export default async function orderSummary(context, order) {
const { currencyCode, shipping: fulfillmentMethods } = order;
const totalDiscounts = [];
const totalShipping = [];
const totalSubtotal = [];
const totalSurcharges = [];
const totalTaxableAmount = [];
const totalTaxes = [];
const totalTotal = [];

// Loop over each fulfillmentGroup (shipping[]), and push all values into `totalX` array
fulfillmentMethods.forEach((fulfillmentMethod) => {
const { invoice: { discounts, shipping, subtotal, surcharges, taxableAmount, taxes, total } } = fulfillmentMethod;

totalDiscounts.push(discounts);
totalShipping.push(shipping);
totalSubtotal.push(subtotal);
totalSurcharges.push(surcharges);
totalTaxableAmount.push(taxableAmount);
totalTaxes.push(taxes);
totalTotal.push(total);
});

// Reduce each `totalX` array to get order total from all fulfillmentGroups
const totalDiscountsAmount = totalDiscounts.reduce((acc, value) => acc + value, 0);
const totalShippingAmount = totalShipping.reduce((acc, value) => acc + value, 0);
const totalSubtotalAmount = totalSubtotal.reduce((acc, value) => acc + value, 0);
const totalSurchargesAmount = totalSurcharges.reduce((acc, value) => acc + value, 0);
const totalTaxableAmountAmount = totalTaxableAmount.reduce((acc, value) => acc + value, 0);
const totalTaxesAmount = totalTaxes.reduce((acc, value) => acc + value, 0);
const totalTotalAmount = totalTotal.reduce((acc, value) => acc + value, 0);

// Calculate effective tax rate of combined fulfillmentGroups
const effectiveTaxRate = totalTaxableAmountAmount > 0 ? totalTaxesAmount / totalTaxableAmountAmount : 0;

return {
discountTotal: {
amount: totalDiscountsAmount,
currencyCode
},
effectiveTaxRate: xformRateToRateObject(effectiveTaxRate),
fulfillmentTotal: {
amount: totalShippingAmount,
currencyCode
},
itemTotal: {
amount: totalSubtotalAmount,
currencyCode
},
surchargeTotal: {
amount: totalSurchargesAmount,
currencyCode
},
taxableAmount: {
amount: totalTaxableAmountAmount,
currencyCode
},
taxTotal: {
amount: totalTaxesAmount,
currencyCode
},
total: {
amount: totalTotalAmount,
currencyCode
}
};
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import orderById from "./orderById";
import orderByReferenceId from "./orderByReferenceId";
import ordersByAccountId from "./ordersByAccountId";

export default {
orderById,
orderByReferenceId
orderByReferenceId,
ordersByAccountId
};
Loading