/**
* Opportunity Tool
*
* Application layer tool for managing Opportunity entities in Twenty CRM.
*/
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { ToolDefinition, ToolResult } from '../../domain/types.js';
import { Opportunity } from '../../domain/twenty-types.js';
import { ITwentyGraphQLClient } from '../../infrastructure/clients/twenty-graphql-client.js';
import { ITwentyRESTClient } from '../../infrastructure/clients/twenty-rest-client.js';
import { ILogger } from '../../infrastructure/logging/logger.js';
export const CREATE_OPPORTUNITY_TOOL_NAME = 'create-opportunity';
export const GET_OPPORTUNITY_TOOL_NAME = 'get-opportunity';
export const LIST_OPPORTUNITIES_TOOL_NAME = 'list-opportunities';
export const CreateOpportunityInputSchema = z.object({
name: z.string().describe('Opportunity name'),
amountMicros: z.number().int().optional().describe('Amount in micros (e.g., 100000000 = $100)'),
currencyCode: z.string().optional().describe('Currency code (e.g., USD, EUR)'),
closeDate: z.string().optional().describe('Expected close date (ISO format)'),
stage: z.string().optional().describe('Opportunity stage'),
companyId: z.string().optional().describe('Associated company ID'),
pointOfContactId: z.string().optional().describe('Point of contact person ID'),
});
export const GetOpportunityInputSchema = z.object({
id: z.string().describe('ID of the opportunity to retrieve'),
});
export const ListOpportunitiesInputSchema = z.object({
limit: z.number().int().positive().optional().describe('Maximum number of results (default: 20)'),
offset: z.number().int().nonnegative().optional().describe('Number of results to skip (default: 0)'),
companyId: z.string().optional().describe('Filter by company ID'),
});
export type CreateOpportunityInput = z.infer<typeof CreateOpportunityInputSchema>;
export type GetOpportunityInput = z.infer<typeof GetOpportunityInputSchema>;
export type ListOpportunitiesInput = z.infer<typeof ListOpportunitiesInputSchema>;
const CREATE_OPPORTUNITY_MUTATION = `
mutation CreateOpportunity($name: String!, $amountMicros: Int, $currencyCode: String, $closeDate: String, $stage: String, $companyId: ID, $pointOfContactId: ID) {
createOpportunity(data: {
name: $name
amount: { amountMicros: $amountMicros, currencyCode: $currencyCode }
closeDate: $closeDate
stage: $stage
companyId: $companyId
pointOfContactId: $pointOfContactId
}) {
id
name
amount { amountMicros currencyCode }
closeDate
stage
companyId
pointOfContactId
createdAt
}
}
`;
const GET_OPPORTUNITY_QUERY = `
query GetOpportunity($id: ID!) {
opportunity(id: $id) {
id
name
amount { amountMicros currencyCode }
closeDate
stage
probability
companyId
pointOfContactId
createdAt
updatedAt
}
}
`;
const LIST_OPPORTUNITIES_QUERY = `
query ListOpportunities($limit: Int, $filter: OpportunityFilterInput) {
opportunities(first: $limit, filter: $filter) {
edges {
node {
id
name
amount { amountMicros currencyCode }
closeDate
stage
pointOfContactId
companyId
createdAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
function createToolDefinition(name: string, description: string, schema: z.ZodObject<any>): ToolDefinition {
const jsonSchema = zodToJsonSchema(schema, { name: `${name}Input`, $refStrategy: 'none' });
const actualSchema = (jsonSchema as any).definitions?.[`${name}Input`] || jsonSchema;
return { name, description, inputSchema: actualSchema as ToolDefinition['inputSchema'] };
}
export class OpportunityTool {
constructor(
private readonly graphqlClient: ITwentyGraphQLClient,
private readonly restClient: ITwentyRESTClient,
private readonly logger: ILogger
) {}
async createOpportunity(args: unknown): Promise<ToolResult> {
const parseResult = CreateOpportunityInputSchema.safeParse(args);
if (!parseResult.success) {
return this.error('Invalid input: ' + parseResult.error.message);
}
const result = await this.graphqlClient.mutate<{ createOpportunity: Opportunity }>(
CREATE_OPPORTUNITY_MUTATION,
parseResult.data
);
if (!result?.createOpportunity) {
return this.error('Failed to create opportunity');
}
return {
content: [{ type: 'text', text: JSON.stringify({ success: true, opportunity: result.createOpportunity }, null, 2) }],
};
}
async getOpportunity(args: unknown): Promise<ToolResult> {
const parseResult = GetOpportunityInputSchema.safeParse(args);
if (!parseResult.success) {
return this.error('Invalid input: ' + parseResult.error.message);
}
const result = await this.graphqlClient.query<{ opportunity: Opportunity }>(
GET_OPPORTUNITY_QUERY,
parseResult.data
);
if (!result?.opportunity) {
return this.error('Opportunity not found');
}
return {
content: [{ type: 'text', text: JSON.stringify({ success: true, opportunity: result.opportunity }, null, 2) }],
};
}
async listOpportunities(args: unknown): Promise<ToolResult> {
const parseResult = ListOpportunitiesInputSchema.safeParse(args);
if (!parseResult.success) {
return this.error('Invalid input: ' + parseResult.error.message);
}
const { limit = 20, companyId } = parseResult.data;
const filter = companyId ? { companyId: { eq: companyId } } : undefined;
const result = await this.graphqlClient.query<{
opportunities: {
edges: Array<{ node: any }>;
pageInfo: { hasNextPage: boolean; endCursor: string };
};
}>(LIST_OPPORTUNITIES_QUERY, { limit, filter });
if (!result?.opportunities) {
return this.error('Failed to list opportunities');
}
const opportunities = result.opportunities.edges.map((e) => ({
id: e.node.id,
name: e.node.name,
amount: e.node.amount,
closeDate: e.node.closeDate,
stage: e.node.stage,
pointOfContactId: e.node.pointOfContactId,
companyId: e.node.companyId,
createdAt: e.node.createdAt,
}));
return {
content: [{ type: 'text', text: JSON.stringify({
opportunities,
count: opportunities.length,
hasMore: result.opportunities.pageInfo.hasNextPage,
}, null, 2) }],
};
}
private error(message: string): ToolResult {
return { content: [{ type: 'text', text: message }] };
}
}
export function getCreateOpportunityToolDefinition(): ToolDefinition {
return createToolDefinition(CREATE_OPPORTUNITY_TOOL_NAME, 'Create a new opportunity in Twenty CRM', CreateOpportunityInputSchema);
}
export function getGetOpportunityToolDefinition(): ToolDefinition {
return createToolDefinition(GET_OPPORTUNITY_TOOL_NAME, 'Get an opportunity by ID from Twenty CRM', GetOpportunityInputSchema);
}
export function getListOpportunitiesToolDefinition(): ToolDefinition {
return createToolDefinition(LIST_OPPORTUNITIES_TOOL_NAME, 'List opportunities from Twenty CRM', ListOpportunitiesInputSchema);
}
export function createOpportunityTool(
graphqlClient: ITwentyGraphQLClient,
restClient: ITwentyRESTClient,
logger: ILogger
): OpportunityTool {
return new OpportunityTool(graphqlClient, restClient, logger);
}