← 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 };
}