Skip to content

Commit 8cfbf9e

Browse files
ralyodioclaude
andauthored
smshub-nightly + a Money section in pairux-nightly (#26)
smshub-nightly: a nightly email for smshub.dev, revenue first. CoinPay number_orders settled (count + USD, new rentals vs renewals, new paying customers, first-ever payment banner), rentals active/ended/running out and a monthly run-rate, checkouts opened vs paid; the funnel from the CrawlProof tracker rollups (visitors, home, /numbers, /verification-numbers, /register) through signups, confirmations, checkouts and payments with 7/30-day context; inbound SMS, OTP codes and calls on rented numbers. Read-only against smshub's dev2 Supabase and crawlproof's Postgres; internal accounts excluded. pairux-nightly: Money section with outside paid plans by tier, plan checkouts started vs settled (count + USD), cap refusals from PR #146 (the only upgrade-prompt signal that is logged) and a first-ever payment banner. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 6f8a31b commit 8cfbf9e

2 files changed

Lines changed: 538 additions & 4 deletions

File tree

‎bin/pairux-nightly‎

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,10 @@ async function accounts() {
132132
bookings: await q("select host_user_id u, created_at from booking_pages"),
133133
analyses: await q("select host_user_id u, created_at from call_analyses"),
134134
translation: await q("select user_id u, created_at from translation_usage"),
135-
paid: await q("select (select count(*)::int from profiles where plan is not null and plan <> 'free') plans, (select count(*)::int from plan_payments where status in ('paid','confirmed','completed','credited')) payments"),
135+
plans: await q("select id u, plan, plan_expires_at from profiles where plan is not null and plan <> 'free'"),
136+
payments: await q("select user_id u, plan, amount_usd::float8 amount, status, created_at, credited_at from plan_payments"),
137+
// PR #146: a join refused at the plan cap bumps sessions.settings.capRefusedCount; that is when the host sees the upgrade prompt.
138+
caps: await q("select host_user_id host, (settings->>'capRefusedCount')::int refused, settings->>'capRefusedAt' refused_at from sessions where settings ? 'capRefusedCount'"),
136139
};
137140
console.log(JSON.stringify(out));
138141
await sql.close();`;
@@ -204,7 +207,45 @@ async function accounts() {
204207
bookings: allExt(db.bookings),
205208
analyses: allExt(db.analyses), analysesDay: count(db.analyses, DAY),
206209
translation: allExt(db.translation), translationDay: count(db.translation, DAY),
207-
paidPlans: db.paid[0].plans, payments: db.paid[0].payments,
210+
money: money(db, isExt, dayOf),
211+
};
212+
}
213+
214+
/**
215+
* Paid plans and CoinPay plan payments. A plan_payments row is written when a
216+
* checkout starts (status pending); credited_at is set once the webhook settles
217+
* it and the plan is granted. Checkouts carry no record of which button started
218+
* them (pricing page, cap prompt, /upgrade/<plan>), so prompt-driven checkouts
219+
* cannot be told apart.
220+
*/
221+
function money(db, isExt, dayOf) {
222+
const now = Date.now();
223+
const live = db.plans.filter((p) => !p.plan_expires_at || Date.parse(p.plan_expires_at) > now);
224+
const tiers = (rows) => Object.entries(rows.reduce((a, p) => ((a[p.plan] = (a[p.plan] || 0) + 1), a), {})).sort((a, b) => b[1] - a[1]);
225+
const pays = db.payments.filter((p) => isExt(p.u)).map((p) => ({ ...p, day: dayOf(p.created_at), settledDay: dayOf(p.credited_at) }));
226+
const settled = pays.filter((p) => p.credited_at);
227+
const usd = (rows) => rows.reduce((a, p) => a + (Number(p.amount) || 0), 0);
228+
const per = (d) => {
229+
const c = pays.filter((p) => p.day === d), s = settled.filter((p) => p.settledDay === d);
230+
return { started: c.length, startedUsd: usd(c), settled: s.length, settledUsd: usd(s) };
231+
};
232+
const firstSettled = settled.map((p) => p.settledDay).sort()[0] || null;
233+
const caps = db.caps.filter((c) => isExt(c.host));
234+
const capDay = (d) => caps.filter((c) => dayOf(c.refused_at) === d);
235+
return {
236+
tiers: tiers(live.filter((p) => isExt(p.u))),
237+
internalTiers: tiers(live.filter((p) => !isExt(p.u))),
238+
day: per(DAY), prev: per(PREV),
239+
started30: pays.filter((p) => p.day >= addDays(DAY, -29) && p.day <= DAY).length,
240+
startedAll: pays.filter((p) => p.day <= DAY).length,
241+
settledAll: settled.filter((p) => p.settledDay <= DAY).length,
242+
settledUsdAll: usd(settled.filter((p) => p.settledDay <= DAY)),
243+
pendingAll: pays.filter((p) => !p.credited_at && p.day <= DAY).length,
244+
byPlan: tiers(settled.filter((p) => p.settledDay <= DAY)),
245+
firstSettled,
246+
firstEverToday: firstSettled === DAY,
247+
capRooms: caps.length, capRefusedAll: caps.reduce((a, c) => a + (c.refused || 0), 0),
248+
capRoomsDay: capDay(DAY).length,
208249
};
209250
}
210251

@@ -346,6 +387,23 @@ function verdict(r) {
346387
`Last outside signup: ${a.lastSignup ? dayLabel(a.lastSignup, true) : 'never'}.`;
347388
}
348389

390+
const usd = (v) => `$${Number(v || 0).toLocaleString('en-US', { maximumFractionDigits: 2 })}`;
391+
const tierList = (t) => (t.length ? t.map(([k, v]) => `${esc(k)} ${n(v)}`).join(' · ') : 'none');
392+
393+
function moneyHtml(m, row) {
394+
const banner = m.firstEverToday
395+
? `<div style="font-size:15px;font-weight:700;margin:8px 0 4px;padding:10px 12px;border-left:3px solid #8a5a00;background:#fdf3dc;color:#8a5a00">First outside plan payment ever: ${usd(m.day.settledUsd)}.</div>`
396+
: '';
397+
return `<div style="font-size:15px;font-weight:700;margin:20px 0 4px">Money</div>${banner}
398+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-size:13px;border-collapse:collapse">
399+
${row('Paid plans active (outside)', `${tierList(m.tiers)}${m.internalTiers.length ? `<div style="font-size:11px;color:${MUTE}">ours, not counted: ${tierList(m.internalTiers)}</div>` : ''}`)}
400+
${row('Plan checkouts started that day', `${n(m.day.started)} (${usd(m.day.startedUsd)}) · ${esc(dayLabel(PREV))} ${n(m.prev.started)} · ${n(m.started30)} in 30 days · ${n(m.startedAll)} all-time`)}
401+
${row('Plan payments settled that day', `<b>${n(m.day.settled)} (${usd(m.day.settledUsd)})</b> · ${esc(dayLabel(PREV))} ${n(m.prev.settled)} (${usd(m.prev.settledUsd)})`)}
402+
${row('Settled all-time', `${n(m.settledAll)} (${usd(m.settledUsdAll)})${m.byPlan.length ? ` · ${tierList(m.byPlan)}` : ''} · ${n(m.pendingAll)} started but never settled${m.firstSettled ? ` · first ${esc(dayLabel(m.firstSettled, true))}` : ' · <b>no outside payment yet</b>'}`)}
403+
${row('Upgrade prompts (room at its plan cap)', `${n(m.capRoomsDay)} rooms turned someone away that day · ${n(m.capRooms)} rooms, ${n(m.capRefusedAll)} people turned away all-time<div style="font-size:11px;color:${MUTE}">only the cap refusal is logged (sessions.settings.capRefusedCount, PR #146); prompt views and which button started a checkout are not recorded</div>`)}
404+
</table>`;
405+
}
406+
349407
function renderHtml(r, meta) {
350408
const tile = (label, cur, prev, sub) => `<td style="width:25%;background:#f4f4f2;border-radius:8px;padding:10px 12px;vertical-align:top">
351409
<div style="font-size:12px;color:${MUTE}">${label}</div>
@@ -388,6 +446,8 @@ ${tile('Used it', d ? d.active : null, a ? a.days[PREV].active : null, a ? 'outs
388446
<div style="font-size:15px;font-weight:700;margin:18px 0 6px">Last ${STRIP_DAYS} days</div>
389447
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse">${bars}</table>
390448
449+
${a ? moneyHtml(a.money, row) : ''}
450+
391451
<div style="font-size:15px;font-weight:700;margin:20px 0 4px">Meetings</div>
392452
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-size:13px;border-collapse:collapse">
393453
${a ? row('Meetings started that day', `${n(d.meetings)} by outside hosts · ${n(d.together)} with 2+ people joining · ${n(d.joins)} joins (${n(d.guests)} guests)`) : ''}
@@ -398,7 +458,6 @@ ${a ? row('Scheduled meetings', `${n(a.scheduledDay)} created that day · ${n(a.
398458
${a ? row('Recordings', `${n(a.recordingsDay)} that day · ${n(a.recordings)} all-time (${a.recordedHours.toFixed(1)} h)`) : ''}
399459
${a ? row('Channels / orgs', `${n(a.channels)} channels (+${n(a.channelsDay)}) · ${n(a.orgs)} orgs (+${n(a.orgsDay)})`) : ''}
400460
${a ? row('AI call analysis / translation', `${n(a.analyses)} analyses (+${n(a.analysesDay)}) · ${n(a.translation)} translation uses (+${n(a.translationDay)})`) : ''}
401-
${a ? row('Paid', `${n(a.paidPlans)} paid plans · ${n(a.payments)} payments`) : ''}
402461
${a ? row('Users active in the last 30 days', n(a.active30)) : ''}
403462
${a ? row('Latest outside signups', recent) : ''}
404463
</table>
@@ -436,6 +495,7 @@ function renderText(r, meta) {
436495
`pairux.com/download visitors: ${n(r.nginx?.page)}`,
437496
`New outside signups: ${n(d?.signups)} (${n(d?.confirmed)} confirmed) · used it: ${n(d?.active)} · meetings: ${n(d?.meetings)} (${n(d?.together)} with 2+ people)`,
438497
`pairux.com views: ${n(r.views?.[DAY])}`,
498+
a ? `${a.money.firstEverToday ? '*** First outside plan payment ever *** ' : ''}Money: ${n(a.money.day.settled)} plan payments settled (${usd(a.money.day.settledUsd)}), ${n(a.money.day.started)} checkouts started, outside paid plans: ${tierList(a.money.tiers)}, all-time settled ${usd(a.money.settledUsdAll)}` : '',
439499
r.packages.choco ? `Chocolatey: ${n(r.packages.choco.total)} all-time` : '',
440500
r.packages.aur ? `AUR: ${n(r.packages.aur.votes)} votes, popularity ${r.packages.aur.popularity}` : '',
441501
meta.problems.length ? `\nSources skipped: ${meta.problems.join('; ')}` : '',
@@ -445,13 +505,14 @@ function renderText(r, meta) {
445505
function subjectLine(r) {
446506
const a = r.accounts, s = r.series[DAY] || {}, d = a?.days[DAY];
447507
const parts = [
508+
a?.money.day.settled ? `${usd(a.money.day.settledUsd)} paid` : null,
448509
r.nginx ? `${plural(r.nginx.unique, 'script install')}` : 'installs unknown',
449510
s.downloads !== undefined ? `${n(s.downloads)} downloads` : null,
450511
d ? plural(d.signups, 'signup') : null,
451512
d ? plural(d.meetings, 'meeting') : null,
452513
a ? `${n(a.external)} real users total` : null,
453514
].filter(Boolean);
454-
return `PairUX ${dayLabel(DAY)}: ${parts.join(', ')}`;
515+
return `PairUX ${dayLabel(DAY)}: ${a?.money.firstEverToday ? 'FIRST PAYMENT · ' : ''}${parts.join(', ')}`;
455516
}
456517

457518
// ---------------------------------------------------------------- main

0 commit comments

Comments
 (0)