Add taxableFlag to product updates, QUO-Narrative quote fallback, and orphan reconciliation

- Add taxableFlag boolean field to product update schema and forecast patch
- Fall back to QUO-Narrative product customerDescription for quote narrative
- Reconcile orphaned local opportunity records not found in CW during refresh
- Invalidate caches for removed orphaned opportunities
- Add reconciled event and orphanedCount to refresh events
- Update API_ROUTES.md with taxableFlag field documentation
This commit is contained in:
2026-03-09 17:48:47 -05:00
parent ee3e0a7377
commit 5afda8cb34
5 changed files with 67 additions and 13 deletions
@@ -2,6 +2,7 @@ import { prisma } from "../../../constants";
import { events } from "../../globalEvents";
import { opportunityCw } from "./opportunities";
import { OpportunityController } from "../../../controllers/OpportunityController";
import { invalidateAllOpportunityCaches } from "../../cache/opportunityCache";
/**
* Refresh Opportunities
@@ -21,7 +22,7 @@ export const refreshOpportunities = async () => {
// 2. Fetch all DB items with their cwOpportunityId and cwLastUpdated
const dbItems = await prisma.opportunity.findMany({
select: { cwOpportunityId: true, cwLastUpdated: true },
select: { id: true, cwOpportunityId: true, cwLastUpdated: true },
});
const dbMap = new Map(
dbItems.map((item) => [item.cwOpportunityId, item.cwLastUpdated]),
@@ -41,11 +42,35 @@ export const refreshOpportunities = async () => {
}
}
// 3b. Reconcile — find local records that no longer exist in CW
const orphanedItems = dbItems.filter(
(item) => !cwSummaries.has(item.cwOpportunityId),
);
if (orphanedItems.length > 0) {
console.log(
`[refreshOpportunities] Reconciling ${orphanedItems.length} orphaned local record(s) not found in CW`,
);
await Promise.all(
orphanedItems.map(async (item) => {
await prisma.opportunity.delete({ where: { id: item.id } });
await invalidateAllOpportunityCaches(item.cwOpportunityId);
}),
);
events.emit("cw:opportunities:refresh:reconciled", {
orphanedCount: orphanedItems.length,
removedCwIds: orphanedItems.map((i) => i.cwOpportunityId),
});
}
if (staleIds.length === 0) {
events.emit("cw:opportunities:refresh:skipped", {
totalCw: cwSummaries.size,
totalDb: dbItems.length,
staleCount: 0,
orphanedCount: orphanedItems.length,
});
return;
}
@@ -106,5 +131,6 @@ export const refreshOpportunities = async () => {
totalDb: dbItems.length,
staleCount: staleIds.length,
itemsUpdated: updatedCount,
orphanedCount: orphanedItems.length,
});
};
+6
View File
@@ -171,11 +171,17 @@ interface EventTypes {
totalDb: number;
staleCount: number;
itemsUpdated: number;
orphanedCount: number;
}) => void;
"cw:opportunities:refresh:reconciled": (data: {
orphanedCount: number;
removedCwIds: number[];
}) => void;
"cw:opportunities:refresh:skipped": (data: {
totalCw: number;
totalDb: number;
staleCount: number;
orphanedCount: number;
}) => void;
// Cache Events