Bundul
Internal
Browse docs
Waiting for review

archive

api migration.md

A frozen record of a decision at the time. Superseded by a new record rather than edited.

Written by the build · 2 September 2026

API migration: bundul-backend-corebundul-backend-v2

This document summarizes how the main GraphQL mutations/queries and HTTP controllers changed between the legacy bundul-backend-core codebase and bundul-backend-v2.

It is intended as a reference when migrating clients, jobs, or dashboards that used the old API surface.


Conventions

  • Type:
    • GQL Query / GQL Mutation for GraphQL operations
    • REST for HTTP controller endpoints
  • Old name and New name show the operation identifier (and approximate signature) as exposed at the GraphQL/HTTP layer, not the internal service class.
  • Status:
    • Reused – same operation name and broad responsibility, but implementation may be refactored.
    • Changed – same conceptual responsibility but type/behavior changed.
    • Removed – no v2 equivalent (feature dropped or replaced by a different flow).

Payments

GraphQL mutations / queries

Type Old (core) New (v2) Status What it does
GQL Mutation processPayment(accountId, isSplitPayment, splitPaymentFirstPart?, referralCode?, discountCode?) => StripePaymentResponse in payment.resolver.ts processPayment(accountId, isSplitPayment, splitPaymentFirstPart?, referralCode?, discountCode?) => PaymentResponse in payment.resolver.ts (delegates to PaymentOrchestrationService) Changed Orchestrates a full subscription charge for a user. In core this did all work inline and charged Stripe directly; in v2 it uses Passport/CPX, PaymentOrchestrationService, and background jobs, with a new PaymentResponse shape.
GQL Query getBackgroundJobStatus(paymentProcessId?) => [BackgroundJobStatusResponse] in payment.resolver.ts getBackgroundJobStatus(paymentProcessId?) => [BackgroundJobStatusResponse] in payment.resolver.ts Reused Returns status of background payment-related jobs for the current user (unchanged contract, but job types and metadata evolved to match Passport/CPX orchestration).
GQL Mutation (test automations) Multiple test* mutations returning StripePaymentResponse in payment.resolver.ts Equivalent test* mutations returning PaymentResponse in automation-test.resolver.ts Changed & moved Exercise automation flows against test subscriptions. In v2 these are moved to AutomationTestResolver, return PaymentResponse, and rely on config‑driven test credentials instead of hardcoded Stripe/test card data.

REST controllers

Type Old (core) New (v2) Status What it does
REST POST /webhook and POST /webhook/stripe/* (Stripe and generic events) in webhook.controller.ts and stripe module POST /webhook and POST /webhook/passport in webhook.controller.ts Changed / replaced Core listened to generic and Stripe‑specific webhooks and fanned them out to Stripe + internal handlers. In v2 the /webhook root handler is preserved, but Stripe‑specific flows are removed; /webhook/passport is now the primary entrypoint for Passport events.
REST POST /webhook/card-event in webhook.controller.ts (CPX card events + Airtable + Stripe CPX update models) POST /webhook/card-event in webhook.controller.ts (refactored) Changed Endpoint path is the same, but the v2 handler uses the new WebhookService.handleCardEvent implementation and simplified models; Stripe‑specific CPX update documents and some Mailchimp/Resend side effects are removed.
REST POST /cypress-results and POST /cypress-results/handle-burger-trigger in cypress-response.controller.ts POST /subscriptions/update-status in subscriptions.controller.ts (job status updates); Cypress automation is internal‑only Changed & split Core exposed raw Cypress/Zapier payloads as public HTTP endpoints. In v2, Cypress/browser automation is internalized (no direct public /cypress-results), and only a narrow admin API is exposed to update subscription job status. There is no 1:1 public HTTP replacement for /cypress-results/**; clients should use GraphQL subscription/payment flows instead.

Users & authentication

GraphQL mutations / queries

The user API was largely preserved but email/notification providers changed and a number of ad‑hoc “test” utilities were removed.

Type Old (core) New (v2) Status What it does
GQL Query getUserDataVersion() => UserDataVersion getUserDataVersion() => UserDataVersion Reused Returns per‑domain version counters so the mobile app can invalidate caches when subscriptions, Plaid accounts, recurring transactions, or Passport data change.
GQL Mutation createAccount(createUserInput) => AuthenticatedUser createAccount(createUserInput) => AuthenticatedUser Reused Creates a user account and returns auth tokens. Core also updated Mailchimp/Resend audiences; v2 uses the refactored notification stack (Infobip and internal services).
GQL Mutation googleLogin(handleGoogleLoginInput) => HandleGoogleLoginResponse (via google-auth.resolver.ts and users resolver) googleLogin(handleGoogleLoginInput) => HandleGoogleLoginResponse (same operation, updated internals) Reused (implementation changed) Google OAuth login for Bundul. In v2 the underlying Google client/config is refactored and aligned with the new configuration module.
GQL Mutations requestPhoneEmailVerification, verifyPhoneEmailVerification, addPhoneToProfile Same names and signatures in v2 Reused Phone/email OTP login + profile linking. Implementation now uses Infobip (via NotificationService) instead of Mailchimp/Resend/Brevo.
GQL Mutations resendVerificationEmail, password reset/update mutations Same names and signatures in v2 Reused Account verification and password lifecycle. Internal mail provider changed to Infobip/notification stack.
GQL Query getUserProfile getUserProfile Reused Returns the authenticated user profile from the JWT auth context.
GQL Mutation updateUserProfile(payload) => User updateUserProfile(payload) => User Reused Updates profile details (name, contact, etc.).
GQL Mutations/Queries around subscriptions, payments, deactivation (e.g. getUserSplitPaymentOption, virtual card queries, deactivateUser*) Same names and shapes preserved in users.resolver.ts and payment/subscription resolvers Reused High‑level user payment and subscription display APIs; internals are now Passport/CPX‑centric but externally compatible.
GQL Query testFunction() => StripePaymentResponse (email / Mailchimp / Brevo test utility) Removed Removed Ad‑hoc test endpoint used for experimenting with Mailchimp, Brevo, and Infobip. Replaced by dedicated internal test mutations under AutomationTestResolver and non‑public tooling.

Plaid (bank linking & transactions)

Type Old (core) New (v2) Status What it does
GQL Resolver plaidv2.resolver.ts (createPlaidv2, plaidv2 queries) user-plaid.resolver.ts with a focused set of operations Changed & renamed v2 consolidates Plaid logic into UserPlaidResolver under plaid module; legacy Plaidv2 entity and resolver are removed.
GQL Query getPlaidLinkToken and related user‑plaid queries on UsersResolver getPlaidLinkToken, getUserPlaidTransactions, getUserRecurringTransactions, deleteUserTransactions, getUserPlaidAccounts, unlinkPlaidAccount, etc. in user-plaid.resolver.ts Changed & moved Plaid operations are now grouped under the plaid module; shapes are mostly equivalent but some responses were normalized and the queries moved to a dedicated resolver.
GQL Query userRecurringTransactionsCalendarView() => [CalendarMonth] in users.resolver.ts getRecurringTransactionsCalendarView() => [RecurringTransactionsCalendarMonth] in user-plaid.resolver.ts with legacy alias userRecurringTransactionsCalendarView() Changed & moved The recurring-transactions calendar view now belongs to the Plaid API surface instead of UsersResolver. v2 keeps the cleaner Plaid ownership, while also exposing the old query name as a compatibility alias. The GraphQL type names changed from CalendarMonth/Day/Event to RecurringTransactionsCalendarMonth/Day/Event.

Subscriptions & user subscriptions

The core subscription APIs are intentionally preserved so clients don’t have to change their GraphQL calls, but the backing services and orchestration changed.

Type Old (core) New (v2) Status What it does
GQL Resolver subscription.resolver.ts in subscriptions subscription.resolver.ts in subscriptions/core-subscriptions Reused (moved) Manages Bundul subscription definitions, fetching supported subscriptions for a user, and computing due dates. File location and service wiring changed; schema is stable.
GQL Resolver user-subscription.resolver.ts in user-subscriptions user-subscription.resolver.ts in subscriptions/user-subscriptions Reused (moved + expanded) CRUD and orchestration for individual user subscriptions. v2 adds processExistingCustomer/processExistingCustomerPayment style mutations that return PaymentResponse and integrate with the new payment orchestration flow.

Promotions, discounts, and gift cards

Promotions and discount resolvers remain the canonical home for referral and discount flows in v2, but referral ownership moved more clearly into the promotions domain.

Type Old (core) New (v2) Status What it does
GQL Mutation generateReferralCode() => String in users.resolver.ts generateReferralCode() => String in promotions.resolver.ts (compatibility alias) Changed & moved Core generated a referral code from the users domain. In v2, referral ownership lives in promotions; the legacy mutation name is still exposed as a thin compatibility wrapper, but it now reuses the promotions-backed referral-code lifecycle.
GQL Query getMyReferralCode() => String in users.resolver.ts getMyReferralCode() => String | null in promotions.resolver.ts (compatibility alias) Changed & moved The old “my code” string query is preserved as an alias, but the canonical v2 referral read model lives in promotions. The richer canonical v2 query is getReferralCode() => ReferralCode.
GQL Mutation useReferralCode(code) => Boolean in users.resolver.ts useReferralCode(code) => Boolean in promotions.resolver.ts (compatibility alias), canonical v2 mutation processReferralWithCode(code) => Boolean Changed & moved Referral application moved out of users and into promotions. v2 keeps the old mutation name as a compatibility alias while the canonical operation is processReferralWithCode.
GQL Query getReferralCode() => ReferralCode getReferralCode() => ReferralCode in promotions.resolver.ts Reused / canonicalized In v2 this is the canonical referral read operation, returning the full referral-code object rather than only the code string.
GQL Resolver discount.resolver.ts (discount codes) discount.resolver.ts (same queries/mutations) Reused Create, list, and validate discount codes.
GQL Resolver gift-card.resolver.ts (getGiftCard* simple string queries) gift-card.resolver.ts Reused Gift card support endpoints (kept minimal string‑returning API).

Logo resolver & brand data

Type Old (core) New (v2) Status What it does
GQL Resolver logo-resolver.resolver.ts with queries/mutations: resolve by URL, by domain, migration helpers logo-resolver.resolver.ts with a similar set of queries/mutations Reused Resolve service logos and optionally enhance or migrate stored logo metadata, based on brand information. Implementation refined, but external behavior essentially identical.

Airtable

Type Old (core) New (v2) Status What it does
GQL Resolver airtable.resolver.ts (pingAirtable, sync* mutations, table metadata queries) airtable.resolver.ts with the same query/mutation set Reused Syncs Bundul data into Airtable tables and exposes basic table metadata used by internal tools.
REST airtable‑related controllers under airtable Same controllers plus new airtable/triggers controllers (e.g. airtable-trigger.controller.ts, payment-admin.controller.ts) Expanded Adds trigger/admin endpoints for Airtable‑driven workflows (e.g. payment admin actions), still internal‑only.

Email parser

Type Old (core) New (v2) Status What it does
GQL Resolver email-parser.resolver.ts Removed (no direct GraphQL resolver) Removed Core exposed email parsing flows directly over GraphQL; in v2 this is moved to service + controller usage from other modules instead of being a first‑class GraphQL surface.
Domain structure Gmail-only parser/auth coupling in legacy email-parser + google-auth layout Provider-aware email-parser domain with email-parser/auth/{google,microsoft,imap} plus provider factory/interfaces Changed / refactored v2 now owns email access entirely inside the email-parser domain. Authentication/linking is separated by provider, the parser is provider-agnostic, and folder ownership matches the domain boundary more cleanly.
Email classification Mostly provider- and rule-driven extraction flow AI-assisted classification-first extraction flow Changed v2 uses AI-assisted invoice classification/extraction before full parsing, which makes email ingestion less dependent on rigid provider-specific filtering alone.
REST email-parser.controller.ts email-parser.controller.ts (updated) Reused (implementation changed) HTTP entrypoints for connecting email accounts and parsing utility invoices; implementation now uses the v2 email-access schemas, provider-aware mailbox connections, and AI-assisted parsing flow.

Notifications & campaigns

Notification infrastructure moved off Mailchimp/Resend/Brevo to Infobip.

Type Old (core) New (v2) Status What it does
GQL Resolver mailchimp.resolver.ts, mailchimp_marketing.resolver.ts, resend.resolver.ts, brevo-email.resolver.ts No resolvers – replaced by NotificationService (Infobip) and notification-campaign controllers Removed / replaced Legacy email providers have been fully removed. All notification work now goes through Infobip and the notification-campaign module.
REST push-notification.controller.ts push-notification.controller.ts and notification-admin.controller.ts Expanded Push notification HTTP APIs preserved and extended with admin/control endpoints.

Admin, dashboard, invoices, meta‑suite, Stripe

The following legacy GraphQL resolvers have no v2 equivalents and are intentionally dropped:

Type Old (core) New (v2) Status What it did (now removed)
GQL Resolver admin.resolver.ts None Removed Admin‑only GraphQL operations for manual backend administration. Replaced by internal tooling and Airtable triggers.
GQL Resolver invoices.resolver.ts None Removed Invoice generation and queries over GraphQL; invoice logic is not exposed in v2.
GQL Resolver dashboard‑related resolvers None Removed Dashboard aggregation APIs; replaced by reports and Airtable/analytics integrations.
GQL Resolver meta-suite.resolver.ts None Removed Meta/Facebook marketing integrations not used in v2.
GQL Resolver stripe.resolver.ts None Removed Stripe subscription/payment helpers. All payment logic is now Passport/CPX‑only.

Bundul reports

Type Old (core) New (v2) Status What it does
GQL Resolver bundul_report.resolver.ts (getBundulReport, getYearOverYearAnalysis, getBundulReports) bundul-report.resolver.ts with the same queries Reused Analytics/reporting queries (e.g. recurring and YOY subscription analytics). Implementation uses updated services and Plaid integration but keeps the same surface.

Blog & marketing webhooks

Type Old (core) New (v2) Status What it does
GQL Resolver blog.resolver.ts blog.resolver.ts Reused CRUD for Bundul blog posts.
REST wordpress-webhook.controller.ts wordpress-webhook.controller.ts Reused Ingests WordPress webhooks to sync content into Bundul.

Health & app root

Type Old (core) New (v2) Status What it does
REST health.controller.ts health.controller.ts (under health/controllers) Reused Simple health/ready checks for infrastructure monitoring.
REST app.controller.ts app.controller.ts Reused Basic root routes / smoke‑test endpoints.

Frontend‑focused API changes

This section is designed specifically for frontend/mobile teams. It only lists operations and endpoints whose names or semantics changed or were removed, and shows what to call in v2.

GraphQL – operations that changed or moved

Old name (core) New name (v2) Status Notes for clients
Mutation processPayment(accountId, isSplitPayment, splitPaymentFirstPart?, referralCode?, discountCode?) => StripePaymentResponse (in payment.resolver.ts) Mutation processPayment(accountId, isSplitPayment, splitPaymentFirstPart?, referralCode?, discountCode?) => PaymentResponse (in payment.resolver.ts) Changed Same mutation name and args, but the return type changed from StripePaymentResponse to PaymentResponse, and semantics now go through Passport/CPX orchestration. Update client typings and response parsing.
Mutation testXxx*() => StripePaymentResponse (several “test” mutations in payment.resolver.ts) Mutation testXxx*() => PaymentResponse (in automation-test.resolver.ts) Changed & moved For internal/testing only. If any tooling relied on these, update the operation locations and response type. Production apps should not depend on these.
Query getPlaidLinkToken / getUserPlaidTransactions / getUserRecurringTransactions / getUserPlaidAccounts / unlinkPlaidAccount (on UsersResolver) Same query names but on UserPlaidResolver (src/plaid/resolvers/user-plaid.resolver.ts) Moved Operation names and shapes are intentionally kept the same; only the resolver/module changed. Most clients only care about the schema, so this is transparent unless you introspect modules.
Query userRecurringTransactionsCalendarView() => [CalendarMonth] (on UsersResolver) Query getRecurringTransactionsCalendarView() => [RecurringTransactionsCalendarMonth] on UserPlaidResolver, with legacy alias userRecurringTransactionsCalendarView() Changed & moved v2 keeps the canonical recurring calendar query in the Plaid resolver, but also exposes the old query name as a compatibility alias. Clients that depend on GraphQL generated type names should note the new response types are RecurringTransactionsCalendar* rather than Calendar*.
Mutation generateReferralCode() => String / Query getMyReferralCode() => String / Mutation useReferralCode(code) => Boolean (on UsersResolver) Same legacy names exposed as compatibility aliases in PromotionsResolver; canonical v2 operations are getReferralCode() and processReferralWithCode(code) Changed & moved Referral logic now belongs to the promotions domain. v2 keeps the old names so older clients can continue working, but the canonical surface is PromotionsResolver.
Query plaidv2 / Mutation createPlaidv2 (in plaidv2.resolver.ts) No direct equivalent Removed Legacy Plaidv2 entity is removed. Use the dedicated Plaid queries/mutations under UserPlaidResolver instead (same concepts, different type name).
Query testFunction() => StripePaymentResponse (email/Mailchimp/Brevo test function on UsersResolver) No equivalent Removed This was a pure test utility. Any tooling that used it should be migrated to internal admin tools or Infobip/notification‑test flows.
Mutation / Query pairs in email-parser.resolver.ts No direct GraphQL surface Removed Email parser is now accessed via services/controllers; there is no public GraphQL API for these flows in v2.

Everything else in UsersResolver, SubscriptionResolver, UserSubscriptionResolver, DiscountResolver, GiftCardResolver, LogoResolver, AirtableResolver, BundulReportResolver, and BlogResolver is intentionally kept schema‑compatible for clients (names + arguments + return shapes either identical or only extended). Promotions are mostly compatible too, but referral ownership is now explicitly centered in PromotionsResolver, with legacy alias operations preserved where needed.

REST – endpoints that changed or were removed

All paths below are relative to the API base URL (e.g. https://api.bundul.io).

Old endpoint (core) New endpoint (v2) Status What changed / what to do
POST /webhook (generic webhook entry, including Stripe) POST /webhook Changed semantics Still the generic webhook entry, but Stripe‑specific handling is gone. It now routes only Bundul‑specific events; Stripe subscriptions/invoices are no longer supported. If any external system posted Stripe‑formatted events here, that integration must be retired.
POST /webhook/stripe/* (Stripe webhooks) No endpoint Removed Stripe webhooks are completely removed. Payment events now come from Passport/CPX via /webhook/passport and /webhook/card-event. Remove any Stripe webhook configuration pointing at Bundul v2.
POST /webhook/passport (Passport webhooks) POST /webhook/passport Reused Same path and purpose, but implementation is aligned with the new Passport client and background job model. Payload expectations are compatible at the “event type + resource” level.
POST /webhook/card-event (CPX card events; wrote Stripe CPX update docs and Airtable records inline in controller) POST /webhook/card-event Changed semantics only Path is unchanged. v2 refactors logic into WebhookService.handleCardEvent and simplifies models: it no longer maintains the UserStripeCPXPaymentUpdate Stripe bridge. If any consumer depended on the old side effects (e.g. specific Airtable fields tied to Stripe IDs), verify them against the v2 behavior.
POST /cypress-results (Cypress browser automation callback) No endpoint Removed The raw Cypress callback endpoint is gone. Automation flows are now orchestrated internally via jobs and background workers; frontends should not call Cypress endpoints directly. Use GraphQL payment/subscription mutations instead.
POST /cypress-results/handle-burger-trigger No endpoint Removed Airtable/Zapier “burger trigger” now goes through dedicated Airtable triggers and internal orchestration. There is no public replacement endpoint; any external automation must be updated to call the new Airtable trigger flows, not the API directly.
POST /subscriptions/update-status POST /subscriptions/update-status New in v2 This is a new narrow admin endpoint for updating subscription job status (used by internal orchestrators). There was no direct equivalent in core. External frontends generally shouldn’t call this unless explicitly acting as an admin/orchestrator.

This is a record of a decision at the time. It is not edited — write a new record that supersedes it.