wulf-pulse/app/api/config-enrichment/route.ts
Lorentz Hinrichsen 3c3124d8c9 Restructure: rename to Pulse and move app to root
- Renamed project from PSA-Utils to Pulse
- Moved all app files from autotask-app/ to root
- Updated package.json name to 'pulse'
- Updated Docker container names to pulse-app and pulse-redis
- Updated Docker network name to pulse-network
2025-10-28 23:08:54 -04:00

201 lines
7.5 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { getAutotaskClient } from '@/lib/services/autotask-factory';
// Extract serial number from lineItemFullDescription
function extractSerialNumber(description: string | null): string | null {
if (!description) return null;
// Look for "Serial Number(s)" followed by the serial
const match = description.match(/Serial Number\(s\)\s+([A-Z0-9]+)/i);
return match ? match[1] : null;
}
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const configItemId = searchParams.get('configItemId');
const invoiceId = searchParams.get('invoiceId');
// If invoice ID is provided, just return billing items for that invoice
if (invoiceId) {
const autotaskClient = getAutotaskClient();
const billingItems = await autotaskClient.queryEntity('BillingItems', {
filter: [{ op: 'eq', field: 'invoiceID', value: parseInt(invoiceId) }],
});
console.log(`Found ${billingItems.length} billing items for invoice ${invoiceId}`);
return NextResponse.json({
configItem: null,
billingItems,
invoices: [],
tickets: [],
summary: {
totalBilled: billingItems.reduce((sum: number, item: any) => sum + (item.totalAmount || 0), 0),
purchaseDate: null,
lastInvoiceDate: null,
ticketCount: 0,
},
});
}
if (!configItemId) {
return NextResponse.json(
{ error: 'Configuration Item ID or Invoice ID is required' },
{ status: 400 }
);
}
const autotaskClient = getAutotaskClient();
// Get the configuration item
const configItem = await autotaskClient.getConfigurationItemById(parseInt(configItemId));
if (!configItem) {
return NextResponse.json(
{ error: 'Configuration item not found' },
{ status: 404 }
);
}
// Get date range from query params
const startDateParam = searchParams.get('startDate');
const endDateParam = searchParams.get('endDate');
const daysBackParam = searchParams.get('daysBack');
let startDateFilter: string;
let endDateFilter: string;
if (startDateParam && endDateParam) {
// Use explicit date range
startDateFilter = startDateParam;
endDateFilter = endDateParam;
console.log('Using explicit date range:', { startDateFilter, endDateFilter });
} else {
// Fallback to daysBack (for backward compatibility)
const daysBack = parseInt(daysBackParam || '90');
const dateAgo = new Date();
dateAgo.setDate(dateAgo.getDate() - daysBack);
startDateFilter = dateAgo.toISOString().split('T')[0];
endDateFilter = new Date().toISOString().split('T')[0];
console.log('Using daysBack:', daysBack);
}
console.log('Searching for enrichment data:', {
configItemId,
companyID: configItem.companyID,
serialNumber: configItem.serialNumber,
startDate: startDateFilter,
endDate: endDateFilter
});
let invoices: any[] = [];
try {
invoices = await autotaskClient.queryEntity('Invoices', {
filter: [
{ op: 'eq', field: 'companyID', value: configItem.companyID },
{ op: 'gte', field: 'invoiceDateTime', value: startDateFilter },
{ op: 'lte', field: 'invoiceDateTime', value: endDateFilter }
],
});
console.log(`Found ${invoices.length} invoices for company between ${startDateFilter} and ${endDateFilter}`);
} catch (err) {
console.error('Error fetching invoices:', err);
}
// Get billing items (line items) for all invoices - hardware only
let billingItems: any[] = [];
if (invoices.length > 0) {
try {
const invoiceIds = invoices.map(inv => inv.id);
// Fetch only hardware billing items (type 3) to reduce data volume
console.log(`Fetching hardware billing items (type 3) for company ${configItem.companyID}...`);
const allBillingItems = await autotaskClient.queryEntity('BillingItems', {
filter: [
{ op: 'eq', field: 'companyID', value: configItem.companyID },
{ op: 'gte', field: 'itemDate', value: startDateFilter },
{ op: 'lte', field: 'itemDate', value: endDateFilter },
{ op: 'eq', field: 'billingItemType', value: 3 } // Hardware only
],
});
console.log(`Fetched ${allBillingItems.length} hardware billing items`);
if (allBillingItems.length >= 500) {
console.warn('⚠️ Hit API limit of 500 records - older purchases may not be included');
}
// Enrich items (already filtered to hardware at API level)
const enrichedItems = allBillingItems
.map((item: any) => {
const serialNumber = extractSerialNumber(item.lineItemFullDescription);
const profit = (item.totalAmount || 0) - (item.ourCost || 0);
const margin = item.totalAmount ? ((profit / item.totalAmount) * 100).toFixed(1) : '0';
return {
...item,
extractedSerialNumber: serialNumber,
profit,
profitMargin: margin,
};
});
// If we have a config item serial number, filter to matching items
if (configItem.serialNumber) {
console.log(`Looking for serial: ${configItem.serialNumber}`);
console.log(`Extracted serials from billing items:`, enrichedItems.map((i: any) => i.extractedSerialNumber).filter(Boolean));
// Match if serial appears in extracted field OR anywhere in the description
billingItems = enrichedItems.filter((item: any) => {
const serial = configItem.serialNumber!.toLowerCase();
const extractedMatch = item.extractedSerialNumber &&
item.extractedSerialNumber.toLowerCase() === serial;
const descriptionMatch = item.lineItemFullDescription &&
item.lineItemFullDescription.toLowerCase().includes(serial);
return extractedMatch || descriptionMatch;
});
console.log(`Found ${billingItems.length} billing items matching serial ${configItem.serialNumber}`);
// If no exact match, return empty array (only show exact serial matches)
if (billingItems.length === 0) {
console.log(`No exact serial match found for ${configItem.serialNumber}`);
}
} else {
billingItems = enrichedItems;
console.log(`Found ${billingItems.length} hardware billing items (no serial filter)`);
}
} catch (err) {
console.error('Error fetching billing items:', err);
}
}
// Calculate summary
const totalBilled = invoices.reduce((sum: number, invoice: any) =>
sum + (invoice.invoiceTotal || 0), 0
);
const lastInvoiceDate = invoices.length > 0
? invoices.sort((a: any, b: any) =>
new Date(b.invoiceDateTime).getTime() - new Date(a.invoiceDateTime).getTime()
)[0]?.invoiceDateTime
: null;
return NextResponse.json({
configItem,
billingItems,
invoices,
tickets: [],
summary: {
totalBilled,
purchaseDate: null,
lastInvoiceDate,
ticketCount: 0,
},
});
} catch (error) {
console.error('Error in config enrichment:', error);
return NextResponse.json(
{ error: 'Failed to fetch enrichment data' },
{ status: 500 }
);
}
}