diff --git a/README.md b/README.md index 1e3ede8..e3058b0 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,12 @@ Supports throttling and pagification. (NextToken stuff) Examples -------- -Installation: +### Installation: ``` npm i mws-api -S ``` -Initialization: +### Initialization: ```javascript const MWSClient = require('mws-api'); @@ -40,7 +40,7 @@ const mws = new MWSClient({ } ``` -Usage: +### Usage: ```javascript @@ -55,7 +55,7 @@ mws.Orders.ListOrders({ }); ``` -Flat files: +### Flat files: When working with a flat-file response from Amazon, a `parseCSVResult` function is provided as an option to conviniently post-process the result. Returning a Promise will result in the Promise being resolved. @@ -73,3 +73,29 @@ const mws = new MWSClient({ } }); ``` + +### Fulfillment: + +```javascript +mws.FulfillmentOutboundShipment.CreateFulfillmentOrder({ + SellerFulfillmentOrderId: 'order-id', // must be unique for each order + DisplayableOrderId: 'order-id', + DisplayableOrderComment: 'order-comment', + DisplayableOrderDateTime: (new Date()).toISOString(), + FulfillmentAction: 'Hold', // or 'Ship' + ShippingSpeedCategory: 'Standard', // or 'Expedited' or 'Priority' + 'DestinationAddress.Name': 'Chip Douglas', + 'DestinationAddress.Line1': '7662 Beach Blvd', + 'DestinationAddress.Line2': '', + 'DestinationAddress.Line3': '', + 'DestinationAddress.City': 'Buena Park', + 'DestinationAddress.StateOrProvinceCode': 'CA', + 'DestinationAddress.PostalCode': '90620', + 'DestinationAddress.CountryCode': 'US', + LineItems: [{ + Quantity: 1, + SellerFulfillmentOrderItemId: 'order-id' + '_0', + SellerSKU: '101' + }] +}); +``` diff --git a/lib/client.js b/lib/client.js index 5f53749..df601dd 100644 --- a/lib/client.js +++ b/lib/client.js @@ -3,7 +3,7 @@ const _ = require('lodash'); const crypto = require('crypto'); const Promise = require('bluebird'); -const qs = require('querystring'); +const qs = require('qs'); const request = require('request'); const tls = require('tls'); const parseString = Promise.promisify(require('xml2js').parseString); @@ -223,11 +223,7 @@ class AmazonMwsClient { }, {}); const stringToSign = ['POST', this.host, path, qs.stringify(sorted)] - .join('\n') - .replace(/'/g, '%27') - .replace(/\*/g, '%2A') - .replace(/\(/g, '%28') - .replace(/\)/g, '%29'); + .join('\n'); return _.assign({}, query, { Signature: crypto.createHmac('sha256', this.secretAccessKey) diff --git a/lib/complexList.js b/lib/complexList.js deleted file mode 100644 index 8775212..0000000 --- a/lib/complexList.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict'; - -// /** -// * Takes an object and adds an appendTo function that will add -// * each kvp of object to a query. Used when dealing with complex -// * parameters that need to be built in an abnormal or unique way. -// * -// * @param {String} name Name of parameter, prefixed to each key -// * @param {Object} obj Parameters belonging to the complex type -// */ -// function ComplexType(name) { -// this.pre = name; -// var _obj = obj; -// obj.appendTo = function(query) { -// for (var k in _obj) { -// query[name + '.' k] = _obj[k]; -// } -// return query; -// } -// return obj; -// } - -// ComplexType.prototype.appendTo = function(query) { -// for (var k in value) -// } - -class ComplexListType { - /** - * Complex List helper object. Once initialized, you should set - * an add(args) method which pushes a new complex object to members. - * - * @param {String} name Name of Complex Type (including .member or subtype) - */ - constructor(name) { - this.pre = name; - this.members = []; - } - - /** - * Appends each member object as a complex list item - * @param {Object} query Query object to append to - * @return {Object} query - */ - appendTo(query) { - var members = this.members; - for (var i = 0; i < members.length; i++) { - for (var j in members[i]) { - query[this.pre + '.' + (i + 1) + '.' + j] = members[i][j]; - } - } - return query; - } -} - -module.exports = ComplexListType; diff --git a/lib/request.js b/lib/request.js index 306506c..353e369 100644 --- a/lib/request.js +++ b/lib/request.js @@ -31,8 +31,8 @@ function getValueForParam(param, val) { case Type.BOOLEAN: return String(!!val); - case Type.COMPLEX: - // return param.construct(members); // TODO: fix + case Type.OBJECT: + return val; default: return val; @@ -62,13 +62,19 @@ class AmazonMwsRequest { }); this.action = options.action || 'GetServiceStatus'; - options.params = _.mapValues(options.params, function (param, name) { - if (!param.name) { - param.name = name; - } + function deepDefaultNamesToKeys(object) { + object = _.mapValues(object, (param, key) => { + if (!param.name) { + param.name = key; + } + if (param.params) { + deepDefaultNamesToKeys(param.params); + } + return param; + }); + } - return param; - }); + deepDefaultNamesToKeys(options.params); this.params = _.reduce(options.params, function (params, param, name) { const realName = param.name; @@ -137,6 +143,13 @@ class AmazonMwsRequest { return this; } + const allowedParamNames = _.union(_.keys(this.params), _.keys(this.paramsMap)); + if (!_.includes(allowedParamNames, paramName)) { + throw new Error(`Unknown parameter \`${paramName}\`; valid params: ` + _.join(_.map(allowedParamNames, (name) => { + return '`' + name + '`'; + }), ', ')); + } + if (this.paramsMap[paramName]) { paramName = this.paramsMap[paramName]; } @@ -145,26 +158,32 @@ class AmazonMwsRequest { const getValue = _.partial(getValueForParam, param); - function toCollection(value) { - return (_.isString(value) || _.isNumber(value)) ? [value] : value; - } - // Lists need to be sequentially numbered and we take care of that here if (param.list) { - const values = _.map(toCollection(value), getValue); + const values = _.isArray(value) ? value : [value]; _.forEach(values, (value, i) => { - this.values[`${param.name}.${i + 1}`] = value; + const indexedName = `${param.name}.${i + 1}`; + this.assign(indexedName, getValue(value)); }); - } - else { - this.values[param.name] = getValue(value); + } else { + this.assign(param.name, getValue(value)); } } return this; } + assign(name, value) { + if (_.isObject(value)) { + _.assign(this.values, _.mapKeys(value, (_, key) => { + return name + '.' + key; + })); + } else { + this.values[name] = value; + } + } + setMultiple(conf) { _.each(conf, (value, key) => { this.set(key, value); @@ -180,22 +199,32 @@ class AmazonMwsRequest { */ query() { return Promise.try(() => { - const missing = _.filter(this.params, (param) => { - const isList = param.list; - const isRequired = param.required; - - if (!isRequired) { - return false; - } - - const value = isList ? this.values[`${param.name}.1`] : this.values[param.name]; - - // intentional `==` - return value == null; - }); + const missing = checkParams(this.values, this.params, ''); + + // Iterate through params and return list of any required and missing + // Recurse on object-lists + // prefix must be `''` or end with a `.` + function checkParams(values, params, prefix) { + return _.reduce(params, (missingParams, param) => { + const isList = param.list; + const isObject = param.type === Type.OBJECT; + const paramName = prefix + param.name + (isList ? '.1' : ''); + + if (isList && isObject) { + // recurse + return _.concat(missingParams, checkParams(values, param.params, paramName + '.')); + } + + // check param + if (param.required && _.isNil(values[paramName])) { + return _.concat(missingParams, paramName); + } + return missingParams; + }, []); + } if (missing.length > 0) { - throw new Error(`ERROR: Missing required parameter${missing.length > 1 ? 's' : ''}: ${_.map(missing, 'name').join(', ')}!`); + throw new Error(`Missing required parameter${missing.length > 1 ? 's' : ''}: ${_.join(missing,', ')}!`); } return this.values; diff --git a/lib/sections/fulfillment.js b/lib/sections/fulfillment.js index 3481910..a8ae86f 100644 --- a/lib/sections/fulfillment.js +++ b/lib/sections/fulfillment.js @@ -1,7 +1,6 @@ 'use strict'; const _ = require('lodash'); -const ComplexList = require('../complexList'); const Enum = require('../enum'); const Type = require('../types'); @@ -31,63 +30,76 @@ const outboundRequestDefaults = _.defaults({ nextName: 'FulfillmentOutboundShipment' }, fulfillmentRequestDefaults); -/** - * Initialize and create an add function for ComplexList parameters. You can create your - * own custom complex parameters by making an object with an appendTo function that takes - * an object as input and directly sets all of the associated values manually. - */ -const complex = { - /** - * Complex List used for CreateInboundShipment & UpdateInboundShipment requests - * - * QuantityShipped - * SellerSKU - */ - InboundShipmentItems(members) { - return new ComplexList('InboundShipmentItems.member', members); +const sharedObjects = { + inboundShipmentItems: { + name: 'InboundShipmentItems.member', required, list, type: Type.OBJECT, + params: { + QuantityShipped: { required, type: Type.INTEGER }, + SellerSKU: { required }, + } }, - - /** - * Complex List used for CreateInboundShipmentPlan request - * - * SellerSKU - * ASIN - * Quantity - * Condition - */ - InboundShipmentPlanRequestItems(members) { - return new ComplexList('InboundShipmentPlanRequestItems.member', members); + inboundShipmentPlanRequestItems: { + name: 'InboundShipmentPlanRequestItems.member', required, list, type: Type.OBJECT, + params: { + SellerSKU: { required }, + ASIN: {}, + Quantity: { required, type: Type.INTEGER }, + Condition: {}, + } }, - - /** - * The mac-daddy of ComplexListTypes... Used for CreateFulfillmentOrder request - * - * DisplayableComment - * GiftMessage - * PerUnitDeclaredValue.Value - * PerUnitDeclaredValue.CurrencyCode - * Quantity - * SellerFulfillmentOrderItemId - * SellerSKU - */ - CreateLineItems(members) { - return new ComplexList('Items.member', members); + createUpdateLineItems: { + name: 'Items.member', required, list, type: Type.OBJECT, + params: { + DisplayableComment: {}, + GiftMessage: {}, + 'PerUnitDeclaredValue.Value': {}, + 'PerUnitDeclaredValue.CurrencyCode': {}, + Quantity: { required, type: Type.INTEGER }, + SellerFulfillmentOrderItemId: { required }, + SellerSKU: { required } + } }, - - /** - * The step child of above, used for GetFulfillmentPreview - * - * Quantity - * SellerFulfillmentOrderItemId - * SellerSKU - * EstimatedShippingWeight - * ShippingWeightCalculationMethod - */ - PreviewLineItems(members) { - return new ComplexList('Items.member', members); + previewLineItems: { + name: 'Items.member', required, list, type: Type.OBJECT, + params: { + Quantity: { required, type: Type.INTEGER }, + SellerFulfillmentOrderItemId: {}, + SellerSKU: { required }, + EstimatedShippingWeight: {}, + ShippingWeightCalculationMethod: {}, + } } }; +const sharedAddress = { + Name: { required }, + City: { required }, // sorta; not used in Japan + StateOrProvinceCode: { required }, // sorta; not required for inbound shipments + PostalCode: { required }, // sorta; not always required + CountryCode: { required }, + DistrictOrCounty: {}, +}; + +// http://docs.developer.amazonservices.com/en_US/fba_outbound/FBAOutbound_Datatypes.html#Address +const outboundAddress = _.assign({ + Line1: { required }, + Line2: {}, + Line3: {}, + PhoneNumber: {}, +}, sharedAddress); + +// http://docs.developer.amazonservices.com/en_US/fba_inbound/FBAInbound_Datatypes.html#Address +const inboundAddress = _.assign({ + AddressLine1: { required }, + AddressLine2: {}, +}, sharedAddress); + +function cloneAndPrefixKeys(object, prefix) { + return _.mapKeys(_.cloneDeep(object), (_, key) => { + return prefix + '.' + key; + }) +} + const enums = { ResponseGroups() { return new Enum(['Basic', 'Detailed']); @@ -106,37 +118,21 @@ const requests = { GetServiceStatus: {}, CreateInboundShipment: { - params: { + params: _.assign({ ShipmentId: { required }, - Shipmentname: { name: 'InboundShipmentHeader.ShipmentName', required }, - ShipFromName: { name: 'InboundShipmentHeader.ShipFromAddress.Name', required }, - ShipFromAddressLine1: { name: 'InboundShipmentHeader.ShipFromAddress.AddressLine1', required }, - ShipFromAddressLine2: { name: 'InboundShipmentHeader.ShipFromAddress.AddressLine2' }, - ShipFromAddressCity: { name: 'InboundShipmentHeader.ShipFromAddress.City', required }, - ShipFromDistrictOrCounty: { name: 'InboundShipmentHeader.ShipFromAddress.DistrictOrCounty' }, - ShipFromStateOrProvince: { name: 'InboundShipmentHeader.ShipFromAddress.StateOrProvinceCode', required }, - ShipFromPostalCode: { name: 'InboundShipmentHeader.ShipFromAddress.PostalCode', required }, - ShipFromCountryCode: { name: 'InboundShipmentHeader.ShipFromAddress.CountryCode', required }, + ShipmentName: { name: 'InboundShipmentHeader.ShipmentName', required }, DestinationFulfillmentCenterId: { name: 'InboundShipmentHeader.DestinationFulfillmentCenterId', required }, ShipmentStatus: { name: 'InboundShipmentHeader.ShipmentStatus' }, LabelPrepPreference: { name: 'InboundShipmentHeader.LabelPrepPreference' }, - InboundShipmentItems: { type: Type.COMPLEX, required, construct: complex.InboundShipmentItems } - } + InboundShipmentItems: sharedObjects.inboundShipmentItems + }, cloneAndPrefixKeys(inboundAddress, 'InboundShipmentHeader.ShipFromAddress')) }, CreateInboundShipmentPlan: { - parms: { + params: _.assign({ LabelPrepPreference: { required }, - ShipFromName: { name: 'ShipFromAddress.Name' }, - ShipFromAddressLine1: { name: 'ShipFromAddress.AddressLine1' }, - ShipFromCity: { name: 'ShipFromAddress.City' }, - ShipFromStateOrProvince: { name: 'ShipFromAddress.StateOrProvinceCode' }, - ShipFromPostalCode: { name: 'ShipFromAddress.PostalCode' }, - ShipFromCountryCode: { name: 'ShipFromAddress.CountryCode' }, - ShipFromAddressLine2: { name: 'ShipFromAddress.AddressLine2' }, - ShipFromDistrictOrCounty: { name: 'ShipFromAddress.DistrictOrCounty' }, - InboundShipmentPlanRequestItems: { type: Type.COMPLEX, required, construct: complex.InboundShipmentPlanRequestItems } - } + InboundShipmentPlanRequestItems: sharedObjects.inboundShipmentPlanRequestItems + }, cloneAndPrefixKeys(inboundAddress, 'ShipFromAddress')) }, ListInboundShipmentItems: { @@ -169,22 +165,14 @@ const requests = { }, UpdateInboundShipment: { - params: { + params: _.assign({ ShipmentId: { required }, ShipmentName: { name: 'InboundShipmentHeader.ShipmentName', required }, - ShipFromName: { name: 'InboundShipmentHeader.ShipFromAddress.Name', required }, - ShipFromAddressLine1: { name: 'InboundShipmentHeader.ShipFromAddress.AddressLine1', required }, - ShipFromAddressLine2: { name: 'InboundShipmentHeader.ShipFromAddress.AddressLine2' }, - ShipFromAddressCity: { name: 'InboundShipmentHeader.ShipFromAddress.City', required }, - ShipFromDistrictOrCounty: { name: 'InboundShipmentHeader.ShipFromAddress.DistrictOrCounty' }, - ShipFromStateOrProvince: { name: 'InboundShipmentHeader.ShipFromAddress.StateOrProvinceCode', required }, - ShipFromPostalCode: { name: 'InboundShipmentHeader.ShipFromAddress.PostalCode', required }, - ShipFromCountryCode: { name: 'InboundShipmentHeader.ShipFromAddress.CountryCode', required }, DestinationFulfillmentCenterId: { name: 'InboundShipmentHeader.DestinationFulfillmentCenterId', required }, ShipmentStatus: { name: 'InboundShipmentHeader.ShipmentStatus' }, LabelPrepPreference: { name: 'InboundShipmentHeader.LabelPrepPreference' }, - InboundShipmentItems: { type: Type.COMPLEX, required, construct: complex.InboundShipmentItems } - } + InboundShipmentItems: sharedObjects.inboundShipmentItems + }, cloneAndPrefixKeys(inboundAddress, 'InboundShipmentHeader.ShipFromAddress')) } }, @@ -218,27 +206,19 @@ const requests = { }, CreateFulfillmentOrder: { - params: { + params: _.assign({ + MarketplaceId: {}, SellerFulfillmentOrderId: { required }, ShippingSpeedCategory: { required, type: 'fba.ShippingSpeedCategory' }, DisplayableOrderId: { required }, - DisplayableOrderDateTime: { type: Type.TIMESTAMP }, - DisplayableOrderComment: {}, + DisplayableOrderDateTime: { required, type: Type.TIMESTAMP }, + DisplayableOrderComment: { required }, + FulfillmentAction: {}, FulfillmentPolicy: { type: 'fba.FulfillmentPolicy' }, FulfillmentMethod: {}, NotificationEmails: { name: 'NotificationEmailList.member', list }, - DestName: { name: 'DestinationAddress.Name' }, - DestAddressLine1: { name: 'DestinationAddress.Line1' }, - DestAddressLine2: { name: 'DestinationAddress.Line2' }, - DestAddressLine3: { name: 'DestinationAddress.Line3' }, - DestCity: { name: 'DestinationAddress.City' }, - DestStateOrProvince: { name: 'DestinationAddress.StateOrProvinceCode' }, - DestPostalCode: { name: 'DestinationAddress.PostalCode' }, - DestCountryCode: { name: 'DestinationAddress.CountryCode' }, - DestDistrictOrCounty: { name: 'DestinationAddress.DistrictOrCounty' }, - DestPhoneNumber: { name: 'DestinationAddress.PhoneNumber' }, - LineItems: { type: Type.COMPLEX, required, construct: complex.CreateLineItems } - } + LineItems: sharedObjects.createUpdateLineItems + }, cloneAndPrefixKeys(outboundAddress, 'DestinationAddress')) }, GetFulfillmentOrder: { @@ -248,20 +228,11 @@ const requests = { }, GetFulfillmentPreview: { - params: { - ToName: { name: 'Address.Name' }, - ToAddressLine1: { name: 'Address.Line1' }, - ToAddressLine2: { name: 'Address.Line2' }, - ToAddressLine3: { name: 'Address.Line3' }, - ToCity: { name: 'Address.City' }, - ToStateOrProvince: { name: 'Address.StateOrProvinceCode' }, - ToPostalCode: { name: 'Address.PostalCode' }, - ToCountry: { name: 'Address.CountryCode' }, - ToDistrictOrCounty: { name: 'Address.DistrictOrCounty' }, - ToPhoneNumber: { name: 'Address.PhoneNumber' }, - LineItems: { type: Type.COMPLEX, required, construct: complex.PreviewLineItems }, - ShippingSpeeds: { name: 'ShippingSpeedCategories.member', list, type: 'fba.ShippingSpeedCategory' } - } + params: _.assign({ + MarketplaceId: {}, + ShippingSpeeds: { name: 'ShippingSpeedCategories.member', list, type: 'fba.ShippingSpeedCategory' }, + LineItems: sharedObjects.previewLineItems + }, cloneAndPrefixKeys(outboundAddress, 'Address')) }, ListAllFulfillmentOrders: { @@ -272,13 +243,14 @@ const requests = { }, ListAllFulfillmentOrdersByNextToken: { - parmas: { + params: { NextToken: { required } } }, UpdateFulfillmentOrder: { - params: { + params: _.assign({ + MarketplaceId: {}, SellerFulfillmentOrderId: { required }, ShippingSpeedCategory: { type: 'fba.ShippingSpeedCategory' }, DisplayableOrderId: {}, @@ -287,24 +259,13 @@ const requests = { FulfillmentPolicy: { type: 'fba.FulfillmentPolicy' }, FulfillmentAction: {}, NotificationEmails: { name: 'NotificationEmailList.member', list }, - DestName: { name: 'DestinationAddress.Name' }, - DestAddressLine1: { name: 'DestinationAddress.Line1' }, - DestAddressLine2: { name: 'DestinationAddress.Line2' }, - DestAddressLine3: { name: 'DestinationAddress.Line3' }, - DestCity: { name: 'DestinationAddress.City' }, - DestStateOrProvince: { name: 'DestinationAddress.StateOrProvinceCode' }, - DestPostalCode: { name: 'DestinationAddress.PostalCode' }, - DestCountryCode: { name: 'DestinationAddress.CountryCode' }, - DestDistrictOrCounty: { name: 'DestinationAddress.DistrictOrCounty' }, - DestPhoneNumber: { name: 'DestinationAddress.PhoneNumber' }, - LineItems: { type: Type.COMPLEX, construct: complex.CreateLineItems } - } + LineItems: sharedObjects.createUpdateLineItems + }, cloneAndPrefixKeys(outboundAddress, 'DestinationAddress')) } } }; module.exports = { - complex, enums, Inbound: { requests: requests.Inbound, diff --git a/lib/types.js b/lib/types.js index e9c7aaf..b3b9353 100644 --- a/lib/types.js +++ b/lib/types.js @@ -1,6 +1,6 @@ const BODY = Symbol('Body'); const BOOLEAN = Symbol('Boolean'); -const COMPLEX = Symbol('Complex'); +const OBJECT = Symbol('Object'); const DATE = Symbol('Date'); const INTEGER = Symbol('Integer'); const STRING = Symbol('String'); @@ -9,7 +9,7 @@ const TIMESTAMP = Symbol('Timestamp'); module.exports = { BODY, BOOLEAN, - COMPLEX, + OBJECT, DATE, INTEGER, STRING, diff --git a/package.json b/package.json index 4edfe85..2943dd3 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "dependencies": { "bluebird": "^3.5.0", "lodash": "^4.17.4", + "qs": "^6.5.1", "request": "^2.81.0", "xml2js": "^0.4.17" },