← back to Govarbitrage
src/lib/listing-detail.ts
52 lines
import { prisma } from "@/lib/db";
import { getCurrentTier } from "@/lib/current-user";
import { tierDef } from "@/lib/tiers";
import type { Tier } from "@prisma/client";
// Single source of truth for the listing DETAIL payload and its tier gate.
// The list API (/api/listings) redacts PAID_FIELDS on flat rows; the detail
// surfaces (API route + page) return full relations, so here the FREE tier
// gets the money-math relations nulled entirely — costBreakdown, research
// valuations, scores, comparables, and buyer-lead intelligence are exactly
// what the paid tiers buy.
export type ListingDetail = NonNullable<Awaited<ReturnType<typeof queryListingDetail>>>;
function queryListingDetail(id: string) {
return prisma.listing.findUnique({
where: { id },
include: {
research: true,
costBreakdown: true,
scores: { orderBy: { value: "desc" } },
comparables: { orderBy: { price: "desc" } },
notes: { orderBy: { createdAt: "desc" } },
events: { orderBy: { createdAt: "desc" }, take: 20 },
buyerLeads: { orderBy: { createdAt: "desc" } },
buyerPage: true,
outcome: true,
},
});
}
export async function getGatedListingDetail(
id: string
): Promise<{ listing: ListingDetail | null; tier: Tier; gated: boolean }> {
const tier = await getCurrentTier();
const gated = !tierDef(tier).limits.showMoneyMath;
const listing = await queryListingDetail(id);
if (!listing || !gated) return { listing, tier, gated };
const redacted: ListingDetail = {
...listing,
research: null,
costBreakdown: null,
scores: [],
comparables: [],
buyerLeads: [],
buyerPage: null,
outcome: null,
};
return { listing: redacted, tier, gated };
}