+
Skip to content

New place order refactor validate order #6616

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

Merged
merged 15 commits into from
May 18, 2023
Merged
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
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
5 changes: 5 additions & 0 deletions packages/api-plugin-orders/src/mutations/placeOrder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ test("places an anonymous $0 order with no cartId and no payments", async () =>
availablePaymentMethods: ["PAYMENT1"]
}]);

mockContext.queries.getDiscountsTotalForCart = jest.fn().mockName("getDiscountsTotalForCart").mockReturnValueOnce({
discounts: [],
total: 0
});

const orderInput = Factory.orderInputSchema.makeOne({
billingAddress: null,
cartId: null,
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,9 +4,11 @@ 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";
import filterOrders from "./filterOrders.js";

export default {
validateOrder,
filterOrders,
orderById,
orderByReferenceId,
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;
}
2 changes: 2 additions & 0 deletions packages/api-plugin-orders/src/resolvers/Query/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ 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";
import filterOrders from "./filterOrders.js";

export default {
validateOrder,
filterOrders,
orderById,
orderByReferenceId,
Expand Down
48 changes: 48 additions & 0 deletions packages/api-plugin-orders/src/resolvers/Query/validateOrder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
decodeCartOpaqueId,
decodeFulfillmentMethodOpaqueId,
decodeOrderItemsOpaqueIds,
decodeShopOpaqueId
} from "../../xforms/id.js";

/**
* @name Query.validateOrder
* @method
* @memberof Order/GraphQL
* @summary Validate if the order is ready
* @param {Object} parentResult - unused
* @param {Object} args.input - an object of all mutation arguments that were sent by the client
* @param {Object} args.input.order - The order input
* @param {Object[]} args.input.payments - Payment info
* @param {Object} context - an object containing the per-request state
* @returns {Promise<Object>} A validation result object
*/
export default async function validateOrder(parentResult, { input }, context) {
const { order, payments } = input;
const { cartId: opaqueCartId, fulfillmentGroups, shopId: opaqueShopId } = order;

const cartId = opaqueCartId ? decodeCartOpaqueId(opaqueCartId) : null;
const shopId = decodeShopOpaqueId(opaqueShopId);

const transformedFulfillmentGroups = fulfillmentGroups.map((group) => ({
...group,
items: decodeOrderItemsOpaqueIds(group.items),
selectedFulfillmentMethodId: decodeFulfillmentMethodOpaqueId(group.selectedFulfillmentMethodId),
shopId: decodeShopOpaqueId(group.shopId)
}));

const { errors, success } = await context.queries.validateOrder(
context,
{
order: {
...order,
cartId,
fulfillmentGroups: transformedFulfillmentGroups,
shopId
},
payments
}
);

return { errors, success };
}
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载