Skip to content

Commit 4511ab5

Browse files
committed
feat: handle webhook with quickbooks
1 parent c846e11 commit 4511ab5

File tree

5 files changed

+79
-22
lines changed

5 files changed

+79
-22
lines changed

apps/erp/app/routes/api+/integrations.quickbooks.oauth.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,17 @@ export async function loader({ request }: LoaderFunctionArgs) {
7070
id: QuickBooks.id,
7171
active: true,
7272
// @ts-ignore
73-
metadata: auth,
73+
metadata: {
74+
...auth,
75+
tenantId: data.realmId,
76+
},
7477
updatedBy: userId,
7578
companyId: companyId,
7679
}
7780
);
7881

82+
console.log({ createdQuickBooksIntegration });
83+
7984
if (createdQuickBooksIntegration?.data?.metadata) {
8085
const requestUrl = new URL(request.url);
8186

apps/erp/app/routes/api+/webhook.quickbooks.ts

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getCarbonServiceRole } from "@carbon/auth";
1+
import { getCarbonServiceRole, QUICKBOOKS_WEBHOOK_SECRET } from "@carbon/auth";
22

33
import type { ActionFunctionArgs } from "@vercel/remix";
44
import { json } from "@vercel/remix";
@@ -9,17 +9,34 @@ export const config = {
99
runtime: "nodejs",
1010
};
1111

12-
const integrationValidator = z.object({
13-
webhookToken: z.string(),
12+
const quickbooksEventValidator = z.object({
13+
eventNotifications: z.array(
14+
z.object({
15+
realmId: z.string(),
16+
dataChangeEvent: z.object({
17+
entities: z.array(
18+
z.object({
19+
id: z.string(),
20+
name: z.string(),
21+
operation: z.enum(["Create", "Update", "Delete"]),
22+
})
23+
),
24+
}),
25+
})
26+
),
1427
});
1528

1629
function verifyQuickBooksSignature(
1730
payload: string,
18-
signature: string,
19-
webhookToken: string
31+
signature: string
2032
): boolean {
33+
if (!QUICKBOOKS_WEBHOOK_SECRET) {
34+
console.warn("QUICKBOOKS_WEBHOOK_SECRET is not set");
35+
return true;
36+
}
37+
2138
const expectedSignature = crypto
22-
.createHmac("sha256", webhookToken)
39+
.createHmac("sha256", QUICKBOOKS_WEBHOOK_SECRET)
2340
.update(payload)
2441
.digest("base64");
2542

@@ -32,8 +49,48 @@ function verifyQuickBooksSignature(
3249
export async function action({ request, params }: ActionFunctionArgs) {
3350
const serviceRole = await getCarbonServiceRole();
3451

35-
const payload = await request.json();
36-
console.log({ payload });
52+
const payload = await request.clone().json();
53+
54+
const parsedPayload = quickbooksEventValidator.safeParse(payload);
55+
if (!parsedPayload.success) {
56+
return json({ success: false }, { status: 400 });
57+
}
58+
59+
const payloadText = await request.text();
60+
const signatureHeader = request.headers.get("intuit-signature");
61+
62+
if (!signatureHeader) {
63+
return json({ success: false }, { status: 401 });
64+
}
65+
66+
const requestIsValid = verifyQuickBooksSignature(
67+
payloadText,
68+
signatureHeader
69+
);
70+
71+
if (!requestIsValid) {
72+
return json({ success: false }, { status: 401 });
73+
}
74+
75+
const events = parsedPayload.data.eventNotifications;
76+
for await (const event of events) {
77+
const { realmId, dataChangeEvent } = event;
78+
79+
const companyIntegration = await serviceRole
80+
.from("companyIntegration")
81+
.select("*")
82+
.eq("metadata->>tenantId", realmId)
83+
.eq("id", "quickbooks")
84+
.single();
85+
86+
console.log({ companyIntegration });
87+
88+
const { entities } = dataChangeEvent;
89+
for await (const entity of entities) {
90+
const { id, name, operation } = entity;
91+
console.log({ realmId, id, name, operation });
92+
}
93+
}
3794

3895
// const quickbooksIntegration = await serviceRole
3996
// .from("companyIntegration")
@@ -48,17 +105,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
48105
// );
49106

50107
// const payloadText = await request.text();
51-
// const signatureHeader = request.headers.get("intuit-signature");
52-
53-
// if (!signatureHeader) {
54-
// return json({ success: false }, { status: 401 });
55-
// }
56-
57-
// if (
58-
// !verifyQuickBooksSignature(payloadText, signatureHeader, webhookToken)
59-
// ) {
60-
// return json({ success: false }, { status: 401 });
61-
// }
108+
//
62109

63110
// const payload = JSON.parse(payloadText);
64111
// console.log("QuickBooks webhook payload", payload);

apps/erp/vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export default defineConfig({
3333
},
3434
server: {
3535
port: 3000,
36-
allowedHosts: ["8aef5c58f593.ngrok-free.app"],
36+
allowedHosts: ["34cf759c5276.ngrok-free.app"],
3737
},
3838
plugins: [
3939
remix({

packages/auth/src/config/env.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ declare global {
2828
POSTHOG_API_HOST: string;
2929
POSTHOG_PROJECT_PUBLIC_KEY: string;
3030
QUICKBOOKS_CLIENT_SECRET: string;
31+
QUICKBOOKS_WEBHOOK_SECRET: string;
3132
RESEND_API_KEY: string;
3233
RESEND_DOMAIN: string;
3334
SESSION_SECRET: string;
@@ -135,6 +136,11 @@ export const QUICKBOOKS_CLIENT_SECRET = getEnv("QUICKBOOKS_CLIENT_SECRET", {
135136
isSecret: true,
136137
});
137138

139+
export const QUICKBOOKS_WEBHOOK_SECRET = getEnv("QUICKBOOKS_WEBHOOK_SECRET", {
140+
isRequired: false,
141+
isSecret: true,
142+
});
143+
138144
export const RESEND_DOMAIN =
139145
getEnv("RESEND_DOMAIN", {
140146
isRequired: false,

packages/ee/src/accounting/providers/quickbooks.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ export class QuickBooksProvider extends CoreProvider {
7575
accessToken: data.access_token,
7676
refreshToken: data.refresh_token,
7777
expiresAt: new Date(Date.now() + data.expires_in * 1000),
78-
tenantId: data.realmId, // QuickBooks uses realmId as tenant identifier
7978
};
8079
}
8180

0 commit comments

Comments
 (0)