@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.
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +1 -0
- package/LICENSE.md +230 -0
- package/README.md +1 -0
- package/lib/functions-lib/get-certificate.d.ts +2 -0
- package/lib/functions-lib/get-certificate.js +21 -0
- package/lib/functions-lib/get-certificate.js.map +1 -0
- package/lib/functions-lib/pix-auth/construtor.d.ts +15 -0
- package/lib/functions-lib/pix-auth/construtor.js +65 -0
- package/lib/functions-lib/pix-auth/construtor.js.map +1 -0
- package/lib/functions-lib/pix-auth/create-axios.d.ts +6 -0
- package/lib/functions-lib/pix-auth/create-axios.js +20 -0
- package/lib/functions-lib/pix-auth/create-axios.js.map +1 -0
- package/lib/functions-lib/pix-auth/oauth.d.ts +12 -0
- package/lib/functions-lib/pix-auth/oauth.js +36 -0
- package/lib/functions-lib/pix-auth/oauth.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -0
- package/lib/pix-create-transaction.d.ts +73 -0
- package/lib/pix-create-transaction.js +221 -0
- package/lib/pix-create-transaction.js.map +1 -0
- package/lib/pix-list-payments.d.ts +14 -0
- package/lib/pix-list-payments.js +80 -0
- package/lib/pix-list-payments.js.map +1 -0
- package/lib/pix-webhook.d.ts +5 -0
- package/lib/pix-webhook.js +146 -0
- package/lib/pix-webhook.js.map +1 -0
- package/lib/pix.d.ts +77 -0
- package/lib/pix.js +13 -0
- package/lib/pix.js.map +1 -0
- package/package.json +36 -0
- package/src/functions-lib/get-certificate.ts +24 -0
- package/src/functions-lib/pix-auth/construtor.ts +87 -0
- package/src/functions-lib/pix-auth/create-axios.ts +24 -0
- package/src/functions-lib/pix-auth/oauth.ts +50 -0
- package/src/index.ts +2 -0
- package/src/pix-create-transaction.ts +261 -0
- package/src/pix-list-payments.ts +93 -0
- package/src/pix-webhook.ts +171 -0
- package/src/pix.ts +13 -0
- package/tsconfig.json +6 -0
- 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,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
|
+
};
|