- invoice.paid classified welcome-vs-renewal via emailDeliveredAt (durable, webhook-order independent) through new classifySubscriptionEmail() - the old isNewLicense/row-existence signal broke under Stripe's real event order (subscription.created pre-creates the row before the first invoice). - renderLicenseEmail() extracted pure; first purchases now get 'Welcome to DashCaddy Premium' with unlocks panel (SSO/Recipes/Swarm/auto-renew) and 3-step getting-started; renewal copy byte-identical (test-pinned). - 9 new tests (37 total): behavioral classifier coverage, rendered-content assertions both variants, store flag regression, mkdtemp-sandboxed. - Codex judge: A, zero blockers (r5; prior rounds D-D-D-C drove the fixes). Verdict: run output 'A' with empty Blockers (direct codex exec, 2026-09-22).
70 lines
3.1 KiB
JavaScript
70 lines
3.1 KiB
JavaScript
import assert from 'node:assert';
|
|
import fs from 'node:fs';
|
|
import { test } from 'node:test';
|
|
|
|
// Sandbox BEFORE importing store.js — never bind the test to production data.
|
|
const dataDir = fs.mkdtempSync(`${process.env.TMPDIR || '/tmp'}/dashcaddy-license-order-test-`);
|
|
process.env.DATA_DIR = dataDir;
|
|
process.env.DASHCADDY_LICENSE_SECRET = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
|
|
|
const store = await import('../src/store.js');
|
|
|
|
// Production webhook order: Stripe delivers customer.subscription.created BEFORE
|
|
// the first invoice.paid (verified live 2026-09-22: sub created 23:06:08, invoice
|
|
// events 23:06:12). The license row therefore exists when invoice.paid arrives.
|
|
// The welcome-vs-renewal classification MUST survive both arrival orders.
|
|
test('welcome classification is webhook-order independent', () => {
|
|
const uniq = `ordertest_${Date.now()}`;
|
|
const customerId = `cus_${uniq}`;
|
|
const subscriptionId = `sub_${uniq}`;
|
|
const subPatch = {
|
|
subscriptionId,
|
|
customerId,
|
|
customerEmail: `${uniq}@example.com`,
|
|
planCode: 'premium_30d',
|
|
durationDays: 30,
|
|
status: 'active',
|
|
premiumFeatures: { sso: true, recipes: true, swarm: true },
|
|
deactivatedAt: null
|
|
};
|
|
|
|
// ORDER A (production): subscription.created pre-creates the license...
|
|
const preCreated = store.createOrUpdateLicenseBySubscription(subscriptionId, {
|
|
...subPatch,
|
|
expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(),
|
|
lastStripeEventCreated: 1,
|
|
lastStripeEventId: `evt_${uniq}_sub_created`
|
|
});
|
|
assert.equal(preCreated.isNewLicense, true);
|
|
|
|
// ...then invoice.paid arrives. extended must come from emailDeliveredAt
|
|
// (no email ever delivered -> extended=false -> WELCOME email).
|
|
let license = store.findLicenseByCustomerId(customerId);
|
|
assert.equal(Boolean(license.emailDeliveredAt), false,
|
|
'order A: pre-created license with no delivered email must classify as welcome');
|
|
|
|
// simulate the email delivery that invoice.paid triggers:
|
|
store.updateLicense(license.id, {
|
|
emailDeliveryStatus: 'delivered',
|
|
emailDeliveredAt: new Date().toISOString(),
|
|
lastEmailDeliveryId: `in_${uniq}_1`
|
|
});
|
|
|
|
// RENEWAL (next month's invoice.paid): an email WAS delivered -> extended=true.
|
|
license = store.findLicenseByCustomerId(customerId);
|
|
assert.equal(Boolean(license.emailDeliveredAt), true,
|
|
'after a delivered welcome email, the next invoice must classify as renewal/extended');
|
|
});
|
|
|
|
// The server must call the shared classifier, not inline its own logic.
|
|
// (Behavioral coverage of the classifier itself lives in license-classify.test.js.)
|
|
test('server.js delegates classification to classifySubscriptionEmail', () => {
|
|
const src = fs.readFileSync(new URL('../src/server.js', import.meta.url), 'utf8');
|
|
assert.ok(src.includes('classifySubscriptionEmail(license).extended'),
|
|
'handler must use the shared classifier');
|
|
assert.ok(!src.includes('const extended = !license.isNewLicense'),
|
|
'old row-existence classification must be gone');
|
|
assert.ok(!src.includes('const extended = Boolean(license.emailDeliveredAt)'),
|
|
'no inline duplicate of the logic');
|
|
});
|