qbo-mcp-ts
Version:
TypeScript QuickBooks Online MCP Server with enhanced features and dual transport support
358 lines • 12.9 kB
JavaScript
;
/**
* 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