import { createRoute } from "../../../modules/api-utils/createRoute"; import { opportunities } from "../../../managers/opportunities"; import { apiResponse } from "../../../modules/api-utils/apiResponse"; import { ContentfulStatusCode } from "hono/utils/http-status"; import { authMiddleware } from "../../middleware/authorization"; import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions"; import GenericError from "../../../Errors/GenericError"; import { prisma } from "../../../constants"; import { computeSubResourceCacheTTL } from "../../../modules/algorithms/computeSubResourceCacheTTL"; import { computeProductsCacheTTL } from "../../../modules/algorithms/computeProductsCacheTTL"; import { getCachedSite, getCachedNotes, getCachedContacts, getCachedProducts, fetchAndCacheNotes, fetchAndCacheContacts, fetchAndCacheProducts, fetchAndCacheSite, } from "../../../modules/cache/opportunityCache"; import { generatedQuotes } from "../../../managers/generatedQuotes"; /* GET /v1/sales/opportunities/:identifier?include=notes,contacts,products,quotes */ export default createRoute( "get", ["/opportunities/:identifier"], async (c) => { const identifier = c.req.param("identifier"); const includeParam = c.req.query("include") ?? ""; const includes = new Set( includeParam .split(",") .map((s) => s.trim().toLowerCase()) .filter(Boolean), ); // ── Quick DB lookup (≈3ms) to get cwOpportunityId for pre-warming ── const isNumeric = /^\d+$/.test(identifier); const dbRecord = await prisma.opportunity.findFirst({ where: isNumeric ? { cwOpportunityId: Number(identifier) } : { id: identifier }, select: { cwOpportunityId: true, companyCwId: true, siteCwId: true, closedFlag: true, closedDate: true, expectedCloseDate: true, cwLastUpdated: true, statusCwId: true, }, }); if (!dbRecord) { throw new GenericError({ message: "Opportunity not found", name: "OpportunityNotFound", cause: `No opportunity exists with identifier '${identifier}'`, status: 404, }); } // Compute TTLs from DB state const subTtl = computeSubResourceCacheTTL({ closedFlag: dbRecord.closedFlag, closedDate: dbRecord.closedDate, expectedCloseDate: dbRecord.expectedCloseDate, lastUpdated: dbRecord.cwLastUpdated, }); const prodTtl = computeProductsCacheTTL({ closedFlag: dbRecord.closedFlag, closedDate: dbRecord.closedDate, expectedCloseDate: dbRecord.expectedCloseDate, lastUpdated: dbRecord.cwLastUpdated, statusCwId: dbRecord.statusCwId, }); // ── Pre-warm sub-resources only on cache miss ─────────────────────── // Check Redis first — if the background refresh has kept the keys warm, // skip the CW calls entirely. Only fetch-and-cache on a miss. const cwOppId = dbRecord.cwOpportunityId; const _ignoreErrors = (p: Promise) => p.catch(() => {}); const prewarmPromises: Promise[] = []; if (dbRecord.companyCwId && dbRecord.siteCwId) { const compId = dbRecord.companyCwId, siteId = dbRecord.siteCwId; prewarmPromises.push( _ignoreErrors( getCachedSite(compId, siteId).then( (c) => c ?? fetchAndCacheSite(compId, siteId), ), ), ); } if (includes.has("notes") && subTtl) prewarmPromises.push( _ignoreErrors( getCachedNotes(cwOppId).then( (c) => c ?? fetchAndCacheNotes(cwOppId, subTtl), ), ), ); if (includes.has("contacts") && subTtl) prewarmPromises.push( _ignoreErrors( getCachedContacts(cwOppId).then( (c) => c ?? fetchAndCacheContacts(cwOppId, subTtl), ), ), ); if (includes.has("products") && prodTtl) prewarmPromises.push( _ignoreErrors( getCachedProducts(cwOppId).then( (c) => c ?? fetchAndCacheProducts(cwOppId, prodTtl), ), ), ); // fetchItem runs its own CW calls (opp, activities, company) — // these execute concurrently with the sub-resource pre-warming above. const [item] = await Promise.all([ opportunities.fetchItem(identifier), ...prewarmPromises, ]); // Sub-resources now hit warm Redis cache (near-instant) const subResourcePromises: Record> = { _site: item.fetchSite(), }; if (includes.has("notes")) { subResourcePromises.notes = item.fetchNotes(); } if (includes.has("contacts")) { subResourcePromises.contacts = item.fetchContacts(); } if (includes.has("products")) { subResourcePromises.products = item .fetchProducts() .then((products) => products.map((p) => p.toJson())); } if (includes.has("quotes")) { subResourcePromises.quotes = generatedQuotes .fetchByOpportunity(item.id) .then((quotes) => quotes.map((q) => q.toJson())); } const keys = Object.keys(subResourcePromises); const results = await Promise.all(keys.map((k) => subResourcePromises[k])); // Apply toJson after site is hydrated (side-effect from fetchSite) const gatedData = await processObjectValuePerms( item.toJson(), "obj.opportunity", c.get("user"), ); const originalOpportunityNoteText = (gatedData as any).notes; // Attach sub-resources (skip the internal _site key) keys.forEach((k, i) => { if (k !== "_site") { (gatedData as any)[k] = results[i]; } }); if (includes.has("notes")) { (gatedData as any).opportunityNoteText = typeof originalOpportunityNoteText === "string" ? originalOpportunityNoteText : null; } const response = apiResponse.successful( "Opportunity fetched successfully!", gatedData, ); return c.json(response, response.status as ContentfulStatusCode); }, authMiddleware({ permissions: ["sales.opportunity.fetch"] }), );