@cloudcommerce/app-vindi 2.49.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 E-Com Club Softwares para E-commerce <ti@e-com.club>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # `@cloudcommerce/app-vindi`
package/events.js ADDED
@@ -0,0 +1 @@
1
+ export * from './lib/vindi-events.js';
package/lib/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './vindi';
package/lib/index.js ADDED
@@ -0,0 +1,3 @@
1
+ // eslint-disable-next-line import/prefer-default-export
2
+ export * from './vindi.js';
3
+ // # sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,cAAc,SAAS,CAAC"}
@@ -0,0 +1,5 @@
1
+ import '@cloudcommerce/firebase/lib/init';
2
+ import * as functions from 'firebase-functions/v1';
3
+ export declare const vindi: {
4
+ webhook: functions.HttpsFunction;
5
+ };
@@ -0,0 +1,21 @@
1
+ /* eslint-disable import/prefer-default-export */
2
+ import '@cloudcommerce/firebase/lib/init';
3
+ import * as functions from 'firebase-functions/v1';
4
+ import config from '@cloudcommerce/firebase/lib/config';
5
+ import handleVindiWebhook from '../lib-mjs/vindi-webhook.mjs';
6
+
7
+ const { httpsFunctionOptions } = config.get();
8
+
9
+ export const vindi = {
10
+ webhook: functions
11
+ .region(httpsFunctionOptions.region)
12
+ .runWith(httpsFunctionOptions)
13
+ .https.onRequest((req, res) => {
14
+ if (req.method !== 'POST') {
15
+ res.sendStatus(405);
16
+ } else {
17
+ handleVindiWebhook(req, res);
18
+ }
19
+ }),
20
+ };
21
+ // # sourceMappingURL=vindi-events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vindi-events.js","sourceRoot":"","sources":["../src/vindi-events.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,OAAO,kCAAkC,CAAC;AAC1C,OAAO,KAAK,SAAS,MAAM,uBAAuB,CAAC;AACnD,OAAO,MAAM,MAAM,oCAAoC,CAAC;AACxD,OAAO,kBAAkB,MAAM,8BAA8B,CAAC;AAE9D,MAAM,EAAE,oBAAoB,EAAE,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC;AAE9C,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,OAAO,EAAE,SAAS;SACf,MAAM,CAAC,oBAAoB,CAAC,MAAM,CAAC;SACnC,OAAO,CAAC,oBAAoB,CAAC;SAC7B,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAC5B,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC1B,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QACtB,CAAC;aAAM,CAAC;YACN,kBAAkB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC,CAAC;CACL,CAAC"}
package/lib/vindi.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import '@cloudcommerce/firebase/lib/init';
2
+ import type { AppModuleBody } from '@cloudcommerce/types';
3
+ export declare const listPayments: (modBody: AppModuleBody) => Promise<any>;
4
+ export declare const createTransaction: (modBody: AppModuleBody) => Promise<any>;
package/lib/vindi.js ADDED
@@ -0,0 +1,12 @@
1
+ import '@cloudcommerce/firebase/lib/init';
2
+ import handleListPayments from '../lib-mjs/vindi-list-payments.mjs';
3
+ import handleCreateTransaction from '../lib-mjs/vindi-create-transaction.mjs';
4
+
5
+ export const listPayments = async (modBody) => {
6
+ return handleListPayments(modBody);
7
+ };
8
+
9
+ export const createTransaction = async (modBody) => {
10
+ return handleCreateTransaction(modBody);
11
+ };
12
+ // # sourceMappingURL=vindi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vindi.js","sourceRoot":"","sources":["../src/vindi.ts"],"names":[],"mappings":"AAAA,OAAO,kCAAkC,CAAC;AAE1C,OAAO,kBAAkB,MAAM,oCAAoC,CAAC;AACpE,OAAO,uBAAuB,MAAM,yCAAyC,CAAC;AAE9E,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAAE,OAAsB,EAAE,EAAE;IAC3D,OAAO,kBAAkB,CAAC,OAAO,CAAC,CAAC;AACrC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,EAAE,OAAsB,EAAE,EAAE;IAChE,OAAO,uBAAuB,CAAC,OAAO,CAAC,CAAC;AAC1C,CAAC,CAAC"}
@@ -0,0 +1,74 @@
1
+ import axios from 'axios';
2
+
3
+ export const addInstallments = (
4
+ amount,
5
+ installments,
6
+ gateway = {},
7
+ response = null,
8
+ ) => {
9
+ let maxInterestFree = !(installments.interest_free_min_amount > amount.total)
10
+ ? installments.max_interest_free : 0;
11
+ const maxInstallments = installments.max_number && maxInterestFree
12
+ ? Math.max(installments.max_number, maxInterestFree)
13
+ : installments.max_number || maxInterestFree;
14
+ if (maxInstallments > 1) {
15
+ // default installments option
16
+ if (!installments.monthly_interest) {
17
+ maxInterestFree = maxInstallments;
18
+ }
19
+ const minInstallment = installments.min_installment || 5;
20
+ if (response) {
21
+ response.installments_option = {
22
+ min_installment: minInstallment,
23
+ max_number: maxInterestFree || installments.max_number,
24
+ monthly_interest: maxInterestFree ? 0 : installments.monthly_interest,
25
+ };
26
+ }
27
+ // list installment options
28
+ gateway.installment_options = [];
29
+ for (let number = 2; number <= maxInstallments; number++) {
30
+ const tax = !(maxInterestFree >= number);
31
+ let interest;
32
+ if (tax) {
33
+ interest = installments.monthly_interest / 100;
34
+ }
35
+ const value = !tax ? amount.total / number
36
+ // https://pt.wikipedia.org/wiki/Tabela_Price
37
+ : amount.total * (interest / (1 - (1 + interest) ** -number));
38
+ if (value >= minInstallment) {
39
+ gateway.installment_options.push({
40
+ number,
41
+ value,
42
+ tax,
43
+ });
44
+ }
45
+ }
46
+ }
47
+ return { response, gateway };
48
+ };
49
+
50
+ export const parseVindiStatus = (vindiChargeStatus) => {
51
+ switch (vindiChargeStatus) {
52
+ case 'pending':
53
+ case 'paid':
54
+ return vindiChargeStatus;
55
+ case 'canceled':
56
+ return 'voided';
57
+ case 'processing':
58
+ return 'under_analysis';
59
+ case 'fraud_review':
60
+ return 'authorized';
61
+ default:
62
+ }
63
+ return 'unknown';
64
+ };
65
+
66
+ export const createVindiAxios = (vindiApiKey, isSandbox) => {
67
+ // https://vindi.github.io/api-docs/dist/
68
+ return axios.create({
69
+ baseURL: `https://${(isSandbox ? 'sandbox-' : '')}app.vindi.com.br/api/v1/`,
70
+ headers: {
71
+ Authorization: 'Basic ' + Buffer.from(`${vindiApiKey}:`).toString('base64'),
72
+ },
73
+ });
74
+ };
@@ -0,0 +1,282 @@
1
+ import $config, { logger } from '@cloudcommerce/firebase/lib/config';
2
+ import updateAppData from '@cloudcommerce/firebase/lib/helpers/update-app-data';
3
+ import {
4
+ addInstallments,
5
+ createVindiAxios,
6
+ parseVindiStatus,
7
+ } from './util/vindi-utils.mjs';
8
+
9
+ export default async (modBody) => {
10
+ const { application, params } = modBody;
11
+ const appData = {
12
+ ...application.data,
13
+ ...application.hidden_data,
14
+ };
15
+ if (appData.vindi_api_key) {
16
+ process.env.VINDI_API_KEY = appData.vindi_api_key;
17
+ }
18
+ const { VINDI_API_KEY } = process.env;
19
+ if (!VINDI_API_KEY) {
20
+ return {
21
+ error: 'NO_VINDI_KEYS',
22
+ message: 'Chave de API e/ou criptografia não configurada (lojista deve configurar o aplicativo)',
23
+ };
24
+ }
25
+ const vindiAxios = createVindiAxios(VINDI_API_KEY, appData.vindi_sandbox);
26
+ const { storeId } = $config.get();
27
+
28
+ // https://apx-mods.e-com.plus/api/v1/create_transaction/schema.json?store_id=100
29
+ const orderId = params.order_id;
30
+ const {
31
+ amount, buyer, to, items,
32
+ } = params;
33
+ // https://apx-mods.e-com.plus/api/v1/create_transaction/response_schema.json?store_id=100
34
+ const transaction = {
35
+ amount: amount.total,
36
+ };
37
+
38
+ // must always create Vindi customer before
39
+ const vindiMetadata = {
40
+ order_number: params.order_number,
41
+ store_id: storeId,
42
+ order_id: orderId,
43
+ order_type: params.type,
44
+ };
45
+ const vindiCustomer = {
46
+ name: buyer.fullname,
47
+ email: buyer.email,
48
+ registry_code: buyer.doc_number,
49
+ code: `${buyer.customer_id}:${Date.now()}`,
50
+ phones: [{
51
+ phone_type: !buyer.phone.type || buyer.phone.type === 'personal' ? 'mobile' : 'landline',
52
+ number: `${(buyer.phone.country_code || '55')}${buyer.phone.number}`,
53
+ }],
54
+ notes: `E-Com Plus => Pedido #${params.order_number} em ${params.domain}`,
55
+ metadata: vindiMetadata,
56
+ };
57
+ const parseAddress = (_to) => ({
58
+ street: _to.street,
59
+ number: String(_to.number) || 'S/N',
60
+ additional_details: _to.complement,
61
+ zipcode: _to.zip,
62
+ neighborhood: _to.borough,
63
+ city: _to.city,
64
+ state: _to.province_code || _to.province,
65
+ country: _to.country_code || 'BR',
66
+ });
67
+ if (to && to.street) {
68
+ vindiCustomer.address = parseAddress(to);
69
+ } else if (params.billing_address) {
70
+ vindiCustomer.address = parseAddress(params.billing_address);
71
+ }
72
+
73
+ // strart mounting Vindi bill object
74
+ const vindiBill = {
75
+ code: String(params.order_number),
76
+ metadata: vindiMetadata,
77
+ };
78
+
79
+ let finalAmount = Math.floor(amount.total * 100) / 100;
80
+ if (params.payment_method.code === 'credit_card') {
81
+ let installmentsNumber = params.installments_number;
82
+ if (installmentsNumber > 1) {
83
+ if (appData.installments) {
84
+ // list all installment options
85
+ const { gateway } = addInstallments(amount, appData.installments);
86
+ const installmentOption = gateway.installment_options
87
+ && gateway.installment_options.find(({ number }) => number === installmentsNumber);
88
+ if (installmentOption) {
89
+ transaction.installments = installmentOption;
90
+ transaction.installments.total = Math.round(
91
+ installmentOption.number * installmentOption.value * 100,
92
+ ) / 100;
93
+ finalAmount = transaction.installments.total;
94
+ } else {
95
+ installmentsNumber = 1;
96
+ }
97
+ }
98
+ }
99
+ vindiBill.payment_method_code = 'credit_card';
100
+ vindiBill.installments = installmentsNumber;
101
+ } else {
102
+ // banking billet
103
+ vindiBill.payment_method_code = appData.banking_billet?.is_yapay
104
+ ? 'bank_slip_yapay' : 'bank_slip';
105
+ }
106
+
107
+ try {
108
+ const { data: vindiCustomerRes } = await vindiAxios({
109
+ url: '/customers',
110
+ method: 'post',
111
+ timeout: 12000,
112
+ data: vindiCustomer,
113
+ });
114
+ vindiBill.customer_id = vindiCustomerRes.customer?.id || vindiCustomerRes.id;
115
+
116
+ let vindiProductId = appData.vindi_product_id;
117
+ if (!vindiProductId) {
118
+ // must explicitly create a product before bill
119
+ const { data: vindiProductRes } = await vindiAxios({
120
+ url: '/products',
121
+ method: 'post',
122
+ timeout: 12000,
123
+ data: {
124
+ name: `Pedido na loja ${params.domain}`,
125
+ code: `ecomplus-${Date.now()}`,
126
+ status: 'active',
127
+ description: 'Produto pré-definido para pedidos através da plataforma E-Com Plus',
128
+ pricing_schema: {
129
+ price: 100,
130
+ schema_type: 'flat',
131
+ },
132
+ },
133
+ });
134
+ vindiProductId = vindiProductRes.product?.id || vindiProductRes.id;
135
+ await updateAppData(application, {
136
+ vindi_product_id: vindiProductId,
137
+ }, {
138
+ isHiddenData: true,
139
+ canSendPubSub: false,
140
+ });
141
+ }
142
+
143
+ if (params.credit_card && params.credit_card.hash) {
144
+ vindiBill.payment_profile = {
145
+ payment_method_code: vindiBill.payment_method_code,
146
+ allow_as_fallback: true,
147
+ gateway_token: params.credit_card.hash,
148
+ };
149
+ if (params.payer && params.payer.doc_number) {
150
+ vindiBill.payment_profile.registry_code = params.payer.doc_number;
151
+ }
152
+ }
153
+
154
+ if (params.type === 'recurrence') {
155
+ // async handle plan subscription
156
+ // must register all products and a plan on Vindi
157
+ // check products metadata to prevent duplication
158
+ return {
159
+ data: {
160
+ id: 0,
161
+ charges: [{
162
+ id: 0,
163
+ status: 'pending',
164
+ }],
165
+ },
166
+ };
167
+ }
168
+
169
+ /*
170
+ "Apesar do bill_item suportar um esquema de precificação (pricing_schema)
171
+ com quantidade (quantity), recomendamos utilizar apenas o parâmetro
172
+ amount para evitar complexidade desnecessária no desenvolvimento.
173
+ Se pricing_schema, quantity e amount forem informados ao mesmo tempo,
174
+ garanta que todos sejam mutuamente válidos"
175
+ */
176
+ // create a product for current order
177
+ let description = '';
178
+ if (items.length === 1) {
179
+ description = `${items[0].quantity}x ${items[0].sku} - ${items[0].name}`;
180
+ } else {
181
+ items.forEach(({ quantity, sku }) => {
182
+ description += `${quantity}x ${sku}; `;
183
+ });
184
+ }
185
+ if (description.length > 255) {
186
+ description = description.substring(0, 250) + ' ...';
187
+ }
188
+ vindiBill.bill_items = [{
189
+ product_id: vindiProductId,
190
+ amount: finalAmount,
191
+ description,
192
+ }];
193
+ // create Vindi single bill
194
+ const { data: vindiBillRes } = await vindiAxios({
195
+ url: '/bills',
196
+ method: 'post',
197
+ data: vindiBill,
198
+ });
199
+
200
+ const createdBill = vindiBillRes.bill || vindiBillRes;
201
+ const vindiCharge = createdBill.charges[0];
202
+ if (vindiCharge.amount) {
203
+ transaction.amount = Number(vindiCharge.amount);
204
+ }
205
+ transaction.intermediator = {
206
+ buyer_id: String(vindiBill.customer_id),
207
+ transaction_code: String(vindiCharge.id),
208
+ transaction_reference: String(createdBill.id),
209
+ };
210
+
211
+ if (vindiCharge.payment_method) {
212
+ transaction.intermediator.payment_method = {
213
+ code: vindiCharge.payment_method.code || params.payment_method.code,
214
+ };
215
+ if (vindiCharge.payment_method.name) {
216
+ transaction.intermediator.payment_method.name = vindiCharge.payment_method.name;
217
+ }
218
+ }
219
+ if (vindiCharge.print_url) {
220
+ transaction.payment_link = vindiCharge.print_url;
221
+ if (params.payment_method.code === 'banking_billet') {
222
+ transaction.banking_billet = {
223
+ link: vindiCharge.print_url,
224
+ };
225
+ }
226
+ }
227
+
228
+ const vindiTransaction = vindiCharge.last_transaction;
229
+ if (vindiTransaction) {
230
+ transaction.intermediator.transaction_id = String(vindiTransaction.id);
231
+ if (vindiTransaction.payment_profile && vindiTransaction.payment_profile.token) {
232
+ transaction.credit_card = {
233
+ token: vindiTransaction.payment_profile.token,
234
+ };
235
+ if (vindiTransaction.payment_profile.card_number_last_four) {
236
+ transaction.credit_card.last_digits = vindiTransaction
237
+ .payment_profile.card_number_last_four;
238
+ }
239
+ if (vindiTransaction.payment_profile.payment_company) {
240
+ transaction.credit_card.company = vindiTransaction
241
+ .payment_profile.payment_company.name;
242
+ }
243
+ }
244
+ }
245
+
246
+ transaction.status = {
247
+ updated_at: vindiBillRes.updated_at || vindiBillRes.created_at || new Date().toISOString(),
248
+ current: parseVindiStatus(vindiCharge.status),
249
+ };
250
+ return { transaction };
251
+ } catch (error) {
252
+ // try to debug request error
253
+ const errCode = 'VINDI_BILL_ERR';
254
+ let { message } = error;
255
+ const err = new Error(`${errCode} ${orderId} => ${message}`);
256
+ if (error.response) {
257
+ const { status, data } = error.response;
258
+ if (status !== 401 && status !== 403) {
259
+ if (error.config) {
260
+ err.url = error.config.url;
261
+ err.data = error.config.data;
262
+ } else {
263
+ err.bill = JSON.stringify(vindiBill);
264
+ }
265
+ err.customer = JSON.stringify(vindiCustomer);
266
+ err.status = status;
267
+ if (typeof data === 'object' && data) {
268
+ err.response = JSON.stringify(data);
269
+ } else {
270
+ err.response = data;
271
+ }
272
+ } else if (data?.errors?.[0]?.message) {
273
+ message = data.errors[0].message;
274
+ }
275
+ }
276
+ logger.error(err);
277
+ return {
278
+ error: errCode,
279
+ message,
280
+ };
281
+ }
282
+ };
@@ -0,0 +1,152 @@
1
+ import { addInstallments } from './util/vindi-utils.mjs';
2
+
3
+ export default async (modBody) => {
4
+ const { application, params } = modBody;
5
+ const appData = {
6
+ ...application.data,
7
+ ...application.hidden_data,
8
+ };
9
+ if (appData.vindi_api_key) {
10
+ process.env.VINDI_API_KEY = appData.vindi_api_key;
11
+ }
12
+ if (appData.vindi_public_key) {
13
+ process.env.VINDI_PUBLIC_KEY = appData.vindi_public_key;
14
+ }
15
+ const { VINDI_API_KEY, VINDI_PUBLIC_KEY } = process.env;
16
+ if (!VINDI_API_KEY || !VINDI_PUBLIC_KEY) {
17
+ return {
18
+ error: 'NO_VINDI_KEYS',
19
+ message: 'Chave de API e/ou criptografia não configurada (lojista deve configurar o aplicativo)',
20
+ };
21
+ }
22
+
23
+ // https://apx-mods.e-com.plus/api/v1/list_payments/schema.json?store_id=100
24
+ const amount = params.amount || {};
25
+ // https://apx-mods.e-com.plus/api/v1/list_payments/response_schema.json?store_id=100
26
+ const response = {
27
+ payment_gateways: [],
28
+ };
29
+
30
+ const { discount } = appData;
31
+ if (discount && discount.value > 0) {
32
+ if (discount.apply_at !== 'freight') {
33
+ // default discount option
34
+ const { value } = discount;
35
+ response.discount_option = {
36
+ label: appData.discount_option_label,
37
+ value,
38
+ };
39
+ // specify the discount type and min amount is optional
40
+ ['type', 'min_amount'].forEach((prop) => {
41
+ if (discount[prop]) {
42
+ response.discount_option[prop] = discount[prop];
43
+ }
44
+ });
45
+ }
46
+
47
+ if (amount.total) {
48
+ // check amount value to apply discount
49
+ if (amount.total < discount.min_amount) {
50
+ discount.value = 0;
51
+ } else {
52
+ delete discount.min_amount;
53
+
54
+ // fix local amount object
55
+ const maxDiscount = amount[discount.apply_at || 'subtotal'];
56
+ let discountValue;
57
+ if (discount.type === 'percentage') {
58
+ discountValue = maxDiscount * (discount.value / 100);
59
+ } else {
60
+ discountValue = discount.value;
61
+ if (discountValue > maxDiscount) {
62
+ discountValue = maxDiscount;
63
+ }
64
+ }
65
+ if (discountValue > 0) {
66
+ amount.discount = (amount.discount || 0) + discountValue;
67
+ amount.total -= discountValue;
68
+ if (amount.total < 0) {
69
+ amount.total = 0;
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ // common payment methods data
77
+ const intermediator = {
78
+ name: 'Vindi',
79
+ link: 'https://app.vindi.com.br/',
80
+ code: 'vindi_app',
81
+ };
82
+ const paymentTypes = [];
83
+ if (appData.enable_subscription) {
84
+ paymentTypes.push('recurrence');
85
+ }
86
+ if (!appData.disable_bill) {
87
+ paymentTypes.push('payment');
88
+ }
89
+
90
+ // setup payment gateway objects
91
+ ['credit_card', 'banking_billet'].forEach((paymentMethod) => {
92
+ paymentTypes.forEach((type) => {
93
+ const methodConfig = appData[paymentMethod] || {};
94
+ if (!methodConfig.disable) {
95
+ const isCreditCard = paymentMethod === 'credit_card';
96
+ let label = methodConfig.label || (isCreditCard ? 'Cartão de crédito' : 'Boleto bancário');
97
+ if (type === 'recurrence' && appData.subscription_label) {
98
+ label = appData.subscription_label + label;
99
+ }
100
+ const gateway = {
101
+ label,
102
+ icon: methodConfig.icon,
103
+ text: methodConfig.text,
104
+ payment_method: {
105
+ code: paymentMethod,
106
+ name: `${label} - ${intermediator.name}`,
107
+ },
108
+ type,
109
+ intermediator,
110
+ };
111
+
112
+ if (isCreditCard) {
113
+ if (!gateway.icon) {
114
+ gateway.icon = 'https://ecom-pagarme5.web.app/credit-card.png';
115
+ }
116
+ gateway.js_client = {
117
+ // @TODO: Fix to a project controlled URL
118
+ script_uri: 'https://us-central1-ecom-vindi.cloudfunctions.net/app/vindi-hash.js',
119
+ onload_expression: `window._vindiKey="${VINDI_PUBLIC_KEY}";`
120
+ + `window._vindiSandbox=${Boolean(appData.vindi_sandbox)};`,
121
+ cc_hash: {
122
+ function: '_vindiHash',
123
+ is_promise: true,
124
+ },
125
+ };
126
+ const { installments } = appData;
127
+ if (installments) {
128
+ // list all installment options and default one
129
+ addInstallments(amount, installments, gateway, response);
130
+ }
131
+ }
132
+
133
+ if (methodConfig.discount) {
134
+ gateway.discount = methodConfig.discount;
135
+ } else if (
136
+ discount
137
+ && (discount[paymentMethod] === true
138
+ || (!isCreditCard && discount[paymentMethod] !== false))
139
+ ) {
140
+ gateway.discount = discount;
141
+ if (response.discount_option && !response.discount_option.label) {
142
+ response.discount_option.label = label;
143
+ }
144
+ }
145
+
146
+ response.payment_gateways.push(gateway);
147
+ }
148
+ });
149
+ });
150
+
151
+ return response;
152
+ };
@@ -0,0 +1,108 @@
1
+ import api from '@cloudcommerce/api';
2
+ import { logger } from '@cloudcommerce/firebase/lib/config';
3
+ import getAppData from '@cloudcommerce/firebase/lib/helpers/get-app-data';
4
+ import { parseVindiStatus, createVindiAxios } from './util/vindi-utils.mjs';
5
+
6
+ const handleVindiWebhook = async (req, res) => {
7
+ const vindiEvent = req.body?.event;
8
+ if (!vindiEvent?.data || !vindiEvent.type) {
9
+ return res.sendStatus(400);
10
+ }
11
+ if (vindiEvent.type === 'test') {
12
+ return res.sendStatus(200);
13
+ }
14
+ let vindiSandbox;
15
+ if (!process.env.VINDI_API_KEY) {
16
+ const appData = await getAppData('vindi');
17
+ process.env.VINDI_API_KEY = appData.vindi_api_key;
18
+ vindiSandbox = appData.vindi_sandbox;
19
+ }
20
+ const { VINDI_API_KEY } = process.env;
21
+ if (!VINDI_API_KEY) {
22
+ return res.sendStatus(403);
23
+ }
24
+ const data = vindiEvent.data.id
25
+ ? vindiEvent.data
26
+ : (vindiEvent.data.bill || vindiEvent.data.charge);
27
+ if (!data.id) {
28
+ logger.warn('Vindi Hook unexpected data:', { data });
29
+ return res.sendStatus(400);
30
+ }
31
+
32
+ logger.info(`Vindi Hook ${vindiEvent.type} ${data.id}`);
33
+ const isVindiCharge = vindiEvent.type.startsWith('charge_');
34
+ const axiosVindi = createVindiAxios(VINDI_API_KEY, vindiSandbox);
35
+ const { data: vindiBillRes } = await axiosVindi
36
+ .get('/bills/' + (isVindiCharge ? data.bill.id : data.id));
37
+ const vindiBill = vindiBillRes.bill || vindiBillRes;
38
+ if (!vindiBill?.charges?.[0]) {
39
+ logger.warn(`Vindi bill unset or unexpected for ${data.id}`, { vindiBill });
40
+ return res.sendStatus(204);
41
+ }
42
+ const vindiCharge = vindiBill.charges[0];
43
+
44
+ const ordersFilter = data.metadata?.order_id
45
+ ? `_id=${data.metadata.order_id}`
46
+ : `transactions.intermediator.transaction_code=${vindiCharge.id}`;
47
+ const {
48
+ data: { result: [order] },
49
+ } = await api.get(`orders?${ordersFilter}&fields=_id,transactions&limit=1`);
50
+ if (!order) {
51
+ logger.warn(`Order not found for ${vindiCharge.id}`, {
52
+ ordersFilter,
53
+ body: req.body,
54
+ vindiCharge,
55
+ });
56
+ return res.sendStatus(204);
57
+ }
58
+
59
+ let status;
60
+ switch (vindiEvent.type) {
61
+ case 'charge_rejected':
62
+ status = 'unauthorized';
63
+ break;
64
+ case 'charge_refunded':
65
+ status = 'refunded';
66
+ break;
67
+ default:
68
+ status = parseVindiStatus(vindiCharge.status);
69
+ }
70
+ // single bill
71
+ // async cancell Vindi bill when transaction is cancelled
72
+ switch (status) {
73
+ case 'voided':
74
+ case 'refunded':
75
+ case 'unauthorized':
76
+ axiosVindi.delete('/bills/' + vindiBill.id)
77
+ .catch((error) => {
78
+ if (error.response) {
79
+ const err = new Error('Delete bill error');
80
+ err.config = error.config;
81
+ err.data = JSON.stringify(error.response.data);
82
+ err.status = error.response.status;
83
+ } else {
84
+ logger.error(error);
85
+ }
86
+ });
87
+ break;
88
+ default:
89
+ }
90
+
91
+ const transaction = order.transactions?.find(({ intermediator }) => {
92
+ return intermediator?.transaction_code === String(vindiCharge.id);
93
+ });
94
+ if (transaction?._id) {
95
+ await api.post(`orders/${order._id}/payments_history`, {
96
+ date_time: new Date().toISOString(),
97
+ status,
98
+ transaction_id: transaction._id,
99
+ notification_code: vindiEvent.type + ';' + vindiEvent.created_at,
100
+ flags: ['vindi'],
101
+ });
102
+ logger.info(`Updated ${order._id} to ${status}`);
103
+ return res.sendStatus(201);
104
+ }
105
+ return res.sendStatus(200);
106
+ };
107
+
108
+ export default handleVindiWebhook;
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@cloudcommerce/app-vindi",
3
+ "type": "module",
4
+ "version": "2.49.0",
5
+ "description": "e-com.plus Cloud Commerce app to integrate Vindi Gateway",
6
+ "main": "lib/vindi.js",
7
+ "exports": {
8
+ ".": "./lib/vindi.js",
9
+ "./events": "./lib/vindi-events.js"
10
+ },
11
+ "files": [
12
+ "/lib",
13
+ "/lib-mjs",
14
+ "/assets",
15
+ "/types",
16
+ "/*.{js,mjs,ts}"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/ecomplus/cloud-commerce.git",
21
+ "directory": "packages/apps/vindi"
22
+ },
23
+ "author": "E-Com Club Softwares para E-commerce <ti@e-com.club>",
24
+ "license": "MIT",
25
+ "bugs": {
26
+ "url": "https://github.com/ecomplus/cloud-commerce/issues"
27
+ },
28
+ "homepage": "https://github.com/ecomplus/cloud-commerce/tree/main/packages/apps/vindi#readme",
29
+ "dependencies": {
30
+ "axios": "^1.11.0",
31
+ "firebase-admin": "^13.4.0",
32
+ "firebase-functions": "^6.4.0",
33
+ "@cloudcommerce/api": "2.49.0",
34
+ "@cloudcommerce/firebase": "2.49.0"
35
+ },
36
+ "devDependencies": {
37
+ "@cloudcommerce/types": "2.49.0"
38
+ },
39
+ "scripts": {
40
+ "build": "bash ../../../scripts/build-lib.sh"
41
+ }
42
+ }