Skip to content
This repository has been archived by the owner on May 28, 2023. It is now read-only.

Commit

Permalink
The synchronous version of /api/order/create added regarding vuestore…
Browse files Browse the repository at this point in the history
…front/vue-storefront#2008

Orders are by default passed directly to Magento when sent from frontend
  • Loading branch information
pkarw committed Nov 21, 2018
1 parent 5999991 commit 182f530
Show file tree
Hide file tree
Showing 7 changed files with 356 additions and 296 deletions.
3 changes: 3 additions & 0 deletions config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"port": 8080,
"searchEngine": "elasticsearch"
},
"orders": {
"useServerQueue": false
},
"elasticsearch": {
"host": "localhost",
"port": 9200,
Expand Down
42 changes: 30 additions & 12 deletions src/api/order.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import resource from 'resource-router-middleware';
import { apiStatus } from '../lib/util';
import { merge } from 'lodash';
import PlatformFactory from '../platform/factory';

const Ajv = require('ajv'); // json validator
const kue = require('kue');
const jwa = require('jwa');
const hmac = jwa('HS256');

const _getProxy = (req, config) => {
const platform = config.platform
const factory = new PlatformFactory(config, req)
return factory.getAdapter(platform, 'order')
};


export default ({ config, db }) => resource({

/** Property name to store preloaded entity on `request`. */
Expand All @@ -16,6 +24,7 @@ export default ({ config, db }) => resource({
* POST create an order with JSON payload compliant with models/order.md
*/
create(req, res) {


const ajv = new Ajv();
const orderSchema = require('../models/order.schema.json')
Expand Down Expand Up @@ -48,19 +57,28 @@ export default ({ config, db }) => resource({
}
}

try {
let queue = kue.createQueue(Object.assign(config.kue, { redis: config.redis }));
const job = queue.create('order', incomingOrder).save( function(err){
if(err) {
console.error(err)
apiStatus(res, err, 500);
} else {
apiStatus(res, job.id, 200);
}
if (config.orders.useServerQueue) {
try {
let queue = kue.createQueue(Object.assign(config.kue, { redis: config.redis }));
const job = queue.create('order', incomingOrder).save( function(err){
if(err) {
console.error(err)
apiStatus(res, err, 500);
} else {
apiStatus(res, job.id, 200);
}
})
} catch (e) {
apiStatus(res, e, 500);
}
} else {
const orderProxy = _getProxy(req, config)
orderProxy.create(req.body).then((result) => {
apiStatus(res, result, 200);
}).catch(err => {
console.error(err)
apiStatus(res, err, 500);
})
} catch (e) {
apiStatus(res, e, 500);
}
},

});
6 changes: 6 additions & 0 deletions src/platform/abstract/order.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AbstractOrderProxy {
create (orderData) {
}
}

module.exports = AbstractOrderProxy
287 changes: 287 additions & 0 deletions src/platform/magento2/o2m.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@


const Magento2Client = require('magento2-rest-client').Magento2Client;

const config = require('config')
const Redis = require('redis');
let redisClient = Redis.createClient(config.redis); // redis client
redisClient.on('error', function (err) { // workaround for https://github.com/NodeRedis/node_redis/issues/713
redisClient = Redis.createClient(config.redis); // redis client
});
const countryMapper = require('../../lib/countrymapper')
const Ajv = require('ajv'); // json validator
const ajv = new Ajv(); // validator
const merge = require('lodash/merge')
const orderSchema = require('../../models/order.schema.json')
const orderSchemaExtension = require('../../models/order.schema.extension.json')
const validate = ajv.compile(merge(orderSchema, orderSchemaExtension));

function isNumeric(val) {
return Number(parseFloat(val)).toString() === val;
}

/**
* Send single order to Magento Instance
*
* The Magento2 API: https://magento.stackexchange.com/questions/136028/magento-2-create-order-using-rest-api
*
* @param {json} orderData order data in format as described in '../models/order.md'
* @param {Object} config global CLI configuration
* @param {Function} done callback - @example done(new Error()) - to acknowledge problems
*/
function processSingleOrder(orderData, config, job, done, logger = console) {

const TOTAL_STEPS = 4;
const THREAD_ID = 'ORD:' + (job ? job.id : 1) + ' - '; // job id
let currentStep = 1;

if (!validate(orderData)) { // schema validation of upcoming order
logger.error(THREAD_ID + " Order validation error!", validate.errors);
done(new Error('Error while validating order object', validate.errors));

if(job) job.progress(currentStep++, TOTAL_STEPS);
return;
}
let isThisAuthOrder = parseInt(orderData.user_id) > 0
const userId = orderData.user_id

let apiConfig = config.magento2.api
if (orderData.store_code) {
if (config.availableStores.indexOf(orderData.store_code) >= 0) {
apiConfig = Object.assign({}, apiConfig, { url: apiConfig.url + '/' + orderData.store_code })
console.log('> Store code', orderData.store_code)
} else {
logger.error('Invalid store code', orderData.store_code)
}
}
const api = Magento2Client(apiConfig);

logger.info('> Order Id', orderData.order_id)
logger.info('> Is order authorized?', isThisAuthOrder)
logger.info('> User Id', userId)

let cartId = orderData.cart_id
const cartIdPrepare = isThisAuthOrder ? api.cart.create(null, userId): ( cartId ? new Promise((resolve, reject) => {
resolve (cartId)
}): api.cart.create(null))

logger.info(THREAD_ID + '> Cart Id', cartId)

const processCart = (result) => {
cartId = result

logger.info(THREAD_ID + '< Cart Id', cartId)

// load current cart from the Magento to synchronize elements
api.cart.pull(null, cartId, null, isThisAuthOrder).then((serverItems) => {

const clientItems = orderData.products
const syncPromises = []

logger.info(THREAD_ID + '> Sync between clientItems', clientItems.map((item) => { return { sku: item.sku, qty: item.qty, server_item_id: item.server_item_id, product_option: item.product_option }}))
logger.info(THREAD_ID + '> ... and serverItems', serverItems)

for (const clientItem of clientItems) {
const serverItem = serverItems.find((itm) => {
return itm.sku === clientItem.sku || itm.sku.indexOf(clientItem.sku + '-') >= 0 /* bundle products */
})
if (!serverItem) {
logger.info(THREAD_ID + '< No server item for ' + clientItem.sku)
syncPromises.push(api.cart.update(null, cartId, { // use magento API
sku: clientItem.parentSku && config.cart.setConfigurableProductOptions ? clientItem.parentSku : clientItem.sku,
qty: clientItem.qty,
product_option: clientItem.product_option,
quote_id: cartId
}, isThisAuthOrder))
} else if (serverItem.qty !== clientItem.qty) {
logger.info(THREAD_ID + '< Wrong qty for ' + clientItem.sku, clientItem.qty, serverItem.qty)
syncPromises.push(api.cart.update(null, cartId, { // use magento API
sku: clientItem.parentSku && config.cart.setConfigurableProductOptions ? clientItem.parentSku : clientItem.sku,
qty: clientItem.qty,
product_option: clientItem.product_option,
item_id: serverItem.item_id,
quote_id: cartId
}, isThisAuthOrder))
} else {
logger.info(THREAD_ID + '< Server and client items synced for ' + clientItem.sku) // here we need just update local item_id
}
}

for (const serverItem of serverItems) {
if (serverItem) {
const clientItem = clientItems.find((itm) => {
return itm.sku === serverItem.sku || serverItem.sku.indexOf(itm.sku + '-') >= 0 /* bundle products */
})
if (!clientItem) {
logger.info(THREAD_ID + '< No client item for ' + serverItem.sku + ', removing from server cart') // use magento API
syncPromises.push(api.cart.delete(null, cartId, { // delete server side item if not present if client's cart
sku: serverItem.sku,
item_id: serverItem.item_id
}, isThisAuthOrder))
}
}
}

Promise.all(syncPromises).then((results) => {
if(job) job.progress(currentStep++, TOTAL_STEPS);
logger.info(THREAD_ID + '< Server cart in sync')
logger.debug(THREAD_ID + results)

const billingAddr = orderData.addressInformation.billingAddress;
const shippingAddr = orderData.addressInformation.shippingAddress;
let mappedShippingRegion = 0
let mappedBillingRegion = 0

api.directory.countries().then((countryList) => {
if (shippingAddr.region_id > 0) {
mappedShippingRegion = { regionId: shippingAddr.region_id, regionCode: shippingAddr.region_code }
} else {
mappedShippingRegion = countryMapper.mapCountryRegion(countryList, shippingAddr.country_id, shippingAddr.region_code ? shippingAddr.region_code : shippingAddr.region)
}

if (billingAddr.region_id > 0) {
mappedBillingRegion = { regionId: billingAddr.region_id, regionCode: billingAddr.region_code }
} else {
mappedBillingRegion = countryMapper.mapCountryRegion(countryList, billingAddr.country_id, billingAddr.region_code ? billingAddr.region_code : billingAddr.region)
}

const billingAddressInfo = { // sum up totals
"address": {
"countryId": billingAddr.country_id,
"street": billingAddr.street,
"telephone": billingAddr.telephone,
"postcode": billingAddr.postcode,
"city": billingAddr.city,
"firstname": billingAddr.firstname,
"lastname": billingAddr.lastname,
"email": billingAddr.email,
"regionCode": mappedBillingRegion.regionCode,
"regionId": mappedBillingRegion.regionId,
"company": billingAddr.company,
"vatId": billingAddr.vat_id
}
}

const shippingAddressInfo = { // sum up totals
"addressInformation": {
"shippingAddress": {
"countryId": shippingAddr.country_id,
"street": shippingAddr.street,
"telephone": shippingAddr.telephone,
"postcode": shippingAddr.postcode,
"city": shippingAddr.city,
"firstname": shippingAddr.firstname,
"lastname": shippingAddr.lastname,
"email": shippingAddr.email,
"regionId": mappedShippingRegion.regionId,
"regionCode": mappedShippingRegion.regionCode,
"company": shippingAddr.company
},

"billingAddress": {
"countryId": billingAddr.country_id,
"street": billingAddr.street,
"telephone": billingAddr.telephone,
"postcode": billingAddr.postcode,
"city": billingAddr.city,
"firstname": billingAddr.firstname,
"lastname": billingAddr.lastname,
"email": billingAddr.email,
"regionId": mappedBillingRegion.regionId,
"regionCode": mappedBillingRegion.regionCode,
"company": billingAddr.company,
"vatId": billingAddr.vat_id
},
"shippingMethodCode": orderData.addressInformation.shipping_method_code,
"shippingCarrierCode": orderData.addressInformation.shipping_carrier_code,
"extensionAttributes": orderData.addressInformation.shippingExtraFields
}
}

logger.info(THREAD_ID + '< Billing info', billingAddressInfo)
api.cart.billingAddress(null, cartId, billingAddressInfo, isThisAuthOrder).then((result) => {
logger.info(THREAD_ID + '< Billing address assigned', result)
logger.info(THREAD_ID + '< Shipping info', shippingAddressInfo)
api.cart.shippingInformation(null, cartId, shippingAddressInfo, isThisAuthOrder).then((result) => {
logger.info(THREAD_ID + '< Shipping address assigned', result)

if(job) job.progress(currentStep++, TOTAL_STEPS);

api.cart.order(null, cartId, {
"paymentMethod": {
"method": orderData.addressInformation.payment_method_code,
"additional_data": orderData.addressInformation.payment_method_additional
}
}, isThisAuthOrder).then(result => {
logger.info(THREAD_ID, result)
if(job) job.progress(currentStep++, TOTAL_STEPS);

logger.info(THREAD_ID + '[OK] Order placed with ORDER ID', result);
logger.debug(THREAD_ID + result)
redisClient.set("order$$id$$" + orderData.order_id, JSON.stringify({
platform_order_id: result,
transmited: true,
transmited_at: new Date(),
platform: 'magento2',
order: orderData
}));
redisClient.set("order$$totals$$" + orderData.order_id, JSON.stringify(result[1]));

if(job) job.progress(currentStep++, TOTAL_STEPS);
return done(null, { magentoOrderId: result, transferedAt: new Date() });
}).catch(err => {
logger.error('Error placing an order', err, typeof err)
if (job) job.attempts(6).backoff({ delay: 30*1000, type:'fixed' }).save()
return done(new Error('Error placing an order', err));
})
}).catch((errors) => {
logger.error('Error while adding shipping address', errors)
if (job) job.attempts(3).backoff({ delay: 60*1000, type:'fixed' }).save()
return done(new Error('Error while adding shipping address', errors));
})
}).catch((errors) => {
logger.error('Error while adding billing address', errors)
if (job) job.attempts(3).backoff({ delay: 60*1000, type:'fixed' }).save()
return done(new Error('Error while adding billing address', errors));
})
}).catch((errors) => {
logger.error('Error while synchronizing country list', errors)
if (job) job.attempts(3).backoff({ delay: 30*1000, type:'fixed' }).save()
return done(new Error('Error while syncing country list', errors));
})

}).catch((errors) => {
logger.error('Error while adding products', errors)
if (job) job.attempts(3).backoff({ delay: 30*1000, type:'fixed' }).save()
return done(new Error('Error while adding products', errors));
})
})
}

cartIdPrepare.then(processCart).catch((error) => { // cannot create a quote for specific user, so bypass by placing anonymous order
logger.error(THREAD_ID, error)
logger.info('< Bypassing to anonymous order')
isThisAuthOrder = false

if (isNumeric(cartId)) { // we have numeric id - assigned to the user provided
api.cart.create(null, null).then((result) => {
processCart(result)
// logger.info('< Assigning guest cart with the user')
// api.cart.assign(cartId, userId).then((subres) =>{
// console.info(subres)
// processCart(result)
// }).catch((err) => {
// logger.error(err)
// })
}).catch(error => {
logger.info(error)
return done(new Error('Error while adding products', error));
}) // TODO: assign the guest cart with user at last?
} else {
logger.info(THREAD_ID + '< Using cartId provided with the order', cartId)
processCart(cartId)
}
})
}

module.exports.processSingleOrder = processSingleOrder
Loading

0 comments on commit 182f530

Please sign in to comment.