@cloudcommerce/app-paghiper 0.2.2

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.
@@ -0,0 +1,176 @@
1
+ import type { Orders } from '@cloudcommerce/types';
2
+ import type { Request, Response } from 'firebase-functions';
3
+ import api from '@cloudcommerce/api';
4
+ import logger from 'firebase-functions/logger';
5
+ import { Endpoint } from '@cloudcommerce/api/lib/types';
6
+ import config from '@cloudcommerce/firebase/lib/config';
7
+ import Axios from './create-axios';
8
+
9
+ const { apps } = config.get();
10
+
11
+ const CLIENT_ERR = 'invalidClient';
12
+
13
+ const listOrdersByTransaction = async (transactionCode: string) => {
14
+ let filters = `?transactions.intermediator.transaction_id=${transactionCode}`;
15
+ filters += '&fields=_id,transactions._id,';
16
+ filters += 'transactions.app,transactions.intermediator,transactions.status';
17
+
18
+ // send and return authenticated Store API request
19
+ const { result } = (await api.get(`/orders${filters}` as Endpoint)).data;
20
+ return result as Orders[];
21
+ };
22
+
23
+ const readNotification = async (readNotificationBody: { [x: string]: any }, isPix?: boolean) => {
24
+ // read full notification body from PagHiper API
25
+ // https://dev.paghiper.com/reference#qq
26
+ // returns request promise
27
+ const endpoint = `/${(isPix ? 'invoice' : 'transaction')}/notification/`;
28
+ const { data } = await Axios(isPix).post(endpoint, readNotificationBody);
29
+ return data;
30
+ };
31
+
32
+ export default async (req: Request, res: Response) => {
33
+ const { body } = req;
34
+ const isPix = Boolean(req.params.pix);
35
+ // handle PagHiper notification request
36
+ // https://dev.paghiper.com/reference#qq
37
+ const transactionCode = (body && body.transaction_id);
38
+ if (!transactionCode) {
39
+ return res.sendStatus(400);
40
+ }
41
+
42
+ logger.log(`> Paghiper notification for ${transactionCode}`);
43
+ // const docRef = (await collectionSubscription.doc(transactionCode).get()).data();
44
+ const Apps = (await api.get(
45
+ `applications?app_id=${apps.pagHiper.appId}&fields=hidden_data`,
46
+ )).data.result;
47
+ const configApp = Apps[0].hidden_data?.paghiper_api_key;
48
+ try {
49
+ if (configApp.paghiper_token && configApp.paghiper_api_key === body.apiKey) {
50
+ // list order IDs for respective transaction code
51
+ const orders = await listOrdersByTransaction(transactionCode);
52
+ const paghiperResponse = await readNotification(
53
+ { ...body, token: configApp.paghiper_token },
54
+ isPix,
55
+ );
56
+
57
+ const handleNotification = async (isRetry?: boolean) => {
58
+ let { status } = paghiperResponse.status_request;
59
+ logger.log(`PagHiper ${transactionCode} -> '${status}'`);
60
+ switch (status) {
61
+ case 'pending':
62
+ case 'paid':
63
+ case 'refunded':
64
+ // is the same
65
+ break;
66
+ case 'canceled':
67
+ status = 'voided';
68
+ break;
69
+ case 'processing':
70
+ status = 'under_analysis';
71
+ break;
72
+ case 'reserved':
73
+ // https://atendimento.paghiper.com/hc/pt-br/articles/360016177713
74
+ status = 'authorized';
75
+ break;
76
+ default:
77
+ // ignore unknow status
78
+ return true;
79
+ }
80
+
81
+ try {
82
+ // change transaction status on E-Com Plus API
83
+ const notificationCode = body.notification_id;
84
+ orders.forEach(async ({ _id, transactions }) => {
85
+ let transactionId: string | undefined;
86
+ if (transactions) {
87
+ transactions.forEach((transaction) => {
88
+ const { app, intermediator } = transaction;
89
+ if (intermediator && intermediator.transaction_id === String(transactionCode)) {
90
+ if (transaction.status) {
91
+ if (
92
+ transaction.status.current === status
93
+ || (status === 'pending' && transaction.status.current === 'paid')
94
+ ) {
95
+ // ignore old/duplicated notification
96
+ return;
97
+ }
98
+ }
99
+ if (app && app.intermediator && app.intermediator.code !== 'paghiper') {
100
+ return;
101
+ }
102
+ transactionId = transaction._id;
103
+ }
104
+ });
105
+ }
106
+
107
+ let bodyPaymentHistory: { [x: string]: any };
108
+
109
+ if (typeof status === 'object' && status !== null) {
110
+ // request body object sent as 'status' function param
111
+ bodyPaymentHistory = status;
112
+ } else {
113
+ bodyPaymentHistory = {
114
+ date_time: new Date().toISOString(),
115
+ status,
116
+ };
117
+ if (notificationCode) {
118
+ bodyPaymentHistory.notification_code = notificationCode;
119
+ }
120
+ if (typeof transactionId === 'string' && /^[a-f0-9]{24}$/.test(transactionId)) {
121
+ bodyPaymentHistory.transaction_id = transactionId;
122
+ }
123
+ }
124
+ await api.post(`orders/${_id}/payments_history`, bodyPaymentHistory as any); // TODO: incompatible type
125
+ });
126
+
127
+ return res.status(204).send('SUCCESS');
128
+ } catch (err: any) {
129
+ //
130
+ const { message, response } = err;
131
+ let statusCode: number;
132
+ if (!err.request && err.name !== CLIENT_ERR && err.code !== 'EMPTY') {
133
+ // not Axios error ?
134
+ logger.error(err);
135
+ statusCode = 500;
136
+ } else {
137
+ const resStatus = response && response.status;
138
+ let debugMsg = `[#${transactionCode}] Unhandled notification: `;
139
+ if (err.config) {
140
+ debugMsg += `${err.config.url} `;
141
+ }
142
+ debugMsg += (resStatus || message);
143
+
144
+ if (!isRetry
145
+ && ((resStatus === 401 && response.data && response.data.error_code === 132)
146
+ || resStatus >= 500)
147
+ ) {
148
+ // delay and retry once
149
+ await new Promise((resolve) => {
150
+ setTimeout(() => {
151
+ resolve(true);
152
+ }, 700);
153
+ });
154
+ return handleNotification(true);
155
+ // statusCode = 503;
156
+ }
157
+ logger.error(debugMsg);
158
+ statusCode = 409;
159
+ }
160
+ // return response with error
161
+ return res.status(statusCode)
162
+ .send({
163
+ error: 'paghiper_notification_error',
164
+ message,
165
+ });
166
+ }
167
+ };
168
+ await handleNotification();
169
+ }
170
+
171
+ return res.status(400).send('API key does not match');
172
+ } catch (err: any) {
173
+ logger.error(err);
174
+ return res.sendStatus(500);
175
+ }
176
+ };
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './paghiper';
@@ -0,0 +1,242 @@
1
+ import type {
2
+ AppModuleBody,
3
+ CreateTransactionParams,
4
+ CreateTransactionResponse,
5
+ } from '@cloudcommerce/types';
6
+ import type { PagHiperApp } from '../types/config-app';
7
+ import logger from 'firebase-functions/logger';
8
+ import config from '@cloudcommerce/firebase/lib/config';
9
+ import axios from './functions-lib/create-axios';
10
+
11
+ type ItemsPagHiper = {
12
+ description: string,
13
+ item_id: string,
14
+ quantity: number,
15
+ price_cents: number,
16
+ }
17
+
18
+ const responseError = (status: number | null, error: string, message: string) => {
19
+ return {
20
+ status: status || 409,
21
+ error,
22
+ message,
23
+ };
24
+ };
25
+
26
+ const createTransactionPagHiper = async (
27
+ body: { [x: string]: any },
28
+ isPix?: boolean,
29
+ ): Promise<any> => {
30
+ // create new transaction to PagHiper API
31
+ // https://dev.paghiper.com/reference#gerar-boleto
32
+ if (!isPix && process.env.PAGHIPER_PARTNER_ID) {
33
+ body.partners_id = process.env.PAGHIPER_PARTNER_ID;
34
+ } else if (isPix && process.env.PAGHIPER_PIX_PARTNER_ID) {
35
+ body.partners_id = process.env.PAGHIPER_PIX_PARTNER_ID;
36
+ }
37
+
38
+ // returns request promise
39
+ const endpoint = `/${(isPix ? 'invoice' : 'transaction')}/create/`;
40
+ return new Promise((resolve, reject) => {
41
+ axios(isPix).post(endpoint, body)
42
+ .then(({ data }) => {
43
+ // save transaction ID on database first
44
+ let createRequest: any;
45
+ if (isPix) {
46
+ createRequest = data.pix_create_request;
47
+ }
48
+ if (!createRequest) {
49
+ createRequest = data.create_request;
50
+ }
51
+
52
+ resolve(createRequest);
53
+ })
54
+ .catch(reject);
55
+ });
56
+ };
57
+
58
+ export default async (appData: AppModuleBody) => {
59
+ const locationId = config.get().httpsFunctionOptions.region;
60
+ const baseUri = `https://${locationId}-${process.env.GCLOUD_PROJECT}.cloudfunctions.net`;
61
+
62
+ const webhookUrl = `${baseUri}/paghiper-webhook`;
63
+
64
+ // treat module request body
65
+ const { application } = appData;
66
+ const params = appData.params as CreateTransactionParams;
67
+
68
+ // app configured options
69
+ const configApp = {
70
+ ...application.data,
71
+ ...application.hidden_data,
72
+ } as PagHiperApp;
73
+
74
+ const orderId = params.order_id;
75
+ const orderNumber = params.order_number;
76
+ const {
77
+ amount,
78
+ items,
79
+ buyer,
80
+ to,
81
+ } = params;
82
+
83
+ const billingAddress = params.billing_address;
84
+
85
+ logger.log(`> (App PagHiper): Create transaction for #${orderId}`);
86
+ let transaction: CreateTransactionResponse['transaction'] = {
87
+ amount: amount.total,
88
+ };
89
+
90
+ // params object follows create transaction request schema:
91
+ // https://apx-mods.e-com.plus/api/v1/create_transaction/schema.json?store_id=100
92
+ // const orderId = params.order_id;
93
+ // logger.log(`> Create transaction for #${storeId} ${orderId}`);
94
+ // setup transaction body to PagHiper reference
95
+
96
+ // https://dev.paghiper.com/reference#gerar-boleto
97
+ const address = billingAddress || to;
98
+
99
+ const paghiperTransaction: { [x: string]: any } = {
100
+ order_id: orderId || orderNumber || new Date().getTime().toString(),
101
+ payer_email: buyer.email,
102
+ payer_name: buyer.fullname,
103
+ payer_cpf_cnpj: buyer.doc_number,
104
+ payer_phone: buyer.phone.number,
105
+ payer_street: address?.street || '',
106
+ payer_number: address?.number || '',
107
+ payer_complement: address?.complement || '',
108
+ payer_district: address?.borough || '',
109
+ payer_city: address?.city || '',
110
+ payer_state: address?.province_code || '',
111
+ payer_zip_code: address?.zip ? address.zip.replace(/\D/g, '') : '',
112
+ notification_url: webhookUrl,
113
+ discount_cents: amount.discount ? Math.round(amount.discount * 100) : '',
114
+ shipping_price_cents: amount.freight ? Math.round(amount.freight * 100) : '',
115
+ fixed_description: true,
116
+ type_bank_slip: 'boletoA4',
117
+ days_due_date: 5,
118
+ per_day_interest: true,
119
+ items: [] as ItemsPagHiper[],
120
+ };
121
+
122
+ // parse transaction items list
123
+ items.forEach((item) => {
124
+ paghiperTransaction.items.push({
125
+ description: item.name || item.product_id,
126
+ item_id: item.sku || item.product_id,
127
+ quantity: item.quantity,
128
+ price_cents: Math.round((item.final_price || item.price) * 100),
129
+ });
130
+ });
131
+
132
+ const isPix = params.payment_method.code === 'account_deposit';
133
+ if (isPix) {
134
+ paghiperTransaction.notification_url += '/pix';
135
+ }
136
+
137
+ // use configured PagHiper API key
138
+ paghiperTransaction.apiKey = configApp.paghiper_api_key;
139
+ // merge configured banking billet options
140
+ const options = configApp.banking_billet_options;
141
+ if (typeof options === 'object' && options !== null) {
142
+ // options must have only valid properties for PagHiper transaction object
143
+ Object.keys(options).forEach((prop) => {
144
+ if (options[prop]) {
145
+ paghiperTransaction[prop] = options[prop];
146
+ }
147
+ });
148
+ }
149
+
150
+ try {
151
+ // send request to PagHiper API
152
+ const createRequest = await createTransactionPagHiper(paghiperTransaction, isPix);
153
+
154
+ // transaction created successfully
155
+ // https://dev.paghiper.com/reference#exemplos
156
+ // mount response body
157
+ // https://apx-mods.e-com.plus/api/v1/create_transaction/response_schema.json?store_id=100
158
+ transaction = {
159
+ intermediator: {
160
+ transaction_id: createRequest.transaction_id,
161
+ transaction_code: createRequest.transaction_id,
162
+ transaction_reference: createRequest.order_id,
163
+ },
164
+ amount: createRequest.value_cents
165
+ ? parseInt(createRequest.value_cents, 10) / 100
166
+ // use amount from create transaction request body
167
+ : params.amount.total,
168
+ };
169
+
170
+ if (isPix) {
171
+ // https://dev.paghiper.com/reference#exemplos-pix
172
+ const pixCode = createRequest.pix_code;
173
+ if (transaction.intermediator) {
174
+ transaction.intermediator.transaction_code = pixCode.emv;
175
+ }
176
+ const pixCodeUrls = ['pix_url', 'qrcode_image_url', 'bacen_url'];
177
+ for (let i = 0; i < pixCodeUrls.length; i++) {
178
+ const pixUrl = pixCode[pixCodeUrls[i]];
179
+ if (pixUrl && pixUrl.startsWith('http')) {
180
+ transaction.payment_link = pixUrl;
181
+ break;
182
+ }
183
+ }
184
+ transaction.notes = `<img src="${pixCode.qrcode_image_url}" `
185
+ + 'style="display:block;max-width:100%;margin:0 auto" />';
186
+ } else {
187
+ const bankSlip = createRequest.bank_slip;
188
+ transaction.payment_link = bankSlip.url_slip;
189
+ transaction.banking_billet = {
190
+ code: bankSlip.digitable_line,
191
+ link: bankSlip.url_slip_pdf,
192
+ };
193
+ if (createRequest.due_date) {
194
+ transaction.banking_billet.valid_thru = new Date(createRequest.due_date).toISOString();
195
+ }
196
+ }
197
+
198
+ return {
199
+ redirect_to_payment: false,
200
+ transaction,
201
+ };
202
+ } catch (err: any) {
203
+ let { message } = err;
204
+ let statusCode: number | null;
205
+ if (!err.request) {
206
+ // not Axios error ?
207
+ logger.error('> (App PagHiper) =>', err);
208
+ statusCode = 500;
209
+ } else {
210
+ let debugMsg = 'Can\'t create transaction: ';
211
+ if (err.config) {
212
+ debugMsg += `${err.config.url} `;
213
+ }
214
+ if (err.response) {
215
+ debugMsg += err.response.status;
216
+
217
+ // https://dev.paghiper.com/reference#mensagens-de-retorno-2
218
+ if (err.response.status === 200) {
219
+ const { data } = err.response;
220
+ if (data) {
221
+ debugMsg += ` ${typeof data === 'object' ? JSON.stringify(data) : data}`;
222
+ debugMsg += ` ${JSON.stringify(paghiperTransaction)}`;
223
+ if (data.create_request && data.create_request.response_message) {
224
+ message = data.create_request.response_message;
225
+ }
226
+ }
227
+ }
228
+ } else {
229
+ debugMsg += message;
230
+ }
231
+ logger.error('> (App PagHiper) =>', debugMsg);
232
+ statusCode = 409;
233
+ }
234
+
235
+ // return error status code
236
+ return responseError(
237
+ statusCode,
238
+ 'CREATE_TRANSACTION_ERR',
239
+ message,
240
+ );
241
+ }
242
+ };
@@ -0,0 +1,127 @@
1
+ import type {
2
+ AppModuleBody,
3
+ ListPaymentsParams,
4
+ ListPaymentsResponse,
5
+ } from '@cloudcommerce/types';
6
+ import type { PagHiperApp } from '../types/config-app';
7
+ // import logger from 'firebase-functions/logger';
8
+
9
+ const responseError = (status: number | null, error: string, message: string) => {
10
+ return {
11
+ status: status || 409,
12
+ error,
13
+ message,
14
+ };
15
+ };
16
+
17
+ type Gateway = ListPaymentsResponse['payment_gateways'][number]
18
+ type CodePaymentMethod = Gateway['payment_method']['code']
19
+
20
+ export default async (data: AppModuleBody) => {
21
+ const { application } = data;
22
+ const params = data.params as ListPaymentsParams;
23
+ // https://apx-mods.e-com.plus/api/v1/list_payments/schema.json?store_id=100
24
+ const amount = params.amount || { total: undefined, discount: undefined };
25
+ // const initialTotalAmount = amount.total;
26
+
27
+ const configApp = {
28
+ ...application.data,
29
+ ...application.hidden_data,
30
+ } as PagHiperApp;
31
+
32
+ // setup basic required response object
33
+ const response: ListPaymentsResponse = {
34
+ payment_gateways: [],
35
+ };
36
+
37
+ if (!configApp.paghiper_api_key) {
38
+ // must have configured PagHiper API key and token
39
+ return responseError(
40
+ 400,
41
+ 'LIST_PAYMENTS_ERR',
42
+ 'PagHiper API key is unset on app hidden data (merchant must configure the app)',
43
+ );
44
+ }
45
+
46
+ const intermediator = {
47
+ name: 'PagHiper',
48
+ link: 'https://www.paghiper.com/',
49
+ code: 'paghiper',
50
+ };
51
+
52
+ const listPaymentMethods = ['banking_billet', 'account_deposit'];
53
+
54
+ listPaymentMethods.forEach((paymentMethod) => {
55
+ const isPix = paymentMethod === 'account_deposit';
56
+ const minAmount = configApp.min_amount || isPix ? 3 : 0;
57
+ const methodConfig = isPix ? configApp.pix : configApp;
58
+
59
+ const methodEnable = isPix ? configApp?.pix?.enable : configApp?.pix?.disable_billet;
60
+
61
+ // Workaround for showcase
62
+ const validateAmount = amount.total ? (amount.total >= minAmount) : true;
63
+
64
+ if (methodEnable && validateAmount) {
65
+ let label = methodConfig?.label;
66
+ if (!label) {
67
+ if (isPix) {
68
+ label = 'Pagar com Pix';
69
+ } else {
70
+ label = (!params.lang || params.lang === 'pt_br') ? 'Boleto bancário' : 'Banking billet';
71
+ }
72
+ }
73
+
74
+ const gateway: Gateway = {
75
+ label,
76
+ icon: methodConfig?.icon,
77
+ text: methodConfig?.text,
78
+ payment_method: {
79
+ code: paymentMethod as CodePaymentMethod,
80
+ name: `${label} - ${intermediator.name}`,
81
+ },
82
+ intermediator,
83
+ };
84
+
85
+ if (!gateway.icon && isPix) {
86
+ gateway.icon = 'https://us-central1-ecom-pix.cloudfunctions.net/app/pix.png';
87
+ }
88
+
89
+ const discount = methodConfig?.discount;
90
+ gateway.discount = discount;
91
+
92
+ if (discount) {
93
+ if (discount.value > 0) {
94
+ if (amount.discount && (configApp.cumulative_discount === false)) {
95
+ // can't offer cumulative discount
96
+ delete gateway.discount;
97
+ return;
98
+ }
99
+
100
+ if (discount.apply_at !== 'freight') {
101
+ response.discount_option = {
102
+ apply_at: discount.apply_at,
103
+ type: discount.type,
104
+ value: discount.value,
105
+ label: `${label}`,
106
+ };
107
+ }
108
+
109
+ if (discount.min_amount) {
110
+ // check amount value to apply discount
111
+ if (amount.total && amount.total < discount.min_amount) {
112
+ delete gateway.discount;
113
+ } else {
114
+ delete discount.min_amount;
115
+ }
116
+ }
117
+ } else if (typeof discount.value !== 'number' || Number.isNaN(discount.value)) {
118
+ delete gateway.discount;
119
+ }
120
+ }
121
+
122
+ response.payment_gateways.push(gateway);
123
+ }
124
+ });
125
+
126
+ return response;
127
+ };
@@ -0,0 +1,17 @@
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 handleWebhook from './functions-lib/handle-webhook';
6
+
7
+ export const paghiper = {
8
+ webhook: functions
9
+ .region(config.get().httpsFunctionOptions.region)
10
+ .https.onRequest((req, res) => {
11
+ if (req.method !== 'POST') {
12
+ res.sendStatus(405);
13
+ } else {
14
+ handleWebhook(req, res);
15
+ }
16
+ }),
17
+ };
@@ -0,0 +1,12 @@
1
+ import type { AppModuleBody } from '@cloudcommerce/types';
2
+ import '@cloudcommerce/firebase/lib/init';
3
+ import handleListPayments from './paghiper-list-payments';
4
+ import handleCreateTransaction from './paghiper-create-transaction';
5
+
6
+ export const listPayments = async (modBody: AppModuleBody) => {
7
+ return handleListPayments(modBody);
8
+ };
9
+
10
+ export const createTransaction = async (modBody: AppModuleBody) => {
11
+ return handleCreateTransaction(modBody);
12
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": true
5
+ }
6
+ }
@@ -0,0 +1,34 @@
1
+ type DiscountConfig = {
2
+ apply_at?: 'total' | 'subtotal' | 'freight'
3
+ min_amount?: integer,
4
+ type?: 'percentage' | 'fixed'
5
+ value: number
6
+ }
7
+
8
+ export type PagHiperApp = {
9
+ paghiper_api_key?: string,
10
+ paghiper_token?: string
11
+ label?: string,
12
+ text?: string,
13
+ icon?: string,
14
+ discount?: DiscountConfig
15
+ cumulative_discount?: boolean
16
+ min_amount?: integer,
17
+ banking_billet_options?: {
18
+ days_due_date?: integer
19
+ fixed_description?: boolean,
20
+ late_payment_fine?: integer,
21
+ per_day_interest?: boolean
22
+ early_payment_discounts_cents?: integer,
23
+ early_payment_discounts_days?: integer,
24
+ open_after_day_due?: integer,
25
+ },
26
+ pix?: {
27
+ enable?: boolean,
28
+ disable_billet?: boolean,
29
+ label?: string,
30
+ text?: string,
31
+ icon?: string,
32
+ discount?: DiscountConfig,
33
+ },
34
+ }
package/webhook.js ADDED
@@ -0,0 +1 @@
1
+ export * from './lib/paghiper-webhook';