← back to NationalPaperHangers

lib/services/marketplace.js

104 lines

// Marketplace totals for admin dashboards. Pure read functions over
// `bookings` + `payment_events`. Two scopes:
//   - per-installer (what one studio sees in its admin)
//   - platform-wide (what NPH staff sees in /admin/billing)
// Defensive against empty tables / NULL sums; always returns numeric zeros.

const db = require('../db');

function num(v) { return v == null ? 0 : Number(v); }

async function getInstallerMarketplaceTotals(installerId) {
  // Deposits paid: bookings where deposit_status='paid' for this installer.
  // Platform fee: sum of application_fee_cents from payment_events for this
  //   installer's PI succeeded events.
  // Pending payouts (platform_hold): bookings where deposit_status='paid' AND
  //   the installer had no Connect account at charge-time → metadata flagged
  //   the payout for manual transfer. We approximate by counting paid bookings
  //   where the installer's stripe_account_id is NULL right now.
  const summary = await db.one(
    `SELECT
       COALESCE(SUM(b.deposit_amount_cents) FILTER (WHERE b.deposit_status='paid'), 0)::bigint AS deposits_paid_cents,
       COUNT(*) FILTER (WHERE b.deposit_status='paid')::int  AS count_paid,
       COUNT(*) FILTER (WHERE b.deposit_status='failed')::int AS count_failed,
       COUNT(*) FILTER (WHERE b.deposit_status='requires_payment')::int AS count_pending
     FROM bookings b
     WHERE b.installer_id = $1`,
    [installerId]
  ) || {};

  const fees = await db.one(
    `SELECT COALESCE(SUM(pe.application_fee_cents), 0)::bigint AS platform_fee_cents
       FROM payment_events pe
      WHERE pe.installer_id = $1
        AND pe.event_type = 'payment_intent.succeeded'`,
    [installerId]
  ) || {};

  const installer = await db.one(
    `SELECT stripe_account_id, stripe_account_charges_enabled FROM installers WHERE id = $1`,
    [installerId]
  ) || {};

  // If no Connect account or charges not enabled, all paid deposits are still
  // sitting in the platform balance pending manual transfer at onboarding.
  const onConnect = !!installer.stripe_account_id && !!installer.stripe_account_charges_enabled;
  const payoutsPending = onConnect ? 0 : (num(summary.deposits_paid_cents) - num(fees.platform_fee_cents));

  return {
    deposits_paid_cents: num(summary.deposits_paid_cents),
    platform_fee_cents:  num(fees.platform_fee_cents),
    payouts_pending_cents: Math.max(0, payoutsPending),
    count_paid:    num(summary.count_paid),
    count_failed:  num(summary.count_failed),
    count_pending: num(summary.count_pending),
    on_connect:    onConnect
  };
}

async function getPlatformMarketplaceTotals() {
  const summary = await db.one(
    `SELECT
       COALESCE(SUM(deposit_amount_cents) FILTER (WHERE deposit_status='paid'), 0)::bigint AS deposits_paid_cents,
       COUNT(*) FILTER (WHERE deposit_status='paid')::int   AS count_paid,
       COUNT(*) FILTER (WHERE deposit_status='failed')::int AS count_failed,
       COUNT(*) FILTER (WHERE deposit_status='requires_payment')::int AS count_pending,
       COUNT(DISTINCT installer_id) FILTER (WHERE deposit_status='paid')::int AS active_installers
     FROM bookings`
  ) || {};

  const fees = await db.one(
    `SELECT COALESCE(SUM(application_fee_cents), 0)::bigint AS platform_fee_cents
       FROM payment_events
      WHERE event_type = 'payment_intent.succeeded'`
  ) || {};

  // Platform-hold sum: paid bookings where the installer is NOT on Connect.
  const hold = await db.one(
    `SELECT COALESCE(SUM(b.deposit_amount_cents), 0)::bigint AS held_cents
       FROM bookings b
       JOIN installers i ON i.id = b.installer_id
      WHERE b.deposit_status = 'paid'
        AND (i.stripe_account_id IS NULL OR i.stripe_account_charges_enabled = false)`
  ) || {};

  // Subtract platform fees retained from the held total (rough approx).
  const heldNet = Math.max(0, num(hold.held_cents) - num(fees.platform_fee_cents));

  return {
    deposits_paid_cents:  num(summary.deposits_paid_cents),
    platform_fee_cents:   num(fees.platform_fee_cents),
    transferred_cents:    Math.max(0, num(summary.deposits_paid_cents) - num(fees.platform_fee_cents) - heldNet),
    payouts_pending_cents: heldNet,
    count_paid:    num(summary.count_paid),
    count_failed:  num(summary.count_failed),
    count_pending: num(summary.count_pending),
    active_installers: num(summary.active_installers)
  };
}

module.exports = {
  getInstallerMarketplaceTotals,
  getPlatformMarketplaceTotals
};