@cloudcommerce/app-correios 0.29.0 → 0.31.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,4 +1,4 @@
1
1
 
2
- > @cloudcommerce/app-correios@0.28.5 build /home/leo/code/ecomplus/cloud-commerce/packages/apps/correios
2
+ > @cloudcommerce/app-correios@0.30.0 build /home/leo/code/ecomplus/cloud-commerce/packages/apps/correios
3
3
  > bash ../../../scripts/build-lib.sh
4
4
 
package/lib/correios.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  import type { AppModuleBody } from '@cloudcommerce/types';
2
+ import '@cloudcommerce/firebase/lib/init';
2
3
  export declare const calculateShipping: (modBody: AppModuleBody) => Promise<any>;
package/lib/correios.js CHANGED
@@ -1,4 +1,5 @@
1
- import handleCalculateShipping from '../lib-mjs/calculate-correios.mjs';
1
+ import '@cloudcommerce/firebase/lib/init';
2
+ import handleCalculateShipping from '../lib-mjs/calculate-shipping.mjs';
2
3
 
3
4
  export const calculateShipping = async (modBody) => {
4
5
  return handleCalculateShipping(modBody);
@@ -1 +1 @@
1
- {"version":3,"file":"correios.js","sourceRoot":"","sources":["../src/correios.ts"],"names":[],"mappings":"AAEA,OAAO,uBAAuB,MAAM,mCAAmC,CAAC;AAExE,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,EAAE,OAAsB,EAAE,EAAE;IAChE,OAAO,uBAAuB,CAAC,OAAO,CAAC,CAAC;AAC1C,CAAC,CAAC"}
1
+ {"version":3,"file":"correios.js","sourceRoot":"","sources":["../src/correios.ts"],"names":[],"mappings":"AAEA,OAAO,kCAAkC,CAAC;AAC1C,OAAO,uBAAuB,MAAM,mCAAmC,CAAC;AAExE,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,EAAE,OAAsB,EAAE,EAAE;IAChE,OAAO,uBAAuB,CAAC,OAAO,CAAC,CAAC;AAC1C,CAAC,CAAC"}
@@ -0,0 +1,430 @@
1
+ import logger from 'firebase-functions/logger';
2
+ import config from '@cloudcommerce/firebase/lib/config';
3
+ import { getFirestore, Timestamp } from 'firebase-admin/firestore';
4
+ import {
5
+ getDocId,
6
+ parseZipCode,
7
+ findBaseWeight,
8
+ setCredentials,
9
+ dataToDoc,
10
+ docToData,
11
+ } from './utils/constants-parsers.mjs';
12
+ import calculateV2 from './correios-v2.mjs';
13
+ import { newCorreios } from './utils/correios-axios.mjs';
14
+
15
+ export default async ({ params, application }) => {
16
+ const configApp = {
17
+ ...application.data,
18
+ ...application.hidden_data,
19
+ };
20
+
21
+ const { metafields } = config.get();
22
+
23
+ setCredentials(configApp);
24
+
25
+ // setup basic required response object
26
+ const response = {
27
+ shipping_services: [],
28
+ };
29
+
30
+ if (configApp.free_shipping_from_value >= 0) {
31
+ response.free_shipping_from_value = configApp.free_shipping_from_value;
32
+ }
33
+ if (!params.to) {
34
+ // just a free shipping preview with no shipping address received
35
+ // respond only with free shipping option
36
+ return response;
37
+ }
38
+
39
+ const cepDestino = params.to ? params.to.zip.replace(/\D/g, '') : '';
40
+
41
+ let cepOrigem;
42
+ if (params.from) {
43
+ cepOrigem = params.from.zip.replace(/\D/g, '');
44
+ } else if (configApp.zip) {
45
+ cepOrigem = configApp.zip.replace(/\D/g, '');
46
+ }
47
+
48
+ if (!cepOrigem) {
49
+ // must have configured origin zip code to continue
50
+ return {
51
+ error: 'CALCULATE_ERR',
52
+ message: 'Zip code is unset on app hidden data (merchant must configure the app)',
53
+ };
54
+ }
55
+ if (!params.items) {
56
+ return {
57
+ error: 'CALCULATE_EMPTY_CART',
58
+ message: 'Cannot calculate shipping without cart items',
59
+ };
60
+ }
61
+
62
+ const checkZipCode = (rule) => {
63
+ // validate rule zip range
64
+ if (cepDestino && rule.zip_range) {
65
+ const { min, max } = rule.zip_range;
66
+ return Boolean((!min || cepDestino >= min) && (!max || cepDestino <= max));
67
+ }
68
+ return true;
69
+ };
70
+
71
+ // search for configured free shipping rule
72
+ if (Array.isArray(configApp.shipping_rules)) {
73
+ for (let i = 0; i < configApp.shipping_rules.length; i++) {
74
+ const rule = configApp.shipping_rules[i];
75
+ if (rule.free_shipping && checkZipCode(rule)) {
76
+ if (!rule.min_amount) {
77
+ response.free_shipping_from_value = 0;
78
+ break;
79
+ } else if (!(response.free_shipping_from_value <= rule.min_amount)) {
80
+ response.free_shipping_from_value = rule.min_amount;
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ // optinal predefined or configured service codes
87
+ let serviceCodes;
88
+ if (params.service_code) {
89
+ serviceCodes = [params.service_code];
90
+ } else if (configApp.services?.[0]?.service_code) {
91
+ serviceCodes = configApp.services.map((service) => service.service_code);
92
+ }
93
+
94
+ // optional params to Correios services
95
+ let vlDeclarado = 0;
96
+ // const servicosAdicionais = [];
97
+ if (params.subtotal && !configApp.no_declare_value) {
98
+ vlDeclarado = params.subtotal;
99
+ }
100
+
101
+ // calculate weight and pkg value from items list
102
+ const pkg = {
103
+ dimensions: {
104
+ width: {
105
+ value: 0,
106
+ unit: 'cm',
107
+ },
108
+ height: {
109
+ value: 0,
110
+ unit: 'cm',
111
+ },
112
+ length: {
113
+ value: 0,
114
+ unit: 'cm',
115
+ },
116
+ },
117
+ weight: {
118
+ value: 0,
119
+ unit: 'g',
120
+ },
121
+ };
122
+
123
+ let cubicWeight = 0;
124
+ let psObj = 0;
125
+
126
+ params.items.forEach(({
127
+ price,
128
+ quantity,
129
+ dimensions,
130
+ weight,
131
+ }) => {
132
+ if (!params.subtotal && !configApp.no_declare_value) {
133
+ vlDeclarado += price * quantity;
134
+ }
135
+ // sum physical weight
136
+ let weightValue;
137
+ if (weight && weight.value) {
138
+ switch (weight.unit) {
139
+ case 'kg':
140
+ weightValue = weight.value * 1000;
141
+ break;
142
+ case 'g':
143
+ weightValue = weight.value;
144
+ break;
145
+ case 'mg':
146
+ weightValue = weight.value / 1000;
147
+ break;
148
+ default:
149
+ weightValue = weight.value;
150
+ }
151
+ if (weightValue) {
152
+ pkg.weight.value += weightValue * quantity;
153
+ }
154
+ }
155
+
156
+ // sum total items dimensions to calculate cubic weight
157
+ if (dimensions) {
158
+ const sumDimensions = {};
159
+ Object.keys(dimensions).forEach((side) => {
160
+ const dimension = dimensions[side];
161
+ if (dimension && dimension.value) {
162
+ let dimensionValue;
163
+ switch (dimension.unit) {
164
+ case 'cm':
165
+ dimensionValue = dimension.value;
166
+ break;
167
+ case 'm':
168
+ dimensionValue = dimension.value * 100;
169
+ break;
170
+ case 'mm':
171
+ dimensionValue = dimension.value / 10;
172
+ break;
173
+ default:
174
+ dimensionValue = dimension.value;
175
+ }
176
+ // add/sum current side to final dimensions object
177
+ if (dimensionValue) {
178
+ sumDimensions[side] = sumDimensions[side]
179
+ ? sumDimensions[side] + dimensionValue
180
+ : dimensionValue;
181
+ }
182
+ }
183
+ });
184
+ // calculate cubic weight
185
+ // https://suporte.boxloja.pro/article/82-correios-calculo-frete
186
+ // (C x L x A) / 6.000
187
+ Object.keys(sumDimensions).forEach((side) => {
188
+ if (sumDimensions[side]) {
189
+ cubicWeight = cubicWeight > 0
190
+ ? cubicWeight * sumDimensions[side]
191
+ : sumDimensions[side];
192
+ pkg.dimensions[side].value += sumDimensions[side] * quantity;
193
+ }
194
+ });
195
+ if (cubicWeight > 0) {
196
+ cubicWeight /= 6; // representation in g
197
+ }
198
+ }
199
+ if (!configApp.free_no_weight_shipping || weightValue > 0) {
200
+ psObj += quantity * (cubicWeight < 5 || weightValue > cubicWeight
201
+ ? weightValue : cubicWeight);
202
+ }
203
+ });
204
+
205
+ let correios;
206
+ let correiosResult;
207
+ let docId;
208
+ let isFromDb = false;
209
+
210
+ try {
211
+ correios = await newCorreios();
212
+ if (!correios.$contract || !correios.$contract.nuContrato) {
213
+ throw new Error('Correios contract not found');
214
+ }
215
+ } catch (err) {
216
+ err.app = '[calculate Correios]';
217
+ logger.error(err);
218
+ const message = err.message || '[calculate Correios] Contract not found';
219
+ return {
220
+ error: 'CALCULATE_ERR',
221
+ message,
222
+ };
223
+ }
224
+
225
+ const correiosParams = {
226
+ cepOrigem,
227
+ cepDestino: parseZipCode(cepDestino),
228
+ psObjeto: findBaseWeight(psObj),
229
+ };
230
+
231
+ try {
232
+ const docParams = {
233
+ ...correiosParams,
234
+ nuContrato: correios.$contract.nuContrato,
235
+ serviceCodes,
236
+ };
237
+
238
+ docId = getDocId(docParams);
239
+ const docRef = getFirestore().doc(docId);
240
+ const docSnapshot = await docRef.get();
241
+ if (docSnapshot.exists) {
242
+ const docData = docSnapshot.data();
243
+ const expiresIn = 1000 * 60 * 60 * 24 * 30
244
+ * (metafields?.correiosResultsExpirationMonths || 3);
245
+
246
+ const now = Timestamp.now().toMillis();
247
+ if ((docData.t.toMillis() + expiresIn) > now) {
248
+ correiosResult = docToData(docData);
249
+ isFromDb = true;
250
+ } else {
251
+ docRef.delete();
252
+ }
253
+ }
254
+ } catch (err) {
255
+ logger.error(new Error(`[calculate Correios] Cannot get doc ID ${docId}`));
256
+ logger.error(err);
257
+ }
258
+
259
+ // fallback Calculate
260
+ if (!correiosResult) {
261
+ try {
262
+ const { data } = await calculateV2({
263
+ correiosParams,
264
+ serviceCodes,
265
+ correios,
266
+ });
267
+ correiosResult = data;
268
+
269
+ // save in database
270
+ if (docId) {
271
+ getFirestore()
272
+ .doc(docId)
273
+ .set({
274
+ ...dataToDoc(data),
275
+ t: Timestamp.fromDate(new Date()),
276
+ })
277
+ .catch(logger.error);
278
+ }
279
+ } catch (err) {
280
+ const message = err?.response?.data?.[0]?.txErro || err.message;
281
+ return {
282
+ error: 'CALCULATE_FAILED',
283
+ message,
284
+ };
285
+ }
286
+ }
287
+
288
+ correiosResult.forEach(({
289
+ coProduto,
290
+ pcProduto,
291
+ prazoEntrega,
292
+ txErro,
293
+ }) => {
294
+ if (txErro) {
295
+ logger.warn(`[calculate Correios] alert/error ${coProduto}`, {
296
+ pcProduto,
297
+ prazoEntrega,
298
+ txErro,
299
+ });
300
+ }
301
+ if (!pcProduto || !prazoEntrega) {
302
+ return;
303
+ }
304
+ // find respective configured service label
305
+ let serviceName;
306
+ switch (coProduto) {
307
+ case '04014':
308
+ case '03220':
309
+ case '03204':
310
+ serviceName = 'SEDEX';
311
+ break;
312
+ case '04510':
313
+ case '03298':
314
+ serviceName = 'PAC';
315
+ break;
316
+ default:
317
+ break;
318
+ }
319
+ let label = serviceName || `Correios ${coProduto}`;
320
+ if (Array.isArray(configApp.services)) {
321
+ for (let i = 0; i < configApp.services.length; i++) {
322
+ const service = configApp.services[i];
323
+ if (service && service.service_code === coProduto && service.label) {
324
+ label = service.label;
325
+ }
326
+ }
327
+ }
328
+
329
+ // parse to E-Com Plus shipping line object
330
+ const parseMoney = (str) => (Number(str.replace(',', '.') || 0));
331
+ let pcFinal = parseMoney(`${pcProduto}`);
332
+ let valorAvisoRecebimento;
333
+ let valorMaoPropria;
334
+ let taxDeclaredValue;
335
+
336
+ // https://api.correios.com.br/preco/v1/servicos-adicionais/03220
337
+ // https://www.correios.com.br/enviar/servicos-adicionais
338
+ if (params.own_hand) {
339
+ valorMaoPropria = configApp.own_hand_price || metafields.correiosOwnHandPrice || 8.75;
340
+ pcFinal += valorMaoPropria;
341
+ }
342
+
343
+ if (params.receipt) {
344
+ valorAvisoRecebimento = configApp.receipt_price || metafields.correiosReceiptPrice || 7.4;
345
+ pcFinal += valorAvisoRecebimento;
346
+ }
347
+
348
+ if (vlDeclarado) {
349
+ // pre check for maximum allowed declared value
350
+ if (vlDeclarado > 10000) {
351
+ vlDeclarado = 10000;
352
+ }
353
+ taxDeclaredValue = parseInt(vlDeclarado * 0.02 * 100, 10) / 100;
354
+ pcFinal += taxDeclaredValue || 0;
355
+ }
356
+
357
+ const shippingLine = {
358
+ from: {
359
+ ...params.from,
360
+ zip: cepOrigem,
361
+ },
362
+ to: params.to,
363
+ package: pkg,
364
+ price: parseMoney(pcProduto) || pcFinal,
365
+ declared_value: vlDeclarado,
366
+ declared_value_price: taxDeclaredValue > 0 ? taxDeclaredValue : 0,
367
+ own_hand: Boolean(params.own_hand),
368
+ own_hand_price: valorMaoPropria,
369
+ receipt: Boolean(params.receipt),
370
+ receipt_price: valorAvisoRecebimento,
371
+ discount: 0,
372
+ total_price: pcFinal,
373
+ delivery_time: {
374
+ days: Number(prazoEntrega),
375
+ working_days: true,
376
+ },
377
+ posting_deadline: {
378
+ days: 3,
379
+ ...configApp.posting_deadline,
380
+ },
381
+ flags: [`correios-${isFromDb ? 'off' : 'api'}`],
382
+ };
383
+
384
+ // search for discount by shipping rule
385
+ if (Array.isArray(configApp.shipping_rules)) {
386
+ for (let i = 0; i < configApp.shipping_rules.length; i++) {
387
+ const rule = configApp.shipping_rules[i];
388
+ if (
389
+ rule
390
+ && (!rule.service_code || rule.service_code === coProduto)
391
+ && checkZipCode(rule)
392
+ && !(rule.min_amount > params.subtotal)
393
+ ) {
394
+ // valid shipping rule
395
+ if (rule.free_shipping) {
396
+ shippingLine.discount += shippingLine.total_price;
397
+ shippingLine.total_price = 0;
398
+ break;
399
+ } else if (rule.discount) {
400
+ let discountValue = rule.discount.value;
401
+ if (rule.discount.percentage) {
402
+ discountValue *= (shippingLine.total_price / 100);
403
+ }
404
+ if (discountValue) {
405
+ shippingLine.discount += discountValue;
406
+ shippingLine.total_price -= discountValue;
407
+ if (shippingLine.total_price < 0) {
408
+ shippingLine.total_price = 0;
409
+ }
410
+ }
411
+ break;
412
+ }
413
+ }
414
+ }
415
+ }
416
+
417
+ // push shipping service object to response
418
+ response.shipping_services.push({
419
+ label,
420
+ carrier: 'Correios',
421
+ // https://informederendimentos.com/consulta/cnpj-correios/
422
+ carrier_doc_number: '34028316000103',
423
+ service_code: coProduto,
424
+ service_name: serviceName || label,
425
+ shipping_line: shippingLine,
426
+ });
427
+ });
428
+
429
+ return response;
430
+ };
@@ -0,0 +1,189 @@
1
+ import api from '@cloudcommerce/api';
2
+ import config from '@cloudcommerce/firebase/lib/config';
3
+ import { PubSub } from '@google-cloud/pubsub';
4
+ import { getFirestore, Timestamp } from 'firebase-admin/firestore';
5
+ import logger from 'firebase-functions/logger';
6
+ import calculateV2 from './correios-v2.mjs';
7
+ import { newCorreios } from './utils/correios-axios.mjs';
8
+ import {
9
+ weights,
10
+ zipRangeStep,
11
+ getDocId,
12
+ setCredentials,
13
+ dataToDoc,
14
+ } from './utils/constants-parsers.mjs';
15
+
16
+ const VERSION = 1;
17
+
18
+ const firstZipCode = 1000001;
19
+ const maxZipCode = 99999999;
20
+ const lastZipCode = maxZipCode - zipRangeStep + 2;
21
+
22
+ const getAppData = async () => {
23
+ const [application] = (await api.get(
24
+ `applications?app_id=${config.get().apps.correios.appId}&fields=hidden_data,data`,
25
+ )).data.result;
26
+
27
+ return {
28
+ ...application.data,
29
+ ...application.hidden_data,
30
+ };
31
+ };
32
+
33
+ const fillDb = async (state) => {
34
+ if (state?.nextZipCode && state.V !== VERSION) {
35
+ logger.warn(`Skip old dup call for (${state.nextZipCode})`);
36
+ return false;
37
+ }
38
+
39
+ const continueDbFill = async (retryZipCode) => {
40
+ let json;
41
+ let isRetry = false;
42
+ if (retryZipCode && (!state?.retries || state.retries < 3)) {
43
+ json = { ...state };
44
+ json.nextZipCode = retryZipCode;
45
+ json.retries = state?.retries ? state.retries + 1 : 1;
46
+ isRetry = true;
47
+ }
48
+ if (!isRetry) {
49
+ if (state?.nextZipCode && state.nextZipCode < lastZipCode) {
50
+ json = { ...state };
51
+ } else {
52
+ json = {};
53
+ }
54
+ }
55
+
56
+ json.V = VERSION;
57
+ await new PubSub()
58
+ .topic('continueDbFill')
59
+ .publishMessage({ json });
60
+ };
61
+
62
+ let configApp;
63
+
64
+ if (
65
+ !process.env.CORREIOS_POSTCARD
66
+ || !process.env.CORREIOS_USER
67
+ || !process.env.CORREIOS_ACCESS_CODE
68
+ ) {
69
+ logger.info('> Set credentials');
70
+ try {
71
+ configApp = await getAppData();
72
+ if (configApp) {
73
+ setCredentials(configApp);
74
+ }
75
+ } catch {
76
+ logger.warn('Skipped (404)');
77
+ return continueDbFill();
78
+ }
79
+ }
80
+
81
+ if (!state?.cepOrigem) {
82
+ logger.info('Starting');
83
+ if (!state) {
84
+ state = {};
85
+ }
86
+
87
+ state.serviceCodes = configApp?.services
88
+ && configApp?.services.length
89
+ && configApp?.services.map((service) => service.service_code);
90
+
91
+ state.cepOrigem = configApp?.zip?.replace(/\D/g, '');
92
+
93
+ if (!state.cepOrigem || !state.serviceCodes) {
94
+ logger.warn('Skipped (400)');
95
+ return continueDbFill();
96
+ }
97
+ }
98
+
99
+ let zipCode;
100
+ const calculateNextZip = async () => {
101
+ const { cepOrigem, serviceCodes } = state;
102
+
103
+ if (zipCode === lastZipCode) {
104
+ return false;
105
+ }
106
+ zipCode = state.nextZipCode || firstZipCode;
107
+ state.nextZipCode = Math.min(zipCode + zipRangeStep, lastZipCode);
108
+
109
+ const cepDestino = zipCode.toString().padStart(8, '0');
110
+ const _calculateParams = {
111
+ cepOrigem,
112
+ cepDestino,
113
+ };
114
+
115
+ const correios = await newCorreios();
116
+
117
+ if (!correios?.$contract?.nuContrato) {
118
+ logger.warn('Contract not found');
119
+ return false;
120
+ }
121
+
122
+ if (Number(cepDestino) > 2000000) {
123
+ const docSnapshot = await getFirestore()
124
+ .doc(getDocId({
125
+ ..._calculateParams,
126
+ psObjeto: 10000,
127
+ nuContrato: correios?.$contract?.nuContrato,
128
+ serviceCodes,
129
+ })).get();
130
+ if (docSnapshot.exists) {
131
+ const { t } = docSnapshot.data();
132
+ if (t && t.toMillis() > Date.now() - 1000 * 60 * 60 * 24 * 3) {
133
+ logger.warn(`> Skip probably dup call for (${cepDestino})`);
134
+ return false;
135
+ }
136
+ }
137
+ }
138
+ logger.info(`> ZipCode: ${cepDestino}}`);
139
+
140
+ const calculateMany = async (calculateWeights, listServiceCode) => {
141
+ return calculateWeights.map((psObjeto) => {
142
+ const promises = [];
143
+
144
+ const calculateParams = { ..._calculateParams, psObjeto };
145
+ promises.push(
146
+ calculateV2({
147
+ calculateParams,
148
+ correios,
149
+ serviceCodes: listServiceCode,
150
+ })
151
+ .then(async ({ data }) => {
152
+ const docId = getDocId({
153
+ ...calculateParams,
154
+ nuContrato: correios?.$contract?.nuContrato,
155
+ serviceCodes: listServiceCode,
156
+ });
157
+
158
+ await getFirestore().doc(docId)
159
+ .set({
160
+ ...dataToDoc(data),
161
+ t: Timestamp.fromDate(new Date()),
162
+ })
163
+ .catch(logger.error);
164
+ }),
165
+ );
166
+
167
+ return Promise.all(promises);
168
+ });
169
+ };
170
+
171
+ await Promise.all(calculateMany(weights.slice(0, 8), serviceCodes));
172
+ await Promise.all(calculateMany(weights.slice(8, 16), serviceCodes));
173
+ await Promise.allSettled(calculateMany(weights.slice(16), serviceCodes));
174
+ return true;
175
+ };
176
+
177
+ try {
178
+ let shouldNext = await calculateNextZip();
179
+ if (shouldNext !== false) shouldNext = await calculateNextZip();
180
+ if (shouldNext !== false) shouldNext = await calculateNextZip();
181
+ if (state?.retries) state.retries = 0;
182
+ return shouldNext !== false ? continueDbFill() : true;
183
+ } catch {
184
+ logger.info(`> May retry ${zipCode}`);
185
+ return continueDbFill(zipCode);
186
+ }
187
+ };
188
+
189
+ export default fillDb;