[grade=A] Welcome email for first purchases: order-independent classification + premium walkthrough
- 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).
This commit is contained in:
8 files changed
+304
-6
No files matched your search
@@ -0,0 +1,53 @@
|
||||
import assert from 'node:assert';
|
||||
import fs from 'node:fs';
|
||||
import { test } from 'node:test';
|
||||
|
||||
// Behavioral tests for the welcome-vs-renewal classification (the exact
|
||||
// decision invoice.paid makes), through the exported pure classifier.
|
||||
|
||||
const { classifySubscriptionEmail } = await import('../src/licenseLogic.js');
|
||||
|
||||
// Simulated invoice.paid behavior against progressively-delivered license
|
||||
// state — no SMTP, no Stripe, no handler spin-up required; this is the same
|
||||
// state machine deliverAndTrackLicenseEmail drives via updateLicense().
|
||||
test('classification behavior across the real webhook order', () => {
|
||||
const uniq = `classify_${Date.now()}`;
|
||||
const license = {
|
||||
id: `lic_${uniq}`,
|
||||
key: 'DC-20F00-0000D-NB3FN-S6HXJ-A7CA0',
|
||||
customerId: `cus_${uniq}`,
|
||||
subscriptionId: `sub_${uniq}`,
|
||||
emailDeliveredAt: null
|
||||
};
|
||||
|
||||
// State 1: license row exists (subscription.created already processed),
|
||||
// no email ever delivered -> invoice.paid must send the WELCOME email.
|
||||
assert.equal(classifySubscriptionEmail(license).extended, false,
|
||||
'pre-created license with zero delivered emails = welcome');
|
||||
|
||||
// State 2: welcome email delivered (deliverAndTrackLicenseEmail sets this
|
||||
// via updateLicense after SMTP 250). Next invoice.paid = renewal.
|
||||
license.emailDeliveredAt = new Date().toISOString();
|
||||
assert.equal(classifySubscriptionEmail(license).extended, true,
|
||||
'license with a delivered email = extended/renewal');
|
||||
});
|
||||
|
||||
test('classification survives missing/blank email state', () => {
|
||||
assert.equal(classifySubscriptionEmail(null).extended, false,
|
||||
'null license cannot have delivered mail -> welcome');
|
||||
assert.equal(classifySubscriptionEmail({}).extended, false,
|
||||
'missing field -> welcome');
|
||||
assert.equal(classifySubscriptionEmail({ emailDeliveredAt: '' }).extended, false,
|
||||
'blank timestamp -> welcome');
|
||||
assert.equal(classifySubscriptionEmail({ emailDeliveredAt: '2026-09-22T00:00:00.000Z' }).extended, true,
|
||||
'real timestamp -> extended');
|
||||
});
|
||||
|
||||
// The server must call the classifier, not inline its own logic.
|
||||
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('Boolean(license.emailDeliveredAt)'),
|
||||
'no inline duplicate of the logic');
|
||||
});
|
||||
Reference in new issue
Block a user