7d650e4c10
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).
42 lines
2.1 KiB
TypeScript
42 lines
2.1 KiB
TypeScript
/* 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;
|