import { z } from "zod"; import { UpworkClient } from "../../services/upwork-client.js"; // Validation schema for the tool parameters const GetProposalDetailsSchema = z.object({ proposalId: z.string().min(1, "Proposal ID is required") }); // Types for the GraphQL response (based on tested field structure from enhancement guide) interface MoneyAmount { rawValue: number; currency: string; displayValue: string; } interface ProposalStatus { status: string; // "Pending", "Accepted", "Declined", "Withdrawn" } 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 to extract information from proposal data 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 ''; } // Main tool handler export const getProposalDetailsTool = { definition: { name: "upwork_get_proposal_details", description: "Get complete proposal and job details using proposal ID from webhook. Returns comprehensive information about the proposal, job posting, client, and contract terms.", inputSchema: { type: "object", properties: { proposalId: { type: "string", description: "The proposal ID received from Upwork webhook (e.g., '1958709255028473857')" } }, required: ["proposalId"] } }, handler: async (params: any, upworkClient: UpworkClient) => { try { // Validate input parameters const validatedParams = GetProposalDetailsSchema.parse(params); // GraphQL query with tested field structure from enhancement guide const query = ` query vendorProposal($id: ID!) { vendorProposal(id: $id) { id coverLetter proposalCoverLetter status { status } terms { chargeRate { rawValue currency displayValue } upfrontPaymentPercent } user { id nid rid name } organization { id name } marketplaceJobPosting { id content { title description } contractTerms { contractType contractStartDate contractEndDate personsToHire experienceLevel onSiteType fixedPriceContractTerms { amount { rawValue currency displayValue } maxAmount { rawValue currency displayValue } } hourlyContractTerms { engagementType notSureProjectDuration hourlyBudgetType hourlyBudgetMin hourlyBudgetMax } } classification { category { id } skills { id prettyName } } } } } `; // Execute the GraphQL query (token management handled by client) 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; // Extract key information for easy consumption const jobTitle = getJobTitle(proposal); const jobBudget = getJobBudget(proposal); const skills = getSkills(proposal); const proposedRate = getProposedRate(proposal); const status = proposal.status?.status || 'Unknown'; const clientName = proposal.user?.name || 'Unknown Client'; // Format the response with useful summary const summary = [ `šŸ“‹ **Proposal Details** (ID: ${proposal.id})`, `šŸ“Œ **Job**: ${jobTitle}`, `šŸ‘¤ **Client**: ${clientName}`, `šŸ“Š **Status**: ${status}`, `šŸ’° **Job Budget**: ${jobBudget || 'Not specified'}`, `šŸ’µ **Your Rate**: ${proposedRate || 'Not specified'}`, `šŸ·ļø **Skills**: ${skills.slice(0, 5).join(', ') || 'None listed'}`, ``, `šŸ”— **View on Upwork**: https://www.upwork.com/ab/proposals/${proposal.id}`, ``, `šŸ“ **Cover Letter Preview**:`, proposal.coverLetter ? `"${proposal.coverLetter.substring(0, 200)}${proposal.coverLetter.length > 200 ? '...' : ''}"` : 'No cover letter available' ].join('\n'); return { content: [{ type: "text", text: summary + '\n\nšŸ“Š **Full Proposal Data**:\n```json\n' + JSON.stringify(proposal, null, 2) + '\n```' }] }; } catch (error) { return { content: [{ type: "text", text: `āŒ Error retrieving proposal details: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } };