@cloudcommerce/app-melhor-envio 0.3.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.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +1 -0
  3. package/LICENSE.md +230 -0
  4. package/README.md +1 -0
  5. package/events.js +1 -0
  6. package/lib/functions-lib/database.d.ts +18 -0
  7. package/lib/functions-lib/database.js +115 -0
  8. package/lib/functions-lib/database.js.map +1 -0
  9. package/lib/functions-lib/events-to-melhor-envio.d.ts +7 -0
  10. package/lib/functions-lib/events-to-melhor-envio.js +112 -0
  11. package/lib/functions-lib/events-to-melhor-envio.js.map +1 -0
  12. package/lib/functions-lib/new-label.d.ts +42 -0
  13. package/lib/functions-lib/new-label.js +185 -0
  14. package/lib/functions-lib/new-label.js.map +1 -0
  15. package/lib/functions-lib/order-is-valid.d.ts +5 -0
  16. package/lib/functions-lib/order-is-valid.js +40 -0
  17. package/lib/functions-lib/order-is-valid.js.map +1 -0
  18. package/lib/functions-lib/tracking-codes.d.ts +2 -0
  19. package/lib/functions-lib/tracking-codes.js +164 -0
  20. package/lib/functions-lib/tracking-codes.js.map +1 -0
  21. package/lib/index.d.ts +1 -0
  22. package/lib/index.js +2 -0
  23. package/lib/index.js.map +1 -0
  24. package/lib/melhor-envio-events.d.ts +6 -0
  25. package/lib/melhor-envio-events.js +17 -0
  26. package/lib/melhor-envio-events.js.map +1 -0
  27. package/lib/melhor-envio.d.ts +2 -0
  28. package/lib/melhor-envio.js +6 -0
  29. package/lib/melhor-envio.js.map +1 -0
  30. package/lib-mjs/calculate-melhor-envio.mjs +341 -0
  31. package/lib-mjs/functions/client-melhor-envio.mjs +14 -0
  32. package/lib-mjs/functions/error-handling.mjs +62 -0
  33. package/lib-mjs/functions/new-shipment.mjs +119 -0
  34. package/package.json +36 -0
  35. package/src/functions-lib/database.ts +140 -0
  36. package/src/functions-lib/events-to-melhor-envio.ts +137 -0
  37. package/src/functions-lib/new-label.ts +214 -0
  38. package/src/functions-lib/order-is-valid.ts +51 -0
  39. package/src/functions-lib/tracking-codes.ts +191 -0
  40. package/src/index.ts +1 -0
  41. package/src/melhor-envio-events.ts +24 -0
  42. package/src/melhor-envio.ts +7 -0
  43. package/tsconfig.json +6 -0
@@ -0,0 +1,341 @@
1
+ import logger from 'firebase-functions/logger';
2
+ import api from '@cloudcommerce/api';
3
+ import { newShipment, matchService, sortServicesBy } from './functions/new-shipment.mjs';
4
+ import meClient from './functions/client-melhor-envio.mjs';
5
+ import errorHandling from './functions/error-handling.mjs';
6
+
7
+ export default async ({ params, application }) => {
8
+ const appData = {
9
+ ...application.data,
10
+ ...application.hidden_data,
11
+ };
12
+
13
+ if (!appData.access_token) {
14
+ // no Melhor Envio access_token
15
+ return {
16
+ status: 409,
17
+ error: 'CALCULATE_SHIPPING_DISABLED',
18
+ message: 'Melhor Envio Token is unset on app hidden data',
19
+ };
20
+ }
21
+ if (
22
+ appData.skip_no_weight_item
23
+ && params.items && params.items.find(({ weight }) => weight && !weight.value)
24
+ ) {
25
+ return {
26
+ status: 409,
27
+ error: 'CALCULATE_SHIPPING_SKIPPED',
28
+ message: 'Melhor Envio configured to skip no weight items',
29
+ };
30
+ }
31
+ // start mounting response body
32
+ // https://apx-mods.e-com.plus/api/v1/calculate_shipping/response_schema.json?store_id=100
33
+ const response = {
34
+ shipping_services: [],
35
+ };
36
+
37
+ let shippingRules;
38
+ if (Array.isArray(appData.shipping_rules) && appData.shipping_rules.length) {
39
+ shippingRules = appData.shipping_rules;
40
+ } else {
41
+ shippingRules = [];
42
+ }
43
+
44
+ const destinationZip = params.to ? params.to.zip.replace(/\D/g, '') : '';
45
+
46
+ const checkZipCode = (rule) => {
47
+ // validate rule zip range
48
+ if (destinationZip && rule.zip_range) {
49
+ const { min, max } = rule.zip_range;
50
+ return Boolean((!min || destinationZip >= min) && (!max || destinationZip <= max));
51
+ }
52
+ return true;
53
+ };
54
+
55
+ // search for configured free shipping rule
56
+ if (Array.isArray(shippingRules)) {
57
+ for (let i = 0; i < shippingRules.length; i++) {
58
+ const rule = shippingRules[i];
59
+ if (rule.free_shipping && checkZipCode(rule)) {
60
+ if (!rule.min_amount) {
61
+ response.free_shipping_from_value = 0;
62
+ break;
63
+ } else if (!(response.free_shipping_from_value <= rule.min_amount)) {
64
+ response.free_shipping_from_value = rule.min_amount;
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ // params object follows calculate shipping request schema:
71
+ // https://apx-mods.e-com.plus/api/v1/calculate_shipping/schema.json?store_id=100
72
+ if (!params.to) {
73
+ // respond only with free shipping option
74
+ return response;
75
+ }
76
+
77
+ if (params.items) {
78
+ const intZipCode = parseInt(params.to.zip.replace(/\D/g, ''), 10);
79
+ const token = appData.access_token;
80
+ const { sandbox } = appData;
81
+
82
+ if (!appData.merchant_address) {
83
+ // get merchant_address
84
+ const { data } = await meClient(token, sandbox).get('/');
85
+ const merchantAddress = data.address;
86
+
87
+ // update config.merchant_address
88
+ appData.merchant_address = merchantAddress;
89
+ // save merchant_address in hidden_data
90
+
91
+ const bodyUpdate = {
92
+ merchant_address: merchantAddress,
93
+ };
94
+ try {
95
+ await api.patch(`applications/${application._id}/hidden_data`, bodyUpdate);
96
+ } catch (err) {
97
+ logger.error('>>(App Melhor Envio): !<> Update merchant_address failed', err.message);
98
+ }
99
+ }
100
+
101
+ let schema;
102
+ try {
103
+ schema = newShipment(appData, params);
104
+ } catch (e) {
105
+ logger.error('>>(App Melhor Envio): NEW_SHIPMENT_PARSE_ERR', e);
106
+ return {
107
+ status: 400,
108
+ error: 'CALCULATE_ERR',
109
+ message: 'Unexpected Error Try Later',
110
+ };
111
+ }
112
+
113
+ // calculate the shipment
114
+ try {
115
+ const data = await meClient(token, sandbox).post('/shipment/calculate', schema);
116
+
117
+ let errorMsg;
118
+ data.forEach((service) => {
119
+ let isAvailable = true;
120
+ // check if service is not disabled
121
+ if (Array.isArray(appData.unavailable_for)) {
122
+ for (let i = 0; i < appData.unavailable_for.length; i++) {
123
+ if (appData.unavailable_for[i] && appData.unavailable_for[i].zip_range
124
+ && appData.unavailable_for[i].service_name
125
+ ) {
126
+ const unavailable = appData.unavailable_for[i];
127
+ if (intZipCode >= unavailable.zip_range.min
128
+ && intZipCode <= unavailable.zip_range.max
129
+ && matchService(unavailable, service.name)
130
+ ) {
131
+ isAvailable = false;
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ if (!service.error && isAvailable) {
138
+ // mounte response body
139
+ const { to } = params;
140
+ const from = {
141
+ zip: appData.merchant_address.postal_code,
142
+ street: appData.merchant_address.address,
143
+ number: parseInt(appData.merchant_address.number, 10),
144
+ };
145
+
146
+ const shippingLine = {
147
+ to,
148
+ from,
149
+ own_hand: service.additional_services.own_hand,
150
+ receipt: service.additional_services.receipt,
151
+ discount: 0,
152
+ total_price: parseFloat(service.price),
153
+ delivery_time: {
154
+ days: parseInt(service.delivery_time, 10),
155
+ working_days: true,
156
+ },
157
+ posting_deadline: {
158
+ days: 3,
159
+ ...appData.posting_deadline,
160
+ },
161
+ custom_fields: [
162
+ {
163
+ field: 'by_melhor_envio',
164
+ value: 'true',
165
+ },
166
+ ],
167
+ };
168
+
169
+ const servicePkg = (service.packages && service.packages[0])
170
+ || (service.volumes && service.volumes[0]);
171
+
172
+ if (servicePkg) {
173
+ shippingLine.package = {};
174
+ if (servicePkg.dimensions) {
175
+ shippingLine.package.dimensions = {
176
+ width: {
177
+ value: servicePkg.dimensions.width,
178
+ },
179
+ height: {
180
+ value: servicePkg.dimensions.height,
181
+ },
182
+ length: {
183
+ value: servicePkg.dimensions.length,
184
+ },
185
+ };
186
+ }
187
+ if (servicePkg.weight) {
188
+ shippingLine.package.weight = {
189
+ value: parseFloat(servicePkg.weight),
190
+ unit: 'kg',
191
+ };
192
+ }
193
+ }
194
+
195
+ if (appData.jadlog_agency) {
196
+ shippingLine.custom_fields.push({
197
+ field: 'jadlog_agency',
198
+ value: String(appData.jadlog_agency),
199
+ });
200
+ }
201
+
202
+ // check for default configured additional/discount price
203
+ if (appData.additional_price) {
204
+ if (appData.additional_price > 0) {
205
+ shippingLine.other_additionals = [{
206
+ tag: 'additional_price',
207
+ label: 'Adicional padrão',
208
+ price: appData.additional_price,
209
+ }];
210
+ } else {
211
+ // negative additional price to apply discount
212
+ shippingLine.discount -= appData.additional_price;
213
+ }
214
+ // update total price
215
+ shippingLine.total_price += appData.additional_price;
216
+ }
217
+
218
+ // search for discount by shipping rule
219
+ if (Array.isArray(shippingRules)) {
220
+ for (let i = 0; i < shippingRules.length; i++) {
221
+ const rule = shippingRules[i];
222
+ if (rule && matchService(rule, service.name)
223
+ && checkZipCode(rule) && !(rule.min_amount > params.subtotal)
224
+ ) {
225
+ // valid shipping rule
226
+ if (rule.free_shipping) {
227
+ shippingLine.discount += shippingLine.total_price;
228
+ shippingLine.total_price = 0;
229
+ break;
230
+ } else if (rule.discount && rule.service_name) {
231
+ let discountValue = rule.discount.value;
232
+ if (rule.discount.percentage) {
233
+ discountValue *= (shippingLine.total_price / 100);
234
+ }
235
+ shippingLine.discount += discountValue;
236
+ shippingLine.total_price -= discountValue;
237
+ if (shippingLine.total_price < 0) {
238
+ shippingLine.total_price = 0;
239
+ }
240
+ break;
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ let label = service.name;
247
+ if (appData.services && Array.isArray(appData.services) && appData.services.length) {
248
+ const serviceFound = appData.services.find((lookForService) => {
249
+ return lookForService && matchService(lookForService, label);
250
+ });
251
+ if (serviceFound && serviceFound.label) {
252
+ label = serviceFound.label;
253
+ }
254
+ }
255
+
256
+ response.shipping_services.push({
257
+ label,
258
+ carrier: service.company.name,
259
+ service_name: service.name,
260
+ service_code: `ME ${service.id}`,
261
+ icon: service.company.picture,
262
+ shipping_line: shippingLine,
263
+ });
264
+ } else {
265
+ errorMsg += `${service.name}, ${service.error} \n`;
266
+ }
267
+ });
268
+
269
+ // sort services?
270
+ if (appData.sort_services && Array.isArray(response.shipping_services)
271
+ && response.shipping_services.length) {
272
+ response.shipping_services.sort(sortServicesBy(appData.sort_services));
273
+ }
274
+
275
+ return (!Array.isArray(response.shipping_services) || !response.shipping_services.length)
276
+ && errorMsg ? {
277
+ status: 400,
278
+ error: 'CALCULATE_ERR_MSG',
279
+ message: errorMsg,
280
+ }
281
+ // success response with available shipping services
282
+ : response;
283
+ } catch (err) {
284
+ //
285
+ let message = 'Unexpected Error Try Later';
286
+ if (err.response && err.isAxiosError) {
287
+ const { data, status, config } = err.response;
288
+ let isAppError = true;
289
+ if (status >= 500) {
290
+ message = 'Melhor Envio offline no momento';
291
+ isAppError = false;
292
+ } else if (data) {
293
+ if (data.errors && typeof data.errors === 'object' && Object.keys(data.errors).length) {
294
+ const errorKeys = Object.keys(data.errors);
295
+ for (let i = 0; i < errorKeys.length; i++) {
296
+ const meError = data.errors[errorKeys[i]];
297
+ if (meError && meError.length) {
298
+ message = Array.isArray(meError) ? meError[0] : meError;
299
+ if (errorKeys[i].startsWith('to.')) {
300
+ // invalid merchant config on ME
301
+ // eg.: 'O campo to.postal code deve ter pelo menos 5 caracteres.'
302
+ isAppError = false;
303
+ break;
304
+ } else {
305
+ message += errorKeys[i];
306
+ }
307
+ }
308
+ }
309
+ } else if (data.error) {
310
+ message = `ME: ${data.error}`;
311
+ } else if (data.message) {
312
+ message = `ME: ${data.message}`;
313
+ }
314
+ }
315
+
316
+ if (isAppError) {
317
+ // debug unexpected error
318
+ logger.error('>>(App Melhor Envio): CalculateShippingErr:', JSON.stringify({
319
+ status,
320
+ data,
321
+ config,
322
+ }, null, 4));
323
+ }
324
+ } else {
325
+ errorHandling(err);
326
+ }
327
+
328
+ return {
329
+ status: 409,
330
+ error: 'CALCULATE_ERR',
331
+ message,
332
+ };
333
+ }
334
+ } else {
335
+ return {
336
+ status: 400,
337
+ error: 'CALCULATE_EMPTY_CART',
338
+ message: 'Cannot calculate shipping without cart items',
339
+ };
340
+ }
341
+ };
@@ -0,0 +1,14 @@
1
+ import axios from 'axios';
2
+
3
+ export default (accessToken, isSandbox) => {
4
+ const headers = {
5
+ Accept: 'application/json',
6
+ 'Content-Type': 'application/json',
7
+ Authorization: `Bearer ${accessToken}`,
8
+ };
9
+
10
+ return axios.create({
11
+ baseURL: `https://${isSandbox ? 'sandbox.' : ''}melhorenvio.com.br/api/v2/me`,
12
+ headers,
13
+ });
14
+ };
@@ -0,0 +1,62 @@
1
+ import logger from 'firebase-functions/logger';
2
+
3
+ const ignoreError = (response) => {
4
+ // check response status code
5
+ // should ignore some error responses
6
+ const { status, data } = response;
7
+ if (status >= 400 && status < 500) {
8
+ switch (status) {
9
+ case 403:
10
+ // ignore resource limits errors
11
+ return true;
12
+
13
+ case 404:
14
+ if (data && data.error_code !== 20) {
15
+ // resource ID not found ?
16
+ // ignore
17
+ return true;
18
+ }
19
+ break;
20
+ default:
21
+ return false;
22
+ }
23
+ }
24
+ // must debug
25
+ return false;
26
+ };
27
+
28
+ export default (err) => {
29
+ // axios error object
30
+ // https://github.com/axios/axios#handling-errors
31
+ if (!err.appAuthRemoved && !err.appErrorLog) {
32
+ // error not treated by App SDK
33
+ if (err.response) {
34
+ if (ignoreError(err.response)) {
35
+ // ignore client error
36
+ return;
37
+ }
38
+ err.responseJSON = JSON.stringify(err.response.data);
39
+ }
40
+
41
+ // debug unexpected response
42
+ logger.error('>>(App Melhor Envio) =>', err);
43
+ } else if (err.appErrorLog && !err.appErrorLogged) {
44
+ // cannot log to app hidden data
45
+ // debug app log error
46
+ const error = err.appErrorLog;
47
+ const { response, config } = error;
48
+
49
+ // handle error response
50
+ if (response) {
51
+ if (ignoreError(response)) {
52
+ return;
53
+ }
54
+ // debug unexpected response
55
+ error.configJSON = {
56
+ originalRequest: JSON.stringify(err.config),
57
+ logRequest: JSON.stringify(config),
58
+ };
59
+ logger.error('>>(App Melhor Envio) => ', error);
60
+ }
61
+ }
62
+ };
@@ -0,0 +1,119 @@
1
+ const convertDimensions = (dimension) => {
2
+ let dimensionValue = 0;
3
+ if (dimension && dimension.unit) {
4
+ switch (dimension.unit) {
5
+ case 'cm':
6
+ dimensionValue = dimension.value;
7
+ break;
8
+ case 'm':
9
+ dimensionValue = dimension.value * 100;
10
+ break;
11
+ case 'mm':
12
+ dimensionValue = dimension.value / 10;
13
+ break;
14
+ default:
15
+ break;
16
+ }
17
+ }
18
+ return dimensionValue;
19
+ };
20
+
21
+ const newShipment = (appConfig, params) => {
22
+ const calculate = {};
23
+ const { to, items } = params;
24
+
25
+ // https://docs.menv.io/?version=latest#9bbc2460-7786-4871-a0cc-2ae3cd54333e
26
+ // creates a new model for calculating freight in the Melhor Envio API.
27
+ calculate.from = appConfig.merchant_address;
28
+
29
+ calculate.to = {
30
+ postal_code: to.zip,
31
+ address: to.street,
32
+ number: to.number,
33
+ };
34
+
35
+ calculate.options = {
36
+ receipt: appConfig.receipt || false,
37
+ own_hand: appConfig.own_hand || false,
38
+ collect: false,
39
+ };
40
+
41
+ calculate.products = [];
42
+
43
+ items.forEach((item) => {
44
+ const { dimensions, weight, quantity } = item;
45
+ // sum physical weight
46
+ let physicalWeight = 0;
47
+ if (weight && weight.value) {
48
+ switch (weight.unit) {
49
+ case 'kg':
50
+ physicalWeight = weight.value;
51
+ break;
52
+ case 'g':
53
+ physicalWeight = weight.value / 1000;
54
+ break;
55
+ case 'mg':
56
+ physicalWeight = weight.value / 1000000;
57
+ break;
58
+ default:
59
+ break;
60
+ }
61
+ }
62
+
63
+ calculate.products.push({
64
+ id: item.product_id,
65
+ weight: physicalWeight,
66
+ width: convertDimensions(dimensions && dimensions.width),
67
+ height: convertDimensions(dimensions && dimensions.height),
68
+ length: convertDimensions(dimensions && dimensions.length),
69
+ quantity: quantity || 1,
70
+ insurance_value: item.final_price || item.price,
71
+ });
72
+ });
73
+
74
+ return calculate;
75
+ };
76
+
77
+ const matchService = (service, name) => {
78
+ const fields = ['service_name', 'service_code'];
79
+ for (let i = 0; i < fields.length; i++) {
80
+ if (service[fields[i]]) {
81
+ return service[fields[i]].trim().toUpperCase() === name.toUpperCase();
82
+ }
83
+ }
84
+ return true;
85
+ };
86
+
87
+ const sortServicesBy = (by) => {
88
+ switch (by) {
89
+ case 'Maior preço':
90
+ return function sort(a, b) {
91
+ return a.shipping_line.total_price < b.shipping_line.total_price;
92
+ };
93
+ case 'Menor preço':
94
+ return function sort(a, b) {
95
+ return a.shipping_line.total_price > b.shipping_line.total_price;
96
+ };
97
+ case 'Maior prazo de entrega':
98
+ return function sort(a, b) {
99
+ return a.shipping_line.delivery_time.days < b.shipping_line.delivery_time.days;
100
+ };
101
+ case 'Menor prazo de entrega':
102
+ return function sort(a, b) {
103
+ return a.shipping_line.delivery_time.days > b.shipping_line.delivery_time.days;
104
+ };
105
+ default:
106
+ break;
107
+ }
108
+
109
+ // default
110
+ return function sort(a, b) {
111
+ return a.shipping_line.total_price > b.shipping_line.total_price;
112
+ };
113
+ };
114
+
115
+ export {
116
+ newShipment,
117
+ matchService,
118
+ sortServicesBy,
119
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@cloudcommerce/app-melhor-envio",
3
+ "type": "module",
4
+ "version": "0.3.0",
5
+ "description": "E-Com Plus Cloud Commerce app to integrate Melhor Envio",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./events": "./lib/melhor-envio-events.js"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/ecomplus/cloud-commerce.git",
14
+ "directory": "packages/apps/melhor-envio"
15
+ },
16
+ "author": "E-Com Club Softwares para E-commerce <ti@e-com.club>",
17
+ "license": "Apache 2.0 with Commons Clause",
18
+ "bugs": {
19
+ "url": "https://github.com/ecomplus/cloud-commerce/issues"
20
+ },
21
+ "homepage": "https://github.com/ecomplus/cloud-commerce/tree/main/packages/apps/melhor-envio#readme",
22
+ "dependencies": {
23
+ "axios": "^1.2.5",
24
+ "firebase-admin": "^11.5.0",
25
+ "firebase-functions": "^4.2.0",
26
+ "@cloudcommerce/firebase": "0.3.0",
27
+ "@cloudcommerce/api": "0.3.0"
28
+ },
29
+ "devDependencies": {
30
+ "@firebase/app-types": "^0.9.0",
31
+ "@cloudcommerce/types": "0.3.0"
32
+ },
33
+ "scripts": {
34
+ "build": "sh ../../../scripts/build-lib.sh"
35
+ }
36
+ }