backend-manager 5.2.19 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/CLAUDE.md +5 -4
- package/docs/admin-post-route.md +2 -0
- package/docs/ai-library.md +29 -2
- package/docs/common-mistakes.md +1 -1
- package/docs/environment-detection.md +87 -3
- package/docs/marketing-campaigns.md +1 -1
- package/docs/payment-system.md +1 -1
- package/docs/testing.md +60 -15
- package/package.json +1 -1
- package/src/cli/commands/install.js +2 -2
- package/src/cli/commands/setup.js +12 -4
- package/src/cli/commands/test.js +1 -1
- package/src/cli/index.js +6 -2
- package/src/defaults/CLAUDE.md +2 -2
- package/src/defaults/test/_init.js +14 -0
- package/src/manager/functions/_legacy/actions/create-post-handler.js +1 -1
- package/src/manager/functions/core/actions/api/admin/edit-post.js +1 -1
- package/src/manager/functions/core/actions/api/general/send-email.js +1 -1
- package/src/manager/functions/core/actions/api/user/delete.js +1 -1
- package/src/manager/functions/core/actions/api/user/oauth2.js +1 -1
- package/src/manager/helpers/analytics.js +3 -1
- package/src/manager/helpers/assistant.js +43 -38
- package/src/manager/index.js +76 -12
- package/src/manager/libraries/ai/index.js +33 -0
- package/src/manager/libraries/ai/providers/openai.js +105 -8
- package/src/manager/libraries/content/ghostii.js +76 -16
- package/src/manager/libraries/email/data/disposable-domains.json +23 -0
- package/src/manager/libraries/email/generators/lib/image-illustrator.js +154 -0
- package/src/manager/libraries/email/generators/newsletter.js +16 -2
- package/src/manager/libraries/payment/discount-codes.js +1 -0
- package/src/manager/routes/admin/post/put.js +1 -1
- package/src/manager/routes/handler/post/post.js +1 -1
- package/src/manager/routes/payments/intent/processors/test.js +1 -1
- package/src/manager/routes/user/delete.js +1 -1
- package/src/manager/routes/user/signup/post.js +4 -2
- package/src/test/runner.js +142 -20
- package/src/test/test-accounts.js +159 -102
- package/test/events/payments/journey-payments-cancel-endpoint.js +6 -2
- package/test/events/payments/journey-payments-cancel.js +6 -3
- package/test/events/payments/journey-payments-failure.js +6 -3
- package/test/events/payments/journey-payments-plan-change.js +6 -3
- package/test/events/payments/journey-payments-refund-webhook.js +6 -2
- package/test/events/payments/journey-payments-suspend.js +6 -3
- package/test/events/payments/journey-payments-trial-cancel.js +6 -3
- package/test/events/payments/journey-payments-trial.js +10 -4
- package/test/events/payments/journey-payments-uid-resolution.js +6 -2
- package/test/events/payments/journey-payments-upgrade.js +6 -3
- package/test/helpers/content/ghostii-blocks.js +134 -0
- package/test/helpers/environment.js +230 -0
- package/test/routes/marketing/webhook-forward.js +14 -7
- package/test/routes/payments/cancel.js +4 -1
- package/test/routes/payments/dispute-alert.js +9 -6
- package/test/routes/payments/intent.js +55 -14
- package/test/routes/payments/portal.js +4 -1
- package/test/routes/payments/refund.js +4 -1
|
@@ -525,14 +525,19 @@ const TEST_ACCOUNTS = {
|
|
|
525
525
|
* Get all test account definitions with resolved emails and dynamic product IDs
|
|
526
526
|
* @param {string} domain - Domain for email addresses (e.g., 'itwcreativeworks.com')
|
|
527
527
|
* @param {object} [config] - BEM config (used to resolve first paid product)
|
|
528
|
+
* @param {object} [extraAccounts] - Project-defined accounts from test/_init.js,
|
|
529
|
+
* keyed by id, each `{ id, uid, email, properties }`. Merged after the built-in
|
|
530
|
+
* accounts; a project account may override a built-in one by reusing its key.
|
|
528
531
|
* @returns {object} Account definitions with resolved emails
|
|
529
532
|
*/
|
|
530
|
-
function getAccountDefinitions(domain, config) {
|
|
533
|
+
function getAccountDefinitions(domain, config, extraAccounts) {
|
|
531
534
|
const paidProduct = getFirstPaidProduct(config);
|
|
532
535
|
const accounts = {};
|
|
533
536
|
|
|
534
|
-
|
|
535
|
-
|
|
537
|
+
const all = { ...TEST_ACCOUNTS, ...(extraAccounts || {}) };
|
|
538
|
+
|
|
539
|
+
for (const [key, account] of Object.entries(all)) {
|
|
540
|
+
const properties = JSON.parse(JSON.stringify(account.properties || {}));
|
|
536
541
|
|
|
537
542
|
// Replace hardcoded 'premium' product with the actual first paid product from config
|
|
538
543
|
if (properties.subscription?.product?.id === 'premium') {
|
|
@@ -543,7 +548,7 @@ function getAccountDefinitions(domain, config) {
|
|
|
543
548
|
accounts[key] = {
|
|
544
549
|
id: account.id,
|
|
545
550
|
uid: account.uid,
|
|
546
|
-
email: account.email.replace('{domain}', domain),
|
|
551
|
+
email: (account.email || '').replace('{domain}', domain),
|
|
547
552
|
properties,
|
|
548
553
|
};
|
|
549
554
|
}
|
|
@@ -556,10 +561,11 @@ function getAccountDefinitions(domain, config) {
|
|
|
556
561
|
* @param {object} admin - Firebase admin instance
|
|
557
562
|
* @param {string} domain - Domain for email addresses (e.g., 'itwcreativeworks.com')
|
|
558
563
|
* @param {object} [config] - BEM config (used to resolve first paid product)
|
|
564
|
+
* @param {object} [extraAccounts] - Project-defined accounts from test/_init.js
|
|
559
565
|
* @returns {Promise<object>} Account credentials with privateKeys
|
|
560
566
|
*/
|
|
561
|
-
async function fetchPrivateKeys(admin, domain, config) {
|
|
562
|
-
const definitions = getAccountDefinitions(domain, config);
|
|
567
|
+
async function fetchPrivateKeys(admin, domain, config, extraAccounts) {
|
|
568
|
+
const definitions = getAccountDefinitions(domain, config, extraAccounts);
|
|
563
569
|
const accounts = {};
|
|
564
570
|
|
|
565
571
|
// Fetch all in parallel
|
|
@@ -626,50 +632,165 @@ async function fetchPrivateKeys(admin, domain, config) {
|
|
|
626
632
|
async function createAccount(admin, account) {
|
|
627
633
|
const userRef = admin.firestore().doc(`users/${account.uid}`);
|
|
628
634
|
|
|
629
|
-
//
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
635
|
+
// The Auth user for this UID was just deleted (deleteTestUsers). Its auth:on-delete
|
|
636
|
+
// trigger deletes the Firestore doc ASYNCHRONOUSLY and the emulator does NOT guarantee
|
|
637
|
+
// it fires (or finishes) before our subsequent createUser()'s auth:on-create. A stale
|
|
638
|
+
// on-delete can therefore land AFTER on-create and silently wipe the freshly-written
|
|
639
|
+
// doc — leaving the account with no api.clientId/privateKey. That intermittent clobber
|
|
640
|
+
// is what made the account-structure validation (and every downstream auth/payment test)
|
|
641
|
+
// flaky. We defend with a verify-and-repair retry: create → wait for the on-create write
|
|
642
|
+
// to be COMPLETE (api keys present, not just metadata.tag) → merge props → re-verify the
|
|
643
|
+
// keys survived. If a late on-delete clobbered the doc, recreate from scratch.
|
|
644
|
+
const maxAttempts = 3;
|
|
645
|
+
|
|
646
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
647
|
+
// Create Firebase Auth user - triggers auth:on-create
|
|
648
|
+
await admin.auth().createUser({
|
|
649
|
+
uid: account.uid,
|
|
650
|
+
email: account.email,
|
|
651
|
+
password: uuid.v4(),
|
|
652
|
+
emailVerified: true,
|
|
653
|
+
}).catch(async (e) => {
|
|
654
|
+
// A retry may find the Auth user already present (its doc was clobbered, not the
|
|
655
|
+
// user). Delete it first so the fresh createUser re-fires a clean on-create.
|
|
656
|
+
if (e.code === 'auth/uid-already-exists') {
|
|
657
|
+
await admin.auth().deleteUser(account.uid).catch(() => {});
|
|
658
|
+
await waitForDocGone(userRef);
|
|
659
|
+
await admin.auth().createUser({
|
|
660
|
+
uid: account.uid,
|
|
661
|
+
email: account.email,
|
|
662
|
+
password: uuid.v4(),
|
|
663
|
+
emailVerified: true,
|
|
664
|
+
});
|
|
665
|
+
} else {
|
|
666
|
+
throw e;
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// Wait for auth:on-create to COMPLETE. Poll on the api keys themselves — the fields the
|
|
671
|
+
// tests actually require — not just metadata.tag, which on its own doesn't prove the
|
|
672
|
+
// doc wasn't subsequently clobbered.
|
|
673
|
+
const ready = await waitForAccountReady(userRef);
|
|
674
|
+
|
|
675
|
+
// Merge test-specific properties (roles, subscription, etc.)
|
|
676
|
+
await userRef.set(account.properties, { merge: true });
|
|
677
|
+
|
|
678
|
+
// Re-verify after the merge: a late on-delete could have struck between the poll and
|
|
679
|
+
// here. If the api keys survived, the account is good. Otherwise loop and recreate.
|
|
680
|
+
const finalDoc = await userRef.get();
|
|
681
|
+
const data = finalDoc.data() || {};
|
|
682
|
+
if (ready && data.api?.clientId && data.api?.privateKey) {
|
|
683
|
+
return { uid: account.uid, email: account.email };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Clobbered (or never completed). Tear down the Auth user so the next attempt starts
|
|
687
|
+
// from a clean slate, then retry.
|
|
688
|
+
if (attempt < maxAttempts) {
|
|
689
|
+
await admin.auth().deleteUser(account.uid).catch(() => {});
|
|
690
|
+
await waitForDocGone(userRef);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Exhausted retries — return anyway so the runner reports the downstream failure with a
|
|
695
|
+
// meaningful test assertion rather than a setup throw.
|
|
696
|
+
return { uid: account.uid, email: account.email };
|
|
697
|
+
}
|
|
636
698
|
|
|
637
|
-
|
|
638
|
-
|
|
699
|
+
/**
|
|
700
|
+
* Poll until a user doc reflects a COMPLETE auth:on-create write (api keys present).
|
|
701
|
+
* Returns true if it became ready within the window, false on timeout.
|
|
702
|
+
*/
|
|
703
|
+
async function waitForAccountReady(userRef) {
|
|
639
704
|
const maxWait = 15000;
|
|
640
705
|
const pollInterval = 500;
|
|
641
706
|
let waited = 0;
|
|
642
707
|
|
|
643
708
|
while (waited < maxWait) {
|
|
644
709
|
const doc = await userRef.get();
|
|
645
|
-
|
|
646
|
-
|
|
710
|
+
const data = doc.exists ? doc.data() : null;
|
|
711
|
+
if (
|
|
712
|
+
data?.metadata?.tag === 'auth:on-create'
|
|
713
|
+
&& data.api?.clientId
|
|
714
|
+
&& data.api?.privateKey
|
|
715
|
+
) {
|
|
716
|
+
return true;
|
|
647
717
|
}
|
|
648
718
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
649
719
|
waited += pollInterval;
|
|
650
720
|
}
|
|
651
721
|
|
|
652
|
-
|
|
653
|
-
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
654
724
|
|
|
655
|
-
|
|
725
|
+
/**
|
|
726
|
+
* Poll until a user doc no longer exists (on-delete settled). Bounded; best-effort.
|
|
727
|
+
*/
|
|
728
|
+
async function waitForDocGone(userRef) {
|
|
729
|
+
const maxWait = 10000;
|
|
730
|
+
const pollInterval = 200;
|
|
731
|
+
let waited = 0;
|
|
732
|
+
|
|
733
|
+
while (waited < maxWait) {
|
|
734
|
+
const doc = await userRef.get();
|
|
735
|
+
if (!doc.exists) {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
739
|
+
waited += pollInterval;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Fallback: force-delete the lingering doc so the next create starts clean.
|
|
743
|
+
await userRef.delete().catch(() => {});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Flush the ENTIRE emulator Firestore — every top-level collection, recursively.
|
|
748
|
+
*
|
|
749
|
+
* SAFETY: this is destructive, so it ONLY runs when connected to the Firestore
|
|
750
|
+
* emulator (`FIRESTORE_EMULATOR_HOST` is set — which the test command always
|
|
751
|
+
* sets). If that env var is absent, this is a no-op, so it can never wipe a real
|
|
752
|
+
* project's data. The emulator DB is entirely test data, so a full flush is the
|
|
753
|
+
* simplest correct "clean slate" — no per-collection allowlist to maintain.
|
|
754
|
+
*
|
|
755
|
+
* @param {object} admin - Firebase admin instance
|
|
756
|
+
*/
|
|
757
|
+
async function flushEmulatorFirestore(admin) {
|
|
758
|
+
if (!process.env.FIRESTORE_EMULATOR_HOST) {
|
|
759
|
+
// Not pointed at the emulator — refuse to mass-delete. No-op.
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const firestore = admin.firestore();
|
|
764
|
+
const collections = await firestore.listCollections().catch(() => []);
|
|
765
|
+
|
|
766
|
+
await Promise.all(
|
|
767
|
+
collections.map((collectionRef) => firestore.recursiveDelete(collectionRef).catch(() => {}))
|
|
768
|
+
);
|
|
656
769
|
}
|
|
657
770
|
|
|
658
771
|
/**
|
|
659
772
|
* Delete all test users (both Auth and Firestore)
|
|
660
|
-
* Uses TEST_ACCOUNTS as the source of truth for
|
|
661
|
-
* Deleting Auth users triggers on-delete which handles
|
|
662
|
-
*
|
|
663
|
-
* Called before test runs to ensure clean
|
|
773
|
+
* Uses TEST_ACCOUNTS (+ any project-defined accounts) as the source of truth for
|
|
774
|
+
* which UIDs to delete. Deleting Auth users triggers on-delete which handles
|
|
775
|
+
* Firestore doc + count decrement.
|
|
776
|
+
* Called before test runs to ensure a clean slate. Flushes the ENTIRE emulator
|
|
777
|
+
* Firestore (the emulator DB is 100% test data — there's nothing to preserve),
|
|
778
|
+
* then deletes the Auth test users. `test/_init.js`'s `setup()` reseeds fixtures
|
|
779
|
+
* afterward. Waits for Firestore docs to be deleted before returning.
|
|
664
780
|
* @param {object} admin - Firebase admin instance
|
|
781
|
+
* @param {object} [extraAccounts] - Project-defined accounts from test/_init.js
|
|
665
782
|
* @returns {Promise<object>} Result with deleted count
|
|
666
783
|
*/
|
|
667
|
-
async function deleteTestUsers(admin) {
|
|
784
|
+
async function deleteTestUsers(admin, extraAccounts) {
|
|
668
785
|
const results = { deleted: [], skipped: [], failed: [] };
|
|
669
786
|
|
|
670
|
-
//
|
|
787
|
+
// Wipe the entire emulator Firestore up front (guarded to emulator-only).
|
|
788
|
+
await flushEmulatorFirestore(admin);
|
|
789
|
+
|
|
790
|
+
// Delete all known test accounts in parallel (built-in + project-defined).
|
|
791
|
+
const allAccounts = { ...TEST_ACCOUNTS, ...(extraAccounts || {}) };
|
|
671
792
|
await Promise.all(
|
|
672
|
-
Object.values(
|
|
793
|
+
Object.values(allAccounts).map(async (account) => {
|
|
673
794
|
try {
|
|
674
795
|
// Delete Firebase Auth user (triggers on-delete which handles Firestore doc + count)
|
|
675
796
|
await admin.auth().deleteUser(account.uid);
|
|
@@ -708,81 +829,16 @@ async function deleteTestUsers(admin) {
|
|
|
708
829
|
})
|
|
709
830
|
);
|
|
710
831
|
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
//
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
// identified solely by an `_test-` doc id prefix (id-keyed). All must be wiped
|
|
720
|
-
// at the start of every run so a test that died mid-execution leaves no
|
|
721
|
-
// ghosts. New collections that participate in tests MUST be added here too.
|
|
722
|
-
const testDataCollections = ['payments-orders', 'payments-webhooks', 'payments-intents', 'payments-disputes'];
|
|
723
|
-
// Collections that exist solely for tests — wipe in full. All docs in these
|
|
724
|
-
// collections come from tests, so a single recursive delete handles cleanup.
|
|
725
|
-
const testOnlyCollections = ['_test', '_test_query'];
|
|
726
|
-
const UID_BATCH_SIZE = 30;
|
|
727
|
-
const TEST_ID_PREFIX = '_test-';
|
|
728
|
-
|
|
729
|
-
const uidBatches = [];
|
|
730
|
-
for (let i = 0; i < testUids.length; i += UID_BATCH_SIZE) {
|
|
731
|
-
uidBatches.push(testUids.slice(i, i + UID_BATCH_SIZE));
|
|
832
|
+
// Realtime Database: wipe the `_test` namespace in full. (The Firestore-wide
|
|
833
|
+
// flush already ran in flushEmulatorFirestore() at the start of this function.)
|
|
834
|
+
// `admin.database()` throws synchronously when no Database URL is configured,
|
|
835
|
+
// so guard the whole thing — RTDB is optional for a project.
|
|
836
|
+
try {
|
|
837
|
+
await admin.database().ref('_test').remove();
|
|
838
|
+
} catch (e) {
|
|
839
|
+
// RTDB not configured / no database URL — ignore.
|
|
732
840
|
}
|
|
733
841
|
|
|
734
|
-
await Promise.all([
|
|
735
|
-
// Mixed collections: scoped delete of test data only.
|
|
736
|
-
...testDataCollections.map(async (collection) => {
|
|
737
|
-
// Pass 1 — owner-keyed
|
|
738
|
-
for (const batch of uidBatches) {
|
|
739
|
-
try {
|
|
740
|
-
const snapshot = await admin.firestore().collection(collection)
|
|
741
|
-
.where('owner', 'in', batch)
|
|
742
|
-
.get();
|
|
743
|
-
|
|
744
|
-
await Promise.all(
|
|
745
|
-
snapshot.docs.map(doc => doc.ref.delete())
|
|
746
|
-
);
|
|
747
|
-
} catch (e) {
|
|
748
|
-
// Collection may not exist yet, or doesn't carry an owner field — ignore
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Pass 2 — id-keyed (catches ownerless test docs)
|
|
753
|
-
// documentId() range scan: `_test-` ≤ id < `_test.` (the next ASCII char after `-` is `.`)
|
|
754
|
-
try {
|
|
755
|
-
const snapshot = await admin.firestore().collection(collection)
|
|
756
|
-
.where(admin.firestore.FieldPath.documentId(), '>=', TEST_ID_PREFIX)
|
|
757
|
-
.where(admin.firestore.FieldPath.documentId(), '<', '_test.')
|
|
758
|
-
.get();
|
|
759
|
-
|
|
760
|
-
await Promise.all(
|
|
761
|
-
snapshot.docs.map(doc => doc.ref.delete())
|
|
762
|
-
);
|
|
763
|
-
} catch (e) {
|
|
764
|
-
// Collection may not exist yet — ignore
|
|
765
|
-
}
|
|
766
|
-
}),
|
|
767
|
-
// Test-only Firestore collections: wipe in full.
|
|
768
|
-
...testOnlyCollections.map(async (collection) => {
|
|
769
|
-
try {
|
|
770
|
-
const snapshot = await admin.firestore().collection(collection).get();
|
|
771
|
-
await Promise.all(snapshot.docs.map(doc => doc.ref.delete()));
|
|
772
|
-
} catch (e) {
|
|
773
|
-
// Collection may not exist yet — ignore
|
|
774
|
-
}
|
|
775
|
-
}),
|
|
776
|
-
// Realtime Database: wipe the `_test` namespace in full.
|
|
777
|
-
(async () => {
|
|
778
|
-
try {
|
|
779
|
-
await admin.database().ref('_test').remove();
|
|
780
|
-
} catch (e) {
|
|
781
|
-
// RTDB may not be configured for this project — ignore
|
|
782
|
-
}
|
|
783
|
-
})(),
|
|
784
|
-
]);
|
|
785
|
-
|
|
786
842
|
return {
|
|
787
843
|
success: results.failed.length === 0,
|
|
788
844
|
deleted: results.deleted.length,
|
|
@@ -798,10 +854,11 @@ async function deleteTestUsers(admin) {
|
|
|
798
854
|
* @param {object} admin - Firebase admin instance
|
|
799
855
|
* @param {string} domain - Domain for email addresses (e.g., 'itwcreativeworks.com')
|
|
800
856
|
* @param {object} [config] - BEM config (used to resolve first paid product)
|
|
857
|
+
* @param {object} [extraAccounts] - Project-defined accounts from test/_init.js
|
|
801
858
|
* @returns {Promise<object>} Result with created/failed counts
|
|
802
859
|
*/
|
|
803
|
-
async function createTestAccounts(admin, domain, config) {
|
|
804
|
-
const definitions = getAccountDefinitions(domain, config);
|
|
860
|
+
async function createTestAccounts(admin, domain, config, extraAccounts) {
|
|
861
|
+
const definitions = getAccountDefinitions(domain, config, extraAccounts);
|
|
805
862
|
const results = { created: [], failed: [] };
|
|
806
863
|
|
|
807
864
|
// Create all accounts in parallel
|
|
@@ -14,10 +14,14 @@ module.exports = {
|
|
|
14
14
|
tests: [
|
|
15
15
|
{
|
|
16
16
|
name: 'setup-paid-subscription',
|
|
17
|
-
async run({ accounts, firestore, assert, state, config, http, waitFor }) {
|
|
17
|
+
async run({ accounts, firestore, assert, state, config, http, waitFor, skip }) {
|
|
18
18
|
const uid = accounts['journey-payments-cancel-route'].uid;
|
|
19
|
+
// Resolve first paid product from config. If the brand has none configured,
|
|
20
|
+
// skip the entire journey — this is a config-gap, not a code failure.
|
|
19
21
|
const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices);
|
|
20
|
-
|
|
22
|
+
if (!paidProduct) {
|
|
23
|
+
skip('No paid product configured in this brand');
|
|
24
|
+
}
|
|
21
25
|
|
|
22
26
|
state.uid = uid;
|
|
23
27
|
state.paidProductId = paidProduct.id;
|
|
@@ -13,12 +13,15 @@ module.exports = {
|
|
|
13
13
|
tests: [
|
|
14
14
|
{
|
|
15
15
|
name: 'setup-paid-subscription',
|
|
16
|
-
async run({ accounts, firestore, assert, state, config, http, waitFor }) {
|
|
16
|
+
async run({ accounts, firestore, assert, state, config, http, waitFor, skip }) {
|
|
17
17
|
const uid = accounts['journey-payments-cancel'].uid;
|
|
18
18
|
|
|
19
|
-
// Resolve first paid product from config
|
|
19
|
+
// Resolve first paid product from config. If the brand has none configured,
|
|
20
|
+
// skip the entire journey — this is a config-gap, not a code failure.
|
|
20
21
|
const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices);
|
|
21
|
-
|
|
22
|
+
if (!paidProduct) {
|
|
23
|
+
skip('No paid product configured in this brand');
|
|
24
|
+
}
|
|
22
25
|
|
|
23
26
|
state.uid = uid;
|
|
24
27
|
state.paidProductId = paidProduct.id;
|
|
@@ -16,12 +16,15 @@ module.exports = {
|
|
|
16
16
|
tests: [
|
|
17
17
|
{
|
|
18
18
|
name: 'setup-paid-subscription',
|
|
19
|
-
async run({ accounts, firestore, assert, state, config, http, waitFor }) {
|
|
19
|
+
async run({ accounts, firestore, assert, state, config, http, waitFor, skip }) {
|
|
20
20
|
const uid = accounts['journey-payments-failure'].uid;
|
|
21
21
|
|
|
22
|
-
// Resolve first paid subscription product
|
|
22
|
+
// Resolve first paid subscription product. If the brand has none configured,
|
|
23
|
+
// skip the entire journey — this is a config-gap, not a code failure.
|
|
23
24
|
const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.type === 'subscription' && p.prices);
|
|
24
|
-
|
|
25
|
+
if (!paidProduct) {
|
|
26
|
+
skip('No paid subscription product configured in this brand');
|
|
27
|
+
}
|
|
25
28
|
|
|
26
29
|
state.uid = uid;
|
|
27
30
|
state.paidProductId = paidProduct.id;
|
|
@@ -13,12 +13,15 @@ module.exports = {
|
|
|
13
13
|
tests: [
|
|
14
14
|
{
|
|
15
15
|
name: 'setup-paid-subscription',
|
|
16
|
-
async run({ accounts, firestore, assert, state, config, http, waitFor }) {
|
|
16
|
+
async run({ accounts, firestore, assert, state, config, http, waitFor, skip }) {
|
|
17
17
|
const uid = accounts['journey-payments-plan-change'].uid;
|
|
18
18
|
|
|
19
|
-
// Resolve two distinct paid subscription products from config
|
|
19
|
+
// Resolve two distinct paid subscription products from config. If the brand
|
|
20
|
+
// has fewer than two, skip the journey — this is a config-gap, not a code failure.
|
|
20
21
|
const paidProducts = config.payment.products.filter(p => p.id !== 'basic' && p.type === 'subscription' && p.prices);
|
|
21
|
-
|
|
22
|
+
if (paidProducts.length < 2) {
|
|
23
|
+
skip('Fewer than two paid subscription products configured in this brand');
|
|
24
|
+
}
|
|
22
25
|
|
|
23
26
|
const productA = paidProducts[0];
|
|
24
27
|
const productB = paidProducts[1];
|
|
@@ -18,11 +18,15 @@ module.exports = {
|
|
|
18
18
|
tests: [
|
|
19
19
|
{
|
|
20
20
|
name: 'setup-paid-subscription',
|
|
21
|
-
async run({ accounts, firestore, assert, state, config, http, waitFor }) {
|
|
21
|
+
async run({ accounts, firestore, assert, state, config, http, waitFor, skip }) {
|
|
22
22
|
const uid = accounts['journey-payments-refund-webhook'].uid;
|
|
23
23
|
|
|
24
|
+
// Resolve a paid product with a monthly price. If the brand has none configured,
|
|
25
|
+
// skip the entire journey — this is a config-gap, not a code failure.
|
|
24
26
|
const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices?.monthly);
|
|
25
|
-
|
|
27
|
+
if (!paidProduct) {
|
|
28
|
+
skip('No paid product with monthly price configured in this brand');
|
|
29
|
+
}
|
|
26
30
|
|
|
27
31
|
state.uid = uid;
|
|
28
32
|
state.paidProductId = paidProduct.id;
|
|
@@ -13,12 +13,15 @@ module.exports = {
|
|
|
13
13
|
tests: [
|
|
14
14
|
{
|
|
15
15
|
name: 'setup-paid-subscription',
|
|
16
|
-
async run({ accounts, firestore, assert, state, config, http, waitFor }) {
|
|
16
|
+
async run({ accounts, firestore, assert, state, config, http, waitFor, skip }) {
|
|
17
17
|
const uid = accounts['journey-payments-suspend'].uid;
|
|
18
18
|
|
|
19
|
-
// Resolve first paid product from config
|
|
19
|
+
// Resolve first paid product from config. If the brand has none configured,
|
|
20
|
+
// skip the entire journey — this is a config-gap, not a code failure.
|
|
20
21
|
const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices);
|
|
21
|
-
|
|
22
|
+
if (!paidProduct) {
|
|
23
|
+
skip('No paid product configured in this brand');
|
|
24
|
+
}
|
|
22
25
|
|
|
23
26
|
state.uid = uid;
|
|
24
27
|
state.paidProductId = paidProduct.id;
|
|
@@ -16,7 +16,7 @@ module.exports = {
|
|
|
16
16
|
tests: [
|
|
17
17
|
{
|
|
18
18
|
name: 'verify-starts-as-basic',
|
|
19
|
-
async run({ accounts, firestore, assert, state, config }) {
|
|
19
|
+
async run({ accounts, firestore, assert, state, config, skip }) {
|
|
20
20
|
const uid = accounts['journey-payments-trial-cancel'].uid;
|
|
21
21
|
const userDoc = await firestore.get(`users/${uid}`);
|
|
22
22
|
|
|
@@ -24,9 +24,12 @@ module.exports = {
|
|
|
24
24
|
assert.equal(userDoc.subscription?.product?.id, 'basic', 'Should start as basic');
|
|
25
25
|
assert.equal(userDoc.subscription?.trial?.claimed, false, 'Trial should not be claimed');
|
|
26
26
|
|
|
27
|
-
// Resolve first paid product with trial from config
|
|
27
|
+
// Resolve first paid product with trial from config. If the brand has none
|
|
28
|
+
// configured, skip the entire journey — this is a config-gap, not a code failure.
|
|
28
29
|
const trialProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices && p.trial?.days);
|
|
29
|
-
|
|
30
|
+
if (!trialProduct) {
|
|
31
|
+
skip('No paid product with trial configured in this brand');
|
|
32
|
+
}
|
|
30
33
|
|
|
31
34
|
state.uid = uid;
|
|
32
35
|
state.paidProductId = trialProduct.id;
|
|
@@ -13,7 +13,7 @@ module.exports = {
|
|
|
13
13
|
tests: [
|
|
14
14
|
{
|
|
15
15
|
name: 'verify-starts-as-basic',
|
|
16
|
-
async run({ accounts, firestore, assert, state, config }) {
|
|
16
|
+
async run({ accounts, firestore, assert, state, config, skip }) {
|
|
17
17
|
const uid = accounts['journey-payments-trial'].uid;
|
|
18
18
|
const userDoc = await firestore.get(`users/${uid}`);
|
|
19
19
|
|
|
@@ -21,9 +21,15 @@ module.exports = {
|
|
|
21
21
|
assert.equal(userDoc.subscription?.product?.id, 'basic', 'Should start as basic');
|
|
22
22
|
assert.equal(userDoc.subscription?.trial?.claimed, false, 'Trial should not be claimed');
|
|
23
23
|
|
|
24
|
-
// Resolve first paid product from config
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// Resolve first paid product WITH a trial from config. The test processor
|
|
25
|
+
// only creates a trialing subscription when product.trial.days > 0, so a
|
|
26
|
+
// brand whose paid products all have trial.days: 0 (e.g. trials disabled)
|
|
27
|
+
// has nothing to exercise here — skip the journey. This is a config-gap,
|
|
28
|
+
// not a code failure, and mirrors journey-payments-trial-cancel.js.
|
|
29
|
+
const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices && p.trial?.days);
|
|
30
|
+
if (!paidProduct) {
|
|
31
|
+
skip('No paid product with trial configured in this brand');
|
|
32
|
+
}
|
|
27
33
|
|
|
28
34
|
state.uid = uid;
|
|
29
35
|
state.paidProductId = paidProduct.id;
|
|
@@ -21,11 +21,15 @@ module.exports = {
|
|
|
21
21
|
tests: [
|
|
22
22
|
{
|
|
23
23
|
name: 'setup-paid-subscription',
|
|
24
|
-
async run({ accounts, firestore, assert, state, config, http, waitFor }) {
|
|
24
|
+
async run({ accounts, firestore, assert, state, config, http, waitFor, skip }) {
|
|
25
25
|
const uid = accounts['journey-payments-uid-resolution'].uid;
|
|
26
26
|
|
|
27
|
+
// Resolve a paid product with a monthly price. If the brand has none configured,
|
|
28
|
+
// skip the entire journey — this is a config-gap, not a code failure.
|
|
27
29
|
const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices?.monthly);
|
|
28
|
-
|
|
30
|
+
if (!paidProduct) {
|
|
31
|
+
skip('No paid product with monthly price configured in this brand');
|
|
32
|
+
}
|
|
29
33
|
|
|
30
34
|
state.uid = uid;
|
|
31
35
|
state.paidProductId = paidProduct.id;
|
|
@@ -13,7 +13,7 @@ module.exports = {
|
|
|
13
13
|
tests: [
|
|
14
14
|
{
|
|
15
15
|
name: 'verify-starts-as-basic',
|
|
16
|
-
async run({ accounts, firestore, assert, state, config }) {
|
|
16
|
+
async run({ accounts, firestore, assert, state, config, skip }) {
|
|
17
17
|
const uid = accounts['journey-payments-upgrade'].uid;
|
|
18
18
|
const userDoc = await firestore.get(`users/${uid}`);
|
|
19
19
|
|
|
@@ -21,9 +21,12 @@ module.exports = {
|
|
|
21
21
|
assert.equal(userDoc.subscription?.product?.id, 'basic', 'Should start as basic');
|
|
22
22
|
assert.equal(userDoc.subscription?.status, 'active', 'Should be active');
|
|
23
23
|
|
|
24
|
-
// Resolve first paid product from config
|
|
24
|
+
// Resolve first paid product from config. If the brand has none configured,
|
|
25
|
+
// skip the entire journey — this is a config-gap, not a code failure.
|
|
25
26
|
const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices);
|
|
26
|
-
|
|
27
|
+
if (!paidProduct) {
|
|
28
|
+
skip('No paid product configured in this brand');
|
|
29
|
+
}
|
|
27
30
|
|
|
28
31
|
state.uid = uid;
|
|
29
32
|
state.paidProductId = paidProduct.id;
|