@cloudcommerce/cli 2.43.0 → 2.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,9 @@
1
+ import { extname } from 'node:path';
1
2
  import {
2
3
  argv, fs, echo, sleep,
3
4
  } from 'zx';
4
5
  import { XMLParser } from 'fast-xml-parser';
6
+ import { parse } from 'csv-parse/sync';
5
7
  import { imageSize } from 'image-size';
6
8
  import api from '@cloudcommerce/api';
7
9
 
@@ -14,13 +16,24 @@ const slugify = (name) => {
14
16
  .replace(/[^a-z0-9_-]/ig, '');
15
17
  };
16
18
  const uploadPicture = async (downloadUrl, authHeaders) => {
17
- const downloadRes = await fetch(downloadUrl);
19
+ const downloadRes = await fetch(downloadUrl, {
20
+ method: 'GET',
21
+ redirect: 'follow',
22
+ headers: {
23
+ 'User-Agent': 'Mozilla/5.0 (compatible; E-commerce Bot/1.0)',
24
+ },
25
+ });
18
26
  if (!downloadRes.ok) {
19
- throw new Error(`Failed downloading ${downloadUrl} with status ${downloadRes.status}`);
27
+ const msg = `Failed downloading ${downloadUrl} with status ${downloadRes.status}`;
28
+ const err = new Error(msg);
29
+ err.statusCode = downloadRes.status || 0;
30
+ throw err;
20
31
  }
21
32
  const contentType = downloadRes.headers.get('content-type');
22
33
  if (!contentType) {
23
- throw new Error(`No mime type returned for ${downloadUrl}`);
34
+ const err = new Error(`No mime type returned for ${downloadUrl}`);
35
+ err.statusCode = 0;
36
+ throw err;
24
37
  }
25
38
  const imageBuffer = await downloadRes.arrayBuffer();
26
39
  const imageBlob = new Blob([imageBuffer], { type: contentType });
@@ -72,29 +85,83 @@ const uploadPicture = async (downloadUrl, authHeaders) => {
72
85
  }
73
86
  return picture;
74
87
  };
88
+ const mapCSVToFeed = (item) => {
89
+ const basePrice = parseFloat(item['preco-cheio']?.replace(',', '.')) || 0;
90
+ const salePrice = parseFloat(item['preco-promocional']?.replace(',', '.')) || 0;
91
+ const isAvailable = item.ativo === 'S';
92
+ const quantity = Number(item['estoque-quantidade']) || 0;
93
+ return {
94
+ 'g:id': item.sku,
95
+ 'g:title': item.nome,
96
+ 'g:description': item['descricao-completa'] || item['seo-tag-description'],
97
+ 'g:image_link': item['imagem-1'],
98
+ 'g:condition': item.usado === 'S' ? 'used' : 'new',
99
+ 'g:shipping_weight': item['peso-em-kg']
100
+ ? `${item['peso-em-kg']?.replace(',', '.')} kg`
101
+ : undefined,
102
+ 'g:additional_image_link': [
103
+ item['imagem-2'],
104
+ item['imagem-3'],
105
+ item['imagem-4'],
106
+ item['imagem-5'],
107
+ ].filter(Boolean),
108
+ 'g:product_type': item['categoria-nome-nivel-1'],
109
+ 'g:availability': (isAvailable && quantity > 0)
110
+ ? 'in stock'
111
+ : 'out of stock',
112
+ 'g:price': `${(basePrice || salePrice)} BRL`,
113
+ 'g:sale_price': (salePrice > 0 && salePrice < basePrice)
114
+ ? `${salePrice} BRL`
115
+ : undefined,
116
+ 'g:brand': item.marca,
117
+ 'g:gtin': item.gtin,
118
+ 'g:mpn': item.mpn,
119
+ 'available': isAvailable,
120
+ 'visible': isAvailable,
121
+ 'quantity': quantity,
122
+ };
123
+ };
75
124
  const importFeed = async () => {
125
+ const {
126
+ ECOM_STORE_ID, ECOM_AUTHENTICATION_ID, ECOM_API_KEY, ECOM_ACCESS_TOKEN,
127
+ } = process.env;
76
128
  const feedFilepath = typeof argv.feed === 'string' ? argv.feed : '';
77
129
  if (!feedFilepath) {
78
130
  await echo`You must specify XML file to import with --feed= argument`;
79
131
  return process.exit(1);
80
132
  }
81
- const parser = new XMLParser({
82
- ignoreAttributes: false,
83
- attributeNamePrefix: '',
84
- });
85
- const json = parser.parse(fs.readFileSync(argv.feed, 'utf8'));
86
- const items = json.rss?.channel?.item?.filter?.((item) => {
87
- return item?.['g:id'] && item['g:title'];
88
- });
133
+ const fileExtension = extname(feedFilepath).toLowerCase();
134
+ await echo`Importing ${fileExtension} file at ${feedFilepath}`;
135
+ await echo`Store ${ECOM_STORE_ID} with ${ECOM_AUTHENTICATION_ID}`;
136
+ let items = [];
137
+ if (fileExtension === '.xml') {
138
+ const parser = new XMLParser({
139
+ ignoreAttributes: false,
140
+ attributeNamePrefix: '',
141
+ });
142
+ const json = parser.parse(fs.readFileSync(argv.feed, 'utf8'));
143
+ items = json.rss?.channel?.item?.filter?.((item) => {
144
+ return item?.['g:id'] && item['g:title'];
145
+ });
146
+ } else if (fileExtension === '.tsv') {
147
+ const csvContent = fs.readFileSync(feedFilepath, 'utf8');
148
+ const csvRecords = parse(csvContent, {
149
+ columns: true,
150
+ skip_empty_lines: true,
151
+ delimiter: '\t',
152
+ });
153
+ items = csvRecords
154
+ .filter((record) => record.sku && record.nome)
155
+ .map(mapCSVToFeed);
156
+ }
89
157
  if (!items?.[0] || typeof items?.[0] !== 'object') {
90
158
  await echo`The XML file does not appear to be a valid RSS 2.0 product feed`;
91
159
  return process.exit(1);
92
160
  }
93
161
  const ecomAuthHeaders = {
94
- 'X-Store-ID': process.env.ECOM_STORE_ID,
95
- 'X-My-ID': process.env.ECOM_AUTHENTICATION_ID,
162
+ 'X-Store-ID': ECOM_STORE_ID,
163
+ 'X-My-ID': ECOM_AUTHENTICATION_ID,
96
164
  };
97
- const { ECOM_ACCESS_TOKEN, ECOM_API_KEY } = process.env;
98
165
  if (ECOM_ACCESS_TOKEN) {
99
166
  ecomAuthHeaders['X-Access-Token'] = ECOM_ACCESS_TOKEN;
100
167
  } else {
@@ -205,7 +272,11 @@ const importFeed = async () => {
205
272
  }
206
273
  }
207
274
  const setStockAndPrices = (productOrVatiation, item) => {
208
- productOrVatiation.quantity = item['g:availability'] === 'in stock' ? 100 : 0;
275
+ if (typeof item.quantity === 'number') {
276
+ productOrVatiation.quantity = item.quantity;
277
+ } else {
278
+ productOrVatiation.quantity = item['g:availability'] === 'in stock' ? 100 : 0;
279
+ }
209
280
  const price = parseFloat(item['g:price'] || item['g:sale_price'] || '');
210
281
  if (price) {
211
282
  const salePrice = parseFloat(item['g:sale_price'] || '');
@@ -272,13 +343,20 @@ const importFeed = async () => {
272
343
  await echo`\n${new Date().toISOString()}`;
273
344
  await echo`${(productItems.length - i)} products to import\n`;
274
345
  await echo`SKU: ${sku}`;
346
+ let slug = item['g:link']
347
+ ? new URL(item['g:link']).pathname.substring(1).toLowerCase()
348
+ : slugify(name);
349
+ if (!/[a-z0-9]/.test(slug.charAt(0))) {
350
+ slug = `r${slug}`;
351
+ }
275
352
  const product = {
276
353
  sku,
277
354
  name,
278
- slug: item['g:link']
279
- ? new URL(item['g:link']).pathname.substring(1).toLowerCase()
280
- : slugify(name),
355
+ slug,
281
356
  condition: item['g:condition'],
357
+ available: item.available,
358
+ visible: item.visible,
359
+ quantity: item.quantity,
282
360
  };
283
361
  const description = item['g:description']?.trim();
284
362
  if (description && description !== name) {
@@ -340,7 +418,8 @@ const importFeed = async () => {
340
418
  // eslint-disable-next-line no-console
341
419
  console.error(err);
342
420
  if (err.statusCode < 500) {
343
- throw err;
421
+ retries = 4;
422
+ break;
344
423
  }
345
424
  retries += 1;
346
425
  await sleep(4000);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cloudcommerce/cli",
3
3
  "type": "module",
4
- "version": "2.43.0",
4
+ "version": "2.44.0",
5
5
  "description": "e-com.plus Cloud Commerce CLI tools",
6
6
  "bin": {
7
7
  "cloudcommerce": "./bin/run.mjs"
@@ -31,6 +31,7 @@
31
31
  "homepage": "https://github.com/ecomplus/cloud-commerce/tree/main/packages/cli#readme",
32
32
  "dependencies": {
33
33
  "@fastify/deepmerge": "^3.1.0",
34
+ "csv-parse": "^5.6.0",
34
35
  "dotenv": "^16.5.0",
35
36
  "fast-xml-parser": "^5.2.3",
36
37
  "image-size": "^2.0.2",
@@ -38,7 +39,7 @@
38
39
  "md5": "^2.3.0",
39
40
  "typescript": "~5.8.3",
40
41
  "zx": "^8.5.4",
41
- "@cloudcommerce/api": "2.43.0"
42
+ "@cloudcommerce/api": "2.44.0"
42
43
  },
43
44
  "scripts": {
44
45
  "build": "bash ../../scripts/build-lib.sh"