Skip to content

Commit afd1572

Browse files
leomp12claude
andcommitted
fix(asaas): Enhance webhook validation and add payment status cron fallback
- Fix webhook auth token validation to match create-transaction format - Add asaasKeyId-based security validation - Refactor status parsing and payment update logic for reusability - Add cronCheckPayments function as fallback for missed webhook events - Improve error handling and logging for better observability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent e6cf7fb commit afd1572

File tree

1 file changed

+140
-51
lines changed

1 file changed

+140
-51
lines changed
Lines changed: 140 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,116 @@
11
/* eslint-disable import/prefer-default-export */
22
import type { Orders } from '@cloudcommerce/types';
3+
import type { AxiosError } from 'axios';
34
import api from '@cloudcommerce/api';
45
import '@cloudcommerce/firebase/lib/init';
56
import * as functions from 'firebase-functions/v1';
67
import config, { logger } from '@cloudcommerce/firebase/lib/config';
78
import getAppData from '@cloudcommerce/firebase/lib/helpers/get-app-data';
9+
import { getAsaasAxios } from './util/asaas-api';
810

911
type PaymentEntry = Exclude<Orders['payments_history'], undefined>[0]
1012

11-
const { httpsFunctionOptions } = config.get();
13+
const {
14+
storeId,
15+
httpsFunctionOptions,
16+
} = config.get();
17+
18+
const setAsaasEnv = async () => {
19+
if (!process.env.ASAAS_API_KEY) {
20+
const appData = await getAppData('asaas');
21+
process.env.ASAAS_API_KEY = appData.asaas_api_key;
22+
if (appData.asaas_sandbox) {
23+
process.env.ASAAS_ENV = 'sandbox';
24+
}
25+
}
26+
};
27+
28+
const parseAsaasStatus = (asaasStatus: string): PaymentEntry['status'] | null => {
29+
switch (asaasStatus) {
30+
case 'CREDIT_CARD_CAPTURE_REFUSED':
31+
return 'unauthorized';
32+
case 'AWAITING_CHARGEBACK_REVERSAL':
33+
case 'CHARGEBACK_DISPUTE':
34+
case 'CHARGEBACK_REQUESTED':
35+
return 'in_dispute';
36+
case 'RECEIVED_IN_CASH_UNDONE':
37+
case 'DELETED':
38+
return 'voided';
39+
case 'REFUND_IN_PROGRESS':
40+
case 'REFUNDED':
41+
return 'refunded';
42+
case 'PENDING':
43+
case 'RESTORED':
44+
return 'pending';
45+
case 'RECEIVED':
46+
case 'CONFIRMED':
47+
return 'paid';
48+
case 'REPROVED_BY_RISK_ANALYSIS':
49+
return 'unauthorized';
50+
case 'AWAITING_RISK_ANALYSIS':
51+
return 'under_analysis';
52+
default:
53+
return null;
54+
}
55+
};
56+
57+
const updatePaymentStatus = async ({
58+
orderId,
59+
transactionId,
60+
asaasStatus,
61+
flag = 'webhook',
62+
}: {
63+
orderId: Orders['_id'],
64+
transactionId?: string,
65+
asaasStatus: string,
66+
flag?: string,
67+
}) => {
68+
const status = parseAsaasStatus(asaasStatus);
69+
if (!status) {
70+
logger.warn(`Unexpected Asaas status for ${orderId}`, { asaasStatus });
71+
return;
72+
}
73+
await api.post(`orders/${orderId}/payments_history`, {
74+
date_time: new Date().toISOString(),
75+
status,
76+
transaction_id: transactionId,
77+
flags: ['asaas', flag, asaasStatus],
78+
});
79+
logger.info(`Updated ${orderId} to ${status}`);
80+
};
1281

1382
export const asaas = {
1483
webhook: functions
1584
.region(httpsFunctionOptions.region)
1685
.runWith(httpsFunctionOptions)
1786
.https.onRequest(async (req, res) => {
1887
const { body, headers } = req;
19-
const asaasStatus = body?.event;
88+
const asaasStatus = (body?.event as string | undefined)?.replace(/^PAYMENT_/, '');
2089
const asaasPaymentId = body?.payment?.id;
2190
if (req.method !== 'POST' || !asaasStatus || !asaasPaymentId) {
2291
res.sendStatus(405);
2392
return;
2493
}
25-
if (!process.env.ASAAS_API_KEY) {
26-
const appData = await getAppData('asaas');
27-
process.env.ASAAS_API_KEY = appData.asaas_api_key;
28-
if (appData.asaas_sandbox) {
29-
process.env.ASAAS_ENV = 'sandbox';
30-
}
31-
}
94+
await setAsaasEnv();
3295
const { ASAAS_API_KEY } = process.env;
3396
if (!ASAAS_API_KEY) {
3497
res.sendStatus(403);
3598
return;
3699
}
37-
if (headers['asaas-access-token'] !== `w1_${ASAAS_API_KEY}`) {
100+
const asaasKeyId = `${ASAAS_API_KEY}`.substring(0, 6) + `${ASAAS_API_KEY}`.slice(-3);
101+
if (headers['asaas-access-token'] !== `${storeId}_${asaasKeyId}`) {
38102
logger.warn('Unauthorized webhook', {
39-
headers,
103+
asaasStatus,
104+
asaasPaymentId,
40105
});
41106
res.sendStatus(401);
42107
return;
43108
}
44109
logger.info(`Asaas webhook ${asaasPaymentId} ${asaasStatus}`);
45-
let status: PaymentEntry['status'] = 'pending';
46-
switch (asaasStatus) {
47-
case 'PAYMENT_CREDIT_CARD_CAPTURE_REFUSED':
48-
status = 'unauthorized';
49-
break;
50-
case 'PAYMENT_AWAITING_CHARGEBACK_REVERSAL':
51-
case 'PAYMENT_CHARGEBACK_DISPUTE':
52-
case 'PAYMENT_CHARGEBACK_REQUESTED':
53-
status = 'in_dispute';
54-
break;
55-
case 'PAYMENT_RECEIVED_IN_CASH_UNDONE':
56-
case 'PAYMENT_DELETED':
57-
status = 'voided';
58-
break;
59-
case 'PAYMENT_REFUND_IN_PROGRESS':
60-
case 'PAYMENT_REFUNDED':
61-
status = 'refunded';
62-
break;
63-
case 'PAYMENT_RESTORED':
64-
status = 'pending';
65-
break;
66-
case 'PAYMENT_RECEIVED':
67-
case 'PAYMENT_CONFIRMED':
68-
status = 'paid';
69-
break;
70-
case 'PAYMENT_REPROVED_BY_RISK_ANALYSIS':
71-
status = 'unauthorized';
72-
break;
73-
case 'PAYMENT_AWAITING_RISK_ANALYSIS':
74-
status = 'under_analysis';
75-
break;
76-
default:
77-
// Ignore unknow status
78-
return;
110+
const status = parseAsaasStatus(asaasStatus);
111+
if (!status) {
112+
res.sendStatus(204);
113+
return;
79114
}
80115
const {
81116
data: { result: [order] },
@@ -94,16 +129,70 @@ export const asaas = {
94129
return intermediator?.transaction_id === String(asaasPaymentId);
95130
});
96131
if (transaction?._id) {
97-
await api.post(`orders/${order._id}/payments_history`, {
98-
date_time: new Date().toISOString(),
99-
status,
100-
transaction_id: transaction._id,
101-
flags: ['asaas', asaasStatus],
132+
await updatePaymentStatus({
133+
orderId: order._id,
134+
transactionId: transaction._id,
135+
asaasStatus,
102136
});
103-
logger.info(`Updated ${order._id} to ${status}`);
104137
res.sendStatus(201);
105138
return;
106139
}
107140
res.sendStatus(200);
108141
}),
142+
143+
cronCheckPayments: functions
144+
.region(config.get().httpsFunctionOptions.region)
145+
.runWith({ timeoutSeconds: 540 })
146+
.pubsub.schedule(process.env.CRONTAB_ASAAS_CHECK_PAYMENTS || '28 15,2 * * *')
147+
.timeZone('America/Sao_Paulo')
148+
.onRun(async () => {
149+
await setAsaasEnv();
150+
const { ASAAS_API_KEY } = process.env;
151+
if (!ASAAS_API_KEY) return;
152+
const d = new Date();
153+
const isOddHourExec = !!(d.getHours() % 2);
154+
d.setDate(d.getDate() - 30);
155+
const endpoint = 'orders'
156+
+ '?fields=_id,transactions'
157+
+ '&transactions.app.intermediator.code=asaas3'
158+
+ '&financial_status.current=pending'
159+
+ `&created_at>=${d.toISOString()}`
160+
+ `&sort=${(isOddHourExec ? '' : '-')}number`
161+
+ '&limit=500' as `orders?${string}`;
162+
const { data: { result: orders } } = await api.get(endpoint);
163+
logger.info(`${orders.length} orders listed`, {
164+
orderIds: orders.map(({ _id }) => _id),
165+
});
166+
const asaasAxios = await getAsaasAxios();
167+
for (let i = 0; i < orders.length; i++) {
168+
const order = orders[i];
169+
const transaction = order.transactions?.find(({ app }) => {
170+
return app?.intermediator?.code === 'asaas3';
171+
});
172+
const asaasPaymentId = transaction?.intermediator?.transaction_id;
173+
if (!asaasPaymentId) continue;
174+
let asaasStatus: string | undefined;
175+
try {
176+
// eslint-disable-next-line no-await-in-loop
177+
const { data } = await asaasAxios.get(`/v3/payments/${asaasPaymentId}/status`);
178+
asaasStatus = data.status;
179+
} catch (_err: any) {
180+
const err: AxiosError = _err;
181+
const status = err.response?.status;
182+
logger.warn(`Failed with ${status} reading Asaas status for ${order._id}`);
183+
if (status === 404 || status === 400) continue;
184+
logger.error(err);
185+
break;
186+
}
187+
if (!asaasStatus) continue;
188+
const status = parseAsaasStatus(asaasStatus);
189+
if (!status || status === 'pending') continue;
190+
updatePaymentStatus({
191+
orderId: order._id,
192+
transactionId: transaction._id,
193+
asaasStatus,
194+
flag: 'cron',
195+
});
196+
}
197+
}),
109198
};

0 commit comments

Comments
 (0)