Files
calendar/packages/lib/server/repository/PrismaAgentRepository.ts
T
+1
Carina WollendorferGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>amit@cal.com <samit91848@gmail.com>Amit SharmaCarinaWollicoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>Udit TakkarBenny Joocal.comMorgan
44a3a9eabb feat: form submitted workflow triggers #1 (#23704)
* feat: add 5 new workflow triggers for booking events

- Add BOOKING_REJECTED, BOOKING_REQUESTED, BOOKING_PAYMENT_INITIATED, BOOKING_PAID, BOOKING_NO_SHOW_UPDATED to WorkflowTriggerEvents enum
- Update workflow constants to include new trigger options
- Implement workflow trigger logic for booking rejected and requested events
- Add translations for new workflow triggers following {enum}_trigger format
- Generate updated Prisma types for new schema changes

Co-Authored-By: amit@cal.com <samit91848@gmail.com>

* fix: type check, remove as any

* feat: add workflow trigger for BOOKING_REQUESTED in handleNewBooking.ts

- Add WorkflowTriggerEvents import to handleNewBooking.ts
- Implement workflow trigger logic for BOOKING_REQUESTED in else block
- Filter workflows by BOOKING_REQUESTED trigger and call scheduleWorkflowReminders
- Use proper calendar event object construction without type casting
- Add error handling for workflow reminder scheduling

Co-Authored-By: amit@cal.com <samit91848@gmail.com>

* fix: resolve type errors in workflow trigger implementations

- Add proper database includes for user information in handleConfirmation.ts
- Fix ExtendedCalendarEvent type structure with correct hosts mapping
- Add missing properties to calendar event objects in handleMarkNoShow.ts
- Ensure all workflow triggers follow proper type patterns

Co-Authored-By: amit@cal.com <samit91848@gmail.com>

* feat: add workflow test configurations for new booking triggers

- Add workflow configurations for BOOKING_REQUESTED and BOOKING_PAYMENT_INITIATED in fresh-booking.test.ts
- Add workflow configuration for BOOKING_REJECTED in confirm.handler.test.ts
- Enable previously skipped confirm.handler.test.ts
- Remove workflow test assertions temporarily until triggers are fully functional
- Maintain webhook test coverage while adding workflow test infrastructure

Co-Authored-By: amit@cal.com <samit91848@gmail.com>

* fix: add missing mockSuccessfulVideoMeetingCreation import to confirm.handler.test.ts

- Import mockSuccessfulVideoMeetingCreation from bookingScenario utils
- Add mock call to BOOKING_REJECTED workflow test case
- Resolves ReferenceError that was causing unit test CI failure

Co-Authored-By: amit@cal.com <samit91848@gmail.com>

* add new triggers

* refactor: improve _scheduleWorkflowReminders readability and add missing booking trigger events

- Extract complex conditional logic into helper functions (isImmediateTrigger, isTimeBased, shouldProcessWorkflow)
- Add missing workflow trigger events with immediate execution logic
- Update test workflows to use different actions (EMAIL_ATTENDEE, SMS_ATTENDEE) for better differentiation
- Fix translation function mock in confirm.handler.test.ts using mockNoTranslations utility
- Maintain existing functionality while improving code maintainability

Co-Authored-By: amit@cal.com <samit91848@gmail.com>

* only show customt emplate for form triggers

* filter outside scheduleWorkflowReminder

* fix type check

* chore: add more tests

* test: add comprehensive unit tests for handleMarkNoShow with webhook and workflow coverage

- Create handleMarkNoShow.test.ts following confirm.handler.test.ts pattern
- Add expectBookingNoShowUpdatedWebhookToHaveBeenFired utility function
- Test both webhook and workflow triggers for BOOKING_NO_SHOW_UPDATED
- Cover attendee/host no-show scenarios, multiple attendees, and error cases
- All 6 unit tests pass with proper mocking of external dependencies

Co-Authored-By: amit@cal.com <samit91848@gmail.com>

* Revert "test: add comprehensive unit tests for handleMarkNoShow with webhook and workflow coverage"

This reverts commit 764299220279f0c012392dec24d3150246bfc4ad.

* fix: add new workflow triggers to api/v2

* update swagger docs

* fix: e2e

* fix type check

* fix tests, add test for before after events

* fix unit tests

* revert confirm.handler.test

* fix: unit tests

* dummy form variables

* add routing forms to active on dropdown

* add migration file

* Ui fixes for variables dropdown

* remove other translation keys

* review fixes

* allow routing forms for activeOn

* use repository function to get routing forms

* remove unnecessary code

* adjust logic in update handler

* add triggers to api v2

* remvoe unused file

* rename to getAcitveOnOptions handler

* remove routingFormOptions handler

* clean up getActiveOnOptions

* refactor WorkflowService

* remove logs

* remove unused

* fix: type check

* fix: missed before after events for recurring

* fix: calendarEvent handleMarkNoShow

* fix error message

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* don't query disabled routing forms

* create tasker function

* add tasker code

* move isFormTrigger function

* small adjustments + todo comments

* remove email to host action for form triggers

* throw trpc error if email to host is added as step

* fix dialog on how to use form responses as variables

* remove add variable dropdown for form triggers

* remove form workfows in event workflows tab

* improvements for workflow logic on form submission

* review fixes

* base setup for seperate schedule functions (evt and form)

* add missing BOOKING_PAID workflow trigger

* fix pathname

* fix: test for BOOKING_REQUESTED

* fix activeOn ids

* pass hideBranding and smsReminderNumber

* adjustments to reminderScheduler

* create empty scheduelForForm functions

* pass locale and timezone with form user

* pass formData instead of responses

* pass timeFormat and locale

* reusable function for email sending and reminder creation

* implement scheduleEmailReminderForForm

* remove added editor field from merge conflict

* don't support cal.ai action with form triggers

* throw bad request if form trigger and cal.ai is combined

* add tests for scheduleFormWorkflows

* add form submission tests

* remove form response varibe info

* clean up workflow actions

* fixes for getting template options

* pass triggerType to getAllWorkflows

* move reusable logic to scheduleSMSReminder

* add formdata to param type

* type fixes for text reminder managers

* implement scheduleSMSReminderForForm

* fix import

* fix isAuthorizedToAddActiveOnIds

* disble whatsapp action

* implement triggerFormSubmittedNoEventWorkflow

* code clean up

* Merge branch 'devin/1755107037-add-workflow-triggers' into feat/routing-form-workflow-triggers

* fix type errors

* remove async from getSubmitterEmail

* fix type errors

* revert cal.ai changes

* fix type error

* add sublogger

* code clean up

* fix type errors

* remove label for attendee whatsapp action

* code clean up

* fixes saving teams on org workflows

* fix type error

* code improvements for activeOn ids

* Revert "code improvements for activeOn ids"

This reverts commit 0a3590a4e2ce541b17d63483ad86ed458795a6a3.

* improve variable name

* fix unit tests

* small fixes

* type fixes

* remove unused translation keys

* fix merge conflict issues

* code clean up

* remove SMS action support

* remove more SMS code

* add missing imports

* set custom template for form action

* type fixes

* fix tasker endpoint

* fix duplicate check

* fix workfows.test.ts

* use repository funciton to getHideBranding

* code clean up

* fix hasDuplicateSubmission

* code clean up

* select only needed properties

* remove repository functions

* Revert "remove repository functions"

This reverts commit 7aa47b1c59c9abd7f964ebf26f746934c53a44f4.

* add scheduleWorkflows function

* Revert "add scheduleWorkflows function"

This reverts commit fe5db4fe3b65e2743c95475d585300cab98beed7.

* move type to /types

* Revert "move type to /types"

This reverts commit 91e0152154594b3772a801a426260068a8ccea54.

* revert changes causing type errors

* remove import

* remove unused import

* Revert "remove unused import"

This reverts commit 1916768c875ea5d0ac5598ccb8f9c796c5622dc9.

* revert changed from attempt to fix type errors

* pass object to gt all workflows

* fix isAuthorized check

* trigger filtering

* remove form submitted no event booked code

* remove form submitted no event from schema

* remove more code

* remove test

* fixes

* add getSubmitterEmail function

* add missing workflow DTOs

* small fixes

* use activeOnWithChildren

* fix active on when switching trigger type

* remove add variable dropdown

* add getAllWorkflowsFromRoutingForm to WorkflowService

* fix error caused by undefined evt

* fix type error

* fix type error

* fix tests

* code clean up

* remove console.log

* remove template text form from triggers

* add routing form repoditory function

* fix bug with key

* add missing trigger in update-workflow.input.ts

* ForEvt and ForForm function for aiPhoneCallManager

* chore: add support for form workflows on api v2

* fixup! chore: add support for form workflows on api v2

* use only repository functions in update handler

* move all prisma queries from list.handler

* review suggestions

* chore: handle workflows api v2

* chore: handle workflows api v2, split in 2 endpoints

* fix workflow step creation

* remove connect agent and fixes types

* add type to workflow

* chore: use workflow type in apiv2 WorkflowsOutputService

* update worklfow type on update

* chore: use workflow type in apiv2 WorkflowsOutputService

* fix template body for torm trigger

* some UI fixes for email subject/body

* resetting email body when changing form triggers

* use type field to query workflows

* clean up all old active on values

* remove responseId from all funciton calls

* remove undefined from updateTemplate

* refactor: split routing form and event-type workflows code

* refactor: split routing form and event-type workflows code

* fix template  text when adding action

* chore: don't rename WorkflowActivationDto to avoid ci blocking

* refine update schedule to use only allowed actions

* fix type error

* don't allow whatsapp action with form trigger

* fix type error

* return early if activeOn array is empty

* fix: from step type in BaseFormWorkflowStepDto

* fixup! fix: from step type in BaseFormWorkflowStepDto

* move all prisma calls to repository (service/workflows.ts)

* use FORM_TRIGGER_WORKFLOW_EVENTS for form queries

* use userRepository

* use FORM_TRIGGER_WORKFLOW_EVENTS in isFormTrigger

* code clean up

* code clean up

* add back trpc  import

* fix agent repository functions

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: amit@cal.com <samit91848@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-10-13 12:20:47 +02:00

623 lines
16 KiB
TypeScript

import { Prisma, PrismaClient } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
interface _AgentRawResult {
id: string;
name: string;
providerAgentId: string;
enabled: boolean;
userId: number;
teamId: number | null;
inboundEventTypeId?: number | null;
createdAt: Date;
updatedAt: Date;
user_id?: number;
user_name?: string;
user_email?: string;
team_id?: number;
team_name?: string;
team_slug?: string;
team_logo_url?: string;
}
interface _PhoneNumberRawResult {
id: number;
phoneNumber: string;
subscriptionStatus: string;
provider: string;
outboundAgentId?: string;
}
export class PrismaAgentRepository {
constructor(private prismaClient: PrismaClient) {}
private async getUserAccessibleTeamIds(userId: number): Promise<number[]> {
const memberships = await this.prismaClient.membership.findMany({
where: {
userId,
accepted: true,
},
select: {
teamId: true,
},
});
return memberships.map((membership) => membership.teamId);
}
private async getUserAdminTeamIds(userId: number): Promise<number[]> {
const memberships = await this.prismaClient.membership.findMany({
where: {
userId,
accepted: true,
role: {
in: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
},
select: {
teamId: true,
},
});
return memberships.map((membership) => membership.teamId);
}
async findByIdWithUserAccess({
agentId,
userId,
teamId,
}: {
agentId: string;
userId: number;
teamId?: number;
}) {
const accessibleTeamIds = await this.getUserAccessibleTeamIds(userId);
let whereCondition: Prisma.Sql;
if (teamId) {
// If teamId is provided, check that the user has access to that specific team
if (accessibleTeamIds.includes(teamId)) {
whereCondition = Prisma.sql`id = ${agentId} AND "teamId" = ${teamId}`;
} else {
// If user doesn't have access to the team, only check for personal agents
whereCondition = Prisma.sql`id = ${agentId} AND "userId" = ${userId}`;
}
} else if (accessibleTeamIds.length > 0) {
// No specific teamId provided, check both personal and team agents
whereCondition = Prisma.sql`id = ${agentId} AND ("userId" = ${userId} OR "teamId" IN (${Prisma.join(
accessibleTeamIds
)}))`;
} else {
// User has no team access, only check personal agents
whereCondition = Prisma.sql`id = ${agentId} AND "userId" = ${userId}`;
}
const query = Prisma.sql`
SELECT
id,
name,
"providerAgentId",
enabled,
"userId",
"teamId",
"inboundEventTypeId",
"createdAt",
"updatedAt"
FROM "Agent"
WHERE ${whereCondition}
LIMIT 1
`;
const agents = await this.prismaClient.$queryRaw<_AgentRawResult[]>(query);
return agents.length > 0 ? agents[0] : null;
}
async findByProviderAgentIdWithUserAccess({
providerAgentId,
userId,
}: {
providerAgentId: string;
userId: number;
}) {
const accessibleTeamIds = await this.getUserAccessibleTeamIds(userId);
let whereCondition: Prisma.Sql;
if (accessibleTeamIds.length > 0) {
whereCondition = Prisma.sql`"providerAgentId" = ${providerAgentId} AND ("userId" = ${userId} OR "teamId" IN (${Prisma.join(
accessibleTeamIds
)}))`;
} else {
whereCondition = Prisma.sql`"providerAgentId" = ${providerAgentId} AND "userId" = ${userId}`;
}
const query = Prisma.sql`
SELECT
id,
name,
"providerAgentId",
enabled,
"userId",
"teamId",
"inboundEventTypeId",
"createdAt",
"updatedAt"
FROM "Agent"
WHERE ${whereCondition}
LIMIT 1
`;
const agents = await this.prismaClient.$queryRaw<_AgentRawResult[]>(query);
return agents.length > 0 ? agents[0] : null;
}
async findById({ id }: { id: string }) {
return await this.prismaClient.agent.findUnique({
select: {
id: true,
name: true,
providerAgentId: true,
enabled: true,
userId: true,
teamId: true,
inboundEventTypeId: true,
createdAt: true,
updatedAt: true,
},
where: {
id,
},
});
}
async findByProviderAgentId({ providerAgentId }: { providerAgentId: string }) {
return await this.prismaClient.agent.findUnique({
select: {
id: true,
name: true,
providerAgentId: true,
enabled: true,
userId: true,
teamId: true,
inboundEventTypeId: true,
createdAt: true,
updatedAt: true,
},
where: {
providerAgentId,
},
});
}
async findManyWithUserAccess({
userId,
teamId,
scope = "all",
}: {
userId: number;
teamId?: number;
scope?: "personal" | "team" | "all";
}) {
let whereCondition: Prisma.Sql;
if (scope === "personal") {
whereCondition = Prisma.sql`a."userId" = ${userId}`;
} else if (scope === "team") {
const accessibleTeamIds = await this.getUserAccessibleTeamIds(userId);
if (accessibleTeamIds.length === 0) {
return [];
}
if (teamId) {
// Check if user has access to the specific team
if (!accessibleTeamIds.includes(teamId)) {
return [];
}
whereCondition = Prisma.sql`a."teamId" = ${teamId}`;
} else {
whereCondition = Prisma.sql`a."teamId" IN (${Prisma.join(accessibleTeamIds)})`;
}
} else {
const accessibleTeamIds = await this.getUserAccessibleTeamIds(userId);
if (teamId) {
if (accessibleTeamIds.includes(teamId)) {
whereCondition = Prisma.sql`(a."userId" = ${userId} OR a."teamId" = ${teamId})`;
} else {
whereCondition = Prisma.sql`a."userId" = ${userId}`;
}
} else if (accessibleTeamIds.length > 0) {
whereCondition = Prisma.sql`(a."userId" = ${userId} OR a."teamId" IN (${Prisma.join(
accessibleTeamIds
)}))`;
} else {
whereCondition = Prisma.sql`a."userId" = ${userId}`;
}
}
const query = Prisma.sql`
SELECT
a.id,
a.name,
a."providerAgentId",
a.enabled,
a."userId",
a."teamId",
a."inboundEventTypeId",
a."createdAt",
a."updatedAt",
u.id as user_id,
u.name as user_name,
u.email as user_email,
t.id as team_id,
t.name as team_name,
t.slug as team_slug,
t."logoUrl" as team_logo_url
FROM "Agent" a
LEFT JOIN "users" u ON a."userId" = u.id
LEFT JOIN "Team" t ON a."teamId" = t.id
WHERE ${whereCondition}
ORDER BY a."teamId" ASC, a."createdAt" DESC
`;
const agents = await this.prismaClient.$queryRaw<_AgentRawResult[]>(query);
// Get phone numbers for each agent in a separate query to avoid N+1
const agentIds = agents.map((agent) => agent.id);
const phoneNumbers =
agentIds.length > 0
? await this.prismaClient.$queryRaw<_PhoneNumberRawResult[]>`
SELECT
pn.id,
pn."phoneNumber",
pn."subscriptionStatus",
pn.provider,
pn."outboundAgentId"
FROM "CalAiPhoneNumber" pn
WHERE pn."outboundAgentId" IN (${Prisma.join(agentIds)})
`
: [];
// Map phone numbers to agents
const phoneNumbersByAgent = phoneNumbers.reduce((acc, pn) => {
const agentId = pn.outboundAgentId;
if (agentId) {
if (!acc[agentId]) {
acc[agentId] = [];
}
acc[agentId].push({
id: pn.id,
phoneNumber: pn.phoneNumber,
subscriptionStatus: pn.subscriptionStatus,
provider: pn.provider,
});
}
return acc;
}, {} as Record<string, _PhoneNumberRawResult[]>);
// Transform results to match expected format
return agents.map((agent) => ({
id: agent.id,
name: agent.name,
providerAgentId: agent.providerAgentId,
enabled: agent.enabled,
userId: agent.userId,
teamId: agent.teamId,
inboundEventTypeId: agent.inboundEventTypeId,
createdAt: agent.createdAt,
updatedAt: agent.updatedAt,
user: agent.user_id
? {
id: agent.user_id,
name: agent.user_name ?? null,
email: agent.user_email ?? null,
}
: null,
team: agent.team_id
? {
id: agent.team_id,
name: agent.team_name ?? null,
slug: agent.team_slug ?? null,
logoUrl: agent.team_logo_url ?? null,
}
: null,
outboundPhoneNumbers: phoneNumbersByAgent[agent.id] || [],
}));
}
async findByIdWithUserAccessAndDetails({
id,
userId,
teamId,
}: {
id: string;
userId: number;
teamId?: number;
}) {
const accessibleTeamIds = await this.getUserAccessibleTeamIds(userId);
let whereCondition: Prisma.Sql;
if (teamId) {
// If teamId is provided, check that the user has access to that specific team
if (accessibleTeamIds.includes(teamId)) {
whereCondition = Prisma.sql`a.id = ${id} AND a."teamId" = ${teamId}`;
} else {
// If user doesn't have access to the team, only check for personal agents
whereCondition = Prisma.sql`a.id = ${id} AND a."userId" = ${userId}`;
}
} else if (accessibleTeamIds.length > 0) {
// No specific teamId provided, check both personal and team agents
whereCondition = Prisma.sql`a.id = ${id} AND (a."userId" = ${userId} OR a."teamId" IN (${Prisma.join(
accessibleTeamIds
)}))`;
} else {
// User has no team access, only check personal agents
whereCondition = Prisma.sql`a.id = ${id} AND a."userId" = ${userId}`;
}
const query = Prisma.sql`
SELECT
a.id,
a.name,
a."providerAgentId",
a.enabled,
a."userId",
a."teamId",
a."inboundEventTypeId",
a."createdAt",
a."updatedAt",
u.id as user_id,
u.name as user_name,
u.email as user_email,
t.id as team_id,
t.name as team_name,
t.slug as team_slug
FROM "Agent" a
LEFT JOIN "users" u ON a."userId" = u.id
LEFT JOIN "Team" t ON a."teamId" = t.id
WHERE ${whereCondition}
LIMIT 1
`;
const agents = await this.prismaClient.$queryRaw<_AgentRawResult[]>(query);
if (agents.length === 0) {
return null;
}
const agent = agents[0];
const phoneNumbers = await this.prismaClient.$queryRaw<_PhoneNumberRawResult[]>`
SELECT
pn.id,
pn."phoneNumber",
pn."subscriptionStatus",
pn.provider
FROM "CalAiPhoneNumber" pn
WHERE pn."outboundAgentId" = ${agent.id}
`;
// Transform result to match expected format
return {
id: agent.id,
name: agent.name,
providerAgentId: agent.providerAgentId,
enabled: agent.enabled,
userId: agent.userId,
teamId: agent.teamId,
inboundEventTypeId: agent.inboundEventTypeId,
createdAt: agent.createdAt,
updatedAt: agent.updatedAt,
user: agent.user_id
? {
id: agent.user_id,
name: agent.user_name ?? null,
email: agent.user_email ?? null,
}
: null,
team: agent.team_id
? {
id: agent.team_id,
name: agent.team_name ?? null,
slug: agent.team_slug ?? null,
}
: null,
outboundPhoneNumbers: phoneNumbers.map((pn) => ({
id: pn.id,
phoneNumber: pn.phoneNumber,
subscriptionStatus: pn.subscriptionStatus,
provider: pn.provider,
})),
};
}
async create({
name,
providerAgentId,
userId,
teamId,
}: {
name: string;
providerAgentId: string;
userId: number;
teamId?: number;
}) {
return await this.prismaClient.agent.create({
data: {
name,
providerAgentId,
userId,
teamId,
},
});
}
async findByIdWithAdminAccess({ id, userId, teamId }: { id: string; userId: number; teamId?: number }) {
const adminTeamIds = await this.getUserAdminTeamIds(userId);
let whereCondition: Prisma.Sql;
if (teamId) {
// If teamId is specified, check that user has admin access to that specific team
if (adminTeamIds.includes(teamId)) {
whereCondition = Prisma.sql`id = ${id} AND "teamId" = ${teamId}`;
} else {
// If user doesn't have admin access to the team, only check for personal agents
whereCondition = Prisma.sql`id = ${id} AND "userId" = ${userId}`;
}
} else if (adminTeamIds.length > 0) {
whereCondition = Prisma.sql`id = ${id} AND ("userId" = ${userId} OR "teamId" IN (${Prisma.join(
adminTeamIds
)}))`;
} else {
whereCondition = Prisma.sql`id = ${id} AND "userId" = ${userId}`;
}
const query = Prisma.sql`
SELECT
id,
name,
"providerAgentId",
enabled,
"userId",
"teamId",
"createdAt",
"updatedAt",
"inboundEventTypeId"
FROM "Agent"
WHERE ${whereCondition}
LIMIT 1
`;
const agents = await this.prismaClient.$queryRaw<_AgentRawResult[]>(query);
return agents.length > 0 ? agents[0] : null;
}
async findByIdWithCallAccess({ id, userId }: { id: string; userId: number }) {
const accessibleTeamIds = await this.getUserAccessibleTeamIds(userId);
let whereCondition: Prisma.Sql;
if (accessibleTeamIds.length > 0) {
whereCondition = Prisma.sql`a.id = ${id} AND (a."userId" = ${userId} OR a."teamId" IN (${Prisma.join(
accessibleTeamIds
)}))`;
} else {
whereCondition = Prisma.sql`a.id = ${id} AND a."userId" = ${userId}`;
}
const query = Prisma.sql`
SELECT
a.id,
a.name,
a."providerAgentId",
a.enabled,
a."userId",
a."teamId",
a."inboundEventTypeId",
a."createdAt",
a."updatedAt"
FROM "Agent" a
WHERE ${whereCondition}
LIMIT 1
`;
const agents = await this.prismaClient.$queryRaw<_AgentRawResult[]>(query);
if (agents.length === 0) {
return null;
}
const agent = agents[0];
const phoneNumbers = await this.prismaClient.$queryRaw<{ phoneNumber: string }[]>`
SELECT "phoneNumber"
FROM "CalAiPhoneNumber"
WHERE "outboundAgentId" = ${agent.id}
`;
return {
...agent,
outboundPhoneNumbers: phoneNumbers,
};
}
async delete({ id }: { id: string }) {
return await this.prismaClient.agent.delete({
where: { id },
});
}
async linkOutboundAgentToWorkflow({
workflowStepId,
agentId,
}: {
workflowStepId: number;
agentId: string;
}) {
return await this.prismaClient.workflowStep.update({
where: { id: workflowStepId },
data: { agentId },
});
}
async linkInboundAgentToWorkflow({ workflowStepId, agentId }: { workflowStepId: number; agentId: string }) {
return await this.prismaClient.workflowStep.update({
where: {
id: workflowStepId,
},
data: {
inboundAgentId: agentId,
},
});
}
async updateEventTypeId({ agentId, eventTypeId }: { agentId: string; eventTypeId: number }) {
return await this.prismaClient.agent.update({
where: {
id: agentId,
},
data: {
inboundEventTypeId: eventTypeId,
},
});
}
async canManageTeamResources({ userId, teamId }: { userId: number; teamId: number }): Promise<boolean> {
const result = await this.prismaClient.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count
FROM "Membership"
WHERE "userId" = ${userId}
AND "teamId" = ${teamId}
AND accepted = true
AND role IN ('ADMIN', 'OWNER')
`;
return Number(result[0].count) > 0;
}
async findAgentWithPhoneNumbers(agentId: string) {
return await this.prismaClient.agent.findUnique({
where: { id: agentId },
select: {
id: true,
outboundPhoneNumbers: {
select: {
id: true,
phoneNumber: true,
subscriptionStatus: true,
},
},
},
});
}
async findProviderAgentIdById(agentId: string) {
return await this.prismaClient.agent.findUnique({
where: { id: agentId },
select: { providerAgentId: true },
});
}
}