Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
da2100d
feat: initial commit fulfillment base
sujithvn Nov 1, 2022
5848903
feat: initial commit fulfillment base
sujithvn Oct 17, 2022
b57ea4a
Merge remote-tracking branch 'origin/01-fulfillment-base' into 01-ful…
sujithvn Oct 18, 2022
0de0992
fix: pnpm-lock
sujithvn Oct 18, 2022
18c9870
fix: to generate function name in camel-case
sujithvn Oct 19, 2022
3a2fa08
feat: createFFType test
sujithvn Oct 24, 2022
4acb9e1
feat: ff-type tests
sujithvn Oct 24, 2022
b5edb37
fix: review comment fixes
sujithvn Oct 26, 2022
ebd684c
fix: removed string concatenation logic
sujithvn Oct 27, 2022
3ecab7d
fix: use queries.getCartById delete /util version
sujithvn Oct 28, 2022
fd9d72c
fix: fixed broken test
sujithvn Oct 31, 2022
5a09c25
feat: fulfillment type pickup and method store
sujithvn Oct 19, 2022
5dbcb91
fix: review comments fixes
sujithvn Oct 27, 2022
ef301ba
fix: import fix
sujithvn Oct 29, 2022
d202e97
fix: pnpm-lock update without snyk
sujithvn Nov 3, 2022
e0c056e
feat: fftype-shipping with methods flatrate ups
sujithvn Oct 18, 2022
d5252f5
fix: updated test cases
sujithvn Oct 19, 2022
3156bae
fix: filename fixed
sujithvn Oct 24, 2022
0c30b61
fix: review comment fixes
sujithvn Oct 27, 2022
2d9044d
fix: picked up the fix #6578 from old plugin
sujithvn Nov 3, 2022
37e3dd6
fix: renamed collection in test.js
sujithvn Oct 28, 2022
70bb0b9
fix: import fix
sujithvn Oct 29, 2022
975ea93
fix: renamed top-level folder ups to dynamic-rate
sujithvn Oct 29, 2022
2d543a3
fix: pnpm-lock without snyk
sujithvn Nov 3, 2022
2da2803
feat: changes related to carts plugin
sujithvn Oct 20, 2022
d2d37ec
feat: changes related to catalogs plugin
sujithvn Oct 20, 2022
b455518
feat: changes related to orders plugin
sujithvn Oct 20, 2022
d8f2465
feat: changes related to products plugin
sujithvn Oct 20, 2022
25a91b6
feat: integration test updates
sujithvn Oct 20, 2022
7314195
feat: switch from old shipments to fulfillment
sujithvn Nov 3, 2022
b047c03
feat: test setFulfillmentTypeForItems
sujithvn Oct 24, 2022
0ddcaa4
fix: package and plugin.json update
sujithvn Oct 29, 2022
d88a878
fix: review comment combine destructure
sujithvn Nov 3, 2022
1cdfafe
feat: new validateOrder and placeOrder refactor
sujithvn Oct 20, 2022
0520273
feat: first set of tests placeOrder refactor
sujithvn Oct 25, 2022
07c5436
fix: review comments fixes
sujithvn Oct 27, 2022
adf38b0
fix: export getCartById as query
sujithvn Nov 3, 2022
867434c
fix: pnpm-lock update without snyk
sujithvn Nov 3, 2022
79eac5e
feat: data migration for fulfillment feature
sujithvn Nov 4, 2022
c5ab64e
feat: migrate shopsetting for default ff-type
sujithvn Oct 4, 2022
a6356cb
fix: migration script fix
sujithvn Oct 6, 2022
3d016d7
fix: review comment fixes
sujithvn Oct 13, 2022
3acaeb4
fix: typo in 2.js
sujithvn Oct 13, 2022
bc2974d
fix: review comment fixes2
sujithvn Nov 7, 2022
3404407
fix: pnpm-lock without snyk
sujithvn Nov 7, 2022
278c8d4
fix: review comment fixes 1
sujithvn Nov 10, 2022
346460d
fix: review comment fixes 2
sujithvn Nov 15, 2022
da70978
fix: review comment fix 3
sujithvn Nov 15, 2022
5deec1c
fix: fix validatePermission
sujithvn Nov 16, 2022
7dfa566
fix: fix validatePermissions
sujithvn Nov 16, 2022
6e1c711
fix: review comment fixes
sujithvn Nov 17, 2022
ddca358
fix: review comment fixes
sujithvn Nov 17, 2022
3862d7b
fix: review comment fixes
sujithvn Nov 21, 2022
1e21608
fix: review comment fixes
sujithvn Mar 10, 2023
c774be7
Merge branch 'feat/fulfillment-types' into 00-fulfillment-base
sujithvn Mar 10, 2023
37536fa
fix: test errors and pnpm-lock
sujithvn Mar 10, 2023
7ed401f
Merge 00-fulfillment-base into new-fulfillment-type-shipping
sujithvn Mar 10, 2023
9f16175
fix: review comment updates
sujithvn Mar 10, 2023
e9497a0
Merge 00-fulfillment-base into new-fulfillment-type-pickup
sujithvn Mar 10, 2023
22b0c79
fix: unit test fix
sujithvn Mar 10, 2023
24a3202
fix: review comments
sujithvn Mar 10, 2023
edaa82b
fix: review comment updates
sujithvn Mar 10, 2023
13caba9
Merge branch 'feat/fulfillment-types' into new-fulfillment-impacted-p…
sujithvn Mar 10, 2023
61bc0a3
fix: unit test update
sujithvn Mar 11, 2023
3e6b7d2
fix: variable reuse
sujithvn Mar 12, 2023
a2bc593
Merge branch '00-fulfillment-base' into new-fulfillment-impacted-plugins
sujithvn Mar 13, 2023
bf821f7
Merge branch 'new-fulfillment-type-shipping' into new-fulfillment-imp…
sujithvn Mar 13, 2023
1e8b011
Merge fulfillment-pickup into impacted plugins
sujithvn Mar 13, 2023
a1cfcd7
fix: integration test updates
sujithvn Mar 13, 2023
1d94aa6
fix: filename case change
sujithvn Mar 13, 2023
059e4f0
fix: filename case 2
sujithvn Mar 13, 2023
ff3d042
Merge branch '00-fulfillment-base' into new-placeOrder-refactor-valid…
sujithvn Mar 13, 2023
3806ed8
Merge branch 'new-fulfillment-type-shipping' into new-placeOrder-refa…
sujithvn Mar 13, 2023
e7b3d23
Merge branch 'new-fulfillment-type-pickup' into new-placeOrder-refact…
sujithvn Mar 13, 2023
0117580
fix: pnpm-lock merge
sujithvn Mar 13, 2023
68d8313
Merge branch 'new-fulfillment-impacted-plugins' into new-placeOrder-r…
sujithvn Mar 13, 2023
8cf2151
fix: failing test fixes
sujithvn Mar 13, 2023
17909c3
fix: missed out review comment
sujithvn Mar 13, 2023
141145c
Merge pull request #6613 from reactioncommerce/new-fulfillment-type-p…
sujithvn May 18, 2023
c7a719d
Merge branch '00-fulfillment-base' of github.com:reactioncommerce/rea…
sujithvn May 18, 2023
f790476
Merge branch '00-fulfillment-base' into new-fulfillment-type-shipping
sujithvn May 18, 2023
fe15a55
fix: fixed test errors
sujithvn May 18, 2023
97946d6
Merge pull request #6614 from reactioncommerce/new-fulfillment-type-s…
sujithvn May 18, 2023
bf77d11
Merge branch '00-fulfillment-base' of github.com:reactioncommerce/rea…
sujithvn May 18, 2023
761bd13
Merge branch '00-fulfillment-base' into new-fulfillment-impacted-plugins
sujithvn May 18, 2023
5fed7d5
Merge pull request #6615 from reactioncommerce/new-fulfillment-impact…
sujithvn May 18, 2023
258e551
Merge branch '00-fulfillment-base' of github.com:reactioncommerce/rea…
sujithvn May 18, 2023
5b36b12
Merge branch '00-fulfillment-base' into new-placeOrder-refactor-valid…
sujithvn May 18, 2023
344bbf6
Merge pull request #6616 from reactioncommerce/new-placeOrder-refacto…
sujithvn May 18, 2023
1b09f81
Merge branch '00-fulfillment-base' of github.com:reactioncommerce/rea…
sujithvn May 18, 2023
9193072
Merge branch '00-fulfillment-base' into new-data-migration-fulfillment
sujithvn May 18, 2023
13bab48
fix: linter error
sujithvn May 18, 2023
1ec99f9
Merge pull request #6633 from reactioncommerce/new-data-migration-ful…
sujithvn May 18, 2023
ddff28d
Merge branch '00-fulfillment-base' of github.com:reactioncommerce/rea…
sujithvn May 18, 2023
df5ed5e
fix: changes picked from closed pr 6543
sujithvn May 18, 2023
7503a64
fix: picked chgs from closed pr 6545
sujithvn May 18, 2023
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