import { z } from "zod"; import { UpworkClient } from "../../services/upwork-client.js"; // Validation schema for the tool parameters const FormatProposalNotificationSchema = z.object({ proposalId: z.string().min(1, "Proposal ID is required"), eventType: z.string().min(1, "Event type is required") }); // Types for the GraphQL response (same as get-proposal-details) interface MoneyAmount { rawValue: number; currency: string; displayValue: string; } interface ProposalStatus { status: string; } interface ProposalTerms { chargeRate?: MoneyAmount; upfrontPaymentPercent?: number; } interface ContractTerms { contractType: "FIXED" | "HOURLY"; experienceLevel?: string; personsToHire?: number; onSiteType?: string; fixedPriceContractTerms?: { amount?: MoneyAmount; maxAmount?: MoneyAmount; }; hourlyContractTerms?: { engagementType?: string; hourlyBudgetType?: string; hourlyBudgetMin?: number; hourlyBudgetMax?: number; }; } interface JobPosting { id: string; content?: { title?: string; description?: string; }; contractTerms?: ContractTerms; classification?: { skills?: Array<{ id: string; prettyName: string; }>; }; } interface ProposalDetails { id: string; coverLetter?: string; status?: ProposalStatus; terms?: ProposalTerms; marketplaceJobPosting?: JobPosting; user?: { id: string; name?: string; }; } // Utility functions function getJobTitle(proposal: ProposalDetails): string { return proposal.marketplaceJobPosting?.content?.title || 'Unknown Job'; } function getJobBudget(proposal: ProposalDetails): string { const contractTerms = proposal.marketplaceJobPosting?.contractTerms; if (!contractTerms) return ''; if (contractTerms.contractType === 'FIXED' && contractTerms.fixedPriceContractTerms?.amount) { return contractTerms.fixedPriceContractTerms.amount.displayValue || ''; } if (contractTerms.contractType === 'HOURLY' && contractTerms.hourlyContractTerms) { const { hourlyBudgetMin, hourlyBudgetMax } = contractTerms.hourlyContractTerms; if (hourlyBudgetMin && hourlyBudgetMax) { return `$${hourlyBudgetMin}-$${hourlyBudgetMax}/hr`; } } return ''; } function getSkills(proposal: ProposalDetails): string[] { return proposal.marketplaceJobPosting?.classification?.skills ?.map(skill => skill.prettyName) .filter(Boolean) || []; } function getProposedRate(proposal: ProposalDetails): string { if (proposal.terms?.chargeRate?.displayValue) { return proposal.terms.chargeRate.displayValue; } return ''; } // Format notification message with emojis and rich formatting function formatProposalNotification(proposal: ProposalDetails, eventType: string): string { const statusEmoji = { 'Pending': 'ā³', 'Accepted': 'āœ…', 'Declined': 'āŒ', 'Withdrawn': '🚫' }[proposal.status?.status || ''] || 'šŸ“‹'; const actionEmoji = { 'VendorCreate': 'šŸ“', 'VendorUpdate': 'šŸ”„', 'VendorDelete': 'šŸ—‘ļø' }[eventType] || 'ā“'; const jobTitle = getJobTitle(proposal); const jobBudget = getJobBudget(proposal); const skills = getSkills(proposal).slice(0, 5); // Limit to 5 skills const proposedRate = getProposedRate(proposal); const clientName = proposal.user?.name || 'Client'; const lines = [ `${actionEmoji} **Proposal ${eventType.replace('Vendor', '')}** ${statusEmoji}`, `šŸ“Œ **Job**: ${jobTitle}${jobBudget ? ` - ${jobBudget}` : ''}`, `šŸ‘¤ **Client**: ${clientName}`, `šŸ’µ **Your Rate**: ${proposedRate || 'Not specified'}`, `šŸ”— **View**: https://www.upwork.com/ab/proposals/${proposal.id}` ]; if (skills.length > 0) { lines.push(`šŸ·ļø **Skills**: ${skills.join(', ')}`); } return lines.join('\n'); } // Main tool handler export const formatProposalNotificationTool = { definition: { name: "upwork_format_proposal_notification", description: "Format a proposal for notification with job details and emojis. Perfect for Discord/Slack notifications when proposal webhooks are received.", inputSchema: { type: "object", properties: { proposalId: { type: "string", description: "The proposal ID received from Upwork webhook" }, eventType: { type: "string", description: "The webhook event type (VendorCreate, VendorUpdate, VendorDelete, etc.)" } }, required: ["proposalId", "eventType"] } }, handler: async (params: any, upworkClient: UpworkClient) => { try { // Validate input parameters const validatedParams = FormatProposalNotificationSchema.parse(params); // Simplified GraphQL query for notification formatting const query = ` query vendorProposal($id: ID!) { vendorProposal(id: $id) { id status { status } terms { chargeRate { rawValue currency displayValue } } user { id name } marketplaceJobPosting { id content { title } contractTerms { contractType fixedPriceContractTerms { amount { rawValue currency displayValue } } hourlyContractTerms { hourlyBudgetMin hourlyBudgetMax } } classification { skills { id prettyName } } } } } `; // Execute the GraphQL query const result = await upworkClient.graphqlQuery(query, { id: validatedParams.proposalId }); if (!result.data?.vendorProposal) { return { content: [{ type: "text", text: `āŒ No proposal found with ID: ${validatedParams.proposalId}` }], isError: true }; } const proposal: ProposalDetails = result.data.vendorProposal; // Generate the formatted notification const notification = formatProposalNotification(proposal, validatedParams.eventType); // Extract details for additional context const details = { jobTitle: getJobTitle(proposal), jobBudget: getJobBudget(proposal), status: proposal.status?.status, skills: getSkills(proposal).slice(0, 3), proposedRate: getProposedRate(proposal), clientName: proposal.user?.name }; return { content: [{ type: "text", text: `šŸ”” **Formatted Notification:**\n\n${notification}\n\nšŸ“Š **Additional Details:**\n\`\`\`json\n${JSON.stringify(details, null, 2)}\n\`\`\`` }] }; } catch (error) { return { content: [{ type: "text", text: `āŒ Error formatting proposal notification: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } };