Skip to main content
Glama

Analytics MCP

by Ninoambaraa
paypal-analytics-tools.ts11 kB
import { createTool } from '@mastra/core'; import z from 'zod'; import { fetchPaypalCardInfo, type CardInfoRecord } from './paypal-charge-tool'; const dateRangeSchema = z.object({ startDate: z .string() .describe('Start date of transaction charge period (format: YYYY-MM-DD)'), endDate: z .string() .describe('End date of transaction charge period (format: YYYY-MM-DD)'), }); const currencyBreakdownSchema = z.object({ currency: z.string(), transactionCount: z.number(), refundedCount: z.number(), grossAmountMinor: z.number(), grossAmountMajor: z.number(), averageTicketMajor: z.number().nullable(), }); const customerBreakdownSchema = z.object({ customerId: z.string(), transactionCount: z.number(), grossAmountMinor: z.number(), grossAmountMajor: z.number(), lastTransactionId: z.string(), lastTransactionCreated: z.string(), currencyBreakdown: z.array(currencyBreakdownSchema), }); const chargeSummaryOutputSchema = z.object({ range: z.object({ startDate: z.string(), endDate: z.string(), }), totalTransactions: z.number(), refundedTransactions: z.number(), currencyBreakdown: z.array(currencyBreakdownSchema), insights: z.array(z.string()), }); const topCustomersOutputSchema = z.object({ range: z.object({ startDate: z.string(), endDate: z.string(), }), totalCustomers: z.number(), topCustomers: z.array(customerBreakdownSchema), insights: z.array(z.string()), }); export const paypalChargeSummaryTool = createTool({ id: 'paypal-charge-summary-tool', description: 'Summaries PayPal Express Checkout performance for a date range (totals, currency mix).', inputSchema: dateRangeSchema, outputSchema: chargeSummaryOutputSchema, execute: async ({ context }) => { const charges = await fetchPaypalCardInfo({ start: parseDate(context.startDate, 'startDate', true), end: parseDate(context.endDate, 'endDate', false), }); return buildChargeSummary(context.startDate, context.endDate, charges); }, }); export const paypalTopCustomersTool = createTool({ id: 'paypal-top-customers-tool', description: 'Returns the largest PayPal customers by transaction volume for a date range.', inputSchema: dateRangeSchema.extend({ limit: z .number() .int() .min(1) .max(50) .default(5) .describe('Number of top customers to return (1-50)'), }), outputSchema: topCustomersOutputSchema, execute: async ({ context }) => { const charges = await fetchPaypalCardInfo({ start: parseDate(context.startDate, 'startDate', true), end: parseDate(context.endDate, 'endDate', false), }); return buildTopCustomersSummary(context.startDate, context.endDate, charges, context.limit ?? 5); }, }); function buildChargeSummary(startDate: string, endDate: string, charges: CardInfoRecord[]) { const currencyBreakdown = aggregateByCurrency(charges); const refundedTransactions = charges.filter((charge) => isRefunded(charge.status)).length; const summary = { range: { startDate, endDate }, totalTransactions: charges.length, refundedTransactions, currencyBreakdown, }; return { ...summary, insights: generateChargeSummaryInsights({ startDate, endDate, totalTransactions: charges.length, refundedTransactions, currencyBreakdown, }), }; } function buildTopCustomersSummary( startDate: string, endDate: string, charges: CardInfoRecord[], limit: number, ) { const customerMap = new Map< string, { transactionCount: number; grossAmountMinor: number; grossAmountMajor: number; lastTransactionId: string; lastTransactionCreated: string; charges: CardInfoRecord[]; } >(); for (const charge of charges) { const customerId = deriveCustomerId(charge); const existing = customerMap.get(customerId); if (!existing) { customerMap.set(customerId, { transactionCount: 1, grossAmountMinor: charge.amountMinor, grossAmountMajor: charge.amountMajor, lastTransactionId: charge.transactionId, lastTransactionCreated: charge.createdAtUtc, charges: [charge], }); continue; } existing.transactionCount += 1; existing.grossAmountMinor += charge.amountMinor; existing.grossAmountMajor += charge.amountMajor; existing.charges.push(charge); if (new Date(charge.createdAtUtc).getTime() > new Date(existing.lastTransactionCreated).getTime()) { existing.lastTransactionCreated = charge.createdAtUtc; existing.lastTransactionId = charge.transactionId; } } const customerSummaries = Array.from(customerMap.entries()) .map(([customerId, data]) => { const currencyBreakdown = aggregateByCurrency(data.charges); return { customerId, transactionCount: data.transactionCount, grossAmountMinor: data.grossAmountMinor, grossAmountMajor: Number(data.grossAmountMajor.toFixed(2)), lastTransactionId: data.lastTransactionId, lastTransactionCreated: data.lastTransactionCreated, currencyBreakdown, }; }) .sort((a, b) => b.grossAmountMajor - a.grossAmountMajor) .slice(0, limit); const summary = { range: { startDate, endDate }, totalCustomers: customerMap.size, topCustomers: customerSummaries, }; return { ...summary, insights: generateTopCustomerInsights({ startDate, endDate, totalCustomers: customerMap.size, topCustomers: customerSummaries, limit, }), }; } type CurrencyBreakdown = z.infer<typeof currencyBreakdownSchema>; type TopCustomerSummary = z.infer<typeof customerBreakdownSchema>; function generateChargeSummaryInsights(params: { startDate: string; endDate: string; totalTransactions: number; refundedTransactions: number; currencyBreakdown: CurrencyBreakdown[]; }): string[] { const { startDate, endDate, totalTransactions, refundedTransactions, currencyBreakdown } = params; if (totalTransactions === 0) { return [`No PayPal transactions recorded between ${startDate} and ${endDate}.`]; } const insights: string[] = []; const refundRate = totalTransactions ? (refundedTransactions / totalTransactions) * 100 : 0; insights.push( `Processed ${totalTransactions} PayPal transactions from ${startDate} to ${endDate}; ${refundedTransactions} (${formatPercentage( refundRate, )}) were refunded or reversed.`, ); const topCurrency = currencyBreakdown[0]; if (topCurrency) { const averageTicket = topCurrency.averageTicketMajor !== null ? `${formatMajor(topCurrency.averageTicketMajor)} average ticket` : 'average ticket unavailable'; insights.push( `${topCurrency.currency.toUpperCase()} led PayPal volume with ${topCurrency.transactionCount} transactions totaling ${formatMajor( topCurrency.grossAmountMajor, )}; ${averageTicket}.`, ); } return insights; } function generateTopCustomerInsights(params: { startDate: string; endDate: string; totalCustomers: number; topCustomers: TopCustomerSummary[]; limit: number; }): string[] { const { startDate, endDate, totalCustomers, topCustomers, limit } = params; if (totalCustomers === 0) { return [`No PayPal customer activity detected between ${startDate} and ${endDate}.`]; } const insights: string[] = []; const displayedCount = topCustomers.length; const displayedGross = topCustomers.reduce((sum, customer) => sum + customer.grossAmountMajor, 0); insights.push( `${totalCustomers} PayPal customers transacted between ${startDate} and ${endDate}; top ${displayedCount} accounted for ${formatMajor( Number(displayedGross.toFixed(2)), )}.`, ); const leader = topCustomers[0]; if (leader) { const lastDate = formatDate(leader.lastTransactionCreated); insights.push( `Top customer ${leader.customerId} contributed ${formatMajor( leader.grossAmountMajor, )} across ${leader.transactionCount} transactions; last activity ${lastDate}.`, ); } if (totalCustomers > limit && displayedCount > 0) { insights.push(`Additional ${totalCustomers - displayedCount} customers generated the remainder of volume.`); } return insights; } function aggregateByCurrency(charges: CardInfoRecord[]) { const map = new Map< string, { transactionCount: number; refundedCount: number; grossAmountMinor: number; grossAmountMajor: number } >(); for (const charge of charges) { const currency = (charge.currency ?? 'USD').toUpperCase(); const entry = map.get(currency) ?? { transactionCount: 0, refundedCount: 0, grossAmountMinor: 0, grossAmountMajor: 0 }; entry.transactionCount += 1; entry.grossAmountMinor += charge.amountMinor; entry.grossAmountMajor += charge.amountMajor; entry.refundedCount += isRefunded(charge.status) ? 1 : 0; map.set(currency, entry); } const breakdown = Array.from(map.entries()).map(([currency, entry]) => ({ currency, transactionCount: entry.transactionCount, refundedCount: entry.refundedCount, grossAmountMinor: entry.grossAmountMinor, grossAmountMajor: Number(entry.grossAmountMajor.toFixed(2)), averageTicketMajor: entry.transactionCount > 0 ? Number((entry.grossAmountMajor / entry.transactionCount).toFixed(2)) : null, })); breakdown.sort((a, b) => b.grossAmountMajor - a.grossAmountMajor); return breakdown; } function isRefunded(status: string | null): boolean { if (!status) return false; const normalized = status.toLowerCase(); return normalized.includes('refund') || normalized === 'reversed' || normalized === 'cancelled'; } function deriveCustomerId(charge: CardInfoRecord): string { return charge.customerEmail ?? charge.customerPhone ?? 'unknown'; } function parseDate(date: string, field: 'startDate' | 'endDate', isStart: boolean): Date { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date); if (!match) { throw new Error(`Invalid ${field} supplied. Expected format YYYY-MM-DD, received: ${date}`); } const [, yearStr, monthStr, dayStr] = match; const year = Number(yearStr); const monthIndex = Number(monthStr) - 1; const day = Number(dayStr); const ms = isStart ? Date.UTC(year, monthIndex, day, 0, 0, 0, 0) : Date.UTC(year, monthIndex, day, 23, 59, 59, 999); if (Number.isNaN(ms)) { throw new Error(`Invalid ${field} supplied. Expected format YYYY-MM-DD, received: ${date}`); } return new Date(ms); } function formatPercentage(value: number): string { return `${value.toFixed(1)}%`; } function formatMajor(value: number): string { return value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2, }); } function formatDate(iso: string): string { const date = new Date(iso); if (Number.isNaN(date.getTime())) return iso; return date.toISOString().slice(0, 10); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Ninoambaraa/superalink-mcp-analytics'

If you have feedback or need assistance with the MCP directory API, please join our Discord server