UNPKG

qbo-mcp-ts

Version:

TypeScript QuickBooks Online MCP Server with enhanced features and dual transport support

358 lines 12.9 kB
"use strict"; /** * Invoice service for QuickBooks operations */ Object.defineProperty(exports, "__esModule", { value: true }); exports.InvoiceService = void 0; const types_1 = require("../types"); const date_parser_1 = require("../utils/date-parser"); const logger_1 = require("../utils/logger"); const cache_1 = require("./cache"); const queue_1 = require("./queue"); class InvoiceService { api; constructor(api) { this.api = api; } /** * Get invoices with filtering */ async getInvoices(params) { try { // Validate input const validated = types_1.GetInvoicesSchema.parse(params); // Build cache key const cacheKey = `invoices:${JSON.stringify(validated)}`; // Check cache const cached = await cache_1.cacheService.get(cacheKey); if (cached) { logger_1.logger.info('Returning cached invoice list'); return cached; } // Build query let query = 'SELECT * FROM Invoice'; const conditions = []; // Status filter if (validated.status && validated.status !== 'all') { switch (validated.status) { case 'unpaid': conditions.push("Balance > '0'"); break; case 'paid': conditions.push("Balance = '0'"); break; case 'overdue': conditions.push(`Balance > '0' AND DueDate < '${date_parser_1.DateParser.parse('today')}'`); break; } } // Customer filter if (validated.customerName) { // First, find the customer const customer = await this.findCustomerByName(validated.customerName); if (customer) { conditions.push(`CustomerRef = '${customer.Id}'`); } } // Date range filter if (validated.dateFrom) { conditions.push(`TxnDate >= '${date_parser_1.DateParser.parse(validated.dateFrom)}'`); } if (validated.dateTo) { conditions.push(`TxnDate <= '${date_parser_1.DateParser.parse(validated.dateTo)}'`); } // Amount filters if (validated.minAmount !== undefined) { conditions.push(`TotalAmt >= '${validated.minAmount}'`); } if (validated.maxAmount !== undefined) { conditions.push(`TotalAmt <= '${validated.maxAmount}'`); } // Add conditions to query if (conditions.length > 0) { query += ' WHERE ' + conditions.join(' AND '); } // Add ordering and limit query += ' ORDER BY TxnDate DESC'; if (validated.limit) { query += ` MAXRESULTS ${validated.limit}`; } // Execute query with queue const result = await queue_1.queueService.add(() => this.api.query(query)); // Format response const invoices = result.QueryResponse?.Invoice || []; const response = this.formatInvoiceList(invoices); // Cache the result await cache_1.cacheService.set(cacheKey, response, 300); // 5 minutes return response; } catch (error) { logger_1.logger.error('Failed to get invoices', error); throw error; } } /** * Create a new invoice */ async createInvoice(params) { try { // Validate input const validated = types_1.CreateInvoiceSchema.parse(params); // Find customer const customer = await this.findCustomerByName(validated.customerName); if (!customer) { throw new types_1.ValidationError(`Customer not found: ${validated.customerName}`); } // Build invoice object const invoice = { CustomerRef: { value: customer.Id, name: customer.DisplayName, }, Line: validated.items.map((item, index) => ({ LineNum: index + 1, Description: item.description, Amount: item.amount, DetailType: 'SalesItemLineDetail', SalesItemLineDetail: { ItemRef: { value: '1', // Default service item name: 'Services', }, UnitPrice: item.unitPrice || item.amount, Qty: item.quantity || 1, }, })), }; // Set due date if (validated.dueDate) { invoice.DueDate = date_parser_1.DateParser.parse(validated.dueDate); } else { // Default to 30 days invoice.DueDate = date_parser_1.DateParser.addBusinessDays('today', 30); } // Add memo if provided if (validated.memo) { invoice.PrivateNote = validated.memo; } // Create invoice with queue const created = await queue_1.queueService.add(() => this.api.post('/invoice', invoice)); logger_1.logger.info('Created invoice', { invoiceId: created.Id, customer: validated.customerName, total: created.TotalAmt, }); // Send email if requested if (validated.emailToCustomer) { await this.sendInvoice({ invoiceId: created.Id, }); } // Clear cache await cache_1.cacheService.delete('invoices:*'); return created; } catch (error) { logger_1.logger.error('Failed to create invoice', error); throw error; } } /** * Send invoice via email */ async sendInvoice(params) { try { // Validate input const validated = types_1.SendInvoiceSchema.parse(params); // Send with queue await queue_1.queueService.add(() => this.api.sendEmail('Invoice', validated.invoiceId, validated.email)); logger_1.logger.info('Sent invoice', { invoiceId: validated.invoiceId, email: validated.email, }); } catch (error) { logger_1.logger.error('Failed to send invoice', error); throw error; } } /** * Get invoice by ID */ async getInvoiceById(invoiceId) { try { const cacheKey = `invoice:${invoiceId}`; // Check cache const cached = await cache_1.cacheService.get(cacheKey); if (cached) { return cached; } // Get from API const invoice = await queue_1.queueService.add(() => this.api.get(`/invoice/${invoiceId}`)); // Cache it await cache_1.cacheService.set(cacheKey, invoice, 600); // 10 minutes return invoice; } catch (error) { logger_1.logger.error('Failed to get invoice by ID', error); throw error; } } /** * Update an invoice */ async updateInvoice(invoiceId, updates) { try { // Get current invoice const current = await this.getInvoiceById(invoiceId); // Merge updates const updated = { ...current, ...updates, Id: invoiceId, SyncToken: current.MetaData?.LastUpdatedTime, }; // Update with queue const result = await queue_1.queueService.add(() => this.api.post('/invoice', updated)); // Clear cache await cache_1.cacheService.delete(`invoice:${invoiceId}`); await cache_1.cacheService.delete('invoices:*'); return result; } catch (error) { logger_1.logger.error('Failed to update invoice', error); throw error; } } /** * Delete an invoice */ async deleteInvoice(invoiceId) { try { // Get current invoice for sync token const current = await this.getInvoiceById(invoiceId); // Delete with queue await queue_1.queueService.add(() => this.api.post('/invoice', { Id: invoiceId, SyncToken: current.MetaData?.LastUpdatedTime, Active: false, })); // Clear cache await cache_1.cacheService.delete(`invoice:${invoiceId}`); await cache_1.cacheService.delete('invoices:*'); logger_1.logger.info('Deleted invoice', { invoiceId }); } catch (error) { logger_1.logger.error('Failed to delete invoice', error); throw error; } } /** * Get invoice PDF */ async getInvoicePDF(invoiceId) { try { return await queue_1.queueService.add(() => this.api.downloadPDF('Invoice', invoiceId)); } catch (error) { logger_1.logger.error('Failed to get invoice PDF', error); throw error; } } /** * Find customer by name */ async findCustomerByName(name) { try { const query = `SELECT * FROM Customer WHERE DisplayName = '${name}' OR CompanyName = '${name}'`; const result = await this.api.query(query); const customers = result.QueryResponse?.Customer || []; return customers[0] || null; } catch (error) { logger_1.logger.error('Failed to find customer', error); return null; } } /** * Format invoice list for response */ formatInvoiceList(invoices) { let totalAmount = 0; const formatted = invoices.map((inv) => { totalAmount += inv.TotalAmt; return { invoiceNumber: inv.DocNumber || `INV-${inv.Id}`, customer: inv.CustomerRef.name || 'Unknown', date: date_parser_1.DateParser.formatDisplay(inv.TxnDate), dueDate: inv.DueDate ? date_parser_1.DateParser.formatDisplay(inv.DueDate) : 'N/A', total: `$${inv.TotalAmt.toFixed(2)}`, balance: `$${(inv.Balance || 0).toFixed(2)}`, status: this.getInvoiceStatus(inv), id: inv.Id, }; }); return { summary: `Found ${invoices.length} invoice${invoices.length !== 1 ? 's' : ''}`, count: invoices.length, totalAmount, invoices: formatted, }; } /** * Determine invoice status */ getInvoiceStatus(invoice) { if (!invoice.Balance || invoice.Balance === 0) { return 'Paid'; } if (invoice.DueDate && date_parser_1.DateParser.isOverdue(invoice.DueDate)) { return 'Overdue'; } return 'Unpaid'; } /** * Get aging report for invoices */ async getAgingReport() { try { const invoices = await this.getInvoices({ status: 'unpaid' }); const aging = { current: [], '30days': [], '60days': [], '90days': [], over90: [], }; const today = new Date(); for (const inv of invoices.invoices) { if (inv.status === 'Unpaid' || inv.status === 'Overdue') { const daysOld = date_parser_1.DateParser.daysBetween(inv.date, today); if (daysOld <= 30) { aging.current.push(inv); } else if (daysOld <= 60) { aging['30days'].push(inv); } else if (daysOld <= 90) { aging['60days'].push(inv); } else if (daysOld <= 120) { aging['90days'].push(inv); } else { aging['over90'].push(inv); } } } return aging; } catch (error) { logger_1.logger.error('Failed to get aging report', error); throw error; } } } exports.InvoiceService = InvoiceService; //# sourceMappingURL=invoice.js.map