ThreadCount Community edition

Uniform stock management for healthcare linen rooms: the coordinator app, the phone counter and the staff app, for your own server. Built from d947f89 on 2026-09-15. Licensed under the Functional Source License (FSL-1.1-ALv2).
This commit is contained in:
ThreadCount
2026-09-15 18:27:45 +10:00
commit 7d650e4c10
298 changed files with 45868 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
/* Sending the billing emails from the app: who gets them, and the fire-and-forget send.
*
* The words live in lib/billing-mail.cjs so the reminder timer and the preview script render the
* same templates without a TypeScript build. Recipients are the billing contact when one is set,
* otherwise every active admin of the facility — never a staff account, never a wearer. */
import { prisma } from "@/lib/db";
import { sendTo } from "@/lib/mail";
import * as T from "@/lib/billing-mail.cjs";
import { PRICES } from "@/lib/plan";
export type Rendered = { subject: string; text: string; html: string };
export type FacilityCtx = { facility: string; contact: string };
export type Card = { brand: string; last4: string } | null;
export const PRICE_CENTS = { monthly: PRICES.hostedMonthly * 100, annual: PRICES.hostedAnnual * 100 } as const;
/** Where a facility's billing mail goes. */
export async function billingRecipients(facilityId: string): Promise<{ to: string[]; ctx: FacilityCtx }> {
const fac = await prisma.facility.findUnique({ where: { id: facilityId }, select: { name: true, billingEmail: true } });
if (!fac) return { to: [], ctx: { facility: "", contact: "" } };
const contact = fac.billingEmail.trim().toLowerCase();
if (contact) return { to: [contact], ctx: { facility: fac.name, contact } };
const admins = await prisma.user.findMany({ where: { facilityId, role: "ADMIN", inactive: false }, select: { email: true } });
return { to: admins.map((a) => a.email.toLowerCase()), ctx: { facility: fac.name, contact: "" } };
}
/** Render with a template and send to every recipient. Never throws, never awaited by callers. */
export async function sendBillingMail(facilityId: string, render: (ctx: FacilityCtx) => Rendered): Promise<number> {
try {
const { to, ctx } = await billingRecipients(facilityId);
if (!to.length) return 0;
const m = render(ctx);
const results = await Promise.all(to.map((a) => sendTo(a, m.subject, m.text, m.html)));
return results.filter(Boolean).length;
} catch (e) {
console.error("[billing mail] failed:", e instanceof Error ? e.message : e);
return 0;
}
}
export const templates = T;