+
Skip to content

03 placeOrder refactor and validateOrder #6583

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions packages/api-plugin-carts/src/queries/getCartById.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import getCartByIdUtil from "../util/getCartById.js";
/**
* @name getCartById
* @method
* @memberof Cart
* @summary Gets a cart from the db by ID. If there is an account for the request, verifies that the
* account has permission to access the cart. Optionally throws an error if not found.
* @param {Object} context - an object containing the per-request state
* @param {String} cartId The cart ID
* @param {Object} [options] Options
* @param {String} [options.cartToken] Cart token, required if it's an anonymous cart
* @param {Boolean} [options.throwIfNotFound] Default false. Throw a not-found error rather than return null `cart`
* @returns {Object|null} The cart document, or null if not found and `throwIfNotFound` was false
*/
export default async function getCartById(context, cartId, { cartToken, throwIfNotFound = false } = {}) {
return getCartByIdUtil(context, cartId, { cartToken, throwIfNotFound });
}
2 changes: 2 additions & 0 deletions packages/api-plugin-carts/src/queries/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import accountCartByAccountId from "./accountCartByAccountId.js";
import anonymousCartByCartId from "./anonymousCartByCartId.js";
import getCartById from "./getCartById.js";
import getCommonOrderForCartGroup from "./getCommonOrderForCartGroup.js";

export default {
accountCartByAccountId,
anonymousCartByCartId,
getCartById,
getCommonOrderForCartGroup
};
46 changes: 46 additions & 0 deletions packages/api-plugin-carts/src/util/getCartById.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import mockContext from "@reactioncommerce/api-utils/tests/mockContext.js";
import ReactionError from "@reactioncommerce/reaction-error";
import getCartById from "./getCartById.js";

test("should throw when cart not found and throwIfNotFound is true", async () => {
const cartId = "123";
const cartToken = "xyz";
mockContext.collections.Cart.findOne.mockReturnValueOnce(null);
const expectedError = new ReactionError("not-found", "Cart not found");
await expect(getCartById(mockContext, cartId, { cartToken, throwIfNotFound: true })).rejects.toThrow(expectedError);
});

test("should return null when cart not found and throwIfNotFound is false", async () => {
const cartId = "123";
const cartToken = "xyz";
mockContext.collections.Cart.findOne.mockReturnValueOnce(null);
await expect(getCartById(mockContext, cartId, { cartToken, throwIfNotFound: false })).toMatchObject({});
});

test("should throw when cart found but accountId does not match", async () => {
const cartId = "123";
const cartToken = "xyz";
mockContext.accountId = "accountId123";
const cart = {
_id: cartId,
anonymousAccessToken: cartToken,
accountId: "accountId456"
};
mockContext.collections.Cart.findOne.mockReturnValueOnce(Promise.resolve(cart));
const expectedError = new ReactionError("access-denied", "Access Denied");
await expect(getCartById(mockContext, cartId, { cartToken, throwIfNotFound: true })).rejects.toThrow(expectedError);
});

test("should return cart when cart found and accountId matches", async () => {
const cartId = "123";
const cartToken = "xyz";
mockContext.accountId = "accountId123";
const cart = {
_id: cartId,
anonymousAccessToken: cartToken,
accountId: "accountId123"
};
mockContext.collections.Cart.findOne.mockReturnValueOnce(Promise.resolve(cart));
const result = await getCartById(mockContext, cartId, { cartToken, throwIfNotFound: true });
expect(result).toMatchObject(cart);
});
274 changes: 3 additions & 271 deletions packages/api-plugin-orders/src/mutations/placeOrder.js
Original file line number Diff line number Diff line change
@@ -1,108 +1,4 @@
import _ from "lodash";
import SimpleSchema from "simpl-schema";
import Logger from "@reactioncommerce/logger";
import Random from "@reactioncommerce/random";
import ReactionError from "@reactioncommerce/reaction-error";
import getAnonymousAccessToken from "@reactioncommerce/api-utils/getAnonymousAccessToken.js";
import buildOrderFulfillmentGroupFromInput from "../util/buildOrderFulfillmentGroupFromInput.js";
import verifyPaymentsMatchOrderTotal from "../util/verifyPaymentsMatchOrderTotal.js";
import { Order as OrderSchema, orderInputSchema, Payment as PaymentSchema, paymentInputSchema } from "../simpleSchemas.js";

const inputSchema = new SimpleSchema({
"order": orderInputSchema,
"payments": {
type: Array,
optional: true
},
"payments.$": paymentInputSchema
});

/**
* @summary Create all authorized payments for a potential order
* @param {String} [accountId] The ID of the account placing the order
* @param {Object} [billingAddress] Billing address for the order as a whole
* @param {Object} context - The application context
* @param {String} currencyCode Currency code for interpreting the amount of all payments
* @param {String} email Email address for the order
* @param {Number} orderTotal Total due for the order
* @param {Object[]} paymentsInput List of payment inputs
* @param {Object} [shippingAddress] Shipping address, if relevant, for fraud detection
* @param {String} shop shop that owns the order
* @returns {Object[]} Array of created payments
*/
async function createPayments({
accountId,
billingAddress,
context,
currencyCode,
email,
orderTotal,
paymentsInput,
shippingAddress,
shop
}) {
// Determining which payment methods are enabled for the shop
const availablePaymentMethods = shop.availablePaymentMethods || [];

// Verify that total of payment inputs equals total due. We need to be sure
// to do this before creating any payment authorizations
verifyPaymentsMatchOrderTotal(paymentsInput || [], orderTotal);

// Create authorized payments for each
const paymentPromises = (paymentsInput || []).map(async (paymentInput) => {
const { amount, method: methodName } = paymentInput;

// Verify that this payment method is enabled for the shop
if (!availablePaymentMethods.includes(methodName)) {
throw new ReactionError("payment-failed", `Payment method not enabled for this shop: ${methodName}`);
}

// Grab config for this payment method
let paymentMethodConfig;
try {
paymentMethodConfig = context.queries.getPaymentMethodConfigByName(methodName);
} catch (error) {
Logger.error(error);
throw new ReactionError("payment-failed", `Invalid payment method name: ${methodName}`);
}

// Authorize this payment
const payment = await paymentMethodConfig.functions.createAuthorizedPayment(context, {
accountId, // optional
amount,
billingAddress: paymentInput.billingAddress || billingAddress,
currencyCode,
email,
shippingAddress, // optional, for fraud detection, the first shipping address if shipping to multiple
shopId: shop._id,
paymentData: {
...(paymentInput.data || {})
} // optional, object, blackbox
});

const paymentWithCurrency = {
...payment,
// This is from previous support for exchange rates, which was removed in v3.0.0
currency: { exchangeRate: 1, userCurrency: currencyCode },
currencyCode
};

PaymentSchema.validate(paymentWithCurrency);

return paymentWithCurrency;
});

let payments;
try {
payments = await Promise.all(paymentPromises);
payments = payments.filter((payment) => !!payment); // remove nulls
} catch (error) {
Logger.error("createOrder: error creating payments", error.message);
throw new ReactionError("payment-failed", `There was a problem authorizing this payment: ${error.message}`);
}

return payments;
}
import prepareOrder from "../util/orderValidators/prepareOrder.js";

/**
* @method placeOrder
Expand All @@ -112,173 +8,9 @@ async function createPayments({
* @returns {Promise<Object>} Object with `order` property containing the created order
*/
export default async function placeOrder(context, input) {
const cleanedInput = inputSchema.clean(input); // add default values and such
inputSchema.validate(cleanedInput);

const { order: orderInput, payments: paymentsInput } = cleanedInput;
const {
billingAddress,
cartId,
currencyCode,
customFields: customFieldsFromClient,
email,
fulfillmentGroups,
ordererPreferredLanguage,
shopId
} = orderInput;
const { accountId, appEvents, collections, getFunctionsOfType, userId } = context;
const { Orders, Cart } = collections;

const shop = await context.queries.shopById(context, shopId);
if (!shop) throw new ReactionError("not-found", "Shop not found");

if (!userId && !shop.allowGuestCheckout) {
throw new ReactionError("access-denied", "Guest checkout not allowed");
}

let cart;
if (cartId) {
cart = await Cart.findOne({ _id: cartId });
if (!cart) {
throw new ReactionError("not-found", "Cart not found while trying to place order");
}
}


// We are mixing concerns a bit here for now. This is for backwards compatibility with current
// discount codes feature. We are planning to revamp discounts soon, but until then, we'll look up
// any discounts on the related cart here.
let discounts = [];
let discountTotal = 0;
if (cart) {
const discountsResult = await context.queries.getDiscountsTotalForCart(context, cart);
({ discounts } = discountsResult);
discountTotal = discountsResult.total;
}

// Create array for surcharges to apply to order, if applicable
// Array is populated inside `fulfillmentGroups.map()`
const orderSurcharges = [];

// Create orderId
const orderId = Random.id();


// Add more props to each fulfillment group, and validate/build the items in each group
let orderTotal = 0;
let shippingAddressForPayments = null;
const finalFulfillmentGroups = await Promise.all(fulfillmentGroups.map(async (inputGroup) => {
const { group, groupSurcharges } = await buildOrderFulfillmentGroupFromInput(context, {
accountId,
billingAddress,
cartId,
currencyCode,
discountTotal,
inputGroup,
orderId,
cart
});

// We save off the first shipping address found, for passing to payment services. They use this
// for fraud detection.
if (group.address && !shippingAddressForPayments) shippingAddressForPayments = group.address;

// Push all group surcharges to overall order surcharge array.
// Currently, we do not save surcharges per group
orderSurcharges.push(...groupSurcharges);

// Add the group total to the order total
orderTotal += group.invoice.total;

return group;
}));

const payments = await createPayments({
accountId,
billingAddress,
context,
currencyCode,
email,
orderTotal,
paymentsInput,
shippingAddress: shippingAddressForPayments,
shop
});

// Create anonymousAccessToken if no account ID
const fullToken = accountId ? null : getAnonymousAccessToken();

const now = new Date();

const order = {
_id: orderId,
accountId,
billingAddress,
cartId,
createdAt: now,
currencyCode,
discounts,
email,
ordererPreferredLanguage: ordererPreferredLanguage || null,
payments,
shipping: finalFulfillmentGroups,
shopId,
surcharges: orderSurcharges,
totalItemQuantity: finalFulfillmentGroups.reduce((sum, group) => sum + group.totalItemQuantity, 0),
updatedAt: now,
workflow: {
status: "new",
workflow: ["new"]
}
};

if (fullToken) {
const dbToken = { ...fullToken };
// don't store the raw token in db, only the hash
delete dbToken.token;
order.anonymousAccessTokens = [dbToken];
}

let referenceId;
const createReferenceIdFunctions = getFunctionsOfType("createOrderReferenceId");
if (!createReferenceIdFunctions || createReferenceIdFunctions.length === 0) {
// if the cart has a reference Id, and no custom function is created use that
if (_.get(cart, "referenceId")) { // we want the else to fallthrough if no cart to keep the if/else logic simple
({ referenceId } = cart);
} else {
referenceId = Random.id();
}
} else {
referenceId = await createReferenceIdFunctions[0](context, order, cart);
if (typeof referenceId !== "string") {
throw new ReactionError("invalid-parameter", "createOrderReferenceId function returned a non-string value");
}
if (createReferenceIdFunctions.length > 1) {
Logger.warn("More than one createOrderReferenceId function defined. Using first one defined");
}
}

order.referenceId = referenceId;


// Apply custom order data transformations from plugins
const transformCustomOrderFieldsFuncs = getFunctionsOfType("transformCustomOrderFields");
if (transformCustomOrderFieldsFuncs.length > 0) {
let customFields = { ...(customFieldsFromClient || {}) };
// We need to run each of these functions in a series, rather than in parallel, because
// each function expects to get the result of the previous. It is recommended to disable `no-await-in-loop`
// eslint rules when the output of one iteration might be used as input in another iteration, such as this case here.
// See https://eslint.org/docs/rules/no-await-in-loop#when-not-to-use-it
for (const transformCustomOrderFieldsFunc of transformCustomOrderFieldsFuncs) {
customFields = await transformCustomOrderFieldsFunc({ context, customFields, order }); // eslint-disable-line no-await-in-loop
}
order.customFields = customFields;
} else {
order.customFields = customFieldsFromClient;
}
const { appEvents, collections: { Orders }, userId } = context;

// Validate and save
OrderSchema.validate(order);
const { order, fullToken } = await prepareOrder(context, input, "createOrderObject");
await Orders.insertOne(order);

await appEvents.emit("afterOrderCreate", { createdBy: userId, order });
Expand Down
2 changes: 2 additions & 0 deletions packages/api-plugin-orders/src/queries/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import orders from "./orders.js";
import ordersByAccountId from "./ordersByAccountId.js";
import refunds from "./refunds.js";
import refundsByPaymentId from "./refundsByPaymentId.js";
import validateOrder from "./validateOrder.js";

export default {
validateOrder,
orderById,
orderByReferenceId,
orders,
Expand Down
16 changes: 16 additions & 0 deletions packages/api-plugin-orders/src/queries/validateOrder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import prepareOrder from "../util/orderValidators/prepareOrder.js";

/**
* @name validateOrder
* @method
* @memberof Order
* @summary Validates if the input order details is valid and ready for order processing
* @param {Object} context - an object containing the per-request state
* @param {Object} input - order details, refer inputSchema
* @returns {Promise<Object>} output - validation results
*/
export default async function validateOrder(context, input) {
const { errors, success } = await prepareOrder(context, input, "validateOrder");
const output = { errors, success };
return output;
}
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载