- 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).
63 lines
2.6 KiB
JavaScript
63 lines
2.6 KiB
JavaScript
import assert from 'node:assert';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { test } from 'node:test';
|
|
|
|
// Sandbox BEFORE importing store.js — never bind the test to production data.
|
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dashcaddy-license-welcome-test-'));
|
|
process.env.DATA_DIR = dataDir;
|
|
process.env.DASHCADDY_LICENSE_SECRET = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
|
|
|
const store = await import('../src/store.js');
|
|
const licenseLogic = await import('../src/licenseLogic.js');
|
|
|
|
// Regression guard for the welcome-vs-extended email split:
|
|
// a BRAND-NEW subscription license must report isNewLicense=true (drives the
|
|
// "Welcome to DashCaddy Premium" email), and a second event on the same
|
|
// subscription must report isNewLicense=false (drives the "extended" email).
|
|
test('createOrUpdateLicenseBySubscription flags new vs renewal', () => {
|
|
const uniq = `welcometest_${Date.now()}`;
|
|
const customerId = `cus_${uniq}`;
|
|
const subscriptionId = `sub_${uniq}`;
|
|
|
|
const first = store.createOrUpdateLicenseBySubscription(subscriptionId, {
|
|
subscriptionId,
|
|
customerId,
|
|
customerEmail: `${uniq}@example.com`,
|
|
planCode: 'premium_30d',
|
|
durationDays: 30,
|
|
status: 'active',
|
|
expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(),
|
|
premiumFeatures: { sso: true, recipes: true, swarm: true },
|
|
deactivatedAt: null,
|
|
lastStripeEventCreated: 1,
|
|
lastStripeEventId: `evt_${uniq}_1`
|
|
});
|
|
assert.equal(first.isNewLicense, true, 'first sync must be flagged as a NEW license');
|
|
|
|
const second = store.createOrUpdateLicenseBySubscription(subscriptionId, {
|
|
subscriptionId,
|
|
customerId,
|
|
customerEmail: `${uniq}@example.com`,
|
|
planCode: 'premium_30d',
|
|
durationDays: 30,
|
|
status: 'active',
|
|
expiresAt: new Date(Date.now() + 60 * 86400000).toISOString(),
|
|
premiumFeatures: { sso: true, recipes: true, swarm: true },
|
|
deactivatedAt: null,
|
|
lastStripeEventCreated: 2,
|
|
lastStripeEventId: `evt_${uniq}_2`
|
|
});
|
|
assert.equal(second.id, first.id, 'renewal must reuse the same license row');
|
|
assert.equal(second.key, first.key, 'renewal must keep the same license key');
|
|
assert.equal(second.isNewLicense, false, 'renewal sync must NOT be flagged as new');
|
|
|
|
// server.js wiring: extended = !isNewLicense
|
|
assert.equal(!second.isNewLicense, true, 'renewal must produce extended=true');
|
|
assert.equal(!first.isNewLicense, false, 'first purchase must produce extended=false');
|
|
|
|
// cleanup: deactivate so the test row is inert in the sandbox store
|
|
store.updateLicense(first.id, { active: false, deactivatedAt: new Date().toISOString() });
|
|
});
|