Skip to content

Commit 77eb73b

Browse files
committed
feat(yapay): Setup new Yapay (Vindi Pagamentos) integration app
1 parent ed24d19 commit 77eb73b

File tree

17 files changed

+512
-3
lines changed

17 files changed

+512
-3
lines changed

packages/apps/asaas/src/asaas-list-payments.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const asaasListPayments = async (modBody: AppModuleBody<'list_payments'>)
2121
process.env.ASAAS_ENV = appData.asaas_sandbox
2222
? 'sandbox'
2323
: process.env.ASAAS_ENV || 'live';
24-
if (!process.env.ASAAS_ENV) {
24+
if (!process.env.ASAAS_API_KEY) {
2525
return {
2626
error: 'NO_ASAAS_KEY',
2727
message: 'Chave de API não configurada (lojista deve configurar o aplicativo)',

packages/apps/yapay/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Please refer to GitHub [repository releases](https://github.com/ecomplus/cloud-commerce/releases) or monorepo unified [CHANGELOG.md](https://github.com/ecomplus/cloud-commerce/blob/main/CHANGELOG.md).

packages/apps/yapay/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# `@cloudcommerce/app-yapay`

packages/apps/yapay/events.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './lib/yapay-events.js';

packages/apps/yapay/package.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"name": "@cloudcommerce/app-yapay",
3+
"type": "module",
4+
"version": "0.0.0",
5+
"description": "e-com.plus Cloud Commerce app to integrate Yapay (Vindi Pagamentos)",
6+
"main": "lib/yapay.js",
7+
"exports": {
8+
".": "./lib/yapay.js",
9+
"./events": "./lib/yapay-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/yapay"
22+
},
23+
"author": "E-Com Club Softwares para E-commerce <[email protected]>",
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/yapay#readme",
29+
"scripts": {
30+
"build": "bash ../../../scripts/build-lib.sh"
31+
},
32+
"dependencies": {
33+
"@cloudcommerce/api": "workspace:*",
34+
"@cloudcommerce/firebase": "workspace:*",
35+
"@ecomplus/utils": "1.5.0-rc.6",
36+
"axios": "^1.13.2",
37+
"firebase-admin": "^13.6.0",
38+
"firebase-functions": "^6.6.0"
39+
},
40+
"devDependencies": {
41+
"@cloudcommerce/types": "workspace:*"
42+
}
43+
}

packages/apps/yapay/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// eslint-disable-next-line import/prefer-default-export
2+
export * from './yapay';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import axios from 'axios';
2+
3+
export const getYapayAxios = async () => {
4+
const { YAPAY_API_TOKEN } = process.env;
5+
const baseURL = 'https://api.intermediador.yapay.com.br/api/v3/';
6+
const yapayAxios = axios.create({
7+
baseURL,
8+
headers: {
9+
'User-Agent': 'ecomplus/1.0.0 (Node.js; prod)',
10+
},
11+
});
12+
yapayAxios.interceptors.request.use((config) => {
13+
if (config.data && typeof config.data === 'object' && !config.data.token) {
14+
config.data.token = YAPAY_API_TOKEN;
15+
} else {
16+
config.params = {
17+
token_account: YAPAY_API_TOKEN,
18+
...config.params,
19+
};
20+
}
21+
return config;
22+
});
23+
return yapayAxios;
24+
};
25+
26+
export default getYapayAxios;
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import type {
2+
AppModuleBody,
3+
CreateTransactionResponse,
4+
} from '@cloudcommerce/types';
5+
import type { AxiosError } from 'axios';
6+
import config, { logger } from '@cloudcommerce/firebase/lib/config';
7+
import {
8+
fullName as getFullname,
9+
phone as getPhone,
10+
price as getPrice,
11+
} from '@ecomplus/utils';
12+
import { getYapayAxios } from './util/yapay-api';
13+
14+
export default async (modBody: AppModuleBody<'create_transaction'>) => {
15+
const {
16+
application,
17+
params,
18+
} = modBody;
19+
const appData = {
20+
...application.data,
21+
...application.hidden_data,
22+
};
23+
if (appData.yapay_api_token) {
24+
process.env.YAPAY_API_TOKEN = appData.yapay_api_token;
25+
}
26+
const { YAPAY_API_TOKEN } = process.env;
27+
if (!YAPAY_API_TOKEN) {
28+
logger.warn('Checkout missing Yapay API Token');
29+
return {
30+
error: 'NO_YAPAY_TOKEN',
31+
message: 'Token da conta não configurado (lojista deve configurar o aplicativo)',
32+
};
33+
}
34+
35+
const locationId = config.get().httpsFunctionOptions.region;
36+
const webhookUri = `https://${locationId}-${process.env.GCLOUD_PROJECT}.cloudfunctions.net`
37+
+ '/yapay-webhook';
38+
39+
const {
40+
order_id: orderId,
41+
order_number: orderNumber,
42+
payment_method: paymentMethod,
43+
amount,
44+
buyer,
45+
items,
46+
billing_address: billingAddr,
47+
to: shippingAddr,
48+
} = params;
49+
const customerAddr = billingAddr || shippingAddr;
50+
const yapayAxios = await getYapayAxios();
51+
const transaction: CreateTransactionResponse['transaction'] = {
52+
amount: amount.total,
53+
status: {
54+
current: 'pending',
55+
},
56+
};
57+
58+
if (paymentMethod.code !== 'account_deposit') {
59+
return {
60+
error: 'PAYMENT_METHOD_NOT_SUPPORTED',
61+
message: 'Apenas Pix é suportado no momento',
62+
};
63+
}
64+
65+
try {
66+
const phone = getPhone(buyer);
67+
const phoneContacts: Array<Record<string, any>> = [];
68+
if (phone) {
69+
const phoneNumber = phone.replace(/\D/g, '');
70+
if (phoneNumber.length === 11) {
71+
phoneContacts.push({
72+
type_contact: 'M',
73+
number_contact: phoneNumber,
74+
});
75+
} else if (phoneNumber.length === 10) {
76+
phoneContacts.push({
77+
type_contact: 'H',
78+
number_contact: phoneNumber,
79+
});
80+
}
81+
}
82+
83+
const yapayTransaction = {
84+
customer: {
85+
contacts: phoneContacts.length > 0 ? phoneContacts : undefined,
86+
addresses: customerAddr ? [{
87+
type_address: 'B',
88+
postal_code: customerAddr.zip,
89+
street: customerAddr.street,
90+
number: customerAddr.number?.toString() || 'S/N',
91+
completion: customerAddr.complement,
92+
neighborhood: customerAddr.borough,
93+
city: customerAddr.city,
94+
state: customerAddr.province_code,
95+
}] : undefined,
96+
name: buyer.fullname || getFullname(buyer) || buyer.email,
97+
birth_date: buyer.birth_date?.day && buyer.birth_date?.month && buyer.birth_date?.year
98+
? `${buyer.birth_date.day.toString().padStart(2, '0')}/`
99+
+ `${buyer.birth_date.month.toString().padStart(2, '0')}/`
100+
+ `${buyer.birth_date.year}`
101+
: undefined,
102+
[buyer.registry_type === 'p' ? 'cpf' : 'cnpj']: buyer.doc_number,
103+
email: buyer.email,
104+
},
105+
transaction_product: items.map((item, index) => ({
106+
description: item.name || item.product_id,
107+
quantity: item.quantity.toString(),
108+
price_unit: getPrice(item)?.toFixed(2) || '0.00',
109+
code: (index + 1).toString(),
110+
sku_code: item.sku || item.product_id,
111+
extra: item.variation_id ? `Variação: ${item.variation_id}` : undefined,
112+
})),
113+
transaction: {
114+
available_payment_methods: '27',
115+
customer_ip: params.client_ip,
116+
shipping_type: 'Outro',
117+
shipping_price: amount.freight?.toFixed(2) || '0',
118+
price_discount: amount.discount?.toFixed(2) || '',
119+
url_notification: webhookUri,
120+
free: `Pedido ${orderNumber}`,
121+
},
122+
payment: {
123+
payment_method_id: '27',
124+
},
125+
};
126+
127+
const { data: yapayResponse } = await yapayAxios.post(
128+
'/transactions/payment',
129+
yapayTransaction,
130+
);
131+
if (yapayResponse.message_response?.message !== 'success') {
132+
throw new Error(
133+
yapayResponse.error_response?.message
134+
|| yapayResponse.message_response?.message
135+
|| 'Erro ao criar transação',
136+
);
137+
}
138+
const yapayData = yapayResponse.data_response.transaction;
139+
transaction.intermediator = {
140+
transaction_id: yapayData.transaction_id?.toString(),
141+
transaction_reference: yapayData.order_number?.toString(),
142+
transaction_code: yapayData.payment?.qrcode_original_path?.toString(),
143+
payment_method: {
144+
code: 'account_deposit',
145+
name: yapayData.payment?.payment_method_name,
146+
},
147+
};
148+
if (yapayData.payment.url_payment) {
149+
transaction.payment_link = yapayData.payment.url_payment;
150+
}
151+
if (yapayData.payment.qrcode_path) {
152+
transaction.notes = `<img src="${yapayData.payment.qrcode_path}" style="display:block;margin:0 auto">`;
153+
}
154+
if (yapayData.max_days_to_keep_waiting_payment) {
155+
transaction.account_deposit = {
156+
valid_thru: new Date(yapayData.max_days_to_keep_waiting_payment).toISOString(),
157+
};
158+
}
159+
160+
return { transaction };
161+
} catch (_err) {
162+
const err = _err as AxiosError;
163+
logger.warn(`Failed payment for ${orderId}`, {
164+
orderNumber,
165+
url: err.config?.url,
166+
request: err.config?.data,
167+
response: err.response?.data,
168+
status: err.response?.status,
169+
});
170+
logger.error(err);
171+
return {
172+
error: 'YAPAY_TRANSACTION_ERROR',
173+
message: err.message,
174+
};
175+
}
176+
};
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/* eslint-disable import/prefer-default-export */
2+
import '@cloudcommerce/firebase/lib/init';
3+
import type { Orders } from '@cloudcommerce/types';
4+
import type { Request, Response } from 'firebase-functions/v1';
5+
import * as functions from 'firebase-functions/v1';
6+
import api from '@cloudcommerce/api';
7+
import { Endpoint } from '@cloudcommerce/api/types';
8+
import config, { logger } from '@cloudcommerce/firebase/lib/config';
9+
import getAppData from '@cloudcommerce/firebase/lib/helpers/get-app-data';
10+
import { getYapayAxios } from './util/yapay-api';
11+
12+
type PaymentEntry = Exclude<Orders['payments_history'], undefined>[0]
13+
14+
const parseYapayStatus = (statusId: number): PaymentEntry['status'] => {
15+
switch (statusId) {
16+
case 4:
17+
return 'pending';
18+
case 5:
19+
return 'under_analysis';
20+
case 6:
21+
return 'paid';
22+
case 7:
23+
return 'voided';
24+
case 24:
25+
return 'refunded';
26+
case 87:
27+
return 'unauthorized';
28+
default:
29+
return 'pending';
30+
}
31+
};
32+
33+
const listOrdersByTransaction = async (transactionId: string) => {
34+
const filters = `?transactions.intermediator.transaction_id=${transactionId}`
35+
+ '&fields=_id,transactions._id,transactions.app,transactions.intermediator,transactions.status';
36+
const { result } = (await api.get(`/orders${filters}` as Endpoint)).data;
37+
return result as Orders[];
38+
};
39+
40+
const handleWebhook = async (req: Request, res: Response) => {
41+
const { body } = req;
42+
const transactionToken = body.token_transaction;
43+
if (!transactionToken) {
44+
return res.sendStatus(400);
45+
}
46+
47+
logger.info(`> Yapay notification for ${transactionToken}`);
48+
if (!process.env.YAPAY_API_TOKEN) {
49+
const appData = await getAppData('yapay');
50+
if (appData.yapay_api_token) {
51+
process.env.YAPAY_API_TOKEN = appData.yapay_api_token;
52+
}
53+
}
54+
const { YAPAY_API_TOKEN } = process.env;
55+
if (!YAPAY_API_TOKEN) {
56+
logger.warn('Missing Yapay API Token');
57+
return res.sendStatus(403);
58+
}
59+
60+
try {
61+
const yapayAxios = await getYapayAxios();
62+
const { data: yapayResponse } = await yapayAxios.get('/transactions/get_by_token', {
63+
params: {
64+
token_transaction: transactionToken,
65+
},
66+
});
67+
if (yapayResponse.message_response?.message !== 'success') {
68+
logger.error('Yapay API error', { yapayResponse });
69+
return res.sendStatus(500);
70+
}
71+
72+
const yapayData = yapayResponse.data_response.transaction;
73+
const yapayTransactionId = yapayData.transaction_id.toString();
74+
const status = parseYapayStatus(yapayData.status_id);
75+
logger.info(`Yapay ${yapayTransactionId} -> '${status}' (${yapayData.status_id})`, {
76+
yapayData,
77+
});
78+
const orders = await listOrdersByTransaction(yapayTransactionId);
79+
if (!orders.length) {
80+
logger.warn(`Order not found for transaction ${yapayTransactionId}`);
81+
return res.sendStatus(404);
82+
}
83+
84+
await Promise.all(orders.map(async (order) => {
85+
const { _id: orderId, transactions } = order;
86+
let transactionId: string | undefined;
87+
if (transactions) {
88+
const transaction = transactions.find(
89+
(t) => t.intermediator?.transaction_id === yapayTransactionId,
90+
);
91+
if (transaction) {
92+
if (transaction.status?.current === status) {
93+
return;
94+
}
95+
transactionId = transaction._id;
96+
}
97+
}
98+
await api.post(`orders/${orderId}/payments_history`, {
99+
date_time: new Date().toISOString(),
100+
status,
101+
transaction_id: transactionId,
102+
flags: ['yapay', `${yapayData.status_id}`],
103+
});
104+
}));
105+
106+
return res.sendStatus(200);
107+
} catch (err: any) {
108+
logger.error('Yapay webhook error', err);
109+
return res.status(500).send({
110+
error: 'yapay_webhook_error',
111+
message: err.message,
112+
});
113+
}
114+
};
115+
116+
const { httpsFunctionOptions } = config.get();
117+
118+
export const yapay = {
119+
webhook: functions
120+
.region(httpsFunctionOptions.region)
121+
.runWith(httpsFunctionOptions)
122+
.https.onRequest((req, res) => {
123+
if (req.method !== 'POST') {
124+
res.sendStatus(405);
125+
} else {
126+
handleWebhook(req, res);
127+
}
128+
}),
129+
};

0 commit comments

Comments
 (0)