@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 +21 -0
- package/README.md +1 -0
- package/events.js +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -0
- package/lib/vindi-events.d.ts +5 -0
- package/lib/vindi-events.js +21 -0
- package/lib/vindi-events.js.map +1 -0
- package/lib/vindi.d.ts +4 -0
- package/lib/vindi.js +12 -0
- package/lib/vindi.js.map +1 -0
- package/lib-mjs/util/vindi-utils.mjs +74 -0
- package/lib-mjs/vindi-create-transaction.mjs +282 -0
- package/lib-mjs/vindi-list-payments.mjs +152 -0
- package/lib-mjs/vindi-webhook.mjs +108 -0
- package/package.json +42 -0
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
package/lib/index.js.map
ADDED
|
@@ -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,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
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
|
package/lib/vindi.js.map
ADDED
|
@@ -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
|
+
}
|