├── client
├── index.js
└── templates
│ └── dashboard
│ ├── index.js
│ ├── dashboard.js
│ ├── dashboard.html
│ ├── customFields.html
│ └── customFields.js
├── tests
└── jasmine
│ └── server
│ ├── integration
│ └── methods
│ │ └── productImporter.js
│ └── unit
│ └── productImporter.js
├── server
├── index.js
├── api
│ ├── index.js
│ └── productImporter.js
└── methods
│ └── productImporter.js
├── register.js
├── README.md
└── .eslintrc
/client/index.js:
--------------------------------------------------------------------------------
1 | import './templates/dashboard';
2 |
--------------------------------------------------------------------------------
/tests/jasmine/server/integration/methods/productImporter.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | import './methods/productImporter';
2 |
3 | export * from './api';
4 |
--------------------------------------------------------------------------------
/client/templates/dashboard/index.js:
--------------------------------------------------------------------------------
1 | import './customFields';
2 | import './dashboard';
3 |
--------------------------------------------------------------------------------
/server/api/index.js:
--------------------------------------------------------------------------------
1 | import './productImporter';
2 | export * from './productImporter';
3 |
--------------------------------------------------------------------------------
/tests/jasmine/server/unit/productImporter.js:
--------------------------------------------------------------------------------
1 | beforeAll(function () {
2 | VelocityHelpers.exportGlobals();
3 | });
4 |
5 | describe('getoutfitted:reaction-product-importer product-importer object', function () {
6 | // describe('ProductImporter.existingProduct', function () {
7 | // beforeEach(function () {
8 | // return ReactionCore.Collections.Products.remove({});
9 | // });
10 | // it('should return the correct type of product', function () {
11 | // let product = Factory.create('product');
12 | // let variant = Factory.create('variant');
13 | // console.log(ProductImporter)
14 | // // let result = ProductImporter.existingProduct(product, 'simple');
15 | // // expect(result.title).toBe(product.title);
16 |
17 |
18 | // });
19 | // });
20 | });
21 |
--------------------------------------------------------------------------------
/register.js:
--------------------------------------------------------------------------------
1 | import { Reaction } from '/server/api';
2 |
3 | Reaction.registerPackage({
4 | label: 'Import Products from CSV',
5 | name: 'reaction-product-importer',
6 | icon: 'fa fa-cloud-upload',
7 | autoEnable: false,
8 | settings: {
9 | customFields: {
10 | topProduct: [],
11 | midVariant: [],
12 | variant: []
13 | }
14 | },
15 | registry: [
16 | {
17 | provides: 'dashboard',
18 | label: 'Product Importer',
19 | description: 'Import Products into Reaction from CSV',
20 | route: '/dashboard/product-importer',
21 | icon: 'fa fa-cloud-upload',
22 | container: 'getoutfitted',
23 | template: 'dashboardProductImporter',
24 | name: 'dashboardProductImporter',
25 | workflow: 'productImporterWorkflow',
26 | priority: 2
27 | }, {
28 | provides: 'settings',
29 | label: 'Product Importer Settings',
30 | route: '/dashboard/product-importer/settings',
31 | name: 'settingsProductImporter',
32 | template: 'settingsProductImporter'
33 | }
34 | ],
35 | layout: [{
36 | workflow: 'productImporterWorkflow',
37 | layout: 'coreLayout',
38 | theme: 'default',
39 | enabled: true,
40 | structure: {
41 | template: 'dashboardProductImporter',
42 | layoutHeader: 'goLayoutHeader',
43 | layoutFooter: 'goLayoutFooter',
44 | notFound: 'goNotFound',
45 | dashboardControls: 'dashboardControls',
46 | adminControlsFooter: 'adminControlsFooter'
47 | }
48 | }, {
49 | workflow: 'productImporterWorkflow',
50 | layout: 'getoutfittedLayout',
51 | theme: 'default',
52 | enabled: true,
53 | structure: {
54 | template: 'dashboardProductImporter',
55 | layoutHeader: 'goLayoutHeader',
56 | layoutFooter: 'goLayoutFooter',
57 | notFound: 'goNotFound',
58 | dashboardControls: 'dashboardControls',
59 | adminControlsFooter: 'adminControlsFooter'
60 | }
61 | }]
62 | });
63 |
--------------------------------------------------------------------------------
/server/methods/productImporter.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { check } from 'meteor/check';
3 | import { Packages } from '/lib/collections';
4 | import { Reaction } from '/server/api';
5 | import { _ } from 'meteor/underscore';
6 |
7 | import { ProductImporter } from '../api';
8 |
9 | Meteor.methods({
10 | 'productImporter/importProducts': function (productsList) {
11 | check(productsList, [Object]);
12 | // group each Product by Product ID
13 | let productsById = ProductImporter.groupBy(productsList, 'productId');
14 | _.each(productsById, function (product) {
15 | let productId = ProductImporter.createTopLevelProduct(product);
16 | let ancestors = [productId];
17 | // group each variant by variant title
18 | let variantGroups = ProductImporter.groupBy(product, 'variantTitle');
19 | _.each(variantGroups, function (variantGroup) {
20 | let variantGroupId = ProductImporter.createMidLevelVariant(variantGroup, ancestors);
21 | let variantAncestors = ancestors.concat([variantGroupId]);
22 | // create each sub variant
23 | _.each(variantGroup, function (variant) {
24 | ProductImporter.createVariant(variant, variantAncestors);
25 | });
26 | });
27 | });
28 | return true;
29 | },
30 | 'productImporter/addCustomField': function (productSelector, customField) {
31 | check(productSelector, String);
32 | check(customField, Object);
33 | let variantLevelToBeUpdated = 'settings.customFields.' + productSelector;
34 | let updateObj = {};
35 | updateObj[variantLevelToBeUpdated] = customField;
36 | Packages.update({
37 | name: 'reaction-product-importer',
38 | shopId: Reaction.getShopId()
39 | }, {
40 | $addToSet: updateObj
41 | });
42 | },
43 | 'productImport/removeCustomField': function (removingField) {
44 | check(removingField, {
45 | level: String,
46 | csvColumnName: String,
47 | productFieldName: String,
48 | valueType: String
49 | });
50 | let data = Packages.findOne({
51 | name: 'reaction-product-importer',
52 | shopId: Reaction.getShopId()
53 | });
54 | if (data) {
55 | let customFields = data.settings.customFields;
56 | _.each(customFields[removingField.level], function (field, index) {
57 | let csvColumnName = field.csvColumnName === removingField.csvColumnName;
58 | let productFieldName = field.productFieldName === removingField.productFieldName;
59 | let valueType = field.valueType === removingField.valueType;
60 | if (csvColumnName && productFieldName && valueType && index !== -1) {
61 | customFields[removingField.level].splice(index, 1);
62 | }
63 | });
64 | Packages.update({_id: data._id}, {
65 | $set: {
66 | 'settings.customFields': customFields
67 | }
68 | });
69 | } else {
70 | throw new Error('403 Cannot find package.');
71 | }
72 | }
73 | });
74 |
75 |
--------------------------------------------------------------------------------
/client/templates/dashboard/dashboard.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { Template } from 'meteor/templating';
3 | import { Session } from 'meteor/session';
4 | import { _ } from 'meteor/underscore';
5 | // import Baby from 'babyparse'
6 | import './dashboard.html';
7 |
8 | // Since Papa Parse has no export - this pacakge requires meteor add harrison:papa-parse to be added to project
9 |
10 | Template.dashboardProductImporter.onRendered(function () {
11 | Session.setDefault('importingProducts', false);
12 | });
13 |
14 | Template.dashboardProductImporter.helpers({
15 | importingProducts: function () {
16 | return Session.get('importingProducts');
17 | },
18 | importSize: function () {
19 | return Session.get('importSize');
20 | }
21 | });
22 |
23 | Template.dashboardProductImporter.events({
24 | 'submit #import-products-csv-form': function (event) {
25 | event.preventDefault();
26 | Papa.parse(event.target.csvImportProductsFile.files[0], {
27 | header: true,
28 | complete: function (results) {
29 | if (results && results.data) {
30 | Session.set('importSize', _.size(results.data));
31 | Session.set('importingProducts', true);
32 | Meteor.call('productImporter/importProducts', results.data, function (err, result) {
33 | if (err) {
34 | Alerts.removeSeen();
35 | Alerts.add('Error while importing ' + err, 'danger', {
36 | autoHide: true
37 | });
38 | } else {
39 | Alerts.removeSeen();
40 | Alerts.add('Products Successfully Imported', 'success', {
41 | autoHide: true
42 | });
43 | Session.set('importingProducts', false);
44 | }
45 | });
46 | }
47 | }
48 | });
49 | },
50 | 'click .downloadSample': function (event) {
51 | event.preventDefault();
52 | let data = [{
53 | productId: '1',
54 | topProductType: 'simple',
55 | productTitle: 'Basic Reaction Product',
56 | pageTitle: 'This is a basic product. You can do a lot with it.',
57 | vendor: 'Example Manufacturer',
58 | handle: 'example-product',
59 | variantTitle: 'Basic Example Variant',
60 | variantType: 'variant',
61 | title: 'Option 1 - Red Dwarf',
62 | optionTitle: 'Red',
63 | price: '19.99',
64 | qty: '19',
65 | weight: '35',
66 | taxable: 'true',
67 | hashtags: 'Hashtags, Womens, Red',
68 | metafields: 'Material=Cotten | Quality=Excellent',
69 | description: 'Sign in as administrator to edit.\nYou can clone this product from the product grid.'
70 | }];
71 | let unparse = Papa.unparse(data);
72 | // let unparse = Pap.unparse(data);
73 | let csvData = new Blob([unparse], {type: 'text/csv;charset=utf-8;'});
74 | let csvURL = window.URL.createObjectURL(csvData);
75 | let tempLink = document.createElement('a');
76 | tempLink.href = csvURL;
77 | tempLink.setAttribute('download', 'productImporterTemplate.csv');
78 | tempLink.click();
79 | }
80 | });
81 |
--------------------------------------------------------------------------------
/client/templates/dashboard/dashboard.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Product Importer
4 |
Product Importer allows you to bulk upload products from a CSV file. Each CSV requires a column called ProductId that groups variants together. Then you make the second level variant whatever you want and the third variant whatever you want. You can also control the mapping from one column to a corresponding DB field, or name your columns to correspond
5 |
6 |
12 |
13 | {{#if importingProducts}}
14 |
15 |
Improrting Products from CSV
16 | Loading..
17 |
18 | {{/if}}
19 |
20 |
Special Syntaxes
21 |
22 | top level - there are certain fields that only apply to top level products (topProdctType, pageTitle, handle, hashtags, metafields, description) these need to be on the first of each product but can be filled out for each item.
23 | hashtags - each hashtag need to be separated by a ,
24 | metafields - each key value pair need to be separated by | and each key needs to be separated from value by =
25 | Custom Arrays - can be split by whatever sybmol you enter into the delimiting field
26 | Custom Objects - can be delimited by any field but key values must be separated by =
27 |
28 |
29 |
30 |
31 |
32 |
CSV Example For Basic Template
33 |
34 |
35 |
36 | productId
37 | topProductType
38 | productTitle
39 | pageTitle
40 | vendor
41 | handle
42 | variantTitle
43 | variantType
44 | title
45 | optionTitle
46 | price
47 | qty
48 | weight
49 | taxable
50 | hashtags
51 | metafields
52 | description
53 |
54 |
55 |
56 |
57 | 1
58 | simple
59 | Basic Reaction Product
60 | This is a basic product. You can do a lot with it.
61 | Example Manufacturer
62 | example-product
63 | Basic Example Variant
64 | variant
65 | Option 1 - Red Dwarf
66 | Red
67 | 19.99
68 | 19
69 | 35
70 | true
71 | Womens, Red
72 | Material=Cotten | Quality=Excellent
73 | Sign in as administrator to edit.\nYou can clone this product from the product grid.
74 |
75 |
76 | 1
77 |
78 | Basic Reaction Product
79 |
80 | Example Manufacturer
81 |
82 | Basic Example Variant
83 | variant
84 | Option 2 - Green Tomato
85 | Green
86 | 12.99,
87 | 42
88 | 35
89 | true
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
Download Basic CSV Template
101 |
Download
102 |
103 | {{> customFields}}
104 |
105 |
106 |
--------------------------------------------------------------------------------
/client/templates/dashboard/customFields.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Custom Product Fields
4 |
Product Importer supports the importing of your custom fields, you just need to help us map them out. You need to tell us what the column name is, what the field name is on your products, what type of value you are inserting and finally where to put this (top level product, grouping variant or bottom variant)
5 |
6 | {{#if anyCustomFields}}
7 |
8 |
Current Custom Fields
9 |
10 |
11 |
12 |
13 | Type
14 | Column Name
15 | Products Field
16 | Value Type
17 |
18 |
19 |
20 |
21 | {{#each customTopProducts}}
22 |
23 | Top Level Product
24 | {{csvColumnName}}
25 | {{productFieldName}}
26 | {{valueType}}
27 |
28 |
29 | {{/each}}
30 | {{#each customMidVariant}}
31 |
32 | Middle Level (Grouping) Variant
33 | {{csvColumnName}}
34 | {{productFieldName}}
35 | {{valueType}}
36 |
37 |
38 | {{/each}}
39 | {{#each customVariant}}
40 |
41 | Bottom Level Variant
42 | {{csvColumnName}}
43 | {{productFieldName}}
44 | {{valueType}}
45 |
46 |
47 | {{/each}}
48 |
49 |
50 | {{/if}}
51 |
52 |
Add Custom Fields
53 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/client/templates/dashboard/customFields.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { Template } from 'meteor/templating';
3 | import { Session } from 'meteor/session';
4 | import { Reaction } from '/client/api';
5 | import { Packages } from '/lib/collections';
6 |
7 | import './customFields.html';
8 |
9 | function getProductImporterPackage() {
10 | return Packages.findOne({
11 | name: 'reaction-product-importer',
12 | shopId: Reaction.getShopId()
13 | });
14 | }
15 |
16 | Template.customFields.onRendered(function () {
17 | Session.setDefault('ifArray', false);
18 | Session.setDefault('ifObject', false);
19 | Session.setDefault('arrayOfObjects', false);
20 | Session.setDefault('objectPropertiesCount', 0);
21 | });
22 |
23 | Template.customFields.helpers({
24 | anyCustomFields: function () {
25 | const productImporter = getProductImporterPackage();
26 | return _.some(productImporter.settings.customFields, function (level) {
27 | return level.length > 0;
28 | });
29 | },
30 | customTopProducts: function () {
31 | const productImporter = getProductImporterPackage();
32 | return productImporter.settings.customFields.topProduct;
33 | },
34 | customMidVariant: function () {
35 | const productImporter = getProductImporterPackage();
36 | return productImporter.settings.customFields.midVariant;
37 | },
38 | customVariant: function () {
39 | const productImporter = getProductImporterPackage();
40 | return productImporter.settings.customFields.variant;
41 | },
42 | ifArray: function () {
43 | return Session.get('ifArray');
44 | },
45 | ifObject: function () {
46 | return Session.get('ifObject');
47 | },
48 | arrayOrObject: function () {
49 | return Session.get('ifArray') || Session.get('ifObject');
50 | },
51 | arrayOfObjects: function () {
52 | return Session.get('arrayOfObjects')
53 | },
54 | valuesOfObjects: function () {
55 | let objectPropertiesCount = Session.get('objectPropertiesCount');
56 | let valueFields = '';
57 | _(objectPropertiesCount).times(function (n) {
58 | let m = n + 1;
59 | let input = ""
60 | + "Value Type of Property " + m + " "
61 | + ""
62 | + "String "
63 | + "Number "
64 | + "Boolean "
65 | + "
"
66 | valueFields += input;
67 | });
68 | return valueFields;
69 | }
70 | });
71 |
72 | Template.customFields.events({
73 | 'submit #customFieldsForm': function () {
74 | event.preventDefault();
75 | let customField = {};
76 | customField.csvColumnName = event.target.columnName.value.trim();
77 | customField.productFieldName = event.target.productField.value.trim();
78 | customField.valueType = event.target.typeSelector.value;
79 | const productSelector = event.target.productSelector.value;
80 | if (customField.valueType === 'array' || customField.valueType === 'object') {
81 | customField.options = {};
82 | customField.options.delimiter = event.target.delimiterSymbol.value;
83 | customField.options.typeSelector = event.target.optionTypeSelector.value;
84 | }
85 | if (customField.valueType === 'array' && event.target.optionTypeSelector.value === 'object') {
86 | customField.options.arrayOfObjects = {};
87 | customField.options.arrayOfObjects.propertyCount = parseInt(event.target.objectPropertiesCount.value, 10);
88 | customField.options.arrayOfObjects.delimiter = event.target.objectPropertiesDelimiter.value;
89 | _(customField.options.arrayOfObjects.propertyCount).times(function (n) {
90 |
91 | customField.options.arrayOfObjects[n] = event.target['objectPropertiesCount' + n].value;
92 | })
93 | }
94 |
95 | let columnNameWhiteSpace = customField.csvColumnName.search(/\s/g);
96 | let productFieldNameWhiteSpace = customField.productFieldName.search(/\s/g);
97 | let noWhiteSpace = columnNameWhiteSpace + productFieldNameWhiteSpace === -2;
98 | if (noWhiteSpace) {
99 | Meteor.call('productImporter/addCustomField', productSelector, customField);
100 | } else {
101 | Alerts.removeSeen();
102 | Alerts.add('No Spaces are allow in ColumnName or ProductFieldName', 'danger', {
103 | autoHide: true
104 | });
105 | }
106 | event.target.columnName.value = '';
107 | event.target.productField.value = '';
108 | Session.set('ifArray', false);
109 | Session.set('ifObject', false);
110 | Session.set('arrayOfObjects', false);
111 | Session.set('objectPropertiesCount', 0);
112 | },
113 | 'change form #typeSelector': function () {
114 | event.preventDefault();
115 | let selectedType = event.target.value;
116 | if (selectedType === 'array') {
117 | Session.set('ifArray', true);
118 | Session.set('ifObject', false);
119 | } else if (selectedType === 'object') {
120 | Session.set('ifArray', false);
121 | Session.set('ifObject', true);
122 | } else {
123 | Session.set('ifArray', false);
124 | Session.set('ifObject', false);
125 | }
126 | },
127 | 'change form #optionTypeSelector': function () {
128 | const selectedOption = event.target.value;
129 | if (selectedOption === 'object') {
130 | Session.set('arrayOfObjects', true)
131 | } else {
132 | Session.set('arrayOfObjects', false)
133 | }
134 | },
135 | 'click .remove': function (event) {
136 | event.preventDefault();
137 | let customRemoval = {};
138 | customRemoval.level = event.currentTarget.dataset.level;
139 | customRemoval.csvColumnName = event.currentTarget.dataset.csvColumnName;
140 | customRemoval.productFieldName = event.currentTarget.dataset.productFieldName;
141 | customRemoval.valueType = event.currentTarget.dataset.valueType;
142 | if (Object.keys(customRemoval).length === 4) {
143 | Meteor.call('productImport/removeCustomField', customRemoval);
144 | }
145 | },
146 | 'change form #objectPropertiesCount': function () {
147 | Session.set('objectPropertiesCount', parseInt(event.target.value, 10));
148 | }
149 | });
150 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # reaction-product-importer
2 | Product Importer allows you to bulk upload products from a CSV file. Each CSV requires a column called ProductId that groups variants together. Then you make the second level variant whatever you want and the third variant whatever you want. You can also control the mapping from one column to a corresponding DB field, or name your columns to correspond
3 |
4 | Here's an example of what basic csv import file could look like
5 |
6 | | productId | topProductType | productTitle | pageTitle | vendor | handle | variantTitle | variantType | title | optionTitle | price | qty | weight | taxable | hastags | metatags | description |
7 | |-----------|----------------|------------------------|----------------------------------------------------|----------------------|-----------------|-----------------------|-------------|----------------------|-------------|-------|-----|--------|---------|-----------------------|------------------------------------|------------------------------------------------------------------------------------|
8 | | 1 | simple | Basic Reaction Product | This is a basic product. You can do a lot with it. | Example Manufacturer | example-product | Basic Example Variant | variant | Option 1 - Red Dwarf | Red | 19.99 | 19 | 35 | true | Hashtags, Womens, Red | Material=Cotten, Quality=Excellent | Sign in as administrator to edit.You can clone this product from the product grid. |
9 |
10 |
11 | ### Custom Product Fields
12 | Product Importer supports the importing of your custom fields, you just need to help us map them out. You need to tell us what the column name is, what the field name is on your products, what type of value you are inserting and finally where to put this (top level product, grouping variant or bottom variant)
13 |
14 | 
15 |
16 | ### What kind of fields does Product Importer support?
17 | - top level - there are certain fields that only apply to top level products (topProductType, pageTitle, handle, hastags, metafields, description) these need to be on the first of each product but can be filled out for each item.
18 | - hashtags - each hashtag (tag) need to be separated by a ,
19 | - metafields - each key value pair need to be separated by , and each key needs to be separated from value by =
20 | - Custom Arrays - can be split by whatever symbol you enter into the delimiting field
21 | - Custom Objects - can be delimited by any field but key values must be separated by =
22 |
23 | _**Note:**_ Any custom field you have must be supported by your product schema or it will not import.
24 |
25 | Here's an example of what a more complex csv import file would look like - _custom fields have *italicized* headers_
26 |
27 | | productId | *topProductFunctionalType* | topProductType | variantType | *variantFunctionalType* | productTitle | pageTitle | vendor | handle | *variantTitle* | *title* | optionTitle | price | qty | weight | taxable | hastags | metatags | description | *productType* | *location* | *colors* | *gender* | *cleaningBuffer* |
28 | |-----------|----------------------------|----------------|-------------|-------------------------|---------------------|--------------------------------------------------|-----------|---------------------|----------------|---------------|-------------------|-------|-----|--------|---------|-----------------------------------|------------------------------------------------------|-------------------|---------------|------------|------------------------|----------|------------------|
29 | | 1 | rental | simple | variant | rentalVariant | 3 in 1 | Womens Patagonia 3-in-1 | Patagonia | patagonia-3-in-1 | Small | Light Acai | Color-Acai | 19.99 | 2 | 35 | TRUE | Jacket, Winter | included=functional pockets |ProductDescription | Jacket | A1 | Acai,Marine | Womens | 0 |
30 | | 1 | rental | simple | variant | rentalVariant | 3 in 1 | Womens Patagonia 3-in-1 | Patagonia | patagonia-3-in-1 | Medium | Light Acai | Color-Acai | 20 | 6 | 35 | TRUE | Jacket, Winter | included=functional pockets |ProductDescription | Jacket | A1 | Acai,Marine | Womens | 0 |
31 | | 1 | rental | simple | variant | rentalVariant | 3 in 1 | Womens Patagonia 3-in-1 | Patagonia | patagonia-3-in-1 | Large | Light Acai | Color-Acai | 25 | 8 | 35 | TRUE | Jacket, Winter | included=functional pockets |ProductDescription | Jacket | A1 | Acai,Marine | Womens | 0 |
32 | | 1 | rental | simple | variant | rentalVariant | 3 in 1 | Womens Patagonia 3-in-1 | Patagonia | patagonia-3-in-1 | Extra Large | Light Acai | Color-Acai | 30 | 2 | 35 | TRUE | Jacket, Winter | included=functional pockets |ProductDescription | Jacket | A1 | Acai,Marine | Womens | 0 |
33 | | 1 | rental | simple | variant | rentalVariant | 3 in 1 | Womens Patagonia 3-in-1 | Patagonia | patagonia-3-in-1 | Small | Ultramarine | Color-Marine | 19.99 | 2 | 35 | TRUE | Jacket, Winter | included=functional pockets |ProductDescription | Jacket | A1 | Acai,Marine | Womens | 0 |
34 | | 1 | rental | simple | variant | rentalVariant | 3 in 1 | Womens Patagonia 3-in-1 | Patagonia | patagonia-3-in-1 | Medium | Ultramarine | Color-Marine | 20 | 6 | 35 | TRUE | Jacket, Winter | included=functional pockets |ProductDescription | Jacket | A1 | Acai,Marine | Womens | 0 |
35 | | 1 | rental | simple | variant | rentalVariant | 3 in 1 | Womens Patagonia 3-in-1 | Patagonia | patagonia-3-in-1 | Large | Ultramarine | Color-Marine | 25 | 8 | 35 | TRUE | Jacket, Winter | included=functional pockets |ProductDescription | Jacket | A1 | Acai,Marine | Womens | 0 |
36 | | 1 | rental | simple | variant | rentalVariant | 3 in 1 | Womens Patagonia 3-in-1 | Patagonia | patagonia-3-in-1 | Extra Large | Ultramarine | Color-Marine | 30 | 2 | 35 | TRUE | Jacket, Winter | included=functional pockets |ProductDescription | Jacket | A1 | Acai,Marine | Womens | 0 |
37 | | 2 | rental | simple | variant | rentalVariant | Snowbelle | Womens Patagonia Snowbelle | Patagonia | patagonia-snowbelle | Small | Black | Color-Black | 20 | 6 | 35 | TRUE | Pants, Winter | included=functional pockets |ProductDescription | Pants | B2 | Black | Womens | 0 |
38 | | 2 | rental | simple | variant | rentalVariant | Snowbelle | Womens Patagonia Snowbelle | Patagonia | patagonia-snowbelle | Medium | Black | Color-Black | 25 | 13 | 35 | TRUE | Pants, Winter | included=functional pockets |ProductDescription | Pants | B2 | Black | Womens | 0 |
39 | | 2 | rental | simple | variant | rentalVariant | Snowbelle | Womens Patagonia Snowbelle | Patagonia | patagonia-snowbelle | Large | Black | Color-Black | 25 | 13 | 35 | TRUE | Pants, Winter | included=functional pockets |ProductDescription | Pants | B2 | Black | Womens | 0 |
40 | | 2 | rental | simple | variant | rentalVariant | Snowbelle | Womens Patagonia Snowbelle | Patagonia | patagonia-snowbelle | Extra Large | Black | Color-Black | 35 | 5 | 35 | TRUE | Pants, Winter | included=functional pockets |ProductDescription | Pants | B2 | Black | Womens | 0 |
41 |
42 | This is our _Current Custom Fields_ section looks like for the above csv example.
43 |
44 | 
45 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 |
3 | "parser": "babel-eslint",
4 |
5 | "env": {
6 |
7 | "browser": true,
8 |
9 | "node": true,
10 |
11 | "meteor": true,
12 |
13 | },
14 |
15 | "ecmaFeatures": {
16 |
17 | "arrowFunctions": true,
18 |
19 | "blockBindings": true,
20 |
21 | "classes": true,
22 |
23 | "defaultParams": true,
24 |
25 | "destructuring": true,
26 |
27 | "forOf": true,
28 |
29 | "generators": false,
30 |
31 | "modules": false,
32 |
33 | "objectLiteralComputedProperties": true,
34 |
35 | "objectLiteralDuplicateProperties": false,
36 |
37 | "objectLiteralShorthandMethods": true,
38 |
39 | "objectLiteralShorthandProperties": true,
40 |
41 | "spread": true,
42 |
43 | "superInFunctions": true,
44 |
45 | "templateStrings": true,
46 |
47 | "jsx": true
48 |
49 | },
50 |
51 | "rules": {
52 |
53 | /**
54 |
55 | * Strict mode
56 |
57 | */
58 |
59 | // babel inserts "use strict"; for us
60 |
61 | // http://eslint.org/docs/rules/strict
62 |
63 | "strict": [2, "never"],
64 |
65 |
66 |
67 |
68 | /**
69 |
70 | * ES6
71 |
72 | */
73 |
74 | "no-var": 2, // http://eslint.org/docs/rules/no-var
75 |
76 |
77 |
78 |
79 | /**
80 |
81 | * Variables
82 |
83 | */
84 |
85 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow
86 |
87 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names
88 |
89 | "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars
90 |
91 | "vars": "local",
92 |
93 | "args": "after-used"
94 |
95 | }],
96 |
97 | "no-const-assign": 2, // http://eslint.org/docs/rules/no-const-assign
98 |
99 | "no-use-before-define": [2, "nofunc"], // http://eslint.org/docs/rules/no-use-before-define
100 |
101 |
102 |
103 |
104 | /**
105 |
106 | * Possible errors
107 |
108 | */
109 |
110 | "comma-dangle": [2, "never"], // http://eslint.org/docs/rules/comma-dangle
111 |
112 | "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign
113 |
114 | "no-console": 1, // http://eslint.org/docs/rules/no-console
115 |
116 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger
117 |
118 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert
119 |
120 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition
121 |
122 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys
123 |
124 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case
125 |
126 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty
127 |
128 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign
129 |
130 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast
131 |
132 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi
133 |
134 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign
135 |
136 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations
137 |
138 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp
139 |
140 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace
141 |
142 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls
143 |
144 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays
145 |
146 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable
147 |
148 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan
149 |
150 | "block-scoped-var": 0, // http://eslint.org/docs/rules/block-scoped-var
151 |
152 |
153 |
154 |
155 | /**
156 |
157 | * Best practices
158 |
159 | */
160 |
161 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return
162 |
163 | "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly
164 |
165 | "default-case": 2, // http://eslint.org/docs/rules/default-case
166 |
167 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation
168 |
169 | "allowKeywords": true
170 |
171 | }],
172 |
173 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq
174 |
175 | "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in
176 |
177 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller
178 |
179 | "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return
180 |
181 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null
182 |
183 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval
184 |
185 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native
186 |
187 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind
188 |
189 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough
190 |
191 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal
192 |
193 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval
194 |
195 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks
196 |
197 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func
198 |
199 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str
200 |
201 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign
202 |
203 | "no-new": 2, // http://eslint.org/docs/rules/no-new
204 |
205 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func
206 |
207 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers
208 |
209 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal
210 |
211 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape
212 |
213 | "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign
214 |
215 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto
216 |
217 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare
218 |
219 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign
220 |
221 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url
222 |
223 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare
224 |
225 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences
226 |
227 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal
228 |
229 | "no-with": 2, // http://eslint.org/docs/rules/no-with
230 |
231 | "radix": 2, // http://eslint.org/docs/rules/radix
232 |
233 | "vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top
234 |
235 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife
236 |
237 | "yoda": 2, // http://eslint.org/docs/rules/yoda
238 |
239 | "max-len": [2, 160, 2, {
240 |
241 | "ignoreComments": true,
242 |
243 | "ignoreUrls": true
244 |
245 | }], // http://eslint.org/docs/rules/max-len
246 |
247 | "valid-jsdoc": 2, // http://eslint.org/docs/rules/valid-jsdoc
248 |
249 | "quote-props": [2, "consistent-as-needed"], // http://eslint.org/docs/rules/quote-props
250 |
251 | "no-console": 2, // http://eslint.org/docs/rules/no-console
252 |
253 |
254 |
255 |
256 | /**
257 |
258 | * Style
259 |
260 | */
261 |
262 | "indent": [2, 2], // http://eslint.org/docs/rules/indent
263 |
264 | "brace-style": [2, // http://eslint.org/docs/rules/brace-style
265 |
266 | "1tbs", {
267 |
268 | "allowSingleLine": true
269 |
270 | }
271 |
272 | ],
273 |
274 | "quotes": [
275 |
276 | 2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes
277 |
278 | ],
279 |
280 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase
281 |
282 | "properties": "always"
283 |
284 | }],
285 |
286 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing
287 |
288 | "before": false,
289 |
290 | "after": true
291 |
292 | }],
293 |
294 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style
295 |
296 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last
297 |
298 | "func-names": 0, // http://eslint.org/docs/rules/func-names
299 |
300 | "func-style": [2, "declaration"], // http://eslint.org/docs/rules/func-style
301 |
302 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing
303 |
304 | "beforeColon": false,
305 |
306 | "afterColon": true
307 |
308 | }],
309 |
310 | "new-cap": [2, { // http://eslint.org/docs/rules/new-cap
311 |
312 | "newIsCap": true,
313 |
314 | "capIsNewExceptions": ["Optional"],
315 |
316 | }],
317 |
318 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines
319 |
320 | "max": 2
321 |
322 | }],
323 |
324 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary
325 |
326 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object
327 |
328 | "no-array-constructor": 2, // http://eslint.org/docs/rules/no-array-constructor
329 |
330 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func
331 |
332 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces
333 |
334 | "no-extra-parens": 2, // http://eslint.org/docs/rules/no-extra-parens
335 |
336 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle
337 |
338 | "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var
339 |
340 | "padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks
341 |
342 | "semi": [2, "always"], // http://eslint.org/docs/rules/semi
343 |
344 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing
345 |
346 | "before": false,
347 |
348 | "after": true
349 |
350 | }],
351 |
352 | "space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords
353 |
354 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks
355 |
356 | "space-before-function-paren": [2, {
357 |
358 | "anonymous": "always",
359 |
360 | "named": "never"
361 |
362 | }], // http://eslint.org/docs/rules/space-before-function-paren
363 |
364 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops
365 |
366 | "space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case
367 |
368 | "space-in-parens": [2, "never"], // http://eslint.org/docs/rules/space-in-parens
369 |
370 | "spaced-comment": [2, "always"], // http://eslint.org/docs/rules/spaced-comment
371 |
372 | }
373 |
374 | }
375 |
--------------------------------------------------------------------------------
/server/api/productImporter.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { check } from 'meteor/check';
3 | import { Products, Packages } from '/lib/collections';
4 | import { Reaction, Logger } from '/server/api';
5 | import { _ } from 'meteor/underscore';
6 |
7 | export const ProductImporter = {};
8 | ProductImporter.existingProduct = function (product, type = 'variant') {
9 | check(product, Object);
10 | check(type, String);
11 | if (type !== 'variant') {
12 | return Products.findOne({
13 | title: product.title,
14 | vendor: product.vendor,
15 | ancestors: product.ancestors,
16 | type: type,
17 | handle: product.handle
18 | });
19 | }
20 | return Products.findOne({
21 | title: product.title,
22 | ancestors: product.ancestors,
23 | type: type
24 | });
25 | };
26 |
27 | ProductImporter.anyCustomFields = function (level) {
28 | check(level, String);
29 | let validLevels = ['topProduct', 'midVariant', 'variant'];
30 | if (!_.contains(validLevels, level)) {
31 | Logger.warn('Customized Import does not match level');
32 | return false;
33 | }
34 | let productImporter = Packages.findOne({
35 | name: 'reaction-product-importer',
36 | shopId: Reaction.getShopId()
37 | });
38 | if (productImporter) {
39 | return productImporter.settings.customFields[level].length >= 1;
40 | }
41 | };
42 | ProductImporter.customFields = function (level) {
43 | check(level, String);
44 | let validLevels = ['topProduct', 'midVariant', 'variant'];
45 |
46 | if (!_.contains(validLevels, level)) {
47 | Logger.warn('Customized Import does not match level');
48 | return false;
49 | }
50 | let productImporter = Packages.findOne({
51 | name: 'reaction-product-importer',
52 | shopId: Reaction.getShopId()
53 | });
54 | if (productImporter && productImporter.settings && productImporter.settings.customFields) {
55 | return productImporter.settings.customFields[level];
56 | }
57 | };
58 |
59 | ProductImporter.groupBy = function (productList, groupIdentifier) {
60 | check(productList, [Object]);
61 | check(groupIdentifier, String);
62 | return _.groupBy(productList, function (product) {
63 | return product[groupIdentifier];
64 | });
65 | };
66 |
67 | ProductImporter.parseBasicType = function (value, valueType = 'string') {
68 | check(value, String);
69 | check(valueType, String);
70 | switch (valueType) {
71 | case 'number':
72 | return parseFloat(value, 10);
73 | case 'boolean':
74 | return JSON.parse(value.toLowerCase());
75 | default:
76 | return value;
77 | }
78 | };
79 |
80 | ProductImporter.parseByType = function (value, customField) {
81 | check(value, String);
82 | check(customField, Object);
83 | switch (customField.valueType) {
84 | case 'number':
85 | return parseFloat(value, 10);
86 | case 'boolean':
87 | return JSON.parse(value.toLowerCase());
88 | case 'array':
89 | const arrayValues = value.split(customField.options.delimiter);
90 | if (customField.options.typeSelector === 'object') {
91 | let arrayOfObjects = [];
92 | _.each(arrayValues, function (stringObs) {
93 | let object = {};
94 | let arrayOfKeyValues = stringObs.split(customField.options.arrayOfObjects.delimiter);
95 | _.each(arrayOfKeyValues, function (objectValue, index) {
96 | let keyValues = objectValue.split('=');
97 | let key = keyValues[0].trim();
98 | let v = keyValues[1].trim();
99 | object[key] = ProductImporter.parseBasicType(v, customField.options.arrayOfObjects[index]);
100 | })
101 | arrayOfObjects.push(object);
102 | });
103 | return arrayOfObjects;
104 | } else {
105 | const cleaned = _.map(arrayValues, function (arrayValue) {
106 | return ProductImporter.parseBasicType(arrayValue.trim(), customField.options.typeSelector);
107 | });
108 | return cleaned;
109 | }
110 | case 'object':
111 | const objectValues = value.split(customField.options.delimiter);
112 | let customObject = {};
113 | _.each(objectValues, function (objectValue) {
114 | let keyValues = objectValue.split('=');
115 | let key = keyValues[0].trim();
116 | let v = keyValues[1].trim();
117 | customObject[key] = ProductImporter.parseBasicType(v, customField.options.typeSelector);
118 | });
119 | return customObject;
120 | default:
121 | return value;
122 | }
123 | };
124 |
125 | ProductImporter.createTopLevelProduct = function (product) {
126 | check(product, [Object]);
127 | let baseProduct = product[0];
128 | let sameProduct = _.every(product, function (item) {
129 | const result = baseProduct.productTitle === item.productTitle;
130 | return result;
131 | });
132 | if (!sameProduct) {
133 | Logger.warn('One or more Products with productId ' + baseProduct.productId + ' have different product titles');
134 | }
135 | let maxPricedProduct = _.max(product, function (item) {
136 | return parseInt(item.price, 10);
137 | });
138 | let maxPrice = maxPricedProduct.price;
139 | let minPricedProduct = _.min(product, function (item) {
140 | return parseInt(item.price, 10);
141 | });
142 | let minPrice = minPricedProduct.price;
143 | let prod = {};
144 | prod.ancestors = [];
145 | prod.shopId = Reaction.getShopId();
146 | prod.title = baseProduct.productTitle;
147 | prod.vendor = baseProduct.vendor;
148 | prod.pageTitle = baseProduct.pageTitle;
149 | prod.handle = baseProduct.handle.toLowerCase().trim();
150 | prod.handle = prod.handle.replace(/\s/, '-');
151 | prod.isVisible = false;
152 | prod.description = baseProduct.description;
153 | prod.type = baseProduct.topProductType || 'simple';
154 | prod.price = {};
155 | prod.price.max = maxPrice;
156 | prod.price.min = minPrice;
157 | if (maxPrice > minPrice) {
158 | prod.price.range = minPrice + ' - ' + maxPrice;
159 | } else {
160 | prod.price.range = minPrice;
161 | }
162 |
163 | if (baseProduct.metafields) {
164 | let delimited = baseProduct.metafields.split('|');
165 | prod.metafields = [];
166 | _.each(delimited, function (objectValue) {
167 | let metafield = {};
168 | let keyValues = objectValue.split('=');
169 | let key = keyValues[0].trim();
170 | let v = keyValues[1].trim();
171 | metafield.key = key;
172 | metafield.value = v;
173 | prod.metafields.push(metafield);
174 | });
175 | }
176 | if (this.anyCustomFields('topProduct')) {
177 | let customFields = this.customFields('topProduct');
178 | _.each(customFields, function (customField) {
179 | let result = ProductImporter.parseByType(baseProduct[customField.csvColumnName], customField);
180 | prod[customField.productFieldName] = result;
181 | });
182 | }
183 | let existingProduct = this.existingProduct(prod, prod.type);
184 | if (existingProduct) {
185 | Logger.warn('Found product = ' + existingProduct._id);
186 | Logger.warn(existingProduct.vendor + ' ' + existingProduct.title + ' has already been added.');
187 | return existingProduct._id;
188 | }
189 | let reactionProductId = Products.insert(prod, {selector: {type: prod.type}});
190 | let hashtags = baseProduct.hashtags.split(',');
191 | _.each(hashtags, function (hashtag) {
192 | Meteor.call('products/updateProductTags', reactionProductId, hashtag.trim(), null);
193 | });
194 | Logger.info(prod.vendor + ' ' + prod.title + ' was successfully added to Products.');
195 | return reactionProductId;
196 | };
197 |
198 | ProductImporter.createMidLevelVariant = function (variant, ancestors) {
199 | check(variant, [Object]);
200 | check(ancestors, [String]);
201 | let baseVariant = variant[0];
202 | let sameVariant = _.every(variant, function (item) {
203 | return baseVariant.variantTitle === item.variantTitle;
204 | });
205 | if (!sameVariant) {
206 | Logger.warn('One or more Products with variantTitle ' + baseVariant.variantTitle + ' have different variant titles');
207 | }
208 | let inventory = _.reduce(variant, function (sum, item) {
209 | return sum + parseInt(item.qty, 10);
210 | }, 0);
211 | let prod = {};
212 | prod.ancestors = ancestors;
213 | prod.isVisible = false;
214 | prod.type = baseVariant.variantType || 'variant';
215 | prod.title = baseVariant.variantTitle;
216 | prod.price = baseVariant.price;
217 | prod.inventoryQuantity = inventory;
218 | prod.weight = parseInt(baseVariant.weight, 10);
219 | prod.shopId = Reaction.getShopId();
220 | prod.taxable = baseVariant.taxable.toLowerCase() === 'true';
221 | if (this.anyCustomFields('midVariant')) {
222 | let customFields = this.customFields('midVariant');
223 | _.each(customFields, function (customField) {
224 | let result = ProductImporter.parseByType(baseVariant[customField.csvColumnName], customField);
225 | prod[customField.productFieldName] = result;
226 | });
227 | }
228 | let existingVariant = this.existingProduct(prod, prod.type);
229 | if (existingVariant) {
230 | Logger.warn('Found product = ' + existingVariant._id);
231 | Logger.warn(existingVariant.title + ' has already been added.');
232 | return existingVariant._id;
233 | }
234 | let reactionVariantId = Products.insert(prod, {selector: {type: prod.type}});
235 | Logger.info(prod.title + ' was successfully added to Products as a variant.');
236 | return reactionVariantId;
237 | };
238 |
239 | ProductImporter.createVariant = function (variant, ancestors) {
240 | check(variant, Object);
241 | check(ancestors, [String]);
242 | let prod = {};
243 | prod.ancestors = ancestors;
244 | prod.isVisible = false;
245 | prod.type = variant.variantType || 'variant';
246 | prod.title = variant.title;
247 | prod.optionTitle = variant.optionTitle;
248 | prod.price = variant.price;
249 | prod.inventoryQuantity = parseInt(variant.qty, 10);
250 | prod.weight = parseInt(variant.weight, 10);
251 | prod.shopId = Reaction.getShopId();
252 | prod.taxable = variant.taxable.toLowerCase() === 'true';
253 | if (this.anyCustomFields('variant')) {
254 | let customFields = this.customFields('variant');
255 | _.each(customFields, function (customField) {
256 | let result = ProductImporter.parseByType(variant[customField.csvColumnName], customField);
257 | prod[customField.productFieldName] = result;
258 | });
259 | }
260 | let existingVariant = this.existingProduct(prod, prod.type);
261 | if (existingVariant) {
262 | Logger.warn('Found product = ' + existingVariant._id);
263 | Logger.warn(existingVariant.title + ' has already been added.');
264 | return existingVariant._id;
265 | }
266 | let reactionVariantId = Products.insert(prod, {selector: {type: prod.type}});
267 | Logger.info(prod.title + ' was successfully added to Products as a variant.');
268 | return reactionVariantId;
269 | };
270 |
--------------------------------------------------------------------------------