1bc2de655a
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
89 lines
5.7 KiB
JavaScript
89 lines
5.7 KiB
JavaScript
/* The notice to existing rooms that plans are coming — docs/launch/plans-notice.md is the text.
|
||
*
|
||
* node scripts/send-plans-notice.cjs # dry run: who would get it, and the date
|
||
* node scripts/send-plans-notice.cjs --send # send, go-live 60 days from today
|
||
* node scripts/send-plans-notice.cjs --send --date 2026-11-14
|
||
*
|
||
* Runs on the production box with its .env (SMTP_* and DATABASE_URL). Goes to every active ADMIN
|
||
* of every non-demo facility that is grandfathered — which, before plans are live, is every
|
||
* facility. Refuses without --send. After a real send it appends a line to each facility's plan
|
||
* note so the console shows who was told and when. One mail per administrator, plain text, with
|
||
* replies going to hello@threadcount.tech. The date must be at least sixty days out: that is the
|
||
* notice period the Terms promise.
|
||
*/
|
||
require("dotenv/config");
|
||
const nodemailer = require("nodemailer");
|
||
const { PrismaClient } = require("@prisma/client");
|
||
const { PrismaPg } = require("@prisma/adapter-pg");
|
||
|
||
const args = process.argv.slice(2);
|
||
const SEND = args.includes("--send");
|
||
const dateArg = args[args.indexOf("--date") + 1];
|
||
const DAY = 86_400_000;
|
||
const MIN_NOTICE_DAYS = 60;
|
||
|
||
const goLive = args.includes("--date") ? new Date(dateArg + "T00:00:00+10:00") : new Date(Date.now() + MIN_NOTICE_DAYS * DAY);
|
||
if (Number.isNaN(goLive.getTime())) { console.error("--date must be YYYY-MM-DD"); process.exit(1); }
|
||
if (goLive.getTime() - Date.now() < (MIN_NOTICE_DAYS - 1) * DAY) { console.error(`The go-live date must be at least ${MIN_NOTICE_DAYS} days out — the Terms promise sixty days' notice.`); process.exit(1); }
|
||
const DATE = goLive.toLocaleDateString("en-AU", { day: "numeric", month: "long", year: "numeric", timeZone: "Australia/Brisbane" });
|
||
const TODAY = new Date().toLocaleDateString("en-AU", { day: "numeric", month: "long", year: "numeric", timeZone: "Australia/Brisbane" });
|
||
|
||
function body(first, facility) {
|
||
return `Hello ${first},
|
||
|
||
A note from the person who runs ThreadCount, sixty days ahead, because the pricing page said you would hear before the website did.
|
||
|
||
On ${DATE} ThreadCount will start charging new facilities for hosting. Nothing changes for ${facility}. You signed up while it was free, and it stays free for you — every feature, every report, both apps, no ceiling on staff records — for as long as your facility exists. There is nothing to do, nothing to sign, and nothing on your screens will look different on the day.
|
||
|
||
What is changing, for facilities created from ${DATE}:
|
||
|
||
- The software itself stays free. The code will be published so anyone can run it on their own server, with everything in it.
|
||
- Hosting on threadcount.tech stays free for a room under 60 staff records.
|
||
- A larger facility hosted on threadcount.tech will pay $1,290 a year (or $129 a month), which covers the servers, 35 days of backups, and a person who answers email the next business day.
|
||
- Health services running several facilities will be able to buy them together, with one sign-in across sites.
|
||
|
||
The Terms of Service now carry a Fees section that writes the grandfathering down, so it does not depend on a promise in an email: https://threadcount.tech/terms
|
||
|
||
Why now: running it for other people costs money and time, and I would rather charge new rooms plainly than let the thing quietly stop being maintained. Charging the rooms that trusted it first was never on the table.
|
||
|
||
If you would prefer to pay anyway, or your health service wants the multi-site arrangement, reply to this email. Otherwise, carry on exactly as you are.
|
||
|
||
Kyle
|
||
ThreadCount · hello@threadcount.tech
|
||
`;
|
||
}
|
||
|
||
const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL, max: 1 }) });
|
||
|
||
(async () => {
|
||
const facilities = await prisma.facility.findMany({
|
||
where: { isDemo: false, grandfathered: true },
|
||
select: { id: true, name: true, planNote: true, users: { where: { role: "ADMIN", inactive: false }, select: { email: true, first: true } } },
|
||
orderBy: { createdAt: "asc" },
|
||
});
|
||
console.log(`${SEND ? "Sending" : "Dry run"} · go-live ${DATE} · ${facilities.length} facilities`);
|
||
for (const f of facilities) console.log(` ${f.name}: ${f.users.length} administrator${f.users.length === 1 ? "" : "s"}${f.planNote.includes("Plans notice sent") ? " · ALREADY NOTIFIED" : ""}`);
|
||
if (!SEND) { console.log("\nNothing sent. Add --send to send."); await prisma.$disconnect(); return; }
|
||
|
||
if (!process.env.SMTP_HOST || !process.env.SMTP_USER || !process.env.SMTP_PASS) { console.error("SMTP is not configured in this environment."); process.exit(1); }
|
||
const port = parseInt(process.env.SMTP_PORT || "587", 10);
|
||
const t = nodemailer.createTransport({ host: process.env.SMTP_HOST, port, secure: port === 465, auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS } });
|
||
const subject = `ThreadCount is introducing plans on ${DATE}. Yours stays free.`;
|
||
let sent = 0, failed = 0;
|
||
for (const f of facilities) {
|
||
if (f.planNote.includes("Plans notice sent")) { console.log(` skip ${f.name} — already notified`); continue; }
|
||
for (const u of f.users) {
|
||
try {
|
||
await t.sendMail({ from: process.env.SMTP_FROM || process.env.SMTP_USER, to: u.email, replyTo: "hello@threadcount.tech", subject, text: body(u.first || "there", f.name) });
|
||
sent++;
|
||
} catch (e) {
|
||
failed++; console.error(` FAILED ${f.name}: ${e.message}`);
|
||
}
|
||
}
|
||
const line = `Plans notice sent ${TODAY}, go-live ${DATE}`;
|
||
await prisma.facility.update({ where: { id: f.id }, data: { planNote: (f.planNote ? f.planNote + "\n" : "") + line } });
|
||
}
|
||
console.log(`\nSent ${sent}, failed ${failed}. Go-live ${DATE}: turn Switches › Plans on that day.`);
|
||
await prisma.$disconnect();
|
||
})().catch((e) => { console.error(e); process.exit(1); });
|