@cloudcommerce/app-pix 0.0.127

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +1 -0
  3. package/LICENSE.md +230 -0
  4. package/README.md +1 -0
  5. package/lib/functions-lib/get-certificate.d.ts +2 -0
  6. package/lib/functions-lib/get-certificate.js +21 -0
  7. package/lib/functions-lib/get-certificate.js.map +1 -0
  8. package/lib/functions-lib/pix-auth/construtor.d.ts +15 -0
  9. package/lib/functions-lib/pix-auth/construtor.js +65 -0
  10. package/lib/functions-lib/pix-auth/construtor.js.map +1 -0
  11. package/lib/functions-lib/pix-auth/create-axios.d.ts +6 -0
  12. package/lib/functions-lib/pix-auth/create-axios.js +20 -0
  13. package/lib/functions-lib/pix-auth/create-axios.js.map +1 -0
  14. package/lib/functions-lib/pix-auth/oauth.d.ts +12 -0
  15. package/lib/functions-lib/pix-auth/oauth.js +36 -0
  16. package/lib/functions-lib/pix-auth/oauth.js.map +1 -0
  17. package/lib/index.d.ts +1 -0
  18. package/lib/index.js +3 -0
  19. package/lib/index.js.map +1 -0
  20. package/lib/pix-create-transaction.d.ts +73 -0
  21. package/lib/pix-create-transaction.js +221 -0
  22. package/lib/pix-create-transaction.js.map +1 -0
  23. package/lib/pix-list-payments.d.ts +14 -0
  24. package/lib/pix-list-payments.js +80 -0
  25. package/lib/pix-list-payments.js.map +1 -0
  26. package/lib/pix-webhook.d.ts +5 -0
  27. package/lib/pix-webhook.js +146 -0
  28. package/lib/pix-webhook.js.map +1 -0
  29. package/lib/pix.d.ts +77 -0
  30. package/lib/pix.js +13 -0
  31. package/lib/pix.js.map +1 -0
  32. package/package.json +36 -0
  33. package/src/functions-lib/get-certificate.ts +24 -0
  34. package/src/functions-lib/pix-auth/construtor.ts +87 -0
  35. package/src/functions-lib/pix-auth/create-axios.ts +24 -0
  36. package/src/functions-lib/pix-auth/oauth.ts +50 -0
  37. package/src/index.ts +2 -0
  38. package/src/pix-create-transaction.ts +261 -0
  39. package/src/pix-list-payments.ts +93 -0
  40. package/src/pix-webhook.ts +171 -0
  41. package/src/pix.ts +13 -0
  42. package/tsconfig.json +6 -0
  43. package/webhook.js +1 -0
@@ -0,0 +1,87 @@
1
+ import type { AxiosInstance } from 'axios';
2
+ import * as logger from 'firebase-functions/logger';
3
+ import { getFirestore } from 'firebase-admin/firestore';
4
+ import createAxios from './create-axios';
5
+ import oauth from './oauth';
6
+
7
+ type Option = {
8
+ clientId?: string;
9
+ clientSecret?: string;
10
+ oauthEndpoint?: any;
11
+ pfx?: any;
12
+ tokenData?: any;
13
+ baseURL?: string
14
+ }
15
+
16
+ const firestoreColl = 'pixTokens';
17
+
18
+ export default class Pix {
19
+ preparing: Promise<unknown>;
20
+ axios: AxiosInstance | undefined;
21
+
22
+ constructor(options: Option) {
23
+ const {
24
+ clientId,
25
+ clientSecret,
26
+ baseURL,
27
+ pfx,
28
+ tokenData,
29
+ } = options;
30
+
31
+ const self = this;
32
+
33
+ let documentRef;
34
+ if (firestoreColl) {
35
+ documentRef = getFirestore()
36
+ .doc(`${firestoreColl}/${clientId}:${clientSecret}`);
37
+ }
38
+
39
+ this.preparing = new Promise((resolve, reject) => {
40
+ const authenticate = (token: any) => {
41
+ self.axios = createAxios({ pfx, tokenData: token, baseURL });
42
+ resolve(self);
43
+ };
44
+
45
+ const fetchOauth = () => {
46
+ logger.log('>(App: Pix) OAuth');
47
+ oauth(options)
48
+ // eslint-disable-next-line no-shadow
49
+ .then((tokenData) => {
50
+ logger.log(`>(App: Pix) Token ${JSON.stringify(tokenData)}`);
51
+ authenticate(tokenData);
52
+ if (documentRef) {
53
+ documentRef.set({ tokenData }).catch(logger.error);
54
+ }
55
+ })
56
+ .catch(reject);
57
+ };
58
+
59
+ if (tokenData) {
60
+ authenticate(tokenData);
61
+ } else if (documentRef) {
62
+ documentRef.get()
63
+ .then((documentSnapshot) => {
64
+ if (documentSnapshot.exists) {
65
+ const token = documentSnapshot.get('tokenData');
66
+ const expiresIn = (tokenData && tokenData.expires_in) || 3600;
67
+ const deadline = Date.now() + 10000 - documentSnapshot.updateTime.toDate().getTime();
68
+
69
+ if (deadline <= expiresIn * 1000) {
70
+ authenticate(token);
71
+ } else {
72
+ fetchOauth();
73
+ }
74
+ } else {
75
+ fetchOauth();
76
+ }
77
+ })
78
+ .catch((err: any) => {
79
+ logger.error(err);
80
+ fetchOauth();
81
+ });
82
+ } else {
83
+ fetchOauth();
84
+ }
85
+ });
86
+ }
87
+ }
@@ -0,0 +1,24 @@
1
+ import * as https from 'https';
2
+ import axios from 'axios';
3
+
4
+ export default ({
5
+ pfx,
6
+ tokenData,
7
+ baseURL = 'https://api-pix.gerencianet.com.br',
8
+ }) => {
9
+ const httpsAgent = new https.Agent({
10
+ pfx,
11
+ passphrase: '',
12
+ });
13
+ const headers = {
14
+ 'Content-Type': 'application/json',
15
+ };
16
+ if (tokenData) {
17
+ const Authorization = typeof tokenData === 'string'
18
+ ? tokenData
19
+ : `${tokenData.token_type} ${tokenData.access_token}`;
20
+
21
+ Object.assign(headers, { Authorization });
22
+ }
23
+ return axios.create({ baseURL, headers, httpsAgent });
24
+ };
@@ -0,0 +1,50 @@
1
+ import createAxios from './create-axios';
2
+
3
+ type Option = {
4
+ clientId?: string;
5
+ clientSecret?: string;
6
+ oauthEndpoint?: any;
7
+ pfx?: any;
8
+ tokenData?: { [key: string]: any };
9
+ baseURL?: string
10
+ }
11
+
12
+ export default (options: Option) => new Promise((resolve, reject) => {
13
+ const {
14
+ clientId,
15
+ clientSecret,
16
+ oauthEndpoint,
17
+ pfx,
18
+ tokenData,
19
+ baseURL,
20
+ } = options;
21
+ const auth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
22
+ const axios = createAxios({
23
+ pfx,
24
+ tokenData,
25
+ baseURL,
26
+ });
27
+
28
+ const request = (isRetry?: boolean) => {
29
+ axios({
30
+ url: oauthEndpoint || '/oauth/token',
31
+ method: 'post',
32
+ headers: {
33
+ Authorization: `Basic ${auth}`,
34
+ },
35
+ data: {
36
+ grant_type: 'client_credentials',
37
+ },
38
+ })
39
+ .then(({ data }) => {
40
+ resolve(data);
41
+ })
42
+ .catch((err) => {
43
+ if (!isRetry && err.response && err.response.status >= 429) {
44
+ setTimeout(() => request(true), 4000);
45
+ }
46
+ reject(err);
47
+ });
48
+ };
49
+ request();
50
+ });
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ // eslint-disable-next-line import/prefer-default-export
2
+ export * from './pix';
@@ -0,0 +1,261 @@
1
+ import type { AxiosInstance } from 'axios';
2
+ import type { AppModuleBody } from '@cloudcommerce/types';
3
+ import type { CreateTransactionParams } from '@cloudcommerce/types/modules/create_transaction:params';
4
+ import type { CreateTransactionResponse } from '@cloudcommerce/types/modules/create_transaction:response';
5
+ import type { Firestore } from 'firebase-admin/firestore';
6
+ import * as logger from 'firebase-functions/logger';
7
+ import config from '@cloudcommerce/firebase/lib/config';
8
+ import axios from 'axios';
9
+ import { responseError } from './pix-list-payments';
10
+ import Pix from './functions-lib/pix-auth/construtor';
11
+ import getPfx from './functions-lib/get-certificate';
12
+
13
+ const saveToDb = (
14
+ firestore: Firestore,
15
+ clientId: string,
16
+ clientSecret: string,
17
+ pixApi: any,
18
+ pixAxios: AxiosInstance,
19
+ configApp: { [x: string]: any },
20
+ baseUri: string,
21
+ ) => {
22
+ return new Promise((resolve) => {
23
+ firestore.doc(`pixSetup/${clientId}:${clientSecret}`).get()
24
+ .then((documentSnapshot) => {
25
+ if (!documentSnapshot.exists || !documentSnapshot.get('hasWebhook')) {
26
+ const rand = String(Date.now());
27
+ return documentSnapshot.ref
28
+ .set({
29
+ pixApi,
30
+ rand,
31
+ createdAt: new Date().toISOString(),
32
+ })
33
+ .then(() => pixAxios({
34
+ url: `/v2/webhook/${configApp.pix_key}`,
35
+ method: 'PUT',
36
+ data: {
37
+ webhookUrl: `${baseUri}/pix-webhook/${rand}`,
38
+ },
39
+ }))
40
+
41
+ .then(() => {
42
+ documentSnapshot.ref
43
+ .set({ hasWebhook: true }, { merge: true })
44
+ .catch(logger.error);
45
+
46
+ resolve(true);
47
+ })
48
+ .catch((err) => {
49
+ logger.error('(App:Pix) Error saving firestore => ', err);
50
+ resolve(false);
51
+ });
52
+ }
53
+ resolve(false);
54
+ return null;
55
+ })
56
+ .catch((err) => {
57
+ logger.error('(App:Pix) Error saving firestore => ', err);
58
+ resolve(false);
59
+ });
60
+ });
61
+ };
62
+
63
+ export default async (appData: AppModuleBody, firestore: Firestore) => {
64
+ const locationId = config.get().httpsFunctionOptions.region;
65
+ const baseUri = `https://${locationId}-${process.env.GCLOUD_PROJECT}.cloudfunctions.net`;
66
+ // body was already pre-validated on @/bin/web.js
67
+ // treat module request body
68
+ const { application } = appData;
69
+ const params = appData.params as CreateTransactionParams;
70
+ // app configured options
71
+ const configApp = { ...application.data, ...application.hidden_data };
72
+
73
+ const {
74
+ amount,
75
+ buyer,
76
+ payer,
77
+ items,
78
+ } = params;
79
+
80
+ const orderId = params.order_id;
81
+ logger.log('>(App:Pix) Transaction #', orderId);
82
+
83
+ // https://apx-mods.e-com.plus/api/v1/create_transaction/response_schema.json?store_id=100
84
+ const transaction: CreateTransactionResponse['transaction'] = {
85
+ amount: amount.total,
86
+ };
87
+
88
+ const pixApi = configApp.pix_api;
89
+ let pfx;
90
+ try {
91
+ pfx = await getPfx(pixApi.certificate);
92
+ } catch (e) {
93
+ logger.error(e);
94
+ return responseError(409, 'INVALID_PIX_CERTIFICATE', 'Arquivo de certificado não encontrado ou inválido');
95
+ }
96
+
97
+ let baseURL: string | undefined;
98
+ if (pixApi.host && typeof pixApi.host === 'string') {
99
+ baseURL = pixApi.host.startsWith('http') ? pixApi.host : `https://${pixApi.host}`;
100
+ }
101
+ const isGerencianet = Boolean(baseURL && baseURL.indexOf('gerencianet.') > -1);
102
+ const clientId = pixApi.client_id;
103
+ const clientSecret = pixApi.client_secret;
104
+
105
+ const pix = new Pix({
106
+ clientId,
107
+ clientSecret,
108
+ pfx,
109
+ baseURL,
110
+ oauthEndpoint: pixApi.oauth_endpoint,
111
+ tokenData: pixApi.authorization,
112
+ });
113
+
114
+ let pixCob: { [key: string]: any } | undefined;
115
+
116
+ try {
117
+ await pix.preparing;
118
+
119
+ let txid = [...Array(32)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
120
+ let docNumber: string;
121
+ let registryType: string;
122
+ let fullname: string;
123
+
124
+ if (payer && payer.doc_number) {
125
+ docNumber = payer.doc_number;
126
+ registryType = payer.doc_number.length === 11 ? 'p' : 'j';
127
+ fullname = payer.fullname || buyer.fullname;
128
+ } else {
129
+ docNumber = buyer.doc_number;
130
+ registryType = buyer.registry_type;
131
+ fullname = buyer.fullname;
132
+ }
133
+
134
+ pixCob = {
135
+ calendario: {
136
+ expiracao: (configApp.pix_expiration || 60) * 60,
137
+ },
138
+ devedor: {
139
+ [registryType === 'j' ? 'cnpj' : 'cpf']: docNumber,
140
+ nome: fullname,
141
+ },
142
+ valor: {
143
+ original: amount.total.toFixed(2),
144
+ },
145
+ chave: configApp.pix_key,
146
+ infoAdicionais: [{
147
+ nome: 'pedido',
148
+ valor: `Pedido #${params.order_number} na loja ${params.domain}`,
149
+ }],
150
+ };
151
+
152
+ if (typeof configApp.pix_input === 'string' && configApp.pix_input.length > 3) {
153
+ const solicitacaoPagador = configApp.pix_input.substring(0, 140);
154
+ Object.assign(pixCob, { solicitacaoPagador });
155
+ }
156
+ if (typeof configApp.pix_info === 'string' && configApp.pix_info.length > 3) {
157
+ pixCob.infoAdicionais.unshift({
158
+ nome: 'texto',
159
+ valor: configApp.pix_info.substring(0, 200),
160
+ });
161
+ }
162
+
163
+ items.forEach((item, i) => {
164
+ if (pixCob) {
165
+ pixCob.infoAdicionais.push({
166
+ nome: `item_${(i + 1)}`,
167
+ valor: `${item.quantity}x ${(item.name || item.sku)}`.substring(0, 200),
168
+ });
169
+ }
170
+ });
171
+
172
+ pixCob.infoAdicionais.push({
173
+ nome: 'ecom_order_id',
174
+ valor: String(orderId),
175
+ });
176
+
177
+ if (pix.axios) {
178
+ let { data } = await pix.axios({
179
+ url: `/v2/cob/${txid}`,
180
+ method: 'PUT',
181
+ data: pixCob,
182
+ });
183
+ if (data) {
184
+ const { loc } = data;
185
+ txid = data.txid;
186
+
187
+ const location = (loc && loc.location) || data.location;
188
+ const pixCodeHost = 'https://gerarqrcodepix.com.br/api/v1';
189
+ const pixCodeParams = `&location=${location}`
190
+ + `&nome=${encodeURIComponent(configApp.pix_receiver || params.domain)}`
191
+ + `&cidade=${encodeURIComponent(configApp.pix_city || params.domain)}`;
192
+ const qrCodeUrl = `${pixCodeHost}?pim=12&saida=qr${pixCodeParams}`;
193
+ let qrCodeSrc = qrCodeUrl;
194
+ const brCodeUrl = `${pixCodeHost}?saida=br${pixCodeParams}`;
195
+
196
+ try {
197
+ if (isGerencianet && loc && loc.id) {
198
+ const res = await pix.axios.get(`/v2/loc/${loc.id}/qrcode`);
199
+ qrCodeSrc = res.data.imagemQrcode;
200
+ } else {
201
+ throw new Error('Retry');
202
+ }
203
+ } catch (err: any) {
204
+ data = (await axios.get(brCodeUrl)).data;
205
+ }
206
+
207
+ if (data) {
208
+ transaction.intermediator = {
209
+ payment_method: params.payment_method,
210
+ transaction_id: txid,
211
+ transaction_code: data.brcode || data.qrcode,
212
+ };
213
+ transaction.payment_link = qrCodeUrl;
214
+ transaction.notes = `<img src="${qrCodeSrc}" style="display:block;margin:0 auto">`;
215
+
216
+ await saveToDb(firestore, clientId, clientSecret, pixApi, pix.axios, configApp, baseUri);
217
+
218
+ return {
219
+ status: 200,
220
+ redirect_to_payment: false,
221
+ transaction,
222
+ };
223
+ }
224
+ return responseError(409, 'PIX_REQUEST_ERR_', 'QRCode not found');
225
+ }
226
+ return responseError(409, 'PIX_REQUEST_ERR_', 'Unexpected error creating charge');
227
+ }
228
+ return responseError(409, 'PIX_REQUEST_ERR_', 'Axios instance not created');
229
+ } catch (error: any) {
230
+ logger.error(error);
231
+ // try to debug request error
232
+ let errCode = 'PIX_COB_ERR_';
233
+ let { message } = error;
234
+
235
+ const err = {
236
+ message: `PIX_COB_ERR_ Order: #${orderId} => ${message}`,
237
+ payment: '',
238
+ status: 0,
239
+ response: '',
240
+ };
241
+
242
+ if (error.response) {
243
+ const { status, data } = error.response;
244
+ if (status !== 401 && status !== 403) {
245
+ err.payment = JSON.stringify(pixCob);
246
+ err.status = status;
247
+ errCode += status;
248
+ if (typeof data === 'object' && data) {
249
+ err.response = JSON.stringify(data);
250
+ } else {
251
+ err.response = data;
252
+ }
253
+ } else if (data && data.mensagem) {
254
+ message = data.mensagem;
255
+ }
256
+ }
257
+ // logger.error('(App:Pix) Error => ', err);
258
+
259
+ return responseError(409, errCode, message);
260
+ }
261
+ };
@@ -0,0 +1,93 @@
1
+ import type { AppModuleBody } from '@cloudcommerce/types';
2
+ import type { ListPaymentsParams } from '@cloudcommerce/types/modules/list_payments:params';
3
+ import type { ListPaymentsResponse } from '@cloudcommerce/types/modules/list_payments:response';
4
+
5
+ type Gateway = ListPaymentsResponse['payment_gateways'][number]
6
+
7
+ const responseError = (status: number | null, error: string, message: string) => {
8
+ return {
9
+ status: status || 409,
10
+ error,
11
+ message: `${message} (lojista deve configurar o aplicativo)`,
12
+ };
13
+ };
14
+
15
+ export default (data: AppModuleBody) => {
16
+ const { application } = data;
17
+ const params = data.params as ListPaymentsParams;
18
+ // https://apx-mods.e-com.plus/api/v1/list_payments/schema.json?store_id=100
19
+ const amount = params.amount || { total: undefined, discount: undefined };
20
+ // const initialTotalAmount = amount.total;
21
+
22
+ const configApp = {
23
+ ...application.data,
24
+ ...application.hidden_data,
25
+ };
26
+
27
+ if (!configApp.pix_key) {
28
+ return responseError(409, 'NO_PIX_KEY', 'Chave Pix não configurada');
29
+ }
30
+ const pixApi = configApp.pix_api;
31
+ if (!pixApi.certificate) {
32
+ return responseError(409, 'NO_PIX_CERTIFICATE', 'Certificado .PEM ou .P12 não configurado');
33
+ }
34
+ if ((!pixApi.client_id || !pixApi.client_secret) && !pixApi.authorization) {
35
+ return responseError(409, 'NO_PIX_CREDENTIALS', 'Client ID e/ou Secret não configurados');
36
+ }
37
+ const response: ListPaymentsResponse = {
38
+ payment_gateways: [],
39
+ };
40
+
41
+ // setup payment gateway object
42
+ const gateway: Gateway = {
43
+ label: 'Pagar com Pix',
44
+ // icon: `${baseUri}/pix.png`, // TODO: baseUri
45
+ icon: 'https://us-central1-ecom-pix.cloudfunctions.net/app/pix.png',
46
+ payment_method: {
47
+ code: 'account_deposit',
48
+ name: 'Pix',
49
+ },
50
+ intermediator: {
51
+ name: 'Pix',
52
+ link: 'https://www.bcb.gov.br/estabilidadefinanceira/pix',
53
+ code: 'pixapi',
54
+ },
55
+ ...configApp.gateway_options,
56
+ };
57
+
58
+ const { discount } = configApp;
59
+ if (discount && discount.value > 0
60
+ && (!amount.discount || discount.cumulative_discount !== false)) {
61
+ gateway.discount = {
62
+ apply_at: discount.apply_at,
63
+ type: discount.type,
64
+ value: discount.value,
65
+ };
66
+ if (discount.apply_at !== 'freight') {
67
+ // set as default discount option
68
+ response.discount_option = {
69
+ apply_at: discount.apply_at,
70
+ type: discount.type,
71
+ value: discount.value,
72
+ label: configApp.discount_option_label || 'Pix',
73
+ };
74
+ }
75
+
76
+ if (discount.min_amount) {
77
+ // check amount value to apply discount
78
+ if (amount.total && amount.total < discount.min_amount) {
79
+ delete gateway.discount;
80
+ }
81
+ if (response.discount_option) {
82
+ response.discount_option.min_amount = discount.min_amount;
83
+ }
84
+ }
85
+ }
86
+
87
+ response.payment_gateways.push(gateway);
88
+ return response;
89
+ };
90
+
91
+ export {
92
+ responseError,
93
+ };
@@ -0,0 +1,171 @@
1
+ /* eslint-disable import/prefer-default-export */
2
+ import type { Request, Response } from 'firebase-functions';
3
+ import '@cloudcommerce/firebase/lib/init';
4
+ import api from '@cloudcommerce/api';
5
+ import * as logger from 'firebase-functions/logger';
6
+ import { getFirestore } from 'firebase-admin/firestore';
7
+ import * as functions from 'firebase-functions/v1';
8
+ import config from '@cloudcommerce/firebase/lib/config';
9
+ import Pix from './functions-lib/pix-auth/construtor';
10
+ import getPfx from './functions-lib/get-certificate';
11
+
12
+ const handler = async (req: Request, res: Response, rand: string) => {
13
+ try {
14
+ const querySnapshot = await getFirestore()
15
+ .collection('pixSetup')
16
+ .where('rand', '==', rand)
17
+ .limit(1)
18
+ .get();
19
+
20
+ if (querySnapshot && !querySnapshot.empty) {
21
+ const documentSnapshot = querySnapshot.docs[0];
22
+
23
+ const setHookState = (isValidating = false) => {
24
+ return documentSnapshot.ref
25
+ .set({ isValidating }, { merge: true });
26
+ };
27
+
28
+ if (req.body) {
29
+ logger.log(`> (App:Pix) Webhook (${rand}) ${JSON.stringify(req.body)}`);
30
+ const pixHooks = req.body.pix;
31
+ if (Array.isArray(pixHooks) && pixHooks.length) {
32
+ if (!documentSnapshot.exists) {
33
+ return res.sendStatus(401);
34
+ }
35
+ const pixApi = documentSnapshot.get('pixApi');
36
+ if (!pixApi) {
37
+ return res.sendStatus(410);
38
+ }
39
+ if (documentSnapshot.get('isValidating')) {
40
+ setHookState();
41
+ }
42
+
43
+ let pfx;
44
+ try {
45
+ pfx = await getPfx(pixApi.certificate);
46
+ } catch (err) {
47
+ logger.error(err);
48
+ return res.sendStatus(409);
49
+ }
50
+
51
+ let baseURL: string | undefined;
52
+ if (pixApi.host && typeof pixApi.host === 'string') {
53
+ baseURL = pixApi.host.startsWith('http') ? pixApi.host : `https://${pixApi.host}`;
54
+ }
55
+
56
+ const pix = new Pix({
57
+ clientId: pixApi.client_id,
58
+ clientSecret: pixApi.client_secret,
59
+ pfx,
60
+ baseURL,
61
+ oauthEndpoint: pixApi.oauth_endpoint,
62
+ tokenData: pixApi.authorization,
63
+ });
64
+
65
+ await pix.preparing;
66
+
67
+ await Promise.all(pixHooks.map(async ({ endToEndId, txid }) => {
68
+ if (pix.axios) {
69
+ const { data } = await pix.axios.get(`/v2/cob/${txid}`);
70
+ if (data) {
71
+ logger.log(`> (App:Pix) Read ${txid} ${JSON.stringify(data)}`);
72
+ const { status, infoAdicionais } = data;
73
+ const orderInfo = infoAdicionais.find(({ nome }) => nome === 'ecom_order_id');
74
+ if (orderInfo) {
75
+ const orderId = orderInfo.valor;
76
+ let financialStatus: string | undefined;
77
+
78
+ switch (status.toUpperCase()) {
79
+ case 'CONCLUIDA':
80
+ if (
81
+ Array.isArray(data.pix)
82
+ && data.pix.find(({ valor, devolucoes }) => {
83
+ if (
84
+ data.valor
85
+ && data.valor.original
86
+ && valor < data.valor.original
87
+ ) {
88
+ return false;
89
+ }
90
+
91
+ return devolucoes && devolucoes.find(
92
+ (devolucao: { status: string; }) => {
93
+ return devolucao.status === 'DEVOLVIDO';
94
+ },
95
+ );
96
+ })
97
+ ) {
98
+ financialStatus = 'refunded';
99
+ } else {
100
+ financialStatus = 'paid';
101
+ }
102
+ break;
103
+
104
+ case 'DEVOLVIDO':
105
+ financialStatus = 'refunded';
106
+ break;
107
+ default:
108
+ if (status.startsWith('REMOVIDA_')) {
109
+ financialStatus = 'voided';
110
+ }
111
+ }
112
+
113
+ if (orderId && financialStatus) {
114
+ const bodyPaymentHistory = {
115
+ date_time: new Date().toISOString(),
116
+ status: financialStatus,
117
+ notification_code: endToEndId,
118
+ flags: ['pixapi'],
119
+ } as any; // TODO: incompatible type=> amount and status;
120
+
121
+ await api.post(
122
+ `orders/${orderId}/payments_history`,
123
+ bodyPaymentHistory,
124
+ );
125
+ }
126
+ }
127
+ return data;
128
+ }
129
+ }
130
+ return true;
131
+ }));
132
+
133
+ return res.sendStatus(200);
134
+ }
135
+ // not create axios instance
136
+ return res.sendStatus(500);
137
+ }
138
+ // body not found
139
+ if (!documentSnapshot.exists || !documentSnapshot.get('isValidating')) {
140
+ setHookState(true).catch(logger.error);
141
+ return res.sendStatus(503);
142
+ }
143
+ logger.log('> (App:Pix) Webhook ignored');
144
+ setHookState().catch(logger.error);
145
+ return res.sendStatus(200);
146
+ }
147
+ return res.sendStatus(410);
148
+ } catch (err: any) {
149
+ logger.error(err);
150
+ if (!res.headersSent) {
151
+ return res.sendStatus(502);
152
+ }
153
+ //
154
+ return res.sendStatus(500);
155
+ }
156
+ };
157
+
158
+ export const pix = {
159
+ webhook: functions
160
+ .region(config.get().httpsFunctionOptions.region)
161
+ .https.onRequest(async (req, res) => {
162
+ const { method } = req;
163
+ const rand = req.url.split('/')[1];
164
+
165
+ if (method === 'POST' || method === 'PUT') {
166
+ handler(req, res, rand);
167
+ } else {
168
+ res.sendStatus(405);
169
+ }
170
+ }),
171
+ };