@cloudcommerce/app-pagarme-v5 0.32.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 (36) 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/assets/onload-expression.js +38 -0
  6. package/assets/onload-expression.min.js +1 -0
  7. package/events.js +1 -0
  8. package/lib/index.d.ts +1 -0
  9. package/lib/index.js +2 -0
  10. package/lib/index.js.map +1 -0
  11. package/lib/pagarme-v5-events.d.ts +6 -0
  12. package/lib/pagarme-v5-events.js +21 -0
  13. package/lib/pagarme-v5-events.js.map +1 -0
  14. package/lib/pagarme-v5.d.ts +4 -0
  15. package/lib/pagarme-v5.js +12 -0
  16. package/lib/pagarme-v5.js.map +1 -0
  17. package/lib-mjs/create-pagarme5-transaction.mjs +208 -0
  18. package/lib-mjs/events-to-pagarme5.mjs +209 -0
  19. package/lib-mjs/functions-lib/api-utils.mjs +220 -0
  20. package/lib-mjs/functions-lib/firestore-utils.mjs +24 -0
  21. package/lib-mjs/functions-lib/pagarme/create-axios.mjs +12 -0
  22. package/lib-mjs/functions-lib/pagarme/handle-plans.mjs +69 -0
  23. package/lib-mjs/functions-lib/pagarme/parses-utils.mjs +61 -0
  24. package/lib-mjs/functions-lib/pagarme/payment-subscription.mjs +244 -0
  25. package/lib-mjs/functions-lib/payments/add-installments.mjs +45 -0
  26. package/lib-mjs/list-pagarme5-payments.mjs +218 -0
  27. package/lib-mjs/pagarme5-webhooks.mjs +343 -0
  28. package/package.json +38 -0
  29. package/scripts/build.sh +4 -0
  30. package/scripts/tests.sh +9 -0
  31. package/src/index.ts +1 -0
  32. package/src/pagarme-v5-events.ts +27 -0
  33. package/src/pagarme-v5.ts +12 -0
  34. package/tests/1-list-payments.test.mjs +37 -0
  35. package/tests/2-create-transaction.test.mjs +56 -0
  36. package/tsconfig.json +6 -0
@@ -0,0 +1,244 @@
1
+ import logger from 'firebase-functions/logger';
2
+ import axios from './create-axios.mjs';
3
+ import { parseAddress } from './parses-utils.mjs';
4
+
5
+ const paymentMethods = {
6
+ credit_card: 'credit_card',
7
+ banking_billet: 'boleto',
8
+ account_deposit: 'pix',
9
+ };
10
+
11
+ const parseIntervalPlan = {
12
+ // day, week, month ou year.
13
+ Diaria: {
14
+ interval: 'day',
15
+ interval_count: 1,
16
+ },
17
+ Semanal: {
18
+ interval: 'week',
19
+ interval_count: 1,
20
+ },
21
+ Mensal: {
22
+ interval: 'month',
23
+ interval_count: 1,
24
+
25
+ },
26
+ Bimestral: {
27
+ interval: 'month',
28
+ interval_count: 2,
29
+ },
30
+ Trimestral: {
31
+ interval: 'month',
32
+ interval_count: 3,
33
+ },
34
+ Semestral: {
35
+ interval: 'month',
36
+ interval_count: 6,
37
+ },
38
+ Anual: {
39
+ interval: 'year',
40
+ interval_count: 1,
41
+ },
42
+ };
43
+
44
+ const createSubscription = async (params, appData, storeId, plan, customer) => {
45
+ const pagarmeAxios = axios(appData.pagarme_api_token);
46
+
47
+ const orderId = params.order_id;
48
+ const { amount, items } = params;
49
+
50
+ const paymentMethod = paymentMethods[params.payment_method.code] || 'credit_card';
51
+
52
+ const intervalPlan = parseIntervalPlan[plan.periodicity];
53
+
54
+ const statementDescriptor = appData.soft_descriptor || params?.domain
55
+ .replace('www.', '')
56
+ .replace('https://', '')
57
+ .split('.')[0] || '*';
58
+
59
+ const pagarmeSubscription = {
60
+ code: orderId,
61
+ payment_method: paymentMethod,
62
+ currency: 'BRL',
63
+ interval: intervalPlan.interval || 'month',
64
+ interval_count: intervalPlan.interval_count || 1,
65
+ billing_type: 'prepaid', //
66
+ customer,
67
+ statement_descriptor: (`Assinatura ${statementDescriptor}`).substring(13),
68
+ };
69
+
70
+ pagarmeSubscription.metada = {
71
+ order_number: params.order_number,
72
+ store_id: storeId,
73
+ order_id: orderId,
74
+ platform_integration: 'ecomplus',
75
+ };
76
+
77
+ if (paymentMethod === 'credit_card') {
78
+ pagarmeSubscription.card_token = params.credit_card.hash;
79
+ const address = parseAddress(params.to || params.billing_address);
80
+ pagarmeSubscription.card = {
81
+ billing_address: address,
82
+ };
83
+ }
84
+
85
+ pagarmeSubscription.discounts = [];
86
+ pagarmeSubscription.items = [];
87
+
88
+ items.forEach(async (item) => {
89
+ if (item.quantity > 0) {
90
+ const itemSubscription = {
91
+ name: item.name || item.variation_id || item.product_id,
92
+ quantity: item.quantity,
93
+ description: item.name || item.variation_id || item.product_id,
94
+ id: `pi_${item.sku}`,
95
+ status: 'active',
96
+ pricing_scheme: {
97
+ scheme_type: 'unit',
98
+ price: Math.floor((item.final_price || item.price) * 100),
99
+ },
100
+ };
101
+ // if the item is a bonus, create a discount for repeat one time
102
+ if (
103
+ item.flags && (item.flags.includes('freebie')
104
+ || item.flags.includes('discount-set-free'))
105
+ ) {
106
+ itemSubscription.cycles = 1;
107
+ }
108
+
109
+ pagarmeSubscription.items.push(itemSubscription);
110
+ }
111
+ });
112
+
113
+ if (amount.freight) {
114
+ const itemFreight = {
115
+ name: 'Frete',
116
+ quantity: 1,
117
+ description: 'Frete',
118
+ id: `pi_freight_${orderId}`,
119
+ status: 'active',
120
+ pricing_scheme: {
121
+ scheme_type: 'unit',
122
+ price: Math.floor((amount.freight).toFixed(2) * 1000) / 10,
123
+ },
124
+ };
125
+ pagarmeSubscription.items.push(itemFreight);
126
+ }
127
+
128
+ // console.log('>> amount ', JSON.stringify(amount))
129
+ // Add once discont, but webhook invoce check discount plan
130
+ const discountSubscription = amount.discount && {
131
+ value: `${Math.floor((amount.discount).toFixed(2) * 1000) / 10}`,
132
+ discount_type: 'flat',
133
+ cycles: 1,
134
+ };
135
+
136
+ if (discountSubscription) {
137
+ pagarmeSubscription.discounts.push(discountSubscription);
138
+ }
139
+
140
+ logger.log('[PagarMe V5] Subscription: ', JSON.stringify(pagarmeSubscription));
141
+
142
+ return pagarmeAxios.post(
143
+ '/subscriptions',
144
+ pagarmeSubscription,
145
+ );
146
+ };
147
+
148
+ const createPayment = async (params, appData, customer) => {
149
+ const pagarmeAxios = axios(appData.pagarme_api_token);
150
+
151
+ const { amount, items } = params;
152
+
153
+ const address = parseAddress(params.to || params.billing_address);
154
+
155
+ logger.log('[PagarMe V5] Try payment');
156
+ let discountEach;
157
+ if (amount.discount) {
158
+ const quantityItems = items.reduce((acumulador, item) => {
159
+ return acumulador + (item.quantity || 0);
160
+ }, 0);
161
+ discountEach = amount.discount / quantityItems;
162
+ }
163
+
164
+ const paymentMethod = paymentMethods[params.payment_method.code] || 'credit_card';
165
+ const methodConfig = appData[params.payment_method.code];
166
+
167
+ const statementDescriptor = appData.soft_descriptor || params?.domain
168
+ .replace('www.', '')
169
+ .replace('https://', '')
170
+ .split('.')[0] || '*';
171
+
172
+ const pagarmeOrder = { customer };
173
+
174
+ const phone = customer.phones[0];
175
+ if (amount.freight) {
176
+ pagarmeOrder.shipping = {
177
+ amount: Math.floor((amount.freight) * 100),
178
+ description: 'Frete',
179
+ recipient_name: customer.name,
180
+ recipient_phone: `${phone?.area_code || ''}${phone?.number}`,
181
+ address,
182
+ };
183
+ }
184
+
185
+ if (params.browser_ip) {
186
+ pagarmeOrder.ip = params.browser_ip;
187
+ }
188
+
189
+ pagarmeOrder.items = [];
190
+
191
+ items.forEach(async (item) => {
192
+ if (item.quantity > 0) {
193
+ const itemOrder = {
194
+ quantity: item.quantity,
195
+ description: item.name || item.variation_id || item.product_id,
196
+ code: item.sku || item.variation_id || item.product_id,
197
+ amount: Math.floor(((item.final_price || item.price) - (discountEach || 0)) * 100),
198
+ };
199
+ pagarmeOrder.items.push(itemOrder);
200
+ }
201
+ });
202
+
203
+ const payment = {
204
+ payment_method: paymentMethod,
205
+ amount: Math.floor((amount.total)) * 100,
206
+ };
207
+
208
+ if (paymentMethod === 'credit_card') {
209
+ payment.credit_card = {
210
+ operation_type: 'auth_and_capture', // auth_only
211
+ installments: params.installments_number || 1,
212
+ statement_descriptor: statementDescriptor.substring(13),
213
+ card_token: params.credit_card.hash,
214
+ card: {
215
+ billing_address: address,
216
+ },
217
+ };
218
+ } else if (paymentMethod === 'pix') {
219
+ payment.pix = {
220
+ expires_in: (methodConfig.due_time || 60) * 60,
221
+ };
222
+ } else {
223
+ payment.boleto = {
224
+ due_at: new Date(new Date().getTime()
225
+ + ((methodConfig.days_due_date || 1) * 24 * 60 * 60 * 1000)).toISOString(),
226
+ instructions: methodConfig.instructions || statementDescriptor,
227
+ billing_address: address,
228
+ };
229
+ }
230
+
231
+ pagarmeOrder.payments = [payment];
232
+
233
+ logger.log('[PagarMe V5] Order PagarMe: ', JSON.stringify(pagarmeOrder));
234
+
235
+ return pagarmeAxios.post(
236
+ '/orders',
237
+ pagarmeOrder,
238
+ );
239
+ };
240
+
241
+ export {
242
+ createSubscription,
243
+ createPayment,
244
+ };
@@ -0,0 +1,45 @@
1
+ export default (amount, response, installments = {}, gateway = {}) => {
2
+ const maxInterestFree = installments.max_interest_free;
3
+ const minInstallment = installments.min_installment || 5;
4
+ const qtyPosssibleInstallment = Math.floor((amount.total / minInstallment));
5
+ const maxInstallments = installments.max_number
6
+ || (qtyPosssibleInstallment < 12 ? qtyPosssibleInstallment : 12);
7
+ const monthlyInterest = installments.monthly_interest || 0;
8
+
9
+ if (maxInstallments > 1) {
10
+ if (response) {
11
+ response.installments_option = {
12
+ min_installment: minInstallment,
13
+ max_number: maxInterestFree > 1 ? maxInterestFree : maxInstallments,
14
+ monthly_interest: maxInterestFree > 1 ? 0 : monthlyInterest,
15
+ };
16
+ }
17
+
18
+ const isInterestFreeMinAmount = installments.interest_free_min_amount
19
+ && amount.total >= (installments.interest_free_min_amount);
20
+
21
+ // list installment options
22
+ gateway.installment_options = [];
23
+ for (let number = 2; number <= maxInstallments; number++) {
24
+ const tax = !(maxInterestFree >= number);
25
+ let interest;
26
+ if (tax || !isInterestFreeMinAmount) {
27
+ interest = monthlyInterest / 100;
28
+ }
29
+ let value;
30
+ if ((!tax && isInterestFreeMinAmount) || !interest) {
31
+ value = amount.total / number;
32
+ } else {
33
+ value = amount.total * (interest / (1 - (1 + interest) ** -number));
34
+ }
35
+ if (value && value >= 1) {
36
+ gateway.installment_options.push({
37
+ number,
38
+ value,
39
+ tax: tax || !isInterestFreeMinAmount,
40
+ });
41
+ }
42
+ }
43
+ }
44
+ return { response, gateway };
45
+ };
@@ -0,0 +1,218 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import url from 'node:url';
4
+ import logger from 'firebase-functions/logger';
5
+ import api from '@cloudcommerce/api';
6
+ import addInstallments from './functions-lib/payments/add-installments.mjs';
7
+ import { discountPlanPayment } from './functions-lib/pagarme/handle-plans.mjs';
8
+
9
+ const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
10
+
11
+ export default async (data) => {
12
+ const { application, params } = data;
13
+ const { items } = params;
14
+
15
+ const configApp = {
16
+ ...application.data,
17
+ ...application.hidden_data,
18
+ };
19
+
20
+ if (!process.env.PAGARMEV5_PUBLIC_KEY) {
21
+ const pagarmePublicKey = configApp.pagarme_public_key;
22
+ if (pagarmePublicKey && typeof pagarmePublicKey === 'string') {
23
+ process.env.PAGARMEV5_PUBLIC_KEY = pagarmePublicKey;
24
+ } else {
25
+ logger.warn('Missing PAGARMEV5 PUBLIC KEY');
26
+ }
27
+ }
28
+
29
+ if (!process.env.PAGARMEV5_API_TOKEN) {
30
+ const pagarmeApiKey = configApp.pagarme_api_token;
31
+ if (pagarmeApiKey && typeof pagarmeApiKey === 'string') {
32
+ process.env.PAGARMEV5_API_TOKEN = pagarmeApiKey;
33
+ } else {
34
+ logger.warn('Missing PAGARMEV5 API TOKEN');
35
+ }
36
+ }
37
+
38
+ if (!process.env.PAGARMEV5_API_TOKEN || !process.env.PAGARMEV5_PUBLIC_KEY) {
39
+ return {
40
+ error: 'NO_PAGARMEV5_KEYS',
41
+ message: 'Chave de API e/ou criptografia não configurada (lojista deve configurar o aplicativo)',
42
+ };
43
+ }
44
+
45
+ const categoryIds = configApp.recurrency_category_ids;
46
+ let hasRecurrence = false;
47
+ let isAllRecurring = true;
48
+
49
+ if (categoryIds.length) {
50
+ try {
51
+ const { data: { result } } = await api.get('search/v1', {
52
+ limit: items.length,
53
+ params: {
54
+ _id: items.map((item) => item.product_id),
55
+ 'categories._id': categoryIds,
56
+ },
57
+ });
58
+
59
+ hasRecurrence = result.length > 0;
60
+ isAllRecurring = result.length === items.length;
61
+ } catch (err) {
62
+ logger.error(err);
63
+ }
64
+ }
65
+
66
+ const response = {
67
+ payment_gateways: [],
68
+ };
69
+
70
+ const paymentTypes = [];
71
+ if ((isAllRecurring) && configApp.recurrence && configApp.recurrence.length
72
+ && configApp.recurrence[0].label) {
73
+ paymentTypes.push('recurrence');
74
+ }
75
+
76
+ if ((!hasRecurrence) && (!configApp.credit_card.disable || !configApp.banking_billet.disable
77
+ || !configApp.account_deposit.disable)) {
78
+ paymentTypes.push('payment');
79
+ }
80
+
81
+ // setup payment gateway objects
82
+ const intermediator = {
83
+ name: 'Pagar.me',
84
+ link: 'https://pagar.me/',
85
+ code: 'pagarme',
86
+ };
87
+
88
+ const listPaymentMethod = ['credit_card', 'banking_billet'];
89
+
90
+ if (!configApp.account_deposit?.disable) {
91
+ listPaymentMethod.push('account_deposit');
92
+ }
93
+
94
+ paymentTypes.forEach((type) => {
95
+ // At first the occurrence only with credit card
96
+ const isRecurrence = type === 'recurrence';
97
+ const plans = isRecurrence ? configApp.recurrence : ['single_payment'];
98
+ plans.forEach((plan) => {
99
+ listPaymentMethod.forEach((paymentMethod) => {
100
+ // console.log('>> List Payments ', type, ' ', plan, ' ', paymentMethod)
101
+ const amount = { ...params.amount } || {};
102
+ const isCreditCard = paymentMethod === 'credit_card';
103
+ const isPix = paymentMethod === 'account_deposit';
104
+ const methodConfig = configApp[paymentMethod] || {};
105
+ let methodEnable = isRecurrence ? methodConfig.enable_recurrence : !methodConfig.disable;
106
+
107
+ // Pix not active in recurrence
108
+ methodEnable = isPix && isRecurrence ? false : methodEnable;
109
+
110
+ const minAmount = methodConfig?.min_amount || 0;
111
+ const validateAmount = amount.total
112
+ ? (amount.total >= minAmount) : true; // Workaround for showcase
113
+ if (methodEnable && validateAmount) {
114
+ let label = isRecurrence ? plan.label : methodConfig.label;
115
+ if (!label) {
116
+ if (isCreditCard) {
117
+ label = 'Cartão de crédito';
118
+ } else {
119
+ label = !isPix ? 'Boleto bancário' : 'Pix';
120
+ }
121
+ }
122
+ const gateway = {
123
+ label,
124
+ icon: methodConfig.icon,
125
+ text: methodConfig.text,
126
+ payment_method: {
127
+ code: paymentMethod,
128
+ name: `${isRecurrence ? `Assinatura ${plan.periodicity} ` : ''}`
129
+ + `${label} - ${intermediator.name}`,
130
+ },
131
+ type,
132
+ intermediator,
133
+ };
134
+
135
+ let discount;
136
+ if (isRecurrence) {
137
+ discount = discountPlanPayment(label, plan, amount);
138
+ } else {
139
+ discount = configApp.discount;
140
+ }
141
+
142
+ if (discount) {
143
+ if (isRecurrence) {
144
+ gateway.discount = !plan.discount_first_installment?.disable
145
+ ? plan.discount_first_installment : plan.discount;
146
+ gateway.discount.type = discount.discountOption.type;
147
+ response.discount_option = discount.discountOption;
148
+ } else if (discount[paymentMethod]) {
149
+ gateway.discount = {
150
+ apply_at: discount.apply_at,
151
+ type: discount.type,
152
+ value: discount.value,
153
+ };
154
+
155
+ // check amount value to apply discount
156
+ if (amount.total < (discount.min_amount || 0)) {
157
+ delete gateway.discount;
158
+ } else {
159
+ delete discount.min_amount;
160
+
161
+ // fix local amount object
162
+ const applyDiscount = discount.apply_at;
163
+
164
+ const maxDiscount = amount[applyDiscount || 'subtotal'];
165
+ let discountValue;
166
+ if (discount.type === 'percentage') {
167
+ discountValue = (maxDiscount * discount.value) / 100;
168
+ } else {
169
+ discountValue = discount.value;
170
+ if (discountValue > maxDiscount) {
171
+ discountValue = maxDiscount;
172
+ }
173
+ }
174
+
175
+ if (discountValue) {
176
+ amount.discount = (amount.discount || 0) + discountValue;
177
+ amount.total -= discountValue;
178
+ if (amount.total < 0) {
179
+ amount.total = 0;
180
+ }
181
+ }
182
+ }
183
+ if (response.discount_option) {
184
+ response.discount_option.min_amount = discount.min_amount;
185
+ }
186
+ }
187
+ }
188
+
189
+ if (isCreditCard) {
190
+ if (!gateway.icon) {
191
+ // gateway.icon = `${hostingUri}/credit-card.png`;
192
+ }
193
+ // https://github.com/pagarme/pagarme-js
194
+ gateway.js_client = {
195
+ script_uri: 'https://checkout.pagar.me/v1/tokenizecard.js',
196
+ onload_expression: `window._pagarmeKey="${process.env.PAGARMEV5_PUBLIC_KEY}";`
197
+ + fs.readFileSync(path.join(__dirname, '../assets/onload-expression.min.js'), 'utf8'),
198
+ cc_hash: {
199
+ function: '_pagarmeHash',
200
+ is_promise: true,
201
+ },
202
+ };
203
+ if (!isRecurrence) {
204
+ const { installments } = configApp;
205
+ if (installments) {
206
+ // list all installment options and default one
207
+ addInstallments(amount, response, installments, gateway);
208
+ }
209
+ }
210
+ }
211
+ response.payment_gateways.push(gateway);
212
+ }
213
+ });
214
+ });
215
+ });
216
+
217
+ return response;
218
+ };