A frozen record of a decision at the time. Superseded by a new record rather than edited.
API migration: bundul-backend-core → bundul-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.