Skip to content

Commit 4e5cd05

Browse files
leomp12claude
andcommitted
fix(woovi): Auto-register webhooks and validate by authorization header
- Create webhooks for all relevant Pix events on first transaction - Store webhook IDs in Firestore to manage lifecycle - Validate incoming webhooks by authorization header - Map Woovi events to payment status (paid, voided, refunded) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3813f17 commit 4e5cd05

2 files changed

Lines changed: 83 additions & 21 deletions

File tree

packages/apps/woovi/src/woovi-create-transaction.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import type {
44
} from '@cloudcommerce/types';
55
import type { AxiosError } from 'axios';
66
import { randomUUID } from 'node:crypto';
7-
import { logger } from '@cloudcommerce/firebase/lib/config';
7+
import { getFirestore } from 'firebase-admin/firestore';
8+
import config, { logger } from '@cloudcommerce/firebase/lib/config';
89
import {
910
fullName as getFullname,
1011
phone as getPhone,
@@ -31,6 +32,7 @@ export default async (modBody: AppModuleBody<'create_transaction'>) => {
3132
message: 'AppID não configurado (lojista deve configurar o aplicativo)',
3233
};
3334
}
35+
const wooviKeyId = `${WOOVI_APP_ID}`.substring(0, 6) + `${WOOVI_APP_ID}`.slice(-3);
3436

3537
const {
3638
order_id: orderId,
@@ -113,6 +115,62 @@ export default async (modBody: AppModuleBody<'create_transaction'>) => {
113115
};
114116
}
115117

118+
const {
119+
storeId,
120+
httpsFunctionOptions,
121+
} = config.get();
122+
const locationId = httpsFunctionOptions.region;
123+
const appBaseUri = `https://${locationId}-${process.env.GCLOUD_PROJECT}.cloudfunctions.net`;
124+
const webhookUrl = `${appBaseUri}/woovi-webhook`;
125+
const docRef = getFirestore().doc('wooviSetup/webhook');
126+
const docSnap = await docRef.get();
127+
const webhookSetupData = docSnap.data();
128+
if (webhookSetupData?.wooviKeyId !== wooviKeyId) {
129+
const oldWebhookIds = webhookSetupData?.webhookIds as string[] | undefined;
130+
if (oldWebhookIds?.length) {
131+
await Promise.all(oldWebhookIds.map(async (id) => {
132+
return wooviAxios.delete(`/webhook/${id}`).catch(logger.warn);
133+
}));
134+
}
135+
const webhookEvents = [
136+
'OPENPIX:CHARGE_CREATED',
137+
'OPENPIX:CHARGE_COMPLETED',
138+
'OPENPIX:CHARGE_COMPLETED_NOT_SAME_CUSTOMER_PAYER',
139+
'PIX_AUTOMATIC_COBR_COMPLETED',
140+
'OPENPIX:CHARGE_EXPIRED',
141+
'OPENPIX:TRANSACTION_REFUND_RECEIVED',
142+
'PIX_TRANSACTION_REFUND_SENT_CONFIRMED',
143+
];
144+
const webhookIds: string[] = [];
145+
try {
146+
await Promise.all(webhookEvents.map(async (event) => {
147+
const { data } = await wooviAxios.post('/webhook', {
148+
webhook: {
149+
name: `ecom.plus #${storeId} ${event.replace('OPENPIX:', '').replace('PIX_', '')}`,
150+
event,
151+
url: webhookUrl,
152+
authorization: `${storeId}_${wooviKeyId}`,
153+
isActive: true,
154+
},
155+
});
156+
if (data.webhook?.id) {
157+
webhookIds.push(data.webhook.id);
158+
}
159+
}));
160+
await docRef.set({ wooviKeyId, webhookIds })
161+
.catch(logger.warn);
162+
} catch (_err) {
163+
const err = _err as AxiosError;
164+
logger.warn('Failed saving Woovi webhook', {
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+
}
172+
}
173+
116174
return { transaction };
117175
} catch (_err) {
118176
const err = _err as AxiosError;

packages/apps/woovi/src/woovi-events.ts

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import '@cloudcommerce/firebase/lib/init';
33
import type { Orders } from '@cloudcommerce/types';
44
import type { Request, Response } from 'firebase-functions/v1';
55
import * as functions from 'firebase-functions/v1';
6+
import { getFirestore } from 'firebase-admin/firestore';
67
import api from '@cloudcommerce/api';
78
import config, { logger } from '@cloudcommerce/firebase/lib/config';
8-
import getAppData from '@cloudcommerce/firebase/lib/helpers/get-app-data';
99

1010
type PaymentEntry = Exclude<Orders['payments_history'], undefined>[0]
1111

@@ -37,7 +37,7 @@ const listOrdersByTransaction = async (correlationID: string) => {
3737
const handleWebhook = async (req: Request, res: Response) => {
3838
const { body } = req;
3939
const event = body?.event as string | undefined;
40-
const charge = body?.charge || body?.pix;
40+
const charge = body?.charge || body?.pix?.charge;
4141
if (!event || !charge) {
4242
logger.warn('Woovi webhook missing event or charge', { body });
4343
return res.sendStatus(400);
@@ -48,30 +48,34 @@ const handleWebhook = async (req: Request, res: Response) => {
4848
return res.sendStatus(400);
4949
}
5050
logger.info(`> Woovi notification: ${event}`, { correlationID });
51+
const status = parseWooviStatus(event);
52+
if (!status) {
53+
logger.info(`Ignoring Woovi event: ${event}`);
54+
return res.sendStatus(204);
55+
}
5156

52-
if (!process.env.WOOVI_APP_ID) {
53-
const appData = await getAppData('woovi');
54-
if (appData.woovi_app_id) {
55-
process.env.WOOVI_APP_ID = appData.woovi_app_id;
56-
}
57+
const authorization = req.headers.authorization as string | undefined;
58+
if (!authorization) {
59+
logger.warn('Woovi webhook missing Authorization header');
60+
return res.sendStatus(401);
5761
}
58-
const { WOOVI_APP_ID } = process.env;
59-
if (!WOOVI_APP_ID) {
60-
logger.warn('Missing Woovi AppID');
62+
const docRef = getFirestore().doc('wooviSetup/webhook');
63+
const docSnap = await docRef.get();
64+
const webhookSetupData = docSnap.data();
65+
const { storeId } = config.get();
66+
const expectedAuth = webhookSetupData?.wooviKeyId
67+
? `${storeId}_${webhookSetupData.wooviKeyId}`
68+
: null;
69+
if (!expectedAuth || authorization !== expectedAuth) {
70+
logger.warn('Woovi webhook authorization mismatch', {
71+
authorization,
72+
expectedAuth,
73+
});
6174
return res.sendStatus(403);
6275
}
6376

6477
try {
65-
const status = parseWooviStatus(event);
66-
if (!status) {
67-
logger.info(`Ignoring Woovi event: ${event}`);
68-
return res.sendStatus(204);
69-
}
70-
71-
let orders: Orders[] = [];
72-
if (correlationID) {
73-
orders = await listOrdersByTransaction(correlationID);
74-
}
78+
const orders = await listOrdersByTransaction(correlationID);
7579
if (!orders.length) {
7680
logger.warn('Order not found for Woovi charge', { correlationID });
7781
return res.sendStatus(404);

0 commit comments

Comments
 (0)