diff --git a/imports/plugins/core/orders/client/components/orderDashboard.js b/imports/plugins/core/orders/client/components/orderDashboard.js index 7487cc75a5a..ac9f6beb578 100644 --- a/imports/plugins/core/orders/client/components/orderDashboard.js +++ b/imports/plugins/core/orders/client/components/orderDashboard.js @@ -16,6 +16,7 @@ class OrderDashboard extends Component { handleSelect: PropTypes.func, isLoading: PropTypes.object, multipleSelect: PropTypes.bool, + orderCount: PropTypes.number, orders: PropTypes.array, query: PropTypes.object, renderFlowList: PropTypes.bool, @@ -89,6 +90,7 @@ class OrderDashboard extends Component {
{ if (mediaSubscription.ready() && ordersSubscription.ready()) { const orders = Orders.find().fetch(); - + const orderCount = Orders.find().count(); onData(null, { - orders + orders, + orderCount }); } }; diff --git a/imports/plugins/included/payments-stripe/server/methods/stripe.js b/imports/plugins/included/payments-stripe/server/methods/stripe.js index 15f8850d826..6bdec4059fe 100644 --- a/imports/plugins/included/payments-stripe/server/methods/stripe.js +++ b/imports/plugins/included/payments-stripe/server/methods/stripe.js @@ -34,10 +34,14 @@ function stripeCaptureCharge(paymentMethod) { amount: formatForStripe(paymentMethod.amount) }; + const stripePackage = Packages.findOne(paymentMethod.paymentPackageId); + const stripeKey = stripePackage.settings.api_key || stripePackage.settings.connectAuth.access_token; + try { - const captureResult = StripeApi.methods.captureCharge.call({ + const captureResult = StripeApi.methods.captureCharge({ transactionId: paymentMethod.transactionId, - captureDetails: captureDetails + captureDetails: captureDetails, + apiKey: stripeKey }); if (captureResult.status === "succeeded") { result = { @@ -120,10 +124,7 @@ function buildPaymentMethods(options) { 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 = []; @@ -140,6 +141,11 @@ function buildPaymentMethods(options) { }; }); + const packageData = Packages.findOne({ + name: "reaction-stripe", + shopId: shopId + }); + const paymentMethod = { processor: "Stripe", storedCard: storedCard, @@ -350,7 +356,7 @@ export const methods = { */ "stripe/payment/capture": function (paymentMethod) { check(paymentMethod, Reaction.Schemas.PaymentMethod); - // let result; + const captureDetails = { amount: formatForStripe(paymentMethod.amount) }; @@ -388,7 +394,7 @@ export const methods = { let result; try { - const refundResult = StripeApi.methods.createRefund.call({ refundDetails }); + const refundResult = StripeApi.methods.createRefund({ refundDetails }); Logger.debug(refundResult); if (refundResult && refundResult.object === "refund") { result = { @@ -423,16 +429,18 @@ export const methods = { check(paymentMethod, Reaction.Schemas.PaymentMethod); let result; try { - const refunds = StripeApi.methods.listRefunds.call({ transactionId: paymentMethod.transactionId }); + const refunds = StripeApi.methods.listRefunds({ transactionId: paymentMethod.transactionId }); result = []; - for (const refund of refunds.data) { - result.push({ - type: refund.object, - amount: refund.amount / 100, - created: refund.created * 1000, - currency: refund.currency, - raw: refund - }); + if (refunds) { + for (const refund of refunds.data) { + result.push({ + type: refund.object, + amount: refund.amount / 100, + created: refund.created * 1000, + currency: refund.currency, + raw: refund + }); + } } } catch (error) { Logger.error(error); diff --git a/imports/plugins/included/payments-stripe/server/methods/stripeapi-methods-capture.app-test.js b/imports/plugins/included/payments-stripe/server/methods/stripeapi-methods-capture.app-test.js index 9526ea1f054..fa46ce0b4c7 100644 --- a/imports/plugins/included/payments-stripe/server/methods/stripeapi-methods-capture.app-test.js +++ b/imports/plugins/included/payments-stripe/server/methods/stripeapi-methods-capture.app-test.js @@ -2,6 +2,7 @@ import { Meteor } from "meteor/meteor"; import { expect } from "meteor/practicalmeteor:chai"; import { sinon } from "meteor/practicalmeteor:sinon"; +import { Packages } from "/lib/collections"; import { StripeApi } from "./stripeapi"; const stripeCaptureResult = { @@ -78,10 +79,12 @@ describe("stripe/payment/capture", function () { }); it("should call StripeApi.methods.captureCharge with the proper parameters and return saved = true", function (done) { + const stripePackage = Packages.findOne({ name: "reaction-stripe" }); + const apiKey = stripePackage.settings.api_key; const paymentMethod = { processor: "Stripe", storedCard: "Visa 4242", - paymentPackageId: "vrXutd72c2m7Lenqw", + paymentPackageId: stripePackage._id, paymentSettingsKey: "reaction-stripe", method: "credit", transactionId: "ch_17hZ4wBXXkbZQs3xL5JhlSgS", @@ -90,10 +93,9 @@ describe("stripe/payment/capture", function () { mode: "capture", createdAt: new Date() }; - sandbox.stub(StripeApi.methods.captureCharge, "call", function () { + sandbox.stub(StripeApi.methods, "captureCharge", function () { return stripeCaptureResult; }); - // spyOn(StripeApi.methods.captureCharge, "call").and.returnValue(stripeCaptureResult); let captureResult = null; let captureError = null; @@ -103,11 +105,12 @@ describe("stripe/payment/capture", function () { expect(captureError).to.be.undefined; expect(captureResult).to.not.be.undefined; expect(captureResult.saved).to.be.true; - expect(StripeApi.methods.captureCharge.call).to.have.been.calledWith({ + expect(StripeApi.methods.captureCharge).to.have.been.calledWith({ transactionId: paymentMethod.transactionId, captureDetails: { amount: 1999 - } + }, + apiKey: apiKey }); done(); }); diff --git a/imports/plugins/included/payments-stripe/server/methods/stripeapi-methods-refund.app-test.js b/imports/plugins/included/payments-stripe/server/methods/stripeapi-methods-refund.app-test.js index 9556e17dc1a..ce8cc2ef9b2 100644 --- a/imports/plugins/included/payments-stripe/server/methods/stripeapi-methods-refund.app-test.js +++ b/imports/plugins/included/payments-stripe/server/methods/stripeapi-methods-refund.app-test.js @@ -44,7 +44,7 @@ describe("stripe/refund/create", function () { receipt_number: null }; - sandbox.stub(StripeApi.methods.createRefund, "call", function () { + sandbox.stub(StripeApi.methods, "createRefund", function () { return stripeRefundResult; }); // spyOn(StripeApi.methods.createRefund, "call").and.returnValue(stripeRefundResult); @@ -57,7 +57,7 @@ describe("stripe/refund/create", function () { expect(refundError).to.be.undefined; expect(refundResult).to.not.be.undefined; expect(refundResult.saved).to.be.true; - expect(StripeApi.methods.createRefund.call).to.have.been.calledWith({ + expect(StripeApi.methods.createRefund).to.have.been.calledWith({ refundDetails: { charge: paymentMethod.transactionId, amount: 1999, diff --git a/imports/plugins/included/payments-stripe/server/methods/stripeapi-methods-refundlist.app-test.js b/imports/plugins/included/payments-stripe/server/methods/stripeapi-methods-refundlist.app-test.js index 47e3de3f1df..7aa6b91558c 100644 --- a/imports/plugins/included/payments-stripe/server/methods/stripeapi-methods-refundlist.app-test.js +++ b/imports/plugins/included/payments-stripe/server/methods/stripeapi-methods-refundlist.app-test.js @@ -53,7 +53,7 @@ describe("stripe/refunds/list", function () { has_more: false, url: "/v1/refunds" }; - sandbox.stub(StripeApi.methods.listRefunds, "call", function () { + sandbox.stub(StripeApi.methods, "listRefunds", function () { return stripeRefundListResult; }); @@ -69,7 +69,7 @@ describe("stripe/refunds/list", function () { expect(refundListResult[0].amount).to.equal(19.99); expect(refundListResult[0].currency).to.equal("usd"); - expect(StripeApi.methods.listRefunds.call).to.have.been.calledWith({ + expect(StripeApi.methods.listRefunds).to.have.been.calledWith({ transactionId: paymentMethod.transactionId }); done(); diff --git a/imports/plugins/included/payments-stripe/server/methods/stripeapi.js b/imports/plugins/included/payments-stripe/server/methods/stripeapi.js index 61c7c2995ab..04465176fc0 100644 --- a/imports/plugins/included/payments-stripe/server/methods/stripeapi.js +++ b/imports/plugins/included/payments-stripe/server/methods/stripeapi.js @@ -1,6 +1,6 @@ /* eslint camelcase: 0 */ import _ from "lodash"; -import { ValidatedMethod } from "meteor/mdg:validated-method"; +import { check } from "meteor/check"; import { Meteor } from "meteor/meteor"; import { SimpleSchema } from "meteor/aldeed:simple-schema"; import { Reaction, Logger } from "/server/api"; @@ -43,118 +43,80 @@ const expectedErrors = [ "incorrect_number" ]; -StripeApi.methods.getApiKey = new ValidatedMethod({ - name: "StripeApi.methods.getApiKey", - validate: null, - run() { - const stripePkg = Reaction.getPackageSettingsWithOptions({ - shopId: Reaction.getPrimaryShopId(), - name: "reaction-stripe" - }); - if (stripePkg || stripePkg.settings && stripePkg.settings.api_key) { - return stripePkg.settings.api_key; - } - throw new Meteor.Error("access-denied", "Invalid Stripe Credentials"); +StripeApi.methods.getApiKey = function () { + const stripePkg = Reaction.getPackageSettingsWithOptions({ + shopId: Reaction.getPrimaryShopId(), + name: "reaction-stripe" + }); + if (stripePkg || stripePkg.settings && stripePkg.settings.api_key) { + return stripePkg.settings.api_key; } -}); + throw new Meteor.Error("invalid-credentials", "Invalid Stripe Credentials"); +}; -StripeApi.methods.createCharge = new ValidatedMethod({ - name: "StripeApi.methods.createCharge", - validate: new SimpleSchema({ - chargeObj: { type: chargeObjectSchema }, - apiKey: { type: String, optional: true } - }).validator(), - run({ chargeObj, apiKey }) { - let stripe; - if (!apiKey) { - const dynamicApiKey = StripeApi.methods.getApiKey.call(); - stripe = require("stripe")(dynamicApiKey); - } else { - stripe = require("stripe")(apiKey); - } - try { - const chargePromise = stripe.charges.create(chargeObj); - const promiseResult = Promise.await(chargePromise); - return promiseResult; - } catch (e) { - // Handle "expected" errors differently - if (e.rawType === "card_error" && _.includes(expectedErrors, e.code)) { - Logger.debug("Error from Stripe is expected, not throwing"); - const normalizedError = { - details: e.message - }; - return { error: normalizedError, result: null }; - } - Logger.error("Received unexpected error type: " + e.rawType); - Logger.error(e); - - // send raw error to server log, but sanitized version to client - const sanitisedError = { - details: "An unexpected error has occurred" +StripeApi.methods.createCharge = function ({ chargeObj, apiKey }) { + check(chargeObj, chargeObjectSchema); + + const stripeKey = apiKey || StripeApi.methods.getApiKey(); + const stripe = require("stripe")(stripeKey); + try { + const chargePromise = stripe.charges.create(chargeObj); + const promiseResult = Promise.await(chargePromise); + return promiseResult; + } catch (e) { + // Handle "expected" errors differently + if (e.rawType === "card_error" && _.includes(expectedErrors, e.code)) { + Logger.debug("Error from Stripe is expected, not throwing"); + const normalizedError = { + details: e.message }; - return { error: sanitisedError, result: null }; + return { error: normalizedError, result: null }; } - } -}); + Logger.error("Received unexpected error type: " + e.rawType); + Logger.error(e); -StripeApi.methods.captureCharge = new ValidatedMethod({ - name: "StripeApi.methods.captureCharge", - validate: new SimpleSchema({ - transactionId: { type: String }, - captureDetails: { type: captureDetailsSchema }, - apiKey: { type: String, optional: true } - }).validator(), - run({ transactionId, captureDetails, apiKey }) { - let stripe; - if (!apiKey) { - const dynamicApiKey = StripeApi.methods.getApiKey.call(); - stripe = require("stripe")(dynamicApiKey); - } else { - stripe = require("stripe")(apiKey); - } - const capturePromise = stripe.charges.capture(transactionId, captureDetails); - const captureResults = Promise.await(capturePromise); - return captureResults; + // send raw error to server log, but sanitized version to client + const sanitisedError = { + details: "An unexpected error has occurred" + }; + return { error: sanitisedError, result: null }; } -}); +}; -StripeApi.methods.createRefund = new ValidatedMethod({ - name: "StripeApi.methods.createRefund", - validate: new SimpleSchema({ - refundDetails: { type: refundDetailsSchema }, - apiKey: { type: String, optional: true } - }).validator(), - run({ refundDetails, apiKey }) { - let stripe; - if (!apiKey) { - const dynamicApiKey = StripeApi.methods.getApiKey.call(); - stripe = require("stripe")(dynamicApiKey); - } else { - stripe = require("stripe")(apiKey); - } - const refundPromise = stripe.refunds.create({ charge: refundDetails.charge, amount: refundDetails.amount }); - const refundResults = Promise.await(refundPromise); - return refundResults; - } -}); -StripeApi.methods.listRefunds = new ValidatedMethod({ - name: "StripeApi.methods.listRefunds", - validate: new SimpleSchema({ - transactionId: { type: String }, - apiKey: { type: String, optional: true } - }).validator(), - run({ transactionId, apiKey }) { - let stripe; - if (!apiKey) { - const dynamicApiKey = StripeApi.methods.getApiKey.call(); - stripe = require("stripe")(dynamicApiKey); - } else { - stripe = require("stripe")(apiKey); - } +StripeApi.methods.captureCharge = function ({ transactionId, captureDetails, apiKey }) { + check(transactionId, String); + check(captureDetails, captureDetailsSchema); + + const stripeKey = apiKey || StripeApi.methods.getApiKey(); + const stripe = require("stripe")(stripeKey); + const capturePromise = stripe.charges.capture(transactionId, captureDetails); + const captureResults = Promise.await(capturePromise); + return captureResults; +}; + +StripeApi.methods.createRefund = function ({ refundDetails, apiKey }) { + check(refundDetails, refundDetailsSchema); + + const stripeKey = apiKey || StripeApi.methods.getApiKey(); + const stripe = require("stripe")(stripeKey); + const refundPromise = stripe.refunds.create({ charge: refundDetails.charge, amount: refundDetails.amount }); + const refundResults = Promise.await(refundPromise); + return refundResults; +}; + +StripeApi.methods.listRefunds = function ({ transactionId, apiKey }) { + check(transactionId, String); + + const stripeKey = apiKey || StripeApi.methods.getApiKey(); + const stripe = require("stripe")(stripeKey); + try { const refundListPromise = stripe.refunds.list({ charge: transactionId }); const refundListResults = Promise.await(refundListPromise); return refundListResults; + } catch (error) { + // Logger.error("Encountered an error when trying to list refunds", error); + Logger.error("Encountered an error when trying to list refunds"); } -}); +}; diff --git a/server/imports/fixtures/orders.js b/server/imports/fixtures/orders.js index c6cda497776..1cc1eea8d12 100755 --- a/server/imports/fixtures/orders.js +++ b/server/imports/fixtures/orders.js @@ -129,18 +129,20 @@ export default function () { }, requiresShipping: true, shipping: [{ + shopId: getShopId(), + _id: Random.id(), items: [ { _id: itemIdOne, productId: Random.id(), - shopId: Random.id(), + shopId: getShopId(), variantId: Random.id(), packed: false }, { _id: itemIdTwo, productId: Random.id(), - shopId: Random.id(), + shopId: getShopId(), variantId: Random.id(), packed: false } @@ -148,6 +150,7 @@ export default function () { }], // Shipping Schema billing: [{ _id: Random.id(), + shopId: getShopId(), address: getAddress({ isBillingDefault: true }), paymentMethod: paymentMethod({ method: "credit", diff --git a/server/methods/core/orders.app-test.js b/server/methods/core/orders.app-test.js index 655f383ee99..ef076d84bda 100644 --- a/server/methods/core/orders.app-test.js +++ b/server/methods/core/orders.app-test.js @@ -1,4 +1,3 @@ -import accounting from "accounting-js"; import { Meteor } from "meteor/meteor"; import { check, Match } from "meteor/check"; import { Factory } from "meteor/dburles:factory"; @@ -78,10 +77,6 @@ describe("orders test", function () { }); } - function orderCreditMethod(orderData) { - return orderData.billing.filter(value => value.paymentMethod.method === "credit")[0]; - } - describe("orders/cancelOrder", function () { beforeEach(function () { sandbox.stub(Meteor.server.method_handlers, "orders/sendNotification", function () { @@ -275,22 +270,14 @@ describe("orders test", function () { it("should approve payment", function () { sandbox.stub(Reaction, "hasPermission", () => true); spyOnMethod("approvePayment", order.userId); - const invoice = orderCreditMethod(order).invoice; - const subTotal = invoice.subtotal; - const shipping = invoice.shipping; - const taxes = invoice.taxes; - const discount = invoice.discounts; - const discountTotal = Math.max(0, subTotal - discount); // ensure no discounting below 0. - const total = accounting.toFixed(discountTotal + shipping + taxes, 2); Meteor.call("orders/approvePayment", order); const orderBilling = Orders.findOne({ _id: order._id }).billing[0]; expect(orderBilling.paymentMethod.status).to.equal("approved"); expect(orderBilling.paymentMethod.mode).to.equal("capture"); - expect(orderBilling.invoice.discounts).to.equal(discount); - expect(orderBilling.invoice.total).to.equal(Number(total)); }); }); + describe("orders/shipmentShipped", function () { it("should throw an error if user does not have permission", function () { sandbox.stub(Reaction, "hasPermission", () => false); diff --git a/server/methods/core/orders.js b/server/methods/core/orders.js index 486acb97b3b..f61c188aba5 100644 --- a/server/methods/core/orders.js +++ b/server/methods/core/orders.js @@ -56,6 +56,43 @@ export function ordersInventoryAdjust(orderId) { }); } +// TODO: Marketplace: Is there a reason to do this any other way? Can admins reduce for more +// than one shop +/** + * ordersInventoryAdjustByShop + * adjust inventory for a particular shop when an order is approved + * @param {String} orderId - orderId + * @param {String} shopId - the id of the shop approving the order + * @return {null} no return value + */ +export function ordersInventoryAdjustByShop(orderId, shopId) { + check(orderId, String); + check(shopId, String); + + if (!Reaction.hasPermission("orders")) { + throw new Meteor.Error("access-denied", "Access Denied"); + } + + const order = Orders.findOne(orderId); + order.items.forEach(item => { + if (item.shopId === shopId) { + Products.update({ + _id: item.variants._id + }, { + $inc: { + inventoryQuantity: -item.quantity + } + }, { + publish: true, + selector: { + type: "variant" + } + }); + } + }); +} + + export function orderQuantityAdjust(orderId, refundedItem) { check(orderId, String); @@ -232,38 +269,24 @@ export const methods = { */ "orders/approvePayment": function (order) { check(order, Object); - const invoice = orderCreditMethod(order).invoice; - - // REVIEW: Who should have access to do this for a marketplace? - // Do we have/need a shopId on each order? + const shopId = Reaction.getShopId(); // the shop of the user who is currently logged on if (!Reaction.hasPermission("orders")) { throw new Meteor.Error("access-denied", "Access Denied"); } - this.unblock(); // REVIEW: why unblock here? - - // this is server side check to verify - // that the math all still adds up. - const subTotal = invoice.subtotal; - const shipping = invoice.shipping; - const taxes = invoice.taxes; - const discount = invoice.discounts; - const discountTotal = Math.max(0, subTotal - discount); // ensure no discounting below 0. - const total = accounting.toFixed(Number(discountTotal) + Number(shipping) + Number(taxes), 2); - // Updates flattened inventory count on variants in Products collection - ordersInventoryAdjust(order._id); + ordersInventoryAdjustByShop(order._id, shopId); - return Orders.update({ - "_id": order._id, - "billing.paymentMethod.method": "credit" + const billing = order.billing; + const billingRecord = billing.find((bRecord) => bRecord.shopId === shopId); + billingRecord.paymentMethod.status = "approved"; + billingRecord.paymentMethod.mode = "capture"; + + Orders.update({ + _id: order._id }, { $set: { - "billing.$.paymentMethod.amount": total, - "billing.$.paymentMethod.status": "approved", - "billing.$.paymentMethod.mode": "capture", - "billing.$.invoice.discounts": discount, - "billing.$.invoice.total": Number(total) + billing: billing } }); }, @@ -805,22 +828,20 @@ export const methods = { * orders/capturePayments * @summary Finalize any payment where mode is "authorize" * and status is "approved", reprocess as "capture" - * @todo: add tests working with new payment methods - * @todo: refactor to use non Meteor.namespace * @param {String} orderId - add tracking to orderId * @return {null} no return value */ "orders/capturePayments": (orderId) => { check(orderId, String); - // REVIEW: For marketplace implmentations who should be able to capture payments? - // Probably just the marketplace and not shops/vendors? if (!Reaction.hasPermission("orders")) { throw new Meteor.Error("access-denied", "Access Denied"); } - + const shopId = Reaction.getShopId(); // the shopId of the current user, e.g. merchant const order = Orders.findOne(orderId); - const itemIds = order.shipping[0].items.map((item) => { + // find the appropriate shipping record by shop + const shippingRecord = order.shipping.find((sRecord) => sRecord.shopId === shopId); + const itemIds = shippingRecord.items.map((item) => { return item._id; }); @@ -831,61 +852,62 @@ export const methods = { } // process order..payment.paymentMethod - _.each(order.billing, function (billing) { - const paymentMethod = billing.paymentMethod; - const transactionId = paymentMethod.transactionId; - - if (paymentMethod.mode === "capture" && paymentMethod.status === "approved" && paymentMethod.processor) { - // Grab the amount from the shipment, otherwise use the original amount - const processor = paymentMethod.processor.toLowerCase(); - - Meteor.call(`${processor}/payment/capture`, paymentMethod, (error, result) => { - if (result && result.saved === true) { - const metadata = Object.assign(billing.paymentMethod.metadata || {}, result.metadata || {}); - - Orders.update({ - "_id": orderId, - "billing.paymentMethod.transactionId": transactionId - }, { - $set: { - "billing.$.paymentMethod.mode": "capture", - "billing.$.paymentMethod.status": "completed", - "billing.$.paymentMethod.metadata": metadata - }, - $push: { - "billing.$.paymentMethod.transactions": result - } - }); - - // event onOrderPaymentCaptured used for confirmation hooks - // ie: confirmShippingMethodForOrder is triggered here - Hooks.Events.run("onOrderPaymentCaptured", orderId); - } else { - if (result && result.error) { - Logger.fatal("Failed to capture transaction.", order, paymentMethod.transactionId, result.error); - } else { - Logger.fatal("Failed to capture transaction.", order, paymentMethod.transactionId, error); + // find the billing record based on shopId + const bilingRecord = order.billing.find((bRecord) => bRecord.shopId === shopId); + + const paymentMethod = bilingRecord.paymentMethod; + const transactionId = paymentMethod.transactionId; + + if (paymentMethod.mode === "capture" && paymentMethod.status === "approved" && paymentMethod.processor) { + // Grab the amount from the shipment, otherwise use the original amount + const processor = paymentMethod.processor.toLowerCase(); + + Meteor.call(`${processor}/payment/capture`, paymentMethod, (error, result) => { + if (result && result.saved === true) { + const metadata = Object.assign(bilingRecord.paymentMethod.metadata || {}, result.metadata || {}); + + Orders.update({ + "_id": orderId, + "billing.paymentMethod.transactionId": transactionId + }, { + $set: { + "billing.$.paymentMethod.mode": "capture", + "billing.$.paymentMethod.status": "completed", + "billing.$.paymentMethod.metadata": metadata + }, + $push: { + "billing.$.paymentMethod.transactions": result } + }); - Orders.update({ - "_id": orderId, - "billing.paymentMethod.transactionId": transactionId - }, { - $set: { - "billing.$.paymentMethod.mode": "capture", - "billing.$.paymentMethod.status": "error" - }, - $push: { - "billing.$.paymentMethod.transactions": result - } - }); - - return { error: "orders/capturePayments: Failed to capture transaction" }; + // event onOrderPaymentCaptured used for confirmation hooks + // ie: confirmShippingMethodForOrder is triggered here + Hooks.Events.run("onOrderPaymentCaptured", orderId); + } else { + if (result && result.error) { + Logger.fatal("Failed to capture transaction.", order, paymentMethod.transactionId, result.error); + } else { + Logger.fatal("Failed to capture transaction.", order, paymentMethod.transactionId, error); } - return { error, result }; - }); - } - }); + + Orders.update({ + "_id": orderId, + "billing.paymentMethod.transactionId": transactionId + }, { + $set: { + "billing.$.paymentMethod.mode": "capture", + "billing.$.paymentMethod.status": "error" + }, + $push: { + "billing.$.paymentMethod.transactions": result + } + }); + + return { error: "orders/capturePayments: Failed to capture transaction" }; + } + return { error, result }; + }); + } }, /** diff --git a/server/publications/collections/orders.js b/server/publications/collections/orders.js index 818545cb433..af0a3c2206f 100644 --- a/server/publications/collections/orders.js +++ b/server/publications/collections/orders.js @@ -2,9 +2,11 @@ import { Meteor } from "meteor/meteor"; import { check, Match } from "meteor/check"; import { Roles } from "meteor/alanning:roles"; import { Counts } from "meteor/tmeasday:publish-counts"; +import { ReactiveAggregate } from "./reactiveAggregate"; import { Orders } from "/lib/collections"; import { Reaction } from "/server/api"; + /** * orders */ @@ -63,10 +65,51 @@ Meteor.publish("CustomPaginatedOrders", function (query, options) { if (!shopId) { return this.ready(); } + + // return any order for which the shopId is attached to an item + const selector = { + "items.shopId": shopId + }; if (Roles.userIsInRole(this.userId, ["admin", "owner", "orders"], shopId)) { - Counts.publish(this, "order-count", Orders.find({ shopId: shopId }), { noReady: true }); - return Orders.find({ shopId: shopId }); + ReactiveAggregate(this, Orders, [ + { + $project: { + items: { + $filter: { + input: "$items", + as: "item", + cond: { $eq: ["$$item.shopId", shopId] } + } + }, + billing: { + $filter: { + input: "$billing", + as: "billing", + cond: { $eq: ["$$billing.shopId", shopId] } + } + }, + shipping: { + $filter: { + input: "$shipping", + as: "shipping", + cond: { $eq: ["$$shipping.shopId", shopId] } + } + }, + cartId: 1, + sessionId: 1, + shopId: 1, + workflow: 1, + discount: 1, + tax: 1, + email: 1, + createdAt: 1 + } + } + ], selector); } + + // TODO How to we return this order-count + // Counts.publish(this, "order-count", Orders.find(selector), { noReady: true }); return Orders.find({ shopId: shopId, userId: this.userId diff --git a/server/publications/collections/reactiveAggregate.js b/server/publications/collections/reactiveAggregate.js new file mode 100644 index 00000000000..58b615ade6a --- /dev/null +++ b/server/publications/collections/reactiveAggregate.js @@ -0,0 +1,102 @@ +import _ from "lodash"; +import { Meteor } from "meteor/meteor"; +import { check, Match } from "meteor/check"; +import { Mongo, MongoInternals } from "meteor/mongo"; + + +// Add the aggregate function available in tbe raw collection to normal collections +Mongo.Collection.prototype.aggregate = function (pipelines, options) { + const coll = this._getCollection(); + return Meteor.wrapAsync(coll.aggregate.bind(coll))(pipelines, options); +}; + +Mongo.Collection.prototype._getDb = function () { + if (typeof this._collection._getDb === "function") { + return this._collection._getDb(); + } + const mongoConn = MongoInternals.defaultRemoteCollectionDriver().mongo; + return wrapWithDb(mongoConn); +}; + +Mongo.Collection.prototype._getCollection = function () { + const db = this._getDb(); + return db.collection(this._name); +}; + +function wrapWithDb(mongoConn) { + if (mongoConn.db) { + return mongoConn.db; + } +} + +/** + * Create a Reactive collection from a Mongo aggregate pipeline + * @param {Object} sub - The publication we are creating + * @param {Collection} collection - the collection we are operating on + * @param {Array} pipeline - The mongo aggregation pipeline to run + * @param {Object} options - an object of options + * @returns {Cursor} A mongo cursor for subscription + * @constructor + */ +export function ReactiveAggregate(sub, collection, pipeline, options) { + check(pipeline, Array); + check(options, Match.Optional(Object)); + + const defaultOptions = { + observeSelector: {}, + observeOptions: {}, + clientCollection: collection._name + }; + const subOptions = Object.assign({}, defaultOptions, options); + + let initializing = true; + sub._ids = {}; + sub._iteration = 1; + + function update() { + if (initializing) { + return; + } + + // add and update documents on the client + collection.aggregate(pipeline).forEach(function (doc) { + if (!sub._ids[doc._id]) { + sub.added(subOptions.clientCollection, doc._id, doc); + } else { + sub.changed(subOptions.clientCollection, doc._id, doc); + } + sub._ids[doc._id] = sub._iteration; + }); + // remove documents not in the result anymore + _.forEach(sub._ids, function (value, key) { + if (value !== sub._iteration) { + delete sub._ids[key]; + sub.removed(subOptions.clientCollection, key); + } + }); + sub._iteration++; + } + + // track any changes on the collection used for the aggregation + const query = collection.find(subOptions.observeSelector, subOptions.observeOptions); + const handle = query.observeChanges({ + added: update, + changed: update, + removed: update, + error: function (error) { + throw new Meteor.Error(`Encountered an error while observing ${collection._name}`, error); + } + }); + // observeChanges() will immediately fire an "added" event for each document in the query + // these are skipped using the initializing flag + initializing = false; + // send an initial result set to the client + update(); + // mark the subscription as ready + sub.ready(); + + // stop observing the cursor when the client unsubscribes + sub.onStop(function () { + handle.stop(); + }); +}