fix: BOOKING_PAID webhook & workflow isn't triggering after payment successful (#26044)
* fix booking paid webhook * Update handlePaymentSuccess to include updatedBooking status * Fix videoCallUrl undefined error in BOOKING_PAID workflow trigger - Import getVideoCallUrlFromCalEvent to properly compute meeting URL - Use conditional metadata to avoid passing undefined videoCallUrl - Matches original handleConfirmation.ts behavior Co-Authored-By: anik@cal.com <adhabal2002@gmail.com> * Add missing schedulingType to calendarEventForWorkflow - Added schedulingType field to match original handleConfirmation.ts behavior - This field was present in the original implementation but missing in the refactor Co-Authored-By: anik@cal.com <adhabal2002@gmail.com> * Fix paymentMetadata nullability to match original behavior - Changed from ?? null to optional chaining (undefined) to match original handleConfirmation.ts - This ensures webhook consumers using z.string().optional() schemas continue to work - undefined values are omitted from JSON, while null values would be explicitly included Co-Authored-By: anik@cal.com <adhabal2002@gmail.com> * Fix type errors: add schedulingType to getBooking and use null for metadata - Added schedulingType to getBooking.ts eventType select to make it available - Changed paymentMetadata to use ?? null instead of undefined for type compatibility - The metadata type expects { [key: string]: string | number | boolean | null } Co-Authored-By: anik@cal.com <adhabal2002@gmail.com> * await * add tests --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
f9635df629
commit
4585d7402d
@@ -0,0 +1,362 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import {
|
||||
BookingStatus,
|
||||
WebhookTriggerEvents,
|
||||
WorkflowTriggerEvents,
|
||||
} from "@calcom/prisma/enums";
|
||||
import { handlePaymentSuccess } from "./handlePaymentSuccess";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@calcom/features/bookings/lib/payment/getBooking");
|
||||
vi.mock("@calcom/features/webhooks/lib/getWebhooks");
|
||||
vi.mock("@calcom/features/webhooks/lib/sendOrSchedulePayload");
|
||||
vi.mock("@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType");
|
||||
vi.mock("@calcom/features/ee/workflows/lib/service/WorkflowService");
|
||||
vi.mock("@calcom/features/tasker");
|
||||
vi.mock(
|
||||
"@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials",
|
||||
() => ({
|
||||
getAllCredentialsIncludeServiceAccountKey: vi.fn().mockResolvedValue([]),
|
||||
})
|
||||
);
|
||||
vi.mock("@calcom/features/users/repositories/UserRepository", () => ({
|
||||
UserRepository: class {
|
||||
constructor() {}
|
||||
enrichUserWithItsProfile = vi.fn().mockResolvedValue({ profile: null });
|
||||
},
|
||||
}));
|
||||
vi.mock("@calcom/prisma", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@calcom/prisma")>();
|
||||
return {
|
||||
...actual,
|
||||
default: {
|
||||
...actual.default,
|
||||
payment: { update: vi.fn() },
|
||||
booking: { update: vi.fn() },
|
||||
$transaction: vi.fn(),
|
||||
profile: { findMany: vi.fn().mockResolvedValue([]) },
|
||||
credential: { findMany: vi.fn().mockResolvedValue([]) },
|
||||
team: {
|
||||
findFirst: vi.fn().mockResolvedValue(null),
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("@calcom/lib/getOrgIdFromMemberOrTeamId");
|
||||
vi.mock("@calcom/features/ee/organizations/lib/getBookerUrlServer");
|
||||
vi.mock("@calcom/lib/getTeamIdFromEventType");
|
||||
vi.mock("@calcom/lib/CalEventParser");
|
||||
vi.mock("@calcom/features/ee/billing/credit-service");
|
||||
vi.mock(
|
||||
"@calcom/features/platform-oauth-client/platform-oauth-client.repository",
|
||||
() => ({
|
||||
PlatformOAuthClientRepository: class {
|
||||
constructor() {}
|
||||
getByUserId = vi.fn().mockResolvedValue(null);
|
||||
},
|
||||
})
|
||||
);
|
||||
vi.mock("@calcom/features/platform-oauth-client/get-platform-params");
|
||||
vi.mock("@calcom/features/bookings/lib/handleConfirmation");
|
||||
vi.mock("@calcom/features/bookings/lib/handleBookingRequested");
|
||||
vi.mock("@calcom/emails/email-manager");
|
||||
vi.mock("@calcom/lib/tracing/factory");
|
||||
vi.mock("@calcom/lib/logger", () => ({
|
||||
default: {
|
||||
getSubLogger: vi.fn(() => ({
|
||||
debug: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
import { getBooking } from "@calcom/features/bookings/lib/payment/getBooking";
|
||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType";
|
||||
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
|
||||
import prisma from "@calcom/prisma";
|
||||
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
|
||||
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
||||
import { CreditService } from "@calcom/features/ee/billing/credit-service";
|
||||
import type { TraceContext } from "@calcom/lib/tracing";
|
||||
import { WebhookVersion } from "@calcom/features/webhooks/lib/interface/IWebhookRepository";
|
||||
|
||||
describe("handlePaymentSuccess", () => {
|
||||
const mockBookingId = 1;
|
||||
const mockPaymentId = 100;
|
||||
const mockTraceContext: TraceContext = {
|
||||
traceId: "test-trace-id",
|
||||
spanId: "test-span-id",
|
||||
operation: "test-operation",
|
||||
};
|
||||
|
||||
const mockBooking = {
|
||||
id: mockBookingId,
|
||||
userId: 1,
|
||||
eventTypeId: 10,
|
||||
status: BookingStatus.PENDING,
|
||||
uid: "test-booking-uid",
|
||||
location: "Test Location",
|
||||
smsReminderNumber: null,
|
||||
userPrimaryEmail: "user@example.com",
|
||||
title: "Test Event",
|
||||
description: "Test Description",
|
||||
customInputs: {},
|
||||
startTime: new Date("2025-01-01T10:00:00Z"),
|
||||
endTime: new Date("2025-01-01T10:30:00Z"),
|
||||
attendees: [],
|
||||
responses: {},
|
||||
metadata: null,
|
||||
destinationCalendar: null,
|
||||
eventType: {
|
||||
id: 10,
|
||||
title: "Test Event",
|
||||
description: "Test Description",
|
||||
slug: "test-event",
|
||||
price: 1000,
|
||||
currency: "USD",
|
||||
length: 30,
|
||||
requiresConfirmation: false,
|
||||
teamId: null,
|
||||
parentId: null,
|
||||
schedulingType: null,
|
||||
hosts: [],
|
||||
owner: { hideBranding: false },
|
||||
metadata: {},
|
||||
},
|
||||
} as any;
|
||||
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
email: "user@example.com",
|
||||
name: "Test User",
|
||||
username: "testuser",
|
||||
timeZone: "UTC",
|
||||
isPlatformManaged: false,
|
||||
credentials: [],
|
||||
locale: "en",
|
||||
timeFormat: 12,
|
||||
destinationCalendar: null,
|
||||
} as any;
|
||||
|
||||
const mockEvt = {
|
||||
type: "test-event",
|
||||
title: "Test Event",
|
||||
startTime: "2025-01-01T10:00:00Z",
|
||||
endTime: "2025-01-01T10:30:00Z",
|
||||
organizer: {
|
||||
email: "user@example.com",
|
||||
name: "Test User",
|
||||
timeZone: "UTC",
|
||||
language: { locale: "en", translate: vi.fn() },
|
||||
id: 1,
|
||||
},
|
||||
attendees: [
|
||||
{
|
||||
email: "attendee@example.com",
|
||||
name: "Attendee",
|
||||
timeZone: "UTC",
|
||||
language: { locale: "en", translate: vi.fn() },
|
||||
},
|
||||
],
|
||||
uid: "test-booking-uid",
|
||||
bookingId: mockBookingId,
|
||||
} as any;
|
||||
|
||||
const mockEventType = {
|
||||
id: 10,
|
||||
metadata: {},
|
||||
};
|
||||
|
||||
const mockWebhookSubscriber = {
|
||||
id: "webhook-1",
|
||||
subscriberUrl: "https://example.com/webhook",
|
||||
secret: "webhook-secret",
|
||||
appId: "app-id",
|
||||
payloadTemplate: null,
|
||||
eventTriggers: [WebhookTriggerEvents.BOOKING_PAID],
|
||||
timeUnit: null,
|
||||
time: null,
|
||||
version: WebhookVersion.V_2021_10_20,
|
||||
};
|
||||
|
||||
const mockWorkflow = {
|
||||
id: 1,
|
||||
name: "Test Workflow",
|
||||
trigger: WorkflowTriggerEvents.BOOKING_PAID,
|
||||
time: 24,
|
||||
timeUnit: "HOUR" as const,
|
||||
userId: 1,
|
||||
teamId: null,
|
||||
steps: [],
|
||||
} as any;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Mock getBooking
|
||||
vi.mocked(getBooking).mockResolvedValue({
|
||||
booking: mockBooking,
|
||||
user: mockUser,
|
||||
evt: mockEvt,
|
||||
eventType: mockEventType,
|
||||
});
|
||||
|
||||
// Mock prisma transaction
|
||||
vi.mocked(prisma.$transaction).mockResolvedValue([
|
||||
{ id: mockPaymentId, externalId: "ext-123" },
|
||||
{ status: BookingStatus.ACCEPTED },
|
||||
]);
|
||||
|
||||
// Mock webhook functions
|
||||
vi.mocked(getWebhooks).mockResolvedValue([mockWebhookSubscriber]);
|
||||
vi.mocked(sendPayload).mockResolvedValue({ ok: true, status: 200 });
|
||||
|
||||
// Mock workflow functions
|
||||
vi.mocked(getAllWorkflowsFromEventType).mockResolvedValue([mockWorkflow]);
|
||||
vi.mocked(
|
||||
WorkflowService.scheduleWorkflowsFilteredByTriggerEvent
|
||||
).mockResolvedValue(undefined);
|
||||
|
||||
// Mock utility functions
|
||||
vi.mocked(getOrgIdFromMemberOrTeamId).mockResolvedValue(undefined);
|
||||
vi.mocked(getBookerBaseUrl).mockResolvedValue("https://cal.com");
|
||||
vi.mocked(getTeamIdFromEventType).mockResolvedValue(null);
|
||||
vi.mocked(getVideoCallUrlFromCalEvent).mockReturnValue("");
|
||||
vi.mocked(CreditService.prototype.hasAvailableCredits).mockResolvedValue(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it("should trigger BOOKING_PAID webhooks with correct payload", async () => {
|
||||
await expect(
|
||||
handlePaymentSuccess(mockPaymentId, mockBookingId, mockTraceContext)
|
||||
).rejects.toThrow(); // Function throws HttpCode 200 at the end
|
||||
|
||||
// Verify webhooks were fetched
|
||||
expect(getWebhooks).toHaveBeenCalledWith({
|
||||
userId: mockBooking.userId,
|
||||
eventTypeId: mockBooking.eventTypeId,
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_PAID,
|
||||
teamId: null,
|
||||
orgId: undefined,
|
||||
oAuthClientId: undefined,
|
||||
});
|
||||
|
||||
// Verify webhook payload was sent
|
||||
expect(sendPayload).toHaveBeenCalledWith(
|
||||
mockWebhookSubscriber.secret,
|
||||
WebhookTriggerEvents.BOOKING_PAID,
|
||||
expect.any(String),
|
||||
mockWebhookSubscriber,
|
||||
expect.objectContaining({
|
||||
bookingId: mockBookingId,
|
||||
eventTypeId: mockBooking.eventType.id,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
paymentId: mockPaymentId,
|
||||
metadata: expect.objectContaining({
|
||||
identifier: "cal.com",
|
||||
bookingId: mockBookingId,
|
||||
eventTypeId: mockBooking.eventType.id,
|
||||
bookerEmail: mockEvt.attendees[0].email,
|
||||
eventTitle: mockBooking.eventType.title,
|
||||
externalId: "ext-123",
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should trigger BOOKING_PAID workflows with correct calendar event", async () => {
|
||||
await expect(
|
||||
handlePaymentSuccess(mockPaymentId, mockBookingId, mockTraceContext)
|
||||
).rejects.toThrow(); // Function throws HttpCode 200 at the end
|
||||
|
||||
// Verify workflows were fetched
|
||||
expect(getAllWorkflowsFromEventType).toHaveBeenCalledWith(
|
||||
mockBooking.eventType,
|
||||
mockBooking.userId
|
||||
);
|
||||
|
||||
// Verify workflows were scheduled
|
||||
expect(
|
||||
WorkflowService.scheduleWorkflowsFilteredByTriggerEvent
|
||||
).toHaveBeenCalledWith({
|
||||
workflows: [mockWorkflow],
|
||||
smsReminderNumber: null,
|
||||
calendarEvent: expect.objectContaining({
|
||||
type: mockEvt.type,
|
||||
title: mockEvt.title,
|
||||
startTime: mockEvt.startTime,
|
||||
endTime: mockEvt.endTime,
|
||||
eventType: expect.objectContaining({
|
||||
slug: mockBooking.eventType.slug,
|
||||
}),
|
||||
bookerUrl: "https://cal.com",
|
||||
}),
|
||||
hideBranding: false,
|
||||
triggers: [WorkflowTriggerEvents.BOOKING_PAID],
|
||||
creditCheckFn: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle webhook errors gracefully without blocking", async () => {
|
||||
const webhookError = new Error("Webhook failed");
|
||||
vi.mocked(sendPayload).mockRejectedValueOnce(webhookError);
|
||||
|
||||
// Should not throw (webhook errors are caught)
|
||||
await expect(
|
||||
handlePaymentSuccess(mockPaymentId, mockBookingId, mockTraceContext)
|
||||
).rejects.toThrow(); // Throws HttpCode 200 at the end
|
||||
|
||||
// Verify webhook was still attempted
|
||||
expect(sendPayload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle workflow errors gracefully without blocking", async () => {
|
||||
const workflowError = new Error("Workflow failed");
|
||||
vi.mocked(
|
||||
WorkflowService.scheduleWorkflowsFilteredByTriggerEvent
|
||||
).mockRejectedValueOnce(workflowError);
|
||||
|
||||
// Should not throw (workflow errors are caught)
|
||||
await expect(
|
||||
handlePaymentSuccess(mockPaymentId, mockBookingId, mockTraceContext)
|
||||
).rejects.toThrow(); // Throws HttpCode 200 at the end
|
||||
|
||||
// Verify workflow was still attempted
|
||||
expect(
|
||||
WorkflowService.scheduleWorkflowsFilteredByTriggerEvent
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should include payment metadata in webhook payload", async () => {
|
||||
await expect(
|
||||
handlePaymentSuccess(mockPaymentId, mockBookingId, mockTraceContext)
|
||||
).rejects.toThrow(); // Function throws HttpCode 200 at the end
|
||||
|
||||
expect(sendPayload).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
identifier: "cal.com",
|
||||
bookingId: mockBookingId,
|
||||
eventTypeId: mockBooking.eventType.id,
|
||||
bookerEmail: mockEvt.attendees[0].email,
|
||||
eventTitle: mockBooking.eventType.title,
|
||||
externalId: "ext-123",
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -6,16 +6,27 @@ import { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/book
|
||||
import { handleBookingRequested } from "@calcom/features/bookings/lib/handleBookingRequested";
|
||||
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
|
||||
import { getBooking } from "@calcom/features/bookings/lib/payment/getBooking";
|
||||
import { CreditService } from "@calcom/features/ee/billing/credit-service";
|
||||
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType";
|
||||
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
|
||||
import { getPlatformParams } from "@calcom/features/platform-oauth-client/get-platform-params";
|
||||
import { PlatformOAuthClientRepository } from "@calcom/features/platform-oauth-client/platform-oauth-client.repository";
|
||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
||||
import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
||||
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import tasker from "@calcom/features/tasker";
|
||||
import { HttpError as HttpCode } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import type { TraceContext } from "@calcom/lib/tracing";
|
||||
import { distributedTracing } from "@calcom/lib/tracing/factory";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { BookingStatus, WebhookTriggerEvents, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
||||
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[handlePaymentSuccess]"] });
|
||||
@@ -84,6 +95,10 @@ export async function handlePaymentSuccess(paymentId: number, bookingId: number,
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
externalId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const bookingUpdate = prisma.booking.update({
|
||||
@@ -91,9 +106,127 @@ export async function handlePaymentSuccess(paymentId: number, bookingId: number,
|
||||
id: booking.id,
|
||||
},
|
||||
data: bookingData,
|
||||
select: {
|
||||
status: true,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.$transaction([paymentUpdate, bookingUpdate]);
|
||||
const [payment, updatedBooking] = await prisma.$transaction([paymentUpdate, bookingUpdate]);
|
||||
|
||||
const platformClientParams = platformOAuthClient ? getPlatformParams(platformOAuthClient) : undefined;
|
||||
const teamId = await getTeamIdFromEventType({
|
||||
eventType: {
|
||||
team: { id: booking.eventType?.teamId ?? null },
|
||||
parentId: booking.eventType?.parentId ?? null,
|
||||
},
|
||||
});
|
||||
const triggerForUser = !teamId || (teamId && booking.eventType?.parentId);
|
||||
const userId = triggerForUser ? booking.userId : null;
|
||||
const orgId = await getOrgIdFromMemberOrTeamId({ memberId: userId, teamId });
|
||||
const bookerUrl = await getBookerBaseUrl(orgId ?? null);
|
||||
|
||||
try {
|
||||
// Get workflows for BOOKING_PAID trigger
|
||||
const workflows = await getAllWorkflowsFromEventType(booking.eventType, booking.userId);
|
||||
|
||||
const paymentExternalId = payment.externalId;
|
||||
|
||||
const paymentMetadata = {
|
||||
identifier: "cal.com",
|
||||
bookingId,
|
||||
eventTypeId: booking.eventType?.id ?? null,
|
||||
bookerEmail: evt.attendees[0].email,
|
||||
eventTitle: booking.eventType?.title ?? null,
|
||||
externalId: paymentExternalId ?? null,
|
||||
};
|
||||
|
||||
const eventTypeInfo: EventTypeInfo = {
|
||||
eventTitle: booking.eventType?.title,
|
||||
eventDescription: booking.eventType?.description,
|
||||
requiresConfirmation: booking.eventType?.requiresConfirmation || null,
|
||||
price: booking.eventType?.price,
|
||||
currency: booking.eventType?.currency,
|
||||
length: booking.eventType?.length,
|
||||
};
|
||||
|
||||
const payload: EventPayloadType = {
|
||||
...evt,
|
||||
...eventTypeInfo,
|
||||
bookingId,
|
||||
eventTypeId: booking.eventType?.id,
|
||||
status: updatedBooking.status,
|
||||
smsReminderNumber: booking.smsReminderNumber || undefined,
|
||||
paymentId: paymentId,
|
||||
metadata: paymentMetadata,
|
||||
...(platformClientParams ? platformClientParams : {}),
|
||||
};
|
||||
|
||||
// Trigger BOOKING_PAID webhooks
|
||||
const subscriberMeetingPaid = await getWebhooks({
|
||||
userId,
|
||||
eventTypeId: booking.eventTypeId,
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_PAID,
|
||||
teamId: booking.eventType?.teamId,
|
||||
orgId,
|
||||
oAuthClientId: platformClientParams?.platformClientId,
|
||||
});
|
||||
|
||||
const tracingLogger = distributedTracing.getTracingLogger(updatedTraceContext);
|
||||
const bookingPaidSubscribers = subscriberMeetingPaid.map((sub) =>
|
||||
sendPayload(
|
||||
sub.secret,
|
||||
WebhookTriggerEvents.BOOKING_PAID,
|
||||
new Date().toISOString(),
|
||||
sub,
|
||||
payload
|
||||
).catch((e) => {
|
||||
tracingLogger.error(
|
||||
`Error executing webhook for event: ${WebhookTriggerEvents.BOOKING_PAID}, URL: ${sub.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}`,
|
||||
safeStringify(e)
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
// Wait for webhook invocations to finish before returning
|
||||
await Promise.all(bookingPaidSubscribers);
|
||||
|
||||
// Trigger BOOKING_PAID workflows
|
||||
try {
|
||||
const meetingUrl = getVideoCallUrlFromCalEvent(evt);
|
||||
const calendarEventForWorkflow = {
|
||||
...evt,
|
||||
eventType: {
|
||||
slug: booking.eventType?.slug || "",
|
||||
schedulingType: booking.eventType?.schedulingType,
|
||||
hosts:
|
||||
booking.eventType?.hosts?.map((host) => ({
|
||||
user: {
|
||||
email: host.user.email,
|
||||
destinationCalendar: host.user.destinationCalendar,
|
||||
},
|
||||
})) || [],
|
||||
},
|
||||
bookerUrl: bookerUrl,
|
||||
metadata: meetingUrl ? { videoCallUrl: meetingUrl } : undefined,
|
||||
};
|
||||
|
||||
const creditService = new CreditService();
|
||||
|
||||
await WorkflowService.scheduleWorkflowsFilteredByTriggerEvent({
|
||||
workflows,
|
||||
smsReminderNumber: booking.smsReminderNumber,
|
||||
calendarEvent: calendarEventForWorkflow,
|
||||
hideBranding: !!booking.eventType?.owner?.hideBranding,
|
||||
triggers: [WorkflowTriggerEvents.BOOKING_PAID],
|
||||
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
|
||||
});
|
||||
} catch (error) {
|
||||
log.error("Error while scheduling workflow reminders for booking paid", safeStringify(error));
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Error while triggering BOOKING_PAID webhook", safeStringify(error));
|
||||
}
|
||||
|
||||
if (!isConfirmed) {
|
||||
if (!requiresConfirmation) {
|
||||
await handleConfirmation({
|
||||
@@ -103,7 +236,7 @@ export async function handlePaymentSuccess(paymentId: number, bookingId: number,
|
||||
bookingId: booking.id,
|
||||
booking,
|
||||
paid: true,
|
||||
platformClientParams: platformOAuthClient ? getPlatformParams(platformOAuthClient) : undefined,
|
||||
platformClientParams,
|
||||
traceContext: updatedTraceContext,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -504,103 +504,6 @@ export async function handleConfirmation(args: {
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (paid) {
|
||||
let paymentExternalId: string | undefined;
|
||||
const subscriberMeetingPaid = await getWebhooks({
|
||||
userId,
|
||||
eventTypeId: booking.eventTypeId,
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_PAID,
|
||||
teamId: eventType?.teamId,
|
||||
orgId,
|
||||
oAuthClientId: platformClientParams?.platformClientId,
|
||||
});
|
||||
const bookingWithPayment = await prisma.booking.findUnique({
|
||||
where: {
|
||||
id: bookingId,
|
||||
},
|
||||
select: {
|
||||
payment: {
|
||||
select: {
|
||||
id: true,
|
||||
success: true,
|
||||
externalId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const successPayment = bookingWithPayment?.payment?.find((item) => item.success);
|
||||
if (successPayment) {
|
||||
paymentExternalId = successPayment.externalId;
|
||||
}
|
||||
|
||||
const paymentMetadata = {
|
||||
identifier: "cal.com",
|
||||
bookingId,
|
||||
eventTypeId: eventType?.id,
|
||||
bookerEmail: evt.attendees[0].email,
|
||||
eventTitle: eventType?.title,
|
||||
externalId: paymentExternalId,
|
||||
};
|
||||
|
||||
payload.paymentId = bookingWithPayment?.payment?.[0].id;
|
||||
payload.metadata = {
|
||||
...(paid ? paymentMetadata : {}),
|
||||
};
|
||||
|
||||
const bookingPaidSubscribers = subscriberMeetingPaid.map((sub) =>
|
||||
sendPayload(
|
||||
sub.secret,
|
||||
WebhookTriggerEvents.BOOKING_PAID,
|
||||
new Date().toISOString(),
|
||||
sub,
|
||||
payload
|
||||
).catch((e) => {
|
||||
tracingLogger.error(
|
||||
`Error executing webhook for event: ${WebhookTriggerEvents.BOOKING_PAID}, URL: ${sub.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}`,
|
||||
safeStringify(e)
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
// I don't need to await for this
|
||||
Promise.all(bookingPaidSubscribers);
|
||||
|
||||
try {
|
||||
const calendarEventForWorkflow = {
|
||||
...evt,
|
||||
eventType: {
|
||||
slug: updatedBookings[0].eventType?.slug || "",
|
||||
schedulingType: updatedBookings[0].eventType?.schedulingType,
|
||||
hosts:
|
||||
updatedBookings[0].eventType?.hosts?.map((host) => ({
|
||||
user: {
|
||||
email: host.user.email,
|
||||
destinationCalendar: host.user.destinationCalendar,
|
||||
},
|
||||
})) || [],
|
||||
},
|
||||
bookerUrl: bookerUrl,
|
||||
metadata: { videoCallUrl: meetingUrl },
|
||||
};
|
||||
|
||||
const creditService = new CreditService();
|
||||
|
||||
await WorkflowService.scheduleWorkflowsFilteredByTriggerEvent({
|
||||
workflows,
|
||||
smsReminderNumber: booking.smsReminderNumber,
|
||||
calendarEvent: calendarEventForWorkflow,
|
||||
hideBranding: !!updatedBookings[0].eventType?.owner?.hideBranding,
|
||||
triggers: [WorkflowTriggerEvents.BOOKING_PAID],
|
||||
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
|
||||
});
|
||||
} catch (error) {
|
||||
tracingLogger.error(
|
||||
"Error while scheduling workflow reminders for booking paid",
|
||||
safeStringify(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail
|
||||
console.error(error);
|
||||
|
||||
@@ -72,6 +72,7 @@ export async function getBooking(bookingId: number) {
|
||||
},
|
||||
},
|
||||
slug: true,
|
||||
schedulingType: true,
|
||||
workflows: {
|
||||
select: {
|
||||
workflow: {
|
||||
|
||||
Reference in New Issue
Block a user