decisions
Bundle Run Redesign — Execution Plan (Tracker)
A frozen record of a decision at the time. Superseded by a new record rather than edited.
Written by the build · 2 September 2026
Frozen. A record of a decision at a point in time, not living documentation. Do not update it — supersede it with a new record instead. For how this works today, see
docs/explanation/anddocs/generated/.
Bundle Run Redesign — Execution Plan (Tracker)
Living tracker for the redesign. Design spec: bundle-run-redesign.md.
Update this doc as work lands — flip checkboxes, set status, add PR links + notes.
Status legend: ⬜ Not started · 🟡 In progress · ✅ Done · ⏸️ Blocked · ❌ Dropped
| Phase | Title | Status | PR(s) | Notes |
|---|---|---|---|---|
| 0 | Admin guard | ✅ | branch feat/bundle-run-redesign-phase1 |
AdminBasicAuthGuard (REST) + AdminGqlAuthGuard (GraphQL, added in Phase 3); both on ADMIN_USERNAME/PASSWORD |
| 1 | Run model + engine seam | 🟡 | branch feat/bundle-run-redesign-phase1 |
code complete, build green, unit tests pass; pending commit + staging verify |
| 2 | Parallel swaps + payment triggers | 🟡 | branch feat/bundle-run-redesign-phase1 |
2a + 2b code-complete behind BUNDLE_RUN_DRIVES_EXECUTION (default OFF); build green, tests pass; needs staging verify with flag ON |
| 3 | Read-only admin dashboard | 🟡 | branch feat/bundle-run-redesign-phase1 |
backend read API done (admin GraphQL queries + GraphQL admin guard); build green, tests pass; frontend UI is separate repo |
| 4 | Admin actions | 🟡 | branch feat/bundle-run-redesign-phase1 |
backend mutations + frontend action UI done; build green, 14 backend tests pass; UI verified (login/list live + run-detail/modal mock-render); full live action verify pending real run |
| L | Deferred cleanup | ⬜ | after transition |
Phase 0 — Admin guard
Goal: admin GraphQL resolvers protected with existing webhookUsername/webhookPassword creds.
Acceptance: non-admin call rejected; seeded admin passes; adminRefreshUtilityInvoices no longer unguarded.
- Create
AdminAuthGuardreusingBasicAuthGuardcred validation - Apply guard to admin resolver surface (placeholder resolver ok for now)
- Retrofit guard onto
adminRefreshUtilityInvoicesmutation - Smoke test: authorized vs unauthorized
Phase 1 — Run model + engine seam
Goal: new Mongo run model written alongside the existing Airtable flow; engine abstraction in place. No existing Airtable writes touched.
Acceptance: every processPayment produces a BundleRun + items mirroring the live run; AI login only fires for automated services; nothing in the current flow changes behaviour.
Schemas
-
BundleRunschema (fields per design §3) -
BundleRunItemschema (fields per design §3, incl.history[]) -
CredentialRevealAuditschema -
BundleRunModule+ service (create run, add items, transition item state)
Engine seam
-
ServiceAutomationProviderinterface (login,swapCard) - Provider registry keyed by service → engine
-
browser-useprovider wrappingAiLoginService+CardSwapService -
puppeteerprovider placeholder (throws "not implemented") -
manualprovider (routes toawaiting_manual)
Routing fix
- Gate
AI_LOGIN_REQUESTEDemission onrequiresManualAutomation === false(user-subscription.service.ts) - Stamp
fulfillmentModeper item fromrequiresManualAutomation - automated + no session → item
swap_failed"reconnect required" (replace silentno_automationskip)
Wire-in
-
processPaymentbackground createsBundleRun+ items alongside current Airtable writes - Verify against a live/staging run that Mongo run == Airtable view
Phase 2 — Parallel resumable swaps + payment triggers
Goal: run drives execution (not the in-memory promise Map); shared payment-creation path; toggle live; fee fires exactly once. Acceptance: all-automated run charges successful subset with no Airtable round-trip; process restart mid-run resumes; no path can create two Initials for one user.
Execution (2b — DONE, behind BUNDLE_RUN_DRIVES_EXECUTION, default OFF)
- Per-item bounded-concurrency execution updating
BundleRunItem—BundleRunExecutorService+mapWithConcurrency(no p-limit; ESM) - Run "all done" computed by querying item state (new path is restart-safe; legacy Map path only used when flag OFF)
- Reconcile cron for runs stuck in
swapping—BundleRunReconcileJob(every 10 min) -
awaiting_customerstate +subscriptionsVersionbump + push on mid-swap re-auth - Trigger:
processPaymentbackground emitsBUNDLE_RUN_EXECUTION_REQUESTED(flag ON) instead of legacyrunAutomation; executor listens in JobsModule (no module cycle)
Payment (2a — DONE)
- Extract
createPaymentForItems(userId, subIds)—BundleRunPaymentService(wrapscreateRecurringSubscription) - Initial/Addition derived from live
hasExistingPassportSubscriptions(no run-local flag) - Per-user payment serialization lock around
createPaymentForItems—KeyedMutex(in-process; DB lock needed if multi-instance) -
autoChargeOnFulfillmenttoggle onBundleRun.chargePolicy— consumed by the executor - Auto-charge on item fulfillment when toggle ON; hold for admin (
awaiting_admin) when OFF - [~] Webhook re-point — superseded by the flag design: when
BUNDLE_RUN_DRIVES_EXECUTIONis ON the executor charges viacreatePaymentForItemsdirectly and the Burger/BurgerTrigger path is bypassed entirely (no re-point needed). When OFF, the legacy webhook is untouched.
Verify
- Scenario tests: all-automated pass / partial; mixed ON & OFF; all-manual; re-auth mid-swap —
bundle-run-executor.service.spec - Double-Initial race test (concurrent charges → single fee) —
bundle-run-payment.service.spec+keyed-mutex.spec - Staging verification with flag ON (real swap + charge end-to-end) — not yet run
Phase 3 — Read-only admin dashboard
Goal: admins see per-service run status over Mongo. Acceptance: a live run is fully visible (automated + manual side by side) with statuses, live URLs, screenshots, errors.
- Admin GraphQL: list runs (
adminBundleRuns, filters: status, user, limit) — guarded byAdminGqlAuthGuard - Admin GraphQL: run detail with per-item state (
adminBundleRun) - Live URL / screenshot / error surfacing per item (in
BundleRunItemType) - Thin admin frontend (read-only) — separate frontend repo, not this backend
- Backend read API done:
BundleRunType/BundleRunItemTypeDTOs,BundleRunService.listRuns,AdminGqlAuthGuard(GraphQL-context Basic auth on admin creds)
Phase 4 — Admin actions
Goal: admin fully resolves a run from the dashboard.
Acceptance: a mixed run can be taken from awaiting_admin to fully paid in the dashboard; all creds reveals audited.
Backend mutations (DONE — BundleRunAdminModule, guarded by AdminGqlAuthGuard)
-
markBundleRunManualItem(itemId, done|failed)— charges on done when toggle ON -
revealServiceCredentials(itemId)→ decrypted creds + VC, writesCredentialRevealAudit(admin username from Basic header) -
setBundleRunItemCredsStatus(itemId, valid|invalid)— invalid flags item for re-collect (credsStatusfield added) -
retryBundleRunItems(runId, itemIds)— state-aware (automated→re-queue+execute; awaiting_customer→re-login first; manual→back to queue), Addition-routed via shared charge -
triggerPaymentForRun(runId)— charges all fulfilled-but-unpaid items in one lock-guarded call -
setBundleRunAutoCharge(runId, enabled)— per-run toggle override
Frontend (DONE — bundul-admin)
- Action buttons on run detail: trigger-payment + auto-charge toggle (run-level); per-item Reveal / Done / Fail / Retry / Creds✗ (contextual by status)
-
RevealCredentialsModal(audited reveal, copy buttons, only-present fields) -
api/admin.ts— all 6 mutations via the shared authed fetch client; react-query invalidation on action - Visual verification — login + runs-list verified live (backend up); run-detail + reveal modal verified via client-side mock render (
scripts/preview-run-detail.mjs) since prod DB has no runs yet - NOTE: local backend
.envMONGO_URL points at PROD (/prod) — did NOT seed test data; full live action verification pending a real run (flag-ON bundul in staging)
Phase L — Deferred cleanup (after transition)
Goal: retire the legacy surface once the dashboard is trusted.
- Stop writing plaintext creds to Airtable (
automation-orchestration.serviceBurger write) - Retire Burger / BurgerTrigger writes + Airtable webhook path
- Remove dead
automationServiceUrl/ legacy PuppeteersendLoginPayloadpath - Pick + implement the automation engine (replace
puppeteerplaceholder if chosen)
Open items to resolve (from design §6)
- Verify whether split-payment manual path (
createSubWithoutDoubleCharging) actually charges — currently appears stubbed (out of scope, but confirm) - Deploy caveat (Phase 1):
requiresManualAutomationdefaults totrue. AI login now only fires when it is explicitlyfalse. Before deploying, setrequiresManualAutomation=falseon everyBundulSupportedSubscriptionthat should auto-login, or those services silently become manual (creds-only). -
adminRefreshUtilityInvoices(old notes) no longer exists in the codebase — nothing to retrofit.
Change log
- 2026-06-03 — Manual-flow refinement + global default: (1) global
autoChargeOnFulfillmentdefault flipped to OFF (BundleRunSettingdefault +getAutoChargeDefaultfallback + run-mapper fallback). (2) Routing decision 3: arequiresManualAutomation=falseservice with no registry playbook now routes to manual (resolveRoutingchecksresolveServiceConfig). (3)markManualItemconsolidated to success | failed | requires_otp | creds_wrong (success→done+charge-if-ON; requires_otp→awaiting_customer + customer push/version-bump; creds_wrong→failed+credsStatus invalid); dropped the standalonesetBundleRunItemCredsStatusmutation. (4) Customer OTP round-trip:submitManualSwapOtp(JWT-guardedBundleRunCustomerResolver) +customerProvidedOtpon the item, surfaced on the admin run-detail as an OTP pill. (5) Admin run-detail manual controls → 4 outcome buttons + OTP pill;/settingstoggle already wired. Also fixed two more latent array/Record@Fieldbugs the schema smoke-test caught (PushNotificationDetails.userIds+.data). Builds green (both repos); bundle-run suites 27/27; admin UI mock-reviewed. No commit. - 2026-06-02 — Hardening pass: (1) GraphQL schema boot smoke-test (
bundle-run-schema.spec, viaGraphQLSchemaFactory, no Mongo boot) — closes the "schema only verified at deploy" gap; it caught + forced a fix to a real latent bug (RecurrTransactionsWithoutCapitalOneResponse.transactionswas a bare@Field()on an array →@Field(() => [UserRecurringTransactions])). (2) Audit-log read surface:adminCredentialRevealsquery +CredentialRevealAuditType+BundleRunService.listCredentialReveals+/auditpage in bundul-admin (reveals were write-only). (3) Noted that the working.envMONGO_URL points at PROD — for local dev use the existingdocker-compose.yml(runs API + local Mongo againstbundul-local) or set a local MONGO_URL. (A redundantdocker-compose.local.ymladded here was later removed.) Builds green (both repos); bundle-run backend suite 10/10; audit UI mock-rendered. No commit. - 2026-06-01 — Plan created; design locked. All phases ⬜.
- 2026-06-01 — Phase 0 ✅ (existing
AdminBasicAuthGuard). Phase 1 🟡: run-model schemas,BundleRunModule/BundleRunService,ServiceAutomationProviderseam + registry + 3 providers, AI-login gating onrequiresManualAutomation, shadow-write ofBundleRunintoprocessPaymentbackground. Build green; 23 unit tests pass. Branchfeat/bundle-run-redesign-phase1. - 2026-06-01 — Phase 2a 🟡:
KeyedMutex(per-user lock, CommonModule) +BundleRunPaymentService.createPaymentForItems(lock-guarded; Initial/Addition from livehasExistingPassportSubscriptions; stamps run items). No live-path change — not yet called (consumed in 2b). Build green; +12 unit tests (35 total) incl. the double-Initial race test. - 2026-06-02 — Phase 4 🟡 (backend mutations): new
BundleRunAdminModule(own module to avoid PaymentModule cycle) withBundleRunAdminService+ guardedBundleRunAdminMutationResolver: triggerPaymentForRun, markBundleRunManualItem, revealServiceCredentials (audited viaCredentialRevealAudit), setBundleRunItemCredsStatus (addedcredsStatusfield), retryBundleRunItems (state-aware, re-emits execution event), setBundleRunAutoCharge. Charges go through the lock-guardedBundleRunPaymentService; retries hand back to the executor. Build green; +14 tests (66 total). Frontend buttons + live review next. No commit. - 2026-06-02 — Phase 3 🟡 (backend read API):
AdminGqlAuthGuard(GraphQL-context Basic auth on admin creds, CommonModule),BundleRunType/BundleRunItemTyperead DTOs,BundleRunService.listRuns,BundleRunAdminResolver(adminBundleRuns,adminBundleRun) wired into BundleRunModule. Build green; +9 tests (52 total). Frontend UI is a separate repo. Residual: verify GraphQL schema generates at app boot (unit tests don't boot the GraphQL module). No commit. - 2026-06-01 — Phase 2b 🟡:
BundleRunExecutorService(JobsModule) drives swaps via the provider registry with bounded concurrency, auto-charges perautoChargeOnFulfillment, handlesawaiting_customer(subscriptionsVersion bump + push);BundleRunReconcileJob(10-min cron);mapWithConcurrencyhelper;BUNDLE_RUN_EXECUTION_REQUESTEDevent;processPaymentbranches onBUNDLE_RUN_DRIVES_EXECUTION(default OFF) — legacy path untouched when OFF. Build green; +8 executor tests (43 total). NOT deployed; needs staging verify with flag ON. NOTE: commits were made earlier in error (before the no-commit rule was re-stated) — no commits since.
This is a record of a decision at the time. It is not edited — write a new record that supersedes it.