Skip to content

Conversation

@ItzNotABug
Copy link
Member

@ItzNotABug ItzNotABug commented Dec 18, 2025

What does this PR do?

(Provide a description of what this PR does.)

Test Plan

(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.)

Related PRs and Issues

(If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.)

Have you read the Contributing Guidelines on issues?

(Write your answer here.)

Summary by CodeRabbit

  • New Features

    • Unified organization/team listing and navigation across public and console flows.
    • Consistent plan naming and improved plan selection/plan comparison UI.
  • Bug Fixes

    • More reliable payment confirmations and retries; clearer error states and better coupon apply/remove feedback.
    • Improved address and payment method flows (create/update/delete) with clearer notifications.
  • Refactor

    • Billing and payment flows migrated to consolidated organization/account APIs and Appwrite Models for consistent behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

@ItzNotABug ItzNotABug self-assigned this Dec 18, 2025
@appwrite
Copy link

appwrite bot commented Dec 18, 2025

Console (appwrite/console)

Project ID: 688b7bf400350cbd60e9

Sites (1)
Site Status Logs Preview QR
 console-stage
688b7cf6003b1842c9dc
Ready Ready View Logs Preview URL QR Code

Tip

Storage files get ClamAV malware scanning and encryption by default

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 18, 2025

Walkthrough

The diff migrates the codebase from a legacy billing SDK surface to Appwrite Console Models and new console/account/organizations endpoints. It centralizes team/organization listing with getTeamOrOrganizationList, converts many positional SDK calls to object-shaped parameters, replaces tier utilities with billingIdToPlan and new plan helpers, and switches route/url construction to resolve-based paths. Many Svelte components, stores, and route loaders updated exported prop/types to Models.*, removed or reduced the old billing SDK types, and adjusted Stripe/confirmPayment and payment-method flows to account-scoped APIs.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Billing SDK Refactor' clearly summarizes the main change—a refactoring of the billing SDK—and accurately reflects the extensive modifications throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ItzNotABug ItzNotABug marked this pull request as ready for review December 31, 2025 13:01
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
src/routes/(console)/organization-[organization]/billing/availableCredit.svelte (1)

36-51: Add error handling to the API request.

The request() function lacks error handling. If the API call fails, the UI remains in an inconsistent state with no feedback to the user. This is inconsistent with similar components in the billing directory, which all wrap SDK calls in try-catch blocks.

🔎 Proposed fix to add error handling
 async function request() {
     if (!$organization?.$id) return;
 
     // fast path return!
     if (!areCreditsSupported) return;
 
     /**
      * The initial `creditList` from `+page.ts` can include up to 25 items by default.
      *
      * Technically, we could reuse that for offsets < 25 (i.e., the first 5 pages with limit = 5)
      * to avoid an extra request. But for now, we always fetch fresh data.
      */
-    creditList = await sdk.forConsole.organizations.listCredits({
-        organizationId: $organization.$id,
-        queries: [Query.limit(limit), Query.offset(offset)]
-    });
+    try {
+        creditList = await sdk.forConsole.organizations.listCredits({
+            organizationId: $organization.$id,
+            queries: [Query.limit(limit), Query.offset(offset)]
+        });
+    } catch (error) {
+        console.error('Failed to load credits:', error);
+        // Consider showing user feedback (toast/alert) here
+        return;
+    }
 
     creditList = {
src/lib/components/billing/selectPaymentMethod.svelte (1)

25-29: Avoid manually constructing SDK model objects.

Manually spreading and reconstructing Models.PaymentMethodList violates the principle that SDK models should be treated as immutable data from the API. This pattern can break if the SDK type has readonly properties, class methods, or internal state.

Consider refetching the payment methods list after adding a new card, or check if the SDK provides a method to update the list. The existing invalidate calls on lines 32-35 suggest this data will be refreshed anyway.

💡 Suggested refactor

Since you're already invalidating dependencies, consider removing the manual update and relying on the data refetch:

 async function cardSaved(card: Models.PaymentMethod) {
     value = card.$id;
 
-    if (value) {
-        methods = {
-            ...methods,
-            total: methods.total + 1,
-            paymentMethods: [...methods.paymentMethods, card]
-        };
-    }
-
     await Promise.all([
         invalidate(Dependencies.UPGRADE_PLAN),
         invalidate(Dependencies.ORGANIZATION)
     ]);
 }

If immediate UI update is required before refetch, verify that the SDK models support this mutation pattern or use a local state approach that doesn't mutate the SDK model.

src/lib/stores/billing.ts (1)

534-552: Use organizations.getPaymentMethod instead of account.getPaymentMethod.

checkForMandate retrieves an organization's payment method but uses sdk.forConsole.account.getPaymentMethod without passing the organization ID. This differs from paymentExpired (line 470) which uses sdk.forConsole.organizations.getPaymentMethod with organizationId. For consistency and to ensure proper organization-level access control, use the same endpoint pattern as paymentExpired:

const paymentMethod = await sdk.forConsole.organizations.getPaymentMethod({
    organizationId: org.$id,
    paymentMethodId: paymentId
});
src/routes/(console)/create-organization/+page.ts (1)

45-57: Missing await in getCoupon causes the function to return a Promise instead of resolving it.

The try/catch block won't catch any errors from the SDK call because the promise is returned directly without awaiting. This means errors will propagate as unhandled promise rejections rather than being caught and returning null.

🔎 Proposed fix
 async function getCoupon(url: URL): Promise<Models.Coupon | null> {
     if (url.searchParams.has('code')) {
         const coupon = url.searchParams.get('code');
         try {
-            return sdk.forConsole.account.getCoupon({
+            return await sdk.forConsole.account.getCoupon({
                 couponId: coupon
             });
         } catch (e) {
             return null;
         }
     }
     return null;
 }
src/routes/(console)/organization-[organization]/billing/wizard/addCredit.svelte (1)

26-31: Incorrect error wrapping loses error details.

throw new Error(error) will call error.toString() which typically results in [object Object]. This loses the original error's stack trace and structured information.

🔎 Proposed fix
         } catch (error) {
             couponData.code = coupon;
             couponData.status = 'error';
             $wizard.interceptorNotificationEnabled = false;
-            throw new Error(error);
+            throw error;
         }
src/routes/(public)/sites/deploy/+page.ts (1)

72-77: Inconsistent API call syntax - uses positional arguments instead of object syntax.

This file uses positional arguments for organizations.create:

await sdk.forConsole.organizations.create(
    ID.unique(),
    'Personal Projects',
    BillingPlan.FREE,
    null
);

Other files in this PR (e.g., src/routes/(public)/template-[template]/+page.ts lines 43-47 and src/routes/(public)/sites/deploy/+page.ts lines 88-92) use object syntax:

await sdk.forConsole.organizations.create({
    organizationId: ID.unique(),
    name: 'Personal Projects',
    billingPlan: BillingPlan.FREE
});

This positional syntax may be incorrect or deprecated. Please verify and align with the object-based syntax used elsewhere.

🔎 Suggested fix using object syntax
             if (isCloud) {
-                await sdk.forConsole.organizations.create(
-                    ID.unique(),
-                    'Personal Projects',
-                    BillingPlan.FREE,
-                    null
-                );
+                await sdk.forConsole.organizations.create({
+                    organizationId: ID.unique(),
+                    name: 'Personal Projects',
+                    billingPlan: BillingPlan.FREE
+                });
             } else {
src/routes/(public)/functions/deploy/+page.ts (1)

67-85: Same API syntax inconsistency as sites/deploy.

This file has the identical issue with positional arguments for organizations.create (lines 72-77) that was flagged in src/routes/(public)/sites/deploy/+page.ts. Please apply the same fix to use object syntax.

🔎 Suggested fix using object syntax
             if (isCloud) {
-                await sdk.forConsole.organizations.create(
-                    ID.unique(),
-                    'Personal Projects',
-                    BillingPlan.FREE,
-                    null
-                );
+                await sdk.forConsole.organizations.create({
+                    organizationId: ID.unique(),
+                    name: 'Personal Projects',
+                    billingPlan: BillingPlan.FREE
+                });
             } else {
🧹 Nitpick comments (19)
src/lib/flags.ts (1)

8-20: Consider removing unused code or clarifying its purpose.

The isFlagEnabled function is marked as unused and never called or exported. The file also exports an empty flags object (line 22), suggesting incomplete or placeholder implementation. Consider either:

  1. Removing the unused function to reduce clutter
  2. Adding a comment explaining why this code is being retained for future use
src/routes/(console)/organization-[organization]/billing/taxId.svelte (1)

20-23: LGTM! SDK refactor is correct.

The migration from sdk.forConsole.billing.updateTaxId to sdk.forConsole.organizations.setBillingTaxId with named parameters is well-executed and aligns with the PR objectives.

Optional: Consider adding null-safety for consistency.

Lines 15 and 49 use optional chaining on $organization, but line 21 directly accesses $organization.$id. While the route likely ensures the organization is loaded, adding optional chaining or an early return would improve consistency and defensive coding:

🔎 Optional refactor for null-safety
 async function updateTaxId() {
+    if (!$organization) return;
+    
     try {
         await sdk.forConsole.organizations.setBillingTaxId({
             organizationId: $organization.$id,
             taxId
         });
src/routes/(public)/(guest)/login/+page.ts (1)

8-27: API migration looks good; consider explicit typing for consistency.

The migration to console.getCoupon and console.getCampaign with object-based parameters is correct and matches the patterns in register/+page.ts and apply-credit/+page.ts.

For consistency with similar files in this refactor (e.g., register/+page.ts lines 7-8, apply-credit/+page.ts lines 6-7), consider explicitly typing couponData as Models.Coupon.

🔎 Optional: Add explicit type for consistency
     if (url.searchParams.has('code')) {
         const code = url.searchParams.get('code');
         let campaign: Models.Campaign;
+        let couponData: Models.Coupon;
         try {
-            const couponData = await sdk.forConsole.console.getCoupon({
+            couponData = await sdk.forConsole.console.getCoupon({
                 couponId: code
             });
src/lib/components/billing/alerts/paymentMandate.svelte (1)

10-15: Consider adding error handling for payment operations.

Payment verification failures should provide clear feedback to users. Wrapping the async operations in try-catch would improve the user experience.

🔎 Suggested error handling
 async function verifyPaymentMethod() {
+    try {
         const method = await sdk.forConsole.account.updatePaymentMethodMandateOptions({
             paymentMethodId: $paymentMissingMandate.$id
         });
         await confirmSetup(method.clientSecret, method.providerMethodId);
+    } catch (error) {
+        // Handle error appropriately (e.g., show toast notification)
+        console.error('Payment verification failed:', error);
+    }
 }
src/lib/components/billing/alerts/selectProjectCloud.svelte (1)

45-47: Consider typing the caught error for type safety.

The caught exception e is not typed, and accessing e.message directly is not type-safe. Consider typing it explicitly or using a type guard.

🔎 Suggested improvement
-} catch (e) {
-    error = e.message;
+} catch (e) {
+    error = e instanceof Error ? e.message : String(e);
 }
src/routes/(console)/account/payments/editPaymentModal.svelte (1)

30-32: Consider awaiting invalidate before showing the notification.

The invalidate(Dependencies.PAYMENT_METHODS) call is not awaited. If the user performs another action immediately after closing the modal, the payment methods list might not yet be refreshed, leading to stale data being displayed.

🔎 Proposed fix
             trackEvent(Submit.PaymentMethodUpdate);
-            invalidate(Dependencies.PAYMENT_METHODS);
+            await invalidate(Dependencies.PAYMENT_METHODS);
             show = false;
src/lib/components/billing/emptyCardCloud.svelte (1)

20-20: proPlanName is not reactive to store changes.

proPlanName is assigned once when the component initializes. If plansInfo store updates after mount (e.g., due to data refresh), this value won't update. Consider using $derived if reactivity is needed:

🔎 Proposed fix using $derived
-    const proPlanName = getBasePlanFromGroup(BillingPlanGroup.Pro).name;
+    const proPlanName = $derived(getBasePlanFromGroup(BillingPlanGroup.Pro).name);

However, if plan names are stable after initial load, the current implementation is acceptable.

src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte (1)

18-37: Inconsistent await usage for invalidate().

In removeDefaultMethod (line 30), invalidate() is called without await, while in removeBackupMethod (line 53), it's awaited. This inconsistency could lead to different behaviors—the backup method waits for cache invalidation before proceeding, while the default method does not.

🔎 Proposed fix for consistency
         trackEvent(Submit.OrganizationPaymentDelete);
-        invalidate(Dependencies.ORGANIZATION);
+        await invalidate(Dependencies.ORGANIZATION);
         showDelete = false;

Also applies to: 40-59

src/routes/(console)/organization-[organization]/domains/domain-[domain]/settings/+page.ts (1)

8-16: Add type annotation for the load function.

The load function is missing its PageLoad type annotation which other route loaders in the codebase use for type safety.

Suggested fix
+import type { PageLoad } from './$types';
-export const load = async ({ params, parent, depends }) => {
+export const load: PageLoad = async ({ params, parent, depends }) => {
src/routes/(console)/create-organization/+page.ts (1)

35-43: Simplify redundant conditional check.

The if (plan) check is redundant since url.searchParams.get('plan') returns null if the param doesn't exist, and the outer if already confirms the param exists.

🔎 Proposed simplification
-function getPlanFromUrl(url: URL): string | null {
-    if (url.searchParams.has('plan')) {
-        const plan = url.searchParams.get('plan');
-        if (plan) {
-            return plan;
-        }
-    }
-    return BillingPlan.FREE;
-}
+function getPlanFromUrl(url: URL): string {
+    return url.searchParams.get('plan') ?? BillingPlan.FREE;
+}
src/routes/+page.ts (1)

37-49: Inconsistent path construction between resolve() and base.

Line 39 uses resolve('/') while lines 43-48 use ${base}/.... This inconsistency could lead to different behavior depending on how the app is deployed.

Consider standardizing on one approach throughout the function for maintainability:

     if (userVisitedEducationPage()) {
         await handleGithubEducationMembership(account.name, account.email);
         redirect(303, resolve('/'));
     } else if (organizations.total && !isApplyingCredit) {
         const teamId = account.prefs.organization ?? organizations.teams[0].$id;
         if (!teamId) {
-            redirect(303, `${base}/account/organizations${url.search}`);
+            redirect(303, `${resolve('/(console)/account/organizations')}${url.search}`);
         } else {
-            redirect(303, `${base}/organization-${teamId}${url.search}`);
+            redirect(303, `${resolve(`/(console)/organization-${teamId}`)}${url.search}`);
         }
     } else if (!isApplyingCredit) {
-        redirect(303, `${base}/onboarding/create-project${url.search}`);
+        redirect(303, `${resolve('/(console)/onboarding/create-project')}${url.search}`);
     }
src/routes/(console)/account/organizations/+page.svelte (2)

40-47: Stale comment references old function name.

The comment on line 41 says "use tierToPlan" but the code now uses billingIdToPlan.

🔎 Proposed fix
-        // For known plans, use tierToPlan
+        // For known plans, use billingIdToPlan
         const tierData = billingIdToPlan(billingPlan);

76-84: Union type Models.Preferences | Models.Organization seems overly broad.

The function accepts Models.Preferences but then passes the value to isCloudOrg and isOrganizationOnTrial which expect Models.Organization. This works due to the type guard, but the signature is misleading since the function only returns a meaningful result for Models.Organization.

Consider narrowing the parameter type or documenting why Preferences is accepted:

     function isPayingOrganization(
-        team: Models.Preferences | Models.Organization
+        team: Models.Organization
     ): Models.Organization | null {
src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte (1)

88-104: Missing error handling in reactive payment method fetches.

The reactive blocks that fetch payment methods don't handle errors. If the API calls fail, the promises are silently rejected which could leave the UI in an inconsistent state.

🔎 Proposed fix
     $: if (organization?.backupPaymentMethodId) {
         sdk.forConsole.organizations
             .getPaymentMethod({
                 organizationId: organization.$id,
                 paymentMethodId: organization.backupPaymentMethodId
             })
-            .then((res) => (backupPaymentMethod = res));
+            .then((res) => (backupPaymentMethod = res))
+            .catch(() => (backupPaymentMethod = null));
     }

     $: if (organization?.paymentMethodId) {
         sdk.forConsole.organizations
             .getPaymentMethod({
                 organizationId: organization.$id,
                 paymentMethodId: organization.paymentMethodId
             })
-            .then((res) => (defaultPaymentMethod = res));
+            .then((res) => (defaultPaymentMethod = res))
+            .catch(() => (defaultPaymentMethod = null));
     }
src/routes/(console)/organization-[organization]/change-plan/+page.ts (1)

31-36: Address the TODO or remove it.

The TODO comment questions the current implementation logic. If there's a known simplification, consider implementing it; otherwise, add context or remove the comment.

src/routes/(console)/organization-[organization]/billing/retryPaymentModal.svelte (1)

127-134: Inconsistent invalidate await pattern.

Line 97 awaits invalidate(Dependencies.PAYMENT_METHODS), but lines 133-134 do not await the invalidation calls. Consider awaiting these for consistency, or if intentional fire-and-forget, add a brief comment explaining why.

🔎 Suggested fix for consistent await pattern
-            invalidate(Dependencies.ORGANIZATION);
-            invalidate(Dependencies.INVOICES);
+            await invalidate(Dependencies.ORGANIZATION);
+            await invalidate(Dependencies.INVOICES);
src/lib/stores/stripe.ts (1)

88-94: Hardcoded base URL should use environment configuration.

The URL https://cloud.appwrite.io/console is hardcoded here and again at line 226. Consider using an environment variable or the existing base import from $app/paths combined with window.location.origin for consistency and flexibility across environments.

🔎 Suggested approach
-        const baseUrl = 'https://cloud.appwrite.io/console';
+        const baseUrl = `${window.location.origin}${base}`;
         const accountUrl = `${baseUrl}/account/payments?clientSecret=${clientSecret}`;
         const orgUrl = `${baseUrl}/organization-${organizationId}/billing?clientSecret=${clientSecret}`;

Apply the same pattern at line 226.

src/lib/components/billing/estimatedTotalBox.svelte (1)

27-31: Consider consistent couponId null handling.

The two estimation functions handle empty couponId differently:

  • getEstimate: couponId === '' ? null : couponId
  • getUpdatePlanEstimate: couponId && couponId.length > 0 ? couponId : null

Both approaches work, but the first could pass undefined as-is while the second explicitly converts to null. Consider unifying the logic for maintainability.

🔎 Suggested unified approach
 async function getEstimate(
     billingPlan: string,
     collaborators: string[],
     couponId: string | undefined
 ) {
     try {
         estimation = await sdk.forConsole.organizations.estimationCreateOrganization({
             billingPlan,
             invites: collaborators ?? [],
-            couponId: couponId === '' ? null : couponId
+            couponId: couponId?.length ? couponId : null
         });

Also applies to: 57-62

src/routes/(console)/onboarding/create-project/+page.ts (1)

64-64: Consider using union type for nullable assignment.

The variable is typed as Models.ProjectList but initialized to null. While TypeScript non-strict mode tolerates this, a more accurate type would be Models.ProjectList | null.

🔎 Proposed fix
-let projects: Models.ProjectList = null;
+let projects: Models.ProjectList | null = null;

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte (1)

68-86: Add missing success analytics event for backup payment method.

The addBackupPaymentMethod function lacks the trackEvent(Submit.OrganizationPaymentUpdate) call on success, while the identical addPaymentMethod function includes it on line 57. Add trackEvent(Submit.OrganizationPaymentUpdate); after the invalidate call on line 78 for consistency.

src/routes/(console)/create-organization/+page.svelte (1)

80-85: Potential null dereference when extracting URL parameters.

Both organizationId and invites parameters are extracted without null checks. If a user navigates to ?type=payment_confirmed&id=xxx without the invites param, calling .split(',') on null will throw a TypeError.

🛠️ Suggested fix with null guards
         if (type === 'payment_confirmed') {
             const organizationId = page.url.searchParams.get('id');
-            const invites = page.url.searchParams.get('invites').split(',');
-            await validate(organizationId, invites);
+            const invitesParam = page.url.searchParams.get('invites');
+            if (organizationId && invitesParam) {
+                const invites = invitesParam.split(',').filter(Boolean);
+                await validate(organizationId, invites);
+            }
         }
src/routes/(console)/account/payments/editPaymentModal.svelte (1)

37-51: Critical: Duplicate trackEvent and invalidate calls will cause double tracking and unnecessary cache invalidation.

Lines 43-44 and 46-47 contain duplicated code that will:

  1. Track the PaymentMethodUpdate analytics event twice per submission
  2. Call invalidate(Dependencies.PAYMENT_METHODS) twice (once sync, once awaited)

This appears to be a merge conflict artifact or copy-paste error.

🐛 Proposed fix
 async function handleSubmit() {
     try {
         await sdk.forConsole.account.updatePaymentMethod({
             paymentMethodId: selectedPaymentMethod.$id,
             expiryMonth: parseInt(month),
             expiryYear: year
         });

-        trackEvent(Submit.PaymentMethodUpdate);
-        invalidate(Dependencies.PAYMENT_METHODS);
-        show = false;
         trackEvent(Submit.PaymentMethodUpdate);
         await invalidate(Dependencies.PAYMENT_METHODS);
+        show = false;
         addNotification({
             type: 'success',
             message: 'Your payment method has been updated'
         });
     } catch (e) {
         error = e.message;
         trackError(e, Submit.PaymentMethodUpdate);
     }
 }
src/routes/(public)/sites/deploy/+page.ts (1)

100-105: Silent failure may leave user without feedback.

If organization creation fails, the error is only logged to console. The user receives no notification, and organizations remains empty, which could cause confusion or downstream issues in the UI.

Consider propagating the error or showing a user-facing notification.

src/routes/(public)/functions/deploy/+page.ts (1)

67-88: Same getBasePlanFromGroup undefined risk and code duplication.

This organization creation logic is nearly identical to src/routes/(public)/sites/deploy/+page.ts (lines 83-105). Both have the same potential TypeError if getBasePlanFromGroup returns undefined.

Consider extracting this shared logic into a helper function (e.g., ensureOrganizationExists() in $lib/stores/organization) to reduce duplication and centralize error handling.

src/routes/(console)/organization-[organization]/billing/replaceCard.svelte (1)

97-117: Error handling doesn't propagate to caller.

Both addPaymentMethod and addBackupPaymentMethod catch errors and set the error state but don't rethrow. In handleSubmit (line 79-81), these functions are awaited but errors won't propagate to the outer try-catch, potentially allowing the success notification and show = false to execute even after a failure.

🐛 Suggested fix
     async function addPaymentMethod(paymentMethodId: string) {
-        try {
-            await sdk.forConsole.organizations.setDefaultPaymentMethod({
-                organizationId: organization.$id,
-                paymentMethodId
-            });
-        } catch (err) {
-            error = err.message;
-        }
+        await sdk.forConsole.organizations.setDefaultPaymentMethod({
+            organizationId: organization.$id,
+            paymentMethodId
+        });
     }

     async function addBackupPaymentMethod(paymentMethodId: string) {
-        try {
-            await sdk.forConsole.organizations.setBackupPaymentMethod({
-                organizationId: organization.$id,
-                paymentMethodId
-            });
-        } catch (err) {
-            error = err.message;
-        }
+        await sdk.forConsole.organizations.setBackupPaymentMethod({
+            organizationId: organization.$id,
+            paymentMethodId
+        });
     }
🤖 Fix all issues with AI agents
In @src/lib/stores/billing.ts:
- Around line 570-572: The call to
getBasePlanFromGroup(BillingPlanGroup.Starter).$id may be undefined; first call
getBasePlanFromGroup(BillingPlanGroup.Starter) into a local variable (e.g.,
basePlan) and check for null/undefined before using .$id, then build the queries
array conditionally so you only include Query.notEqual('billingPlan',
basePlan.$id) when basePlan and basePlan.$id exist (otherwise omit that filter
or use a safe default), and pass the resulting queries to
sdk.forConsole.organizations.list to avoid accessing .$id on undefined.
- Around line 532-540: In checkForMissingPaymentMethod, avoid directly accessing
getBasePlanFromGroup(BillingPlanGroup.Starter).$id because getBasePlanFromGroup
can return undefined; first assign const basePlan =
getBasePlanFromGroup(BillingPlanGroup.Starter) and if basePlan is undefined
either return/exit early or throw/log an error, then use basePlan.$id in the
Query.equal call passed to sdk.forConsole.organizations.list; alternatively
guard using optional chaining and a fallback value before building the query so
you never dereference undefined.
- Around line 88-100: getBasePlanFromGroup can return undefined when proPlans is
empty; change its signature to Models.BillingPlan | undefined and explicitly
handle the empty case instead of assuming [0] is defined. In
getBasePlanFromGroup, compute const base =
proPlans.sort((a,b)=>a.order-b.order)[0]; then return base ?? undefined (or
return a sensible fallback plan found from plansInfoStore if intended), and
update callers to handle a possible undefined result; reference: function
getBasePlanFromGroup, variable proPlans, plansInfoStore, and
BillingPlanGroup.Starter.
- Around line 102-111: The function billingIdToPlan silently falls back to
BillingPlanGroup.Pro when billingId is missing from plansInfo; instead make
failures explicit by validating plansInfo.has(billingId) and throwing a
descriptive error (or returning a Result/optional) rather than defaulting to
Pro. Update billingIdToPlan to check plansInfo (via get(plansInfo)) and if the
id is absent throw an Error like "Unknown billingId: <id>" (or change the
signature to return undefined/null or a Result type), and remove the silent
fallback to getBasePlanFromGroup(BillingPlanGroup.Pro) to avoid incorrect
billing/UI behavior.

In @src/routes/(console)/create-organization/+page.svelte:
- Around line 90-97: The validatePayment branch in the validate() flow currently
preloads and navigates to `${base}/console/organization-${org.$id}` which is
inconsistent with the create() flow; update the two references in the
validatePayment success path (the calls around
sdk.forConsole.organizations.validatePayment and the subsequent preloadData and
goto) to use `${base}/organization-${org.$id}` instead so both validate() and
create() navigate to the same canonical `/organization-{id}` URL.

In @src/routes/(console)/organization-[organization]/billing/+page.ts:
- Around line 15-20: The redirect uses a 301 permanent status in the
redirect(...) call which can be cached by browsers; change the status code from
301 to a temporary redirect such as 302 (or 307 if preserving method) in the
redirect(resolve(...)) invocation so authorization-based redirects aren’t
cached—update the redirect(301, resolve(...)) call to redirect(302,
resolve(...)) (or 307) where the redirect(...) and resolve(...) symbols are
used.

In
@src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte:
- Around line 36-39: The exported props backupMethod and primaryMethod are
declared as non-optional but are used with optional chaining (e.g.,
primaryMethod?.lastError, primaryMethod?.userId), so update their types to be
optional/nullable (e.g., Models.PaymentMethod | undefined) in the export
declarations (export let backupMethod and export let primaryMethod) to match
runtime usage and avoid type errors; ensure any other references to those
symbols (backupMethod, primaryMethod) continue to use safe checks or optional
chaining.

In @src/routes/(public)/sites/deploy/+page.ts:
- Around line 88-92: The create call uses
getBasePlanFromGroup(BillingPlanGroup.Starter).$id which can be undefined in the
(public) route because plansInfo is only populated in the (console) layout;
update the organization creation to guard for undefined by calling
getBasePlanFromGroup(BillingPlanGroup.Starter), check its return value (from the
getBasePlanFromGroup helper) and use a safe fallback billing plan id (e.g., a
DEFAULT_PLAN_ID constant or a known starter plan id) or fetch/populate plansInfo
before calling; ensure you reference getBasePlanFromGroup and the organization
creation block (await sdk.forConsole.organizations.create) when adding the
null-check/fallback and logging an informative error if neither a plan nor
fallback exist.
🧹 Nitpick comments (6)
src/routes/(console)/account/payments/editAddressModal.svelte (1)

36-46: API migration looks correct.

The transition from billing.updateAddress to account.updateBillingAddress with named parameters is properly implemented.

Minor optional simplification for the ternary expressions:

✨ Optional simplification
             await sdk.forConsole.account.updateBillingAddress({
                 billingAddressId: selectedAddress.$id,
                 country: selectedAddress.country,
                 streetAddress: selectedAddress.streetAddress,
                 city: selectedAddress.city,
                 state: selectedAddress.state,
-                postalCode: selectedAddress.postalCode ? selectedAddress.postalCode : undefined,
-                addressLine2: selectedAddress.addressLine2
-                    ? selectedAddress.addressLine2
-                    : undefined
+                postalCode: selectedAddress.postalCode || undefined,
+                addressLine2: selectedAddress.addressLine2 || undefined
             });
src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte (1)

40-59: Optional: Inconsistent dialog dismissal timing between the two methods.

Pre-existing behavior: removeBackupMethod sets showDelete = false at line 42 before the async operation completes, causing the dialog to close immediately. In contrast, removeDefaultMethod uses a finally block to close the dialog after the operation.

Consider aligning the two methods for consistent UX where the dialog remains open until the operation completes or fails.

♻️ Suggested alignment
 async function removeBackupMethod() {
     if ($organization?.billingPlan !== BillingPlan.FREE && !hasOtherMethod) return;
-    showDelete = false;

     try {
         await sdk.forConsole.organizations.deleteBackupPaymentMethod({
             organizationId: $organization.$id
         });
         addNotification({
             type: 'success',
             message: `The payment method has been removed from ${$organization.name}`
         });
         trackEvent(Submit.OrganizationBackupPaymentDelete);
         await invalidate(Dependencies.PAYMENT_METHODS);
-        showDelete = false;
     } catch (e) {
         error = e.message;
         trackError(e, Submit.OrganizationBackupPaymentDelete);
+    } finally {
+        showDelete = false;
     }
 }
src/lib/components/support.svelte (1)

22-29: Type assertion is acceptable but consider adding a type guard for safety.

The (team as Models.Organization) assertions work because the runtime objects contain billing details when in cloud mode. However, this relies on implicit knowledge that organizationList.teams contains Organization objects rather than plain Team objects.

Consider extracting a type guard or helper function if this pattern is used elsewhere:

💡 Optional improvement
// In a shared helper file
function hasOrganizationBilling(team: Models.Team): team is Models.Organization {
    return 'billingPlanDetails' in team;
}
src/routes/(console)/onboarding/create-organization/+page.svelte (1)

24-34: Inconsistent null safety between creation and analytics.

Line 27 accesses getBasePlanFromGroup(BillingPlanGroup.Starter).$id without null safety, while line 31 uses optional chaining (?.name). If the function can return undefined (warranting the ?. on line 31), the same protection should be applied on line 27.

♻️ Suggested fix for consistency
             organization = await sdk.forConsole.organizations.create({
                 organizationId: ID.unique(),
                 name: organizationName,
-                billingPlan: getBasePlanFromGroup(BillingPlanGroup.Starter).$id
+                billingPlan: getBasePlanFromGroup(BillingPlanGroup.Starter)?.$id
             });
src/routes/(console)/organization-[organization]/billing/+page.ts (1)

31-39: Repeated type casts suggest incomplete type propagation.

Multiple lines cast organization to Models.Organization (lines 31, 53, 65). If organization from parent() is already typed correctly, these casts are unnecessary. If not, consider fixing the type at the source (in the parent layout's return type).

Also applies to: 46-56, 63-66

src/lib/stores/billing.ts (1)

113-141: TODOs indicate incomplete refactoring.

Lines 113 and 128 contain TODOs suggesting these functions should return BillingPlan objects instead of strings. Consider addressing these before merging or creating tracking issues.

Would you like me to help refactor these functions to return Models.BillingPlan objects, or open an issue to track this work?

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cfd4636 and 5cc88f6.

📒 Files selected for processing (22)
  • src/lib/components/archiveProject.svelte
  • src/lib/components/billing/alerts/paymentMandate.svelte
  • src/lib/components/billing/alerts/selectProjectCloud.svelte
  • src/lib/components/support.svelte
  • src/lib/stores/billing.ts
  • src/lib/stores/sdk.ts
  • src/routes/(console)/+layout.svelte
  • src/routes/(console)/+layout.ts
  • src/routes/(console)/account/payments/editAddressModal.svelte
  • src/routes/(console)/account/payments/editPaymentModal.svelte
  • src/routes/(console)/create-organization/+page.svelte
  • src/routes/(console)/onboarding/create-organization/+page.svelte
  • src/routes/(console)/organization-[organization]/+layout.ts
  • src/routes/(console)/organization-[organization]/billing/+page.svelte
  • src/routes/(console)/organization-[organization]/billing/+page.ts
  • src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte
  • src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte
  • src/routes/(console)/organization-[organization]/billing/replaceAddress.svelte
  • src/routes/(console)/organization-[organization]/billing/replaceCard.svelte
  • src/routes/(console)/project-[region]-[project]/databases/+page.ts
  • src/routes/(public)/functions/deploy/+page.ts
  • src/routes/(public)/sites/deploy/+page.ts
💤 Files with no reviewable changes (1)
  • src/lib/stores/sdk.ts
✅ Files skipped from review due to trivial changes (1)
  • src/routes/(console)/organization-[organization]/+layout.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/routes/(console)/organization-[organization]/billing/replaceAddress.svelte
  • src/lib/components/archiveProject.svelte
  • src/lib/components/billing/alerts/selectProjectCloud.svelte
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx,js,jsx,svelte}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx,svelte}: Import reusable modules from the src/lib directory using the $lib alias
Use minimal comments in code; reserve comments for TODOs or complex logic explanations
Use $lib, $routes, and $themes aliases instead of relative paths for module imports

Files:

  • src/routes/(console)/onboarding/create-organization/+page.svelte
  • src/routes/(console)/+layout.svelte
  • src/lib/components/support.svelte
  • src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte
  • src/routes/(console)/project-[region]-[project]/databases/+page.ts
  • src/routes/(console)/organization-[organization]/billing/+page.ts
  • src/routes/(console)/account/payments/editAddressModal.svelte
  • src/routes/(public)/sites/deploy/+page.ts
  • src/routes/(console)/organization-[organization]/billing/replaceCard.svelte
  • src/routes/(public)/functions/deploy/+page.ts
  • src/routes/(console)/organization-[organization]/billing/+page.svelte
  • src/routes/(console)/+layout.ts
  • src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte
  • src/routes/(console)/account/payments/editPaymentModal.svelte
  • src/lib/stores/billing.ts
  • src/routes/(console)/create-organization/+page.svelte
src/routes/**/*.svelte

📄 CodeRabbit inference engine (AGENTS.md)

Use SvelteKit file conventions: +page.svelte for components, +page.ts for data loaders, +layout.svelte for wrappers, +error.svelte for error handling, and dynamic route params in square brackets like [param]

Files:

  • src/routes/(console)/onboarding/create-organization/+page.svelte
  • src/routes/(console)/+layout.svelte
  • src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte
  • src/routes/(console)/account/payments/editAddressModal.svelte
  • src/routes/(console)/organization-[organization]/billing/replaceCard.svelte
  • src/routes/(console)/organization-[organization]/billing/+page.svelte
  • src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte
  • src/routes/(console)/account/payments/editPaymentModal.svelte
  • src/routes/(console)/create-organization/+page.svelte
**/*.{ts,tsx,js,jsx,svelte,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use 4 spaces for indentation, single quotes, 100 character line width, and no trailing commas per Prettier configuration

Files:

  • src/routes/(console)/onboarding/create-organization/+page.svelte
  • src/routes/(console)/+layout.svelte
  • src/lib/components/support.svelte
  • src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte
  • src/routes/(console)/project-[region]-[project]/databases/+page.ts
  • src/routes/(console)/organization-[organization]/billing/+page.ts
  • src/routes/(console)/account/payments/editAddressModal.svelte
  • src/routes/(public)/sites/deploy/+page.ts
  • src/routes/(console)/organization-[organization]/billing/replaceCard.svelte
  • src/routes/(public)/functions/deploy/+page.ts
  • src/routes/(console)/organization-[organization]/billing/+page.svelte
  • src/routes/(console)/+layout.ts
  • src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte
  • src/routes/(console)/account/payments/editPaymentModal.svelte
  • src/lib/stores/billing.ts
  • src/routes/(console)/create-organization/+page.svelte
**/*.svelte

📄 CodeRabbit inference engine (AGENTS.md)

Use Svelte 5 + SvelteKit 2 syntax with TypeScript for component development

Files:

  • src/routes/(console)/onboarding/create-organization/+page.svelte
  • src/routes/(console)/+layout.svelte
  • src/lib/components/support.svelte
  • src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte
  • src/routes/(console)/account/payments/editAddressModal.svelte
  • src/routes/(console)/organization-[organization]/billing/replaceCard.svelte
  • src/routes/(console)/organization-[organization]/billing/+page.svelte
  • src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte
  • src/routes/(console)/account/payments/editPaymentModal.svelte
  • src/routes/(console)/create-organization/+page.svelte
src/routes/**

📄 CodeRabbit inference engine (AGENTS.md)

Configure dynamic routes using SvelteKit convention with [param] syntax in route directory names

Files:

  • src/routes/(console)/onboarding/create-organization/+page.svelte
  • src/routes/(console)/+layout.svelte
  • src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte
  • src/routes/(console)/project-[region]-[project]/databases/+page.ts
  • src/routes/(console)/organization-[organization]/billing/+page.ts
  • src/routes/(console)/account/payments/editAddressModal.svelte
  • src/routes/(public)/sites/deploy/+page.ts
  • src/routes/(console)/organization-[organization]/billing/replaceCard.svelte
  • src/routes/(public)/functions/deploy/+page.ts
  • src/routes/(console)/organization-[organization]/billing/+page.svelte
  • src/routes/(console)/+layout.ts
  • src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte
  • src/routes/(console)/account/payments/editPaymentModal.svelte
  • src/routes/(console)/create-organization/+page.svelte
src/lib/components/**/*.svelte

📄 CodeRabbit inference engine (AGENTS.md)

Use PascalCase for component file names and place them in src/lib/components/[feature]/ directory structure

Files:

  • src/lib/components/support.svelte
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: Define types inline or in .d.ts files, avoid creating separate .types.ts files
Use TypeScript in non-strict mode; any type is tolerated in this project

Files:

  • src/routes/(console)/project-[region]-[project]/databases/+page.ts
  • src/routes/(console)/organization-[organization]/billing/+page.ts
  • src/routes/(public)/sites/deploy/+page.ts
  • src/routes/(public)/functions/deploy/+page.ts
  • src/routes/(console)/+layout.ts
  • src/lib/stores/billing.ts
🧠 Learnings (7)
📚 Learning: 2025-12-05T09:24:15.846Z
Learnt from: ItzNotABug
Repo: appwrite/console PR: 2670
File: src/routes/(console)/organization-[organization]/+layout.ts:134-150
Timestamp: 2025-12-05T09:24:15.846Z
Learning: In the Appwrite console codebase, `prefs.organization` is maintained to always reference an organization with Platform.Appwrite, so when using preferences.organization as a fallback in redirect logic, no additional platform validation is required.

Applied to files:

  • src/routes/(console)/onboarding/create-organization/+page.svelte
  • src/routes/(public)/sites/deploy/+page.ts
  • src/routes/(public)/functions/deploy/+page.ts
  • src/routes/(console)/create-organization/+page.svelte
📚 Learning: 2025-11-25T03:15:27.539Z
Learnt from: CR
Repo: appwrite/console PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T03:15:27.539Z
Learning: Applies to src/routes/**/*.svelte : Use SvelteKit file conventions: +page.svelte for components, +page.ts for data loaders, +layout.svelte for wrappers, +error.svelte for error handling, and dynamic route params in square brackets like [param]

Applied to files:

  • src/routes/(console)/+layout.svelte
📚 Learning: 2025-10-13T05:13:54.542Z
Learnt from: ItzNotABug
Repo: appwrite/console PR: 2413
File: src/routes/(console)/project-[region]-[project]/databases/table.svelte:33-39
Timestamp: 2025-10-13T05:13:54.542Z
Learning: In Svelte 5, `import { page } from '$app/state'` provides a reactive state proxy that can be accessed directly (e.g., `page.params`), unlike the older `import { page } from '$app/stores'` which returns a readable store requiring the `$page` syntax for auto-subscription in components.

Applied to files:

  • src/routes/(console)/+layout.svelte
  • src/routes/(console)/organization-[organization]/billing/+page.svelte
  • src/lib/stores/billing.ts
📚 Learning: 2025-10-26T10:20:29.792Z
Learnt from: ItzNotABug
Repo: appwrite/console PR: 2509
File: src/routes/(console)/project-[region]-[project]/auth/teams/+page.svelte:47-61
Timestamp: 2025-10-26T10:20:29.792Z
Learning: When deleting teams in the Appwrite Console codebase, only `Dependencies.TEAMS` needs to be invalidated. Additional invalidations for `Dependencies.TEAM` (detail route) and `Dependencies.MEMBERSHIPS` are not necessary because the deleted teams and their memberships no longer exist.

Applied to files:

  • src/lib/components/support.svelte
  • src/routes/(public)/sites/deploy/+page.ts
  • src/routes/(public)/functions/deploy/+page.ts
📚 Learning: 2025-10-07T14:17:11.597Z
Learnt from: ItzNotABug
Repo: appwrite/console PR: 2413
File: src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/terminology.ts:28-28
Timestamp: 2025-10-07T14:17:11.597Z
Learning: In src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/terminology.ts, the empty `vectordb: {}` entry in baseTerminology is intentional. The vectordb database type is not currently used because the backend API is not finalized, and no database type returns 'vectordb' at the moment.

Applied to files:

  • src/routes/(console)/project-[region]-[project]/databases/+page.ts
📚 Learning: 2025-10-26T10:21:24.499Z
Learnt from: ItzNotABug
Repo: appwrite/console PR: 2509
File: src/routes/(console)/project-[region]-[project]/auth/user-[user]/identities/table.svelte:24-39
Timestamp: 2025-10-26T10:21:24.499Z
Learning: SDK methods from `sdk.forProject(...).users`, `sdk.forProject(...).teams`, and other Appwrite SDK endpoints always throw `AppwriteException` from `appwrite.io/console`, so caught errors can be returned directly without normalization in deletion handlers.

Applied to files:

  • src/routes/(console)/+layout.ts
📚 Learning: 2025-11-25T03:15:27.539Z
Learnt from: CR
Repo: appwrite/console PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T03:15:27.539Z
Learning: Applies to **/*.{ts,tsx,js,jsx,svelte} : Use $lib, $routes, and $themes aliases instead of relative paths for module imports

Applied to files:

  • src/lib/stores/billing.ts
🧬 Code graph analysis (5)
src/routes/(console)/project-[region]-[project]/databases/+page.ts (1)
src/lib/stores/organization.ts (1)
  • currentPlan (18-18)
src/routes/(console)/organization-[organization]/billing/+page.ts (4)
src/lib/constants.ts (1)
  • DEFAULT_BILLING_PROJECTS_LIMIT (4-4)
src/lib/stores/organization.ts (1)
  • organization (14-17)
src/lib/helpers/load.ts (3)
  • getPage (8-10)
  • getLimit (12-16)
  • pageToOffset (4-6)
src/lib/stores/billing.ts (1)
  • paymentMethods (62-65)
src/routes/(public)/functions/deploy/+page.ts (4)
src/lib/stores/organization.ts (1)
  • getTeamOrOrganizationList (22-36)
src/lib/system.ts (1)
  • isCloud (26-26)
src/lib/stores/sdk.ts (1)
  • sdk (171-194)
src/lib/stores/billing.ts (1)
  • getBasePlanFromGroup (88-100)
src/routes/(console)/+layout.ts (2)
src/lib/system.ts (1)
  • isCloud (26-26)
src/lib/stores/sdk.ts (1)
  • sdk (171-194)
src/lib/stores/billing.ts (5)
src/lib/sdk/billing.ts (1)
  • BillingPlansMap (15-15)
src/lib/stores/sdk.ts (1)
  • sdk (171-194)
src/lib/stores/organization.ts (1)
  • currentPlan (18-18)
src/lib/system.ts (1)
  • isCloud (26-26)
src/lib/constants.ts (1)
  • NEW_DEV_PRO_UPGRADE_COUPON (6-6)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: e2e
  • GitHub Check: build
🔇 Additional comments (37)
src/routes/(console)/account/payments/editAddressModal.svelte (2)

15-15: LGTM!

Type migration from custom Address to Models.BillingAddress aligns with the centralized Models namespace refactoring.


49-52: Good fix for the notification message.

Changing from "added" to "updated" is semantically correct for an edit modal.

src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte (2)

22-24: LGTM! API migration to organizations namespace is correct.

The change from billing.removeOrganizationPaymentMethod to organizations.deleteDefaultPaymentMethod with the new object parameter structure aligns with the SDK refactor pattern.


45-47: LGTM! Consistent API migration pattern.

The backup payment method deletion correctly migrates to organizations.deleteBackupPaymentMethod with the same object parameter structure.

src/routes/(console)/project-[region]-[project]/databases/+page.ts (1)

47-49: Type migration is correct and verified.

The change from Plan to Models.BillingPlan on line 47 is correct and aligns with the store definition in src/lib/stores/organization.ts. The backupsEnabled property exists on Models.BillingPlan and is safely accessed on line 49 with optional chaining and a nullish coalescing fallback.

src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte (2)

34-34: LGTM!

The type import for the centralized Models namespace is correctly added to support the type migration.


47-66: LGTM!

The API call migration to organizations.setDefaultPaymentMethod with object-based parameters is clean. Error handling, analytics tracking, and cache invalidation are properly preserved.

src/routes/(console)/create-organization/+page.svelte (6)

11-11: LGTM on import updates.

The migration to billingIdToPlan and isPaymentAuthenticationRequired helpers, along with the Models namespace from @appwrite.io/console, aligns with the SDK refactoring objectives.

Also applies to: 15-15


26-28: LGTM on state variable type updates.

Using data.plan for selectedPlan and Partial<Models.Coupon> | null for selectedCoupon properly aligns with the new Models-based type system.


49-51: LGTM on coupon API migration.

The migration from getCouponAccount to sdk.forConsole.console.getCoupon({ couponId }) follows the consolidated API pattern.


114-132: LGTM on create organization API migration.

The migration to sdk.forConsole.organizations.create() with the new payload structure (organizationId, name, billingPlan, couponId, invites, budget, taxId) correctly implements the refactored API. The conditional handling for free vs. paid plans is well-structured.


134-151: LGTM on payment authentication flow.

The use of isPaymentAuthenticationRequired(org) helper and accessing org.organizationId for the PaymentAuthentication type is correct. The redirect URL construction and subsequent validation call are properly handled.


155-169: LGTM on tracking and post-create navigation.

The tracking event properly uses billingIdToPlan(selectedPlan)?.name with optional chaining for safety. The conditional navigation based on isPaymentAuthenticationRequired correctly handles the success flow.

src/lib/components/support.svelte (1)

10-14: LGTM! Clean migration to Models namespace.

The import changes correctly migrate from the legacy plansInfo store lookup to using the Models type from @appwrite.io/console. This aligns with the broader refactoring effort.

src/routes/(console)/account/payments/editPaymentModal.svelte (1)

10-20: LGTM! Type migration to Models namespace.

The import and prop type changes correctly align with the broader SDK refactoring effort.

src/routes/(console)/+layout.svelte (2)

9-9: LGTM! Import updates align with Models namespace migration.

The import changes correctly support the type migration from custom types to Models.Organization.

Also applies to: 58-58


294-317: LGTM! Function signature and billing trial check updated correctly.

The migration from Organization to Models.Organization and the direct access to org.billingTrialDays (instead of deriving from plansInfo) simplifies the logic and aligns with the new data model.

src/routes/(console)/+layout.ts (3)

4-5: LGTM! Import updates for the SDK migration.

The imports correctly bring in Models, Platform, and Query from @appwrite.io/console to support the new API surface.


17-21: LGTM! API migration to console namespace with platform parameter.

The change from billing.getPlans to console.getPlans({ platform: Platform.Appwrite }) correctly implements the new API surface for plan retrieval.


65-72: LGTM! Type-safe plan mapping with Models namespace.

The toPlanMap function signature and implementation correctly use Models.BillingPlanList and Models.BillingPlan types. Using plan.$id as the string key is appropriate for the new model structure.

src/routes/(console)/organization-[organization]/billing/+page.svelte (3)

11-16: LGTM! Import migration from tierToPlan to billingIdToPlan.

The import change correctly aligns with the renamed helper function in the billing store.


49-52: LGTM! API calls migrated to organizations namespace with object parameters.

The changes correctly update:

  • billing.getInvoice(orgId, invoiceId)organizations.getInvoice({ organizationId, invoiceId })
  • billing.updateInvoiceStatus(...)organizations.validateInvoice({ organizationId, invoiceId })

The object parameter style aligns with the new SDK conventions.

Also applies to: 67-70, 80-83


126-134: LGTM! Plan name resolution updated to use billingIdToPlan.

The downgrade alert correctly uses billingIdToPlan() for both the downgraded and current plan names, consistent with the Models-based typing.

src/routes/(public)/sites/deploy/+page.ts (1)

5-10: LGTM! Imports align with the SDK refactor.

The imports correctly bring in the unified helpers (getTeamOrOrganizationList, getBasePlanFromGroup) and updated types (BillingPlanGroup, Models) from the appropriate modules using $lib aliases as per coding guidelines.

src/routes/(console)/onboarding/create-organization/+page.svelte (1)

4-5: LGTM! Imports correctly updated for the refactor.

The imports properly use $lib aliases and align with the new billing SDK structure.

Also applies to: 13-13

src/routes/(public)/functions/deploy/+page.ts (1)

5-10: LGTM! Imports align with the SDK refactor.

Consistent with the sites deploy page refactor.

src/routes/(console)/organization-[organization]/billing/+page.ts (3)

1-9: LGTM! Imports properly structured.

The imports correctly use $lib aliases and bring in the necessary types from @appwrite.io/console.


77-92: LGTM! API calls properly migrated to organizations namespace.

The Promise.all correctly uses the new endpoints (account.listPaymentMethods, account.listBillingAddresses, organizations.getAvailableCredits, console.getPlan) with proper payload shapes.


120-141: LGTM! Helper function properly typed with Models.

The getOrganizationPaymentMethods function correctly uses Models.Organization, Models.PaymentMethodList, and Models.PaymentMethod types.

src/routes/(console)/organization-[organization]/billing/replaceCard.svelte (1)

12-13: LGTM! Type imports and prop definitions properly updated.

Good practice aliasing Stripe's PaymentMethod as StripePaymentMethod to avoid confusion with Appwrite's Models.PaymentMethod.

Also applies to: 23-25, 31-31

src/lib/stores/billing.ts (7)

3-3: LGTM! Imports properly consolidated.

Imports correctly use $lib aliases and bring in necessary types from @appwrite.io/console.

Also applies to: 17-23, 27-27


62-70: LGTM! Derived stores properly typed with Models.

The stores correctly derive from page data with appropriate Model types.


169-196: LGTM! getServiceLimit signature properly updated.

The function correctly accepts an optional Models.BillingPlan parameter and maintains backward compatibility with the existing tier string parameter.


198-224: LGTM! failedInvoice store updated to use organizations API.

The API call correctly uses sdk.forConsole.organizations.listInvoices with the new payload shape.


599-605: LGTM! URL helpers properly use resolve() for type-safe paths.

The upgradeURL derived store and hideBillingHeaderRoutes correctly use SvelteKit's resolve() function for path building.


607-610: LGTM! Type guard properly implemented.

The isPaymentAuthenticationRequired type guard correctly uses the in operator to narrow the union type.


406-408: Undefined access concern is invalid.

The billingIdToPlan() function has a guaranteed return type of Models.BillingPlan and cannot return undefined. It always returns either the mapped plan from plansInfoStore or falls back to getBasePlanFromGroup(BillingPlanGroup.Pro). Therefore, accessing .name on the result is safe and will not cause an undefined access error.

Likely an incorrect or invalid review comment.

Comment on lines 102 to 111
export function billingIdToPlan(billingId: string): Models.BillingPlan {
const plansInfoStore = get(plansInfo);
if (plansInfoStore.has(billingId)) {
return plansInfoStore.get(billingId);
} else {
// fallback to PRO group's 1st item.
// TODO: @itznotabug, @dlohani - but should we fallback to PRO?.
return getBasePlanFromGroup(BillingPlanGroup.Pro);
}
}
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "billingIdToPlan" --type=ts --type=svelte -C2

Repository: appwrite/console

Length of output: 90


🏁 Script executed:

rg -n "billingIdToPlan" --type=ts --type=js -C2

Repository: appwrite/console

Length of output: 1989


🏁 Script executed:

rg -n "plansInfo" --type=ts --type=js -C1 | head -100

Repository: appwrite/console

Length of output: 3972


🏁 Script executed:

rg -n "getNextTierBillingPlan|getPreviousTierBillingPlan" --type=ts --type=js -C2

Repository: appwrite/console

Length of output: 810


🏁 Script executed:

rg -n "getNextTierBillingPlan|getPreviousTierBillingPlan" --type=ts --type=js | grep -v "export function"

Repository: appwrite/console

Length of output: 42


🏁 Script executed:

wc -l src/lib/stores/billing.ts

Repository: appwrite/console

Length of output: 89


🏁 Script executed:

sed -n '102,145p' src/lib/stores/billing.ts

Repository: appwrite/console

Length of output: 1431


🏁 Script executed:

rg -n "getNextTierBillingPlan|getPreviousTierBillingPlan" --type=ts --type=js --type=svelte

Repository: appwrite/console

Length of output: 90


🏁 Script executed:

rg -n "getNextTierBillingPlan|getPreviousTierBillingPlan" --type=ts --type=js

Repository: appwrite/console

Length of output: 249


🏁 Script executed:

rg -n "getNextTierBillingPlan|getPreviousTierBillingPlan" --type=ts

Repository: appwrite/console

Length of output: 249


🏁 Script executed:

git ls-files | xargs rg "getNextTierBillingPlan|getPreviousTierBillingPlan" 2>/dev/null | head -20

Repository: appwrite/console

Length of output: 496


🏁 Script executed:

cat -n src/lib/components/emptyCardImageCloud.svelte

Repository: appwrite/console

Length of output: 1652


🏁 Script executed:

rg -n "enum BillingPlan|class BillingPlan|type BillingPlan" --type=ts

Repository: appwrite/console

Length of output: 199


🏁 Script executed:

sed -n '612,630p' src/lib/constants.ts

Repository: appwrite/console

Length of output: 568


🏁 Script executed:

sed -n '20,90p' src/routes/\(console\)/+layout.ts

Repository: appwrite/console

Length of output: 1680


🏁 Script executed:

sed -n '1,100p' src/routes/\(console\)/project-[region]-[project]/+layout.ts

Repository: appwrite/console

Length of output: 3589


Make fallback behavior for unknown billing IDs explicit.

The function currently falls back to BillingPlanGroup.Pro for unknown billing IDs without raising an error. The TODO comment on line 108 indicates this fallback is deliberate but uncertain. If a billing ID becomes invalid or there's a mismatch between the server state and the plansInfo cache, silently returning Pro could cause incorrect billing behavior or UI display.

Either validate that the billing ID exists before returning it, or throw an error to make failures explicit rather than falling back silently.

🤖 Prompt for AI Agents
In @src/lib/stores/billing.ts around lines 102 - 111, The function
billingIdToPlan silently falls back to BillingPlanGroup.Pro when billingId is
missing from plansInfo; instead make failures explicit by validating
plansInfo.has(billingId) and throwing a descriptive error (or returning a
Result/optional) rather than defaulting to Pro. Update billingIdToPlan to check
plansInfo (via get(plansInfo)) and if the id is absent throw an Error like
"Unknown billingId: <id>" (or change the signature to return undefined/null or a
Result type), and remove the silent fallback to
getBasePlanFromGroup(BillingPlanGroup.Pro) to avoid incorrect billing/UI
behavior.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
src/lib/stores/stripe.ts (4)

88-95: Duplicate clientSecret query parameter in return URL.

clientSecret is included in the URL string at lines 89-90 and then appended again at line 94. This results in a URL with duplicate clientSecret parameters.

Suggested fix
         const baseUrl = 'https://cloud.appwrite.io/console';
-        const accountUrl = `${baseUrl}/account/payments?clientSecret=${clientSecret}`;
-        const orgUrl = `${baseUrl}/organization-${organizationId}/billing?clientSecret=${clientSecret}`;
+        const accountUrl = `${baseUrl}/account/payments`;
+        const orgUrl = `${baseUrl}/organization-${organizationId}/billing`;

         const returnUrl = new URL(organizationId ? orgUrl : accountUrl);

         returnUrl.searchParams.append('clientSecret', clientSecret);
         returnUrl.searchParams.append('paymentMethodId', paymentMethod.$id);

88-88: Hardcoded production URL may break non-production environments.

The baseUrl is hardcoded to https://cloud.appwrite.io/console. This will cause issues in development, staging, or self-hosted environments where the domain differs.

Consider using window.location.origin or an environment variable, similar to line 202 in confirmPayment.


152-156: Throwing string instead of Error loses stack trace.

At line 155, throw e.message throws a string rather than an Error object, losing the stack trace and error type information. This makes debugging harder.

Suggested fix
         } else {
             const e = new Error('Something went wrong');
             trackError(e, Submit.PaymentMethodCreate);
-            throw e.message;
+            throw e;
         }

189-212: Potential issue when orgId is undefined and no route is provided.

When orgId is undefined and route is not provided, resolve() at lines 198-200 will generate a URL with undefined as the organization parameter (e.g., /organization-undefined/billing). This could lead to navigation to an invalid route.

Consider adding a fallback or validation:

Suggested fix
 export async function confirmPayment(config: {
     clientSecret: string;
     paymentMethodId: string;
     orgId?: string;
     route?: string;
 }) {
     const { clientSecret, paymentMethodId, orgId, route } = config;

     try {
-        const resolvedUrl = resolve('/(console)/organization-[organization]/billing', {
-            organization: orgId
-        });
+        const resolvedUrl = orgId
+            ? resolve('/(console)/organization-[organization]/billing', {
+                  organization: orgId
+              })
+            : '/account/payments';

         const url = window.location.origin + (route ? route : resolvedUrl);
src/routes/(console)/organization-[organization]/change-plan/+page.svelte (2)

93-105: URL plan parameter is immediately overwritten.

The plan from URL params (line 95) is unconditionally overwritten at lines 104-105. This means the ?plan=... query parameter has no effect on the selected plan.

If the URL param should take precedence, make the assignment conditional:

Suggested fix
         const plan = params.get('plan');
         if (plan) {
             selectedPlan = plan;
         }

         if (params.get('type') === 'payment_confirmed') {
             const organizationId = params.get('id');
             const invites = params.get('invites')?.split(',') ?? [];
             await validate(organizationId, invites);
         }

-        selectedPlan =
-            $currentPlan?.$id === BillingPlan.SCALE ? BillingPlan.SCALE : BillingPlan.PRO;
+        if (!plan) {
+            selectedPlan =
+                $currentPlan?.$id === BillingPlan.SCALE ? BillingPlan.SCALE : BillingPlan.PRO;
+        }

230-236: Inconsistent event type in error tracking.

The error tracking uses Submit.OrganizationCreate but this validate function is called during the upgrade flow. For consistency with line 226, consider using Submit.OrganizationUpgrade.

Suggested fix
-            trackError(e, Submit.OrganizationCreate);
+            trackError(e, Submit.OrganizationUpgrade);
src/routes/(console)/create-organization/+page.svelte (1)

78-83: Guard against missing/empty invites during payment-confirmed flow.

get('invites') can be null or '', which makes .split(',') produce [''] and can send invalid invites to validatePayment. This can break the payment-confirmed flow when no collaborators were invited.

🐛 Safer parsing
-                const invites = page.url.searchParams.get('invites').split(',');
+                const invitesParam = page.url.searchParams.get('invites');
+                const invites = invitesParam ? invitesParam.split(',').filter(Boolean) : [];
src/routes/(console)/apply-credit/+page.svelte (1)

101-110: Fix payment-confirmation query param mismatch (org vs id).

You append org to the return URL but read id on mount, so organizationId can be null after 3DS and validatePayment fails. Align the key (or support both).

🐛 Proposed fix
-                const organizationId = page.url.searchParams.get('id');
+                const organizationId =
+                    page.url.searchParams.get('org') ??
+                    page.url.searchParams.get('id');
                 collaborators = page.url.searchParams.get('invites').split(',');
-                    if (key !== 'type' && key !== 'id') {
+                    if (key !== 'type' && key !== 'org' && key !== 'id') {
                         params.append(key, value);
                     }

Also applies to: 167-175, 178-189

🤖 Fix all issues with AI agents
In `@src/routes/`(console)/account/payments/+page.svelte:
- Around line 16-19: The code may pass null to confirmPayment because
paymentMethodId is obtained via page.url.searchParams.get() which can return
null; add a null-check (mirror the clientSecret check) and only call
confirmPayment when both clientSecret and paymentMethodId are non-null, or
provide a fallback/early return and log or handle the missing paymentMethodId;
update the block around page.url.searchParams.get('paymentMethodId') and the
confirmPayment({ clientSecret, paymentMethodId }) call to ensure paymentMethodId
is a non-null string before invoking confirmPayment.

In
`@src/routes/`(console)/organization-[organization]/billing/retryPaymentModal.svelte:
- Around line 22-32: The prop "invoice" is declared as non-null Models.Invoice
but elsewhere (when closing the modal) it is set to null, causing a type
mismatch; update the declaration to allow null (e.g., invoice: Models.Invoice |
null) and add null guards where it's accessed in the component/template
(references to invoice in markup and code) or stop assigning null; locate the
export let invoice and any usage points (e.g., places that read invoice.*, the
modal close logic that sets invoice = null) and ensure proper runtime guards or
conditional rendering when invoice is null.
🧹 Nitpick comments (5)
src/lib/stores/stripe.ts (1)

27-57: Consider setting isStripeInitialized after initialization completes.

isStripeInitialized.set(true) is called at line 33, before the async operations (fetching/creating payment methods, mounting elements) complete. If a consumer relies on this flag to indicate the element is ready, they may interact with it prematurely.

Suggested fix
 export async function initializeStripe(node: HTMLElement) {
     if (!get(stripe)) return;

     // cleanup any existing state
     await unmountPaymentElement();

-    isStripeInitialized.set(true);
-
     const methods = await sdk.forConsole.account.listPaymentMethods();
     // ... rest of initialization ...
     paymentElement = elements.create('payment');
     paymentElement.mount(node);
+
+    isStripeInitialized.set(true);
 }
src/routes/(console)/organization-[organization]/change-plan/+page.svelte (1)

44-44: Type annotation should include null.

The variable is initialized to null, but the type annotation Partial<Models.Coupon> doesn't include null. This could cause TypeScript errors with strict null checks.

Suggested fix
-    let selectedCoupon: Partial<Models.Coupon> = null;
+    let selectedCoupon: Partial<Models.Coupon> | null = null;
src/routes/(console)/organization-[organization]/billing/+page.svelte (2)

49-53: Unify organizationId source across SDK calls.
You mix page.params.organization and organization.$id. If those ever diverge (e.g., slug vs ID), calls could target the wrong org. Consider using a single local organizationId.

♻️ Proposed refactor
-        if (page.url.searchParams.has('type')) {
+        if (page.url.searchParams.has('type')) {
+            const organizationId = organization.$id ?? page.params.organization;
             if (page.url.searchParams.get('type') === 'upgrade') {
                 goto($upgradeURL);
             }
@@
-                const invoice = await sdk.forConsole.organizations.getInvoice({
-                    organizationId: page.params.organization,
+                const invoice = await sdk.forConsole.organizations.getInvoice({
+                    organizationId,
                     invoiceId
                 });
@@
-                await sdk.forConsole.organizations.validateInvoice({
-                    organizationId: organization.$id,
+                await sdk.forConsole.organizations.validateInvoice({
+                    organizationId,
                     invoiceId
                 });
@@
-                const invoice = await sdk.forConsole.organizations.getInvoice({
-                    organizationId: page.params.organization,
+                const invoice = await sdk.forConsole.organizations.getInvoice({
+                    organizationId,
                     invoiceId
                 });

Also applies to: 67-70, 80-83


132-137: Guard against billingIdToPlan returning undefined.
If the mapping ever misses (new/unknown plan ID), .name will throw. If billingIdToPlan is guaranteed to return a plan, you can ignore this; otherwise, add a fallback.

♻️ Possible guard
-            Your organization has changed to {billingIdToPlan(organization?.billingPlanDowngrade)
-                .name} plan. You will continue to have access to {billingIdToPlan(
-                organization?.billingPlan
-            ).name} plan features until your billing period ends on {toLocaleDate(
+            Your organization has changed to {billingIdToPlan(organization?.billingPlanDowngrade)
+                ?.name ?? 'Unknown'} plan. You will continue to have access to {billingIdToPlan(
+                organization?.billingPlan
+            )?.name ?? 'Unknown'} plan features until your billing period ends on {toLocaleDate(
                 organization.billingNextInvoiceDate
             )}.
src/routes/(console)/create-organization/+page.svelte (1)

64-76: Validate plan query param before trusting it.

plan comes from the URL and is now assigned directly. Consider constraining it to known values to avoid invalid plan strings propagating into the create call.

♻️ Suggested guard
-        if (page.url.searchParams.has('plan')) {
-            const plan = page.url.searchParams.get('plan');
-            if (plan) {
-                selectedPlan = plan;
-            }
-        }
+        if (page.url.searchParams.has('plan')) {
+            const plan = page.url.searchParams.get('plan');
+            if (plan && Object.values(BillingPlan).includes(plan as BillingPlan)) {
+                selectedPlan = plan as BillingPlan;
+            }
+        }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/routes/(console)/organization-[organization]/billing/retryPaymentModal.svelte (1)

170-178: Template accesses invoice properties without null guards.

While invoice is typed as Models.Invoice | null (line 27), the template directly accesses invoice.grossAmount, invoice.dueAt, and invoice.$id without null checks. The parent component guards with {#if $selectedInvoice}, but the component itself isn't defensive.

Consider adding a template guard or using optional chaining:

♻️ Suggested fix - Add guard at template level
 <FakeModal
     bind:show
     bind:error
     state="warning"
     onSubmit={handleSubmit}
     size="big"
     title="Retry payment">
+    {`#if` invoice}
     <p class="text">
         Your payment of <span class="inline-tag">{formatCurrency(invoice.grossAmount)}</span> due on {toLocaleDate(
             invoice.dueAt
         )} has failed. Retry your payment to avoid service interruptions with your projects.
     </p>

     <Button
         external
         href={`${endpoint}/organizations/${page.params.organization}/invoices/${invoice.$id}/view`}>
         View invoice
     </Button>
     <!-- ... rest of modal content ... -->
+    {/if}
🤖 Fix all issues with AI agents
In `@src/lib/stores/billing.ts`:
- Around line 124-137: The function getPreviousTierBillingPlan currently assumes
getBasePlanFromGroup(BillingPlanGroup.Starter) always returns a plan and
accesses .$id directly; mirror the defensive fix used in getNextTierBillingPlan
by capturing the result into a variable (e.g., const basePlan =
getBasePlanFromGroup(BillingPlanGroup.Starter)), check whether basePlan is
defined before accessing basePlan.$id, and return a safe fallback (such as an
empty string or a known default plan id) when basePlan is undefined so you don’t
call .$id on undefined.
- Around line 402-404: The code calls billingIdToPlan(BillingPlan.FREE).name
which can throw if billingIdToPlan returns undefined; update the message
construction in the billing notification logic (where billingIdToPlan,
BillingPlan.FREE, org.name and resources are used) to safely access the plan
name via optional chaining and a sensible fallback (e.g.,
billingIdToPlan(BillingPlan.FREE)?.name ?? "Free") so the template strings never
attempt to read .name off undefined and still produce a usable message.

In
`@src/routes/`(console)/organization-[organization]/billing/retryPaymentModal.svelte:
- Around line 111-116: Guard against invoice being null before calling
sdk.forConsole.organizations.createInvoicePayment: check that invoice is
non-null (and that invoice.$id exists) and handle the null case (early return,
disable action, or show an error) instead of directly accessing invoice.$id;
update the call site where createInvoicePayment is invoked (the block using
organization.$id, invoice.$id, paymentMethodId) to either use a validated
invoice or bail out so you won't dereference invoice when it's null.
♻️ Duplicate comments (6)
src/lib/stores/billing.ts (6)

528-549: Still missing null check for getBasePlanFromGroup result.

Line 529 extracts starterPlan but doesn't check for undefined before accessing .$id on line 536. If no Starter plans exist, this throws a TypeError.

🐛 Suggested fix
 export async function checkForMissingPaymentMethod() {
     const starterPlan = getBasePlanFromGroup(BillingPlanGroup.Starter);
+    if (!starterPlan) return;

     const orgs = await sdk.forConsole.organizations.list({
         queries: [
             Query.isNull('paymentMethodId'),
             Query.isNull('backupPaymentMethodId'),
             Query.equal('platform', Platform.Appwrite),
             Query.notEqual('billingPlan', starterPlan.$id)
         ]
     });

83-86: Potential runtime error if plansInfo store is empty.

If get(plansInfo) returns undefined (e.g., during SSR or before store population), calling .get(billingPlanId) will throw a TypeError. The optional chaining on line 85 only protects after the .get() call.

🐛 Suggested defensive check
 export function planHasGroup(billingPlanId: string, group: BillingPlanGroup) {
     const plansInfoStore = get(plansInfo);
+    if (!plansInfoStore) return false;
     return plansInfoStore.get(billingPlanId)?.group === group;
 }

88-96: getBasePlanFromGroup can return undefined, causing runtime errors.

When plansInfoStore is empty/undefined or no plans match the group, proPlans.sort(...)[0] returns undefined. The return type declares Models.BillingPlan but undefined is returned in practice. This causes TypeError when callers access .$id (e.g., lines 121, 136, 529, 569).

🐛 Suggested fix
-export function getBasePlanFromGroup(billingPlanGroup: BillingPlanGroup): Models.BillingPlan {
+export function getBasePlanFromGroup(billingPlanGroup: BillingPlanGroup): Models.BillingPlan | undefined {
     const plansInfoStore = get(plansInfo);
+    if (!plansInfoStore?.size) return undefined;

     const proPlans = Array.from(plansInfoStore.values()).filter(
         (plan) => plan.group === billingPlanGroup
     );

-    return proPlans.sort((a, b) => a.order - b.order)[0];
+    if (proPlans.length === 0) return undefined;
+    return proPlans.sort((a, b) => a.order - b.order)[0];
 }

98-107: Silent fallback to Pro for unknown billing IDs may mask errors.

The function silently returns BillingPlanGroup.Pro's base plan for unknown billingId values. The TODO on line 104 acknowledges this uncertainty. If there's a mismatch between server state and the plansInfo cache, this could cause incorrect billing behavior or UI display.

Additionally, if getBasePlanFromGroup(BillingPlanGroup.Pro) returns undefined, this function propagates that undefined while claiming to return Models.BillingPlan.

Consider either throwing an error for unknown IDs or updating the return type to Models.BillingPlan | undefined.


568-570: Same getBasePlanFromGroup undefined risk.

Line 569 accesses getBasePlanFromGroup(BillingPlanGroup.Starter).$id without null safety.

🐛 Suggested fix
+    const starterPlan = getBasePlanFromGroup(BillingPlanGroup.Starter);
+    if (!starterPlan) return;
+
     const organizations = await sdk.forConsole.organizations.list({
-        queries: [Query.notEqual('billingPlan', getBasePlanFromGroup(BillingPlanGroup.Starter).$id)]
+        queries: [Query.notEqual('billingPlan', starterPlan.$id)]
     });

109-122: Unsafe fallback: accessing .$id on potentially undefined value.

Line 121 calls getBasePlanFromGroup(BillingPlanGroup.Pro).$id. If getBasePlanFromGroup returns undefined (no Pro plans found), this throws a TypeError.

🐛 Suggested defensive fix
-    return getBasePlanFromGroup(BillingPlanGroup.Pro).$id;
+    const fallbackPlan = getBasePlanFromGroup(BillingPlanGroup.Pro);
+    if (!fallbackPlan) {
+        throw new Error('No Pro plan found for fallback');
+    }
+    return fallbackPlan.$id;
🧹 Nitpick comments (4)
src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte (1)

75-81: Consider adding type guard for error handling.

Accessing error.message directly on an unknown type may cause TypeScript strict mode issues. While this may be an existing pattern in the codebase, consider using a type guard or assertion.

Suggested improvement
         } catch (error) {
+            const message = error instanceof Error ? error.message : 'An error occurred';
             addNotification({
                 type: 'error',
-                message: error.message
+                message
             });
             trackError(error, Submit.OrganizationPaymentUpdate);
         }
src/routes/(console)/organization-[organization]/billing/+page.ts (1)

32-39: Type annotation doesn't account for null from catch handler.

billingAddressPromise is typed as Promise<Models.BillingAddress>, but the .catch(() => null) on line 38 means it actually resolves to Models.BillingAddress | null. Consider updating the type for accuracy:

♻️ Suggested fix
-    const billingAddressPromise: Promise<Models.BillingAddress> = billingAddressId
+    const billingAddressPromise: Promise<Models.BillingAddress | null> | null = billingAddressId
         ? sdk.forConsole.organizations
               .getBillingAddress({
                   organizationId: organization.$id,
                   billingAddressId
               })
               .catch(() => null)
         : null;
src/lib/helpers/billing.ts (1)

9-13: Consider using forEach for cleaner iteration.

The traditional for loop works correctly, but a forEach would be more idiomatic and concise.

♻️ Optional refactor
-    const plans = plansArray.plans;
-    for (let index = 0; index < plans.length; index++) {
-        const plan = plans[index];
-        plansMap.set(plan.$id, plan);
-    }
+    plansArray.plans.forEach((plan) => plansMap.set(plan.$id, plan));
src/routes/(console)/+layout.ts (1)

33-36: Misleading variable name: fallbackPlansInfoArray is a Map, not an array.

The variable is named fallbackPlansInfoArray but it holds a Map<string, Models.BillingPlan> returned by makePlansMap. Consider renaming for clarity.

♻️ Suggested fix
-    let fallbackPlansInfoArray = plansInfo;
-    if (!fallbackPlansInfoArray) {
-        fallbackPlansInfoArray = makePlansMap(plansArray);
+    let fallbackPlansInfo = plansInfo;
+    if (!fallbackPlansInfo) {
+        fallbackPlansInfo = makePlansMap(plansArray);
     }

And update line 68:

-        plansInfo: fallbackPlansInfoArray,
+        plansInfo: fallbackPlansInfo,

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/lib/stores/billing.ts`:
- Around line 92-100: getBasePlanFromGroup currently calls
Array.from(plansInfoStore?.values()) which will throw if getPlansInfoStore()
returns null and also may return undefined when no matching plans exist; update
the function to safely handle a null/undefined plansInfoStore and empty results
by iterating over an empty array when plansInfoStore is falsy (e.g., use
Array.from(plansInfoStore?.values() ?? [] ) or spread with
...(plansInfoStore?.values() ?? [])), filter by plan.group === billingPlanGroup
into proPlans, then either return the first element or explicitly return
undefined and adjust the function signature to Models.BillingPlan | undefined
(or throw a meaningful error) so the return type matches possible outcomes;
reference getPlansInfoStore, proPlans and getBasePlanFromGroup when applying the
change.

In `@src/routes/`(console)/+layout.ts:
- Around line 61-63: plansInfoStore is being set to the potentially null parent
value plansInfo instead of the computed non-null fallbackPlansInfoArray returned
to the page; update the assignment so the store is populated with the same
resolved value used by the layout — i.e., set plansInfoStore to
fallbackPlansInfoArray (or the computed non-null value) rather than plansInfo,
ensuring symbols plansInfoStore, plansInfo, and fallbackPlansInfoArray are used
to locate and fix the assignment.
♻️ Duplicate comments (6)
src/lib/stores/billing.ts (5)

102-111: Fallback to Pro plan for unknown billing IDs may cause issues.

The fallback to BillingPlanGroup.Pro (line 109) can propagate undefined if getBasePlanFromGroup returns undefined. The TODO comment indicates this design decision needs verification.


113-141: Unsafe .id access on potentially undefined plan.

Lines 125 and 140 access .$id on getBasePlanFromGroup() results without null checks. If the function returns undefined, this will throw.

🐛 Proposed fix
 export function getNextTierBillingPlan(tier: string): string {
     const currentPlanData = billingIdToPlan(tier);
     const currentOrder = currentPlanData.order;
     const plans = get(plansInfo);

     for (const [, plan] of plans) {
         if (plan.order === currentOrder + 1) {
             return plan.$id;
         }
     }

-    return getBasePlanFromGroup(BillingPlanGroup.Pro).$id;
+    return getBasePlanFromGroup(BillingPlanGroup.Pro)?.$id ?? '';
 }

 export function getPreviousTierBillingPlan(tier: string): string {
     const currentPlanData = billingIdToPlan(tier);
     const currentOrder = currentPlanData.order;
     const plans = get(plansInfo);

     for (const [, plan] of plans) {
         if (plan.order === currentOrder - 1) {
             return plan.$id;
         }
     }

-    return getBasePlanFromGroup(BillingPlanGroup.Starter).$id;
+    return getBasePlanFromGroup(BillingPlanGroup.Starter)?.$id ?? '';
 }

406-408: Potential TypeError when accessing .name on billingIdToPlan result.

billingIdToPlan can return undefined (via getBasePlanFromGroup), causing .name access to throw.

🐛 Proposed fix with optional chaining
-let message = `<b>${org.name}</b> has reached <b>75%</b> of the ${billingIdToPlan(BillingPlan.FREE).name} plan's ${resources.find((r) => r.value >= 75).name} limit. Upgrade to ensure there are no service disruptions.`;
+let message = `<b>${org.name}</b> has reached <b>75%</b> of the ${billingIdToPlan(BillingPlan.FREE)?.name ?? 'Free'} plan's ${resources.find((r) => r.value >= 75).name} limit. Upgrade to ensure there are no service disruptions.`;
 if (resources.filter((r) => r.value >= 75)?.length > 1) {
-    message = `Usage for <b>${org.name}</b> has reached 75% of the ${billingIdToPlan(BillingPlan.FREE).name} plan limit. Upgrade to ensure there are no service disruptions.`;
+    message = `Usage for <b>${org.name}</b> has reached 75% of the ${billingIdToPlan(BillingPlan.FREE)?.name ?? 'Free'} plan limit. Upgrade to ensure there are no service disruptions.`;
 }

532-542: starterPlan.$id access without null check.

Line 540 accesses starterPlan.$id which will throw if getBasePlanFromGroup returns undefined. The extraction to a variable (line 533) is a good step, but needs an early return guard.

🐛 Proposed fix with early return
 export async function checkForMissingPaymentMethod() {
     const starterPlan = getBasePlanFromGroup(BillingPlanGroup.Starter);
+    if (!starterPlan) return;

     const orgs = await sdk.forConsole.organizations.list({
         queries: [
             Query.isNull('paymentMethodId'),
             Query.isNull('backupPaymentMethodId'),
             Query.equal('platform', Platform.Appwrite),
             Query.notEqual('billingPlan', starterPlan.$id)
         ]
     });

572-574: Inline getBasePlanFromGroup().$id access without null safety.

Line 573 directly accesses .$id on the result of getBasePlanFromGroup(), which can throw if the function returns undefined.

src/routes/(console)/onboarding/create-project/+page.ts (1)

27-33: Potential null dereference when accessing starterPlan.$id.

getBasePlanFromGroup can return undefined when no plans match the group. Line 32 accesses starterPlan.$id directly, which will throw if starterPlan is undefined. Line 36 correctly uses optional chaining (starterPlan?.name), but line 32 does not.

🐛 Proposed fix with null check
 if (isCloud) {
     const starterPlan = getBasePlanFromGroup(BillingPlanGroup.Starter);
+    if (!starterPlan) {
+        throw new Error('Starter plan not found');
+    }

     const org = await sdk.forConsole.organizations.create({
         organizationId: ID.unique(),
         name: 'Personal projects',
         billingPlan: starterPlan.$id
     });

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants