-
Notifications
You must be signed in to change notification settings - Fork 208
Billing SDK Refactor #2722
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Billing SDK Refactor #2722
Conversation
replace: `billing.listOrganization` calls with `organizations.list`.
…tionDeleteOrganization`, `getOrganizationPlan` and `getRoles`.
Console (appwrite/console)Project ID: Tip Storage files get ClamAV malware scanning and encryption by default |
WalkthroughThe 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)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
…od and its usages.
…hods and its usages.
There was a problem hiding this 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.PaymentMethodListviolates 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: Useorganizations.getPaymentMethodinstead ofaccount.getPaymentMethod.
checkForMandateretrieves an organization's payment method but usessdk.forConsole.account.getPaymentMethodwithout passing the organization ID. This differs frompaymentExpired(line 470) which usessdk.forConsole.organizations.getPaymentMethodwithorganizationId. For consistency and to ensure proper organization-level access control, use the same endpoint pattern aspaymentExpired:const paymentMethod = await sdk.forConsole.organizations.getPaymentMethod({ organizationId: org.$id, paymentMethodId: paymentId });src/routes/(console)/create-organization/+page.ts (1)
45-57: Missingawaitin getCoupon causes the function to return a Promise instead of resolving it.The
try/catchblock 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 returningnull.🔎 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 callerror.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.tslines 43-47 andsrc/routes/(public)/sites/deploy/+page.tslines 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 insrc/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
isFlagEnabledfunction is marked as unused and never called or exported. The file also exports an emptyflagsobject (line 22), suggesting incomplete or placeholder implementation. Consider either:
- Removing the unused function to reduce clutter
- 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.updateTaxIdtosdk.forConsole.organizations.setBillingTaxIdwith 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.getCouponandconsole.getCampaignwith object-based parameters is correct and matches the patterns inregister/+page.tsandapply-credit/+page.ts.For consistency with similar files in this refactor (e.g.,
register/+page.tslines 7-8,apply-credit/+page.tslines 6-7), consider explicitly typingcouponDataasModels.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
eis not typed, and accessinge.messagedirectly 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 awaitinginvalidatebefore 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:proPlanNameis not reactive to store changes.
proPlanNameis assigned once when the component initializes. IfplansInfostore updates after mount (e.g., due to data refresh), this value won't update. Consider using$derivedif 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: Inconsistentawaitusage forinvalidate().In
removeDefaultMethod(line 30),invalidate()is called withoutawait, while inremoveBackupMethod(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
loadfunction is missing itsPageLoadtype 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 sinceurl.searchParams.get('plan')returnsnullif the param doesn't exist, and the outerifalready 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 betweenresolve()andbase.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 typeModels.Preferences | Models.Organizationseems overly broad.The function accepts
Models.Preferencesbut then passes the value toisCloudOrgandisOrganizationOnTrialwhich expectModels.Organization. This works due to the type guard, but the signature is misleading since the function only returns a meaningful result forModels.Organization.Consider narrowing the parameter type or documenting why
Preferencesis 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: Inconsistentinvalidateawait 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/consoleis hardcoded here and again at line 226. Consider using an environment variable or the existingbaseimport from$app/pathscombined withwindow.location.originfor 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 : couponIdgetUpdatePlanEstimate:couponId && couponId.length > 0 ? couponId : nullBoth approaches work, but the first could pass
undefinedas-is while the second explicitly converts tonull. 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.ProjectListbut initialized tonull. While TypeScript non-strict mode tolerates this, a more accurate type would beModels.ProjectList | null.🔎 Proposed fix
-let projects: Models.ProjectList = null; +let projects: Models.ProjectList | null = null;
src/routes/(console)/organization-[organization]/billing/replaceAddress.svelte
Show resolved
Hide resolved
src/routes/(console)/organization-[organization]/settings/deleteOrganizationModal.svelte
Show resolved
Hide resolved
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts
Show resolved
Hide resolved
There was a problem hiding this 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
addBackupPaymentMethodfunction lacks thetrackEvent(Submit.OrganizationPaymentUpdate)call on success, while the identicaladdPaymentMethodfunction includes it on line 57. AddtrackEvent(Submit.OrganizationPaymentUpdate);after theinvalidatecall on line 78 for consistency.src/routes/(console)/create-organization/+page.svelte (1)
80-85: Potential null dereference when extracting URL parameters.Both
organizationIdandinvitesparameters are extracted without null checks. If a user navigates to?type=payment_confirmed&id=xxxwithout theinvitesparam, calling.split(',')onnullwill 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: DuplicatetrackEventandinvalidatecalls will cause double tracking and unnecessary cache invalidation.Lines 43-44 and 46-47 contain duplicated code that will:
- Track the
PaymentMethodUpdateanalytics event twice per submission- 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
organizationsremains 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: SamegetBasePlanFromGroupundefined 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 potentialTypeErrorifgetBasePlanFromGroupreturnsundefined.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
addPaymentMethodandaddBackupPaymentMethodcatch errors and set theerrorstate but don't rethrow. InhandleSubmit(line 79-81), these functions are awaited but errors won't propagate to the outer try-catch, potentially allowing the success notification andshow = falseto 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.updateAddresstoaccount.updateBillingAddresswith 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:
removeBackupMethodsetsshowDelete = falseat line 42 before the async operation completes, causing the dialog to close immediately. In contrast,removeDefaultMethoduses afinallyblock 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 thatorganizationList.teamscontainsOrganizationobjects rather than plainTeamobjects.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).$idwithout null safety, while line 31 uses optional chaining (?.name). If the function can returnundefined(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
organizationtoModels.Organization(lines 31, 53, 65). Iforganizationfromparent()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
BillingPlanobjects instead of strings. Consider addressing these before merging or creating tracking issues.Would you like me to help refactor these functions to return
Models.BillingPlanobjects, or open an issue to track this work?
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (22)
src/lib/components/archiveProject.sveltesrc/lib/components/billing/alerts/paymentMandate.sveltesrc/lib/components/billing/alerts/selectProjectCloud.sveltesrc/lib/components/support.sveltesrc/lib/stores/billing.tssrc/lib/stores/sdk.tssrc/routes/(console)/+layout.sveltesrc/routes/(console)/+layout.tssrc/routes/(console)/account/payments/editAddressModal.sveltesrc/routes/(console)/account/payments/editPaymentModal.sveltesrc/routes/(console)/create-organization/+page.sveltesrc/routes/(console)/onboarding/create-organization/+page.sveltesrc/routes/(console)/organization-[organization]/+layout.tssrc/routes/(console)/organization-[organization]/billing/+page.sveltesrc/routes/(console)/organization-[organization]/billing/+page.tssrc/routes/(console)/organization-[organization]/billing/deleteOrgPayment.sveltesrc/routes/(console)/organization-[organization]/billing/paymentMethods.sveltesrc/routes/(console)/organization-[organization]/billing/replaceAddress.sveltesrc/routes/(console)/organization-[organization]/billing/replaceCard.sveltesrc/routes/(console)/project-[region]-[project]/databases/+page.tssrc/routes/(public)/functions/deploy/+page.tssrc/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.sveltesrc/routes/(console)/+layout.sveltesrc/lib/components/support.sveltesrc/routes/(console)/organization-[organization]/billing/deleteOrgPayment.sveltesrc/routes/(console)/project-[region]-[project]/databases/+page.tssrc/routes/(console)/organization-[organization]/billing/+page.tssrc/routes/(console)/account/payments/editAddressModal.sveltesrc/routes/(public)/sites/deploy/+page.tssrc/routes/(console)/organization-[organization]/billing/replaceCard.sveltesrc/routes/(public)/functions/deploy/+page.tssrc/routes/(console)/organization-[organization]/billing/+page.sveltesrc/routes/(console)/+layout.tssrc/routes/(console)/organization-[organization]/billing/paymentMethods.sveltesrc/routes/(console)/account/payments/editPaymentModal.sveltesrc/lib/stores/billing.tssrc/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.sveltesrc/routes/(console)/+layout.sveltesrc/routes/(console)/organization-[organization]/billing/deleteOrgPayment.sveltesrc/routes/(console)/account/payments/editAddressModal.sveltesrc/routes/(console)/organization-[organization]/billing/replaceCard.sveltesrc/routes/(console)/organization-[organization]/billing/+page.sveltesrc/routes/(console)/organization-[organization]/billing/paymentMethods.sveltesrc/routes/(console)/account/payments/editPaymentModal.sveltesrc/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.sveltesrc/routes/(console)/+layout.sveltesrc/lib/components/support.sveltesrc/routes/(console)/organization-[organization]/billing/deleteOrgPayment.sveltesrc/routes/(console)/project-[region]-[project]/databases/+page.tssrc/routes/(console)/organization-[organization]/billing/+page.tssrc/routes/(console)/account/payments/editAddressModal.sveltesrc/routes/(public)/sites/deploy/+page.tssrc/routes/(console)/organization-[organization]/billing/replaceCard.sveltesrc/routes/(public)/functions/deploy/+page.tssrc/routes/(console)/organization-[organization]/billing/+page.sveltesrc/routes/(console)/+layout.tssrc/routes/(console)/organization-[organization]/billing/paymentMethods.sveltesrc/routes/(console)/account/payments/editPaymentModal.sveltesrc/lib/stores/billing.tssrc/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.sveltesrc/routes/(console)/+layout.sveltesrc/lib/components/support.sveltesrc/routes/(console)/organization-[organization]/billing/deleteOrgPayment.sveltesrc/routes/(console)/account/payments/editAddressModal.sveltesrc/routes/(console)/organization-[organization]/billing/replaceCard.sveltesrc/routes/(console)/organization-[organization]/billing/+page.sveltesrc/routes/(console)/organization-[organization]/billing/paymentMethods.sveltesrc/routes/(console)/account/payments/editPaymentModal.sveltesrc/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.sveltesrc/routes/(console)/+layout.sveltesrc/routes/(console)/organization-[organization]/billing/deleteOrgPayment.sveltesrc/routes/(console)/project-[region]-[project]/databases/+page.tssrc/routes/(console)/organization-[organization]/billing/+page.tssrc/routes/(console)/account/payments/editAddressModal.sveltesrc/routes/(public)/sites/deploy/+page.tssrc/routes/(console)/organization-[organization]/billing/replaceCard.sveltesrc/routes/(public)/functions/deploy/+page.tssrc/routes/(console)/organization-[organization]/billing/+page.sveltesrc/routes/(console)/+layout.tssrc/routes/(console)/organization-[organization]/billing/paymentMethods.sveltesrc/routes/(console)/account/payments/editPaymentModal.sveltesrc/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.tssrc/routes/(console)/organization-[organization]/billing/+page.tssrc/routes/(public)/sites/deploy/+page.tssrc/routes/(public)/functions/deploy/+page.tssrc/routes/(console)/+layout.tssrc/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.sveltesrc/routes/(public)/sites/deploy/+page.tssrc/routes/(public)/functions/deploy/+page.tssrc/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.sveltesrc/routes/(console)/organization-[organization]/billing/+page.sveltesrc/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.sveltesrc/routes/(public)/sites/deploy/+page.tssrc/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
AddresstoModels.BillingAddressaligns 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.removeOrganizationPaymentMethodtoorganizations.deleteDefaultPaymentMethodwith 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.deleteBackupPaymentMethodwith 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
PlantoModels.BillingPlanon line 47 is correct and aligns with the store definition insrc/lib/stores/organization.ts. ThebackupsEnabledproperty exists onModels.BillingPlanand 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
Modelsnamespace is correctly added to support the type migration.
47-66: LGTM!The API call migration to
organizations.setDefaultPaymentMethodwith 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
billingIdToPlanandisPaymentAuthenticationRequiredhelpers, along with theModelsnamespace 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.planforselectedPlanandPartial<Models.Coupon> | nullforselectedCouponproperly aligns with the new Models-based type system.
49-51: LGTM on coupon API migration.The migration from
getCouponAccounttosdk.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 accessingorg.organizationIdfor thePaymentAuthenticationtype 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)?.namewith optional chaining for safety. The conditional navigation based onisPaymentAuthenticationRequiredcorrectly 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
plansInfostore lookup to using theModelstype 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
OrganizationtoModels.Organizationand the direct access toorg.billingTrialDays(instead of deriving fromplansInfo) 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, andQueryfrom@appwrite.io/consoleto support the new API surface.
17-21: LGTM! API migration to console namespace with platform parameter.The change from
billing.getPlanstoconsole.getPlans({ platform: Platform.Appwrite })correctly implements the new API surface for plan retrieval.
65-72: LGTM! Type-safe plan mapping with Models namespace.The
toPlanMapfunction signature and implementation correctly useModels.BillingPlanListandModels.BillingPlantypes. Usingplan.$idas the string key is appropriate for the new model structure.src/routes/(console)/organization-[organization]/billing/+page.svelte (3)
11-16: LGTM! Import migration fromtierToPlantobillingIdToPlan.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 usebillingIdToPlan.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$libaliases 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
$libaliases 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
$libaliases 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
getOrganizationPaymentMethodsfunction correctly usesModels.Organization,Models.PaymentMethodList, andModels.PaymentMethodtypes.src/routes/(console)/organization-[organization]/billing/replaceCard.svelte (1)
12-13: LGTM! Type imports and prop definitions properly updated.Good practice aliasing Stripe's
PaymentMethodasStripePaymentMethodto avoid confusion with Appwrite'sModels.PaymentMethod.Also applies to: 23-25, 31-31
src/lib/stores/billing.ts (7)
3-3: LGTM! Imports properly consolidated.Imports correctly use
$libaliases 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!getServiceLimitsignature properly updated.The function correctly accepts an optional
Models.BillingPlanparameter and maintains backward compatibility with the existing tier string parameter.
198-224: LGTM!failedInvoicestore updated to use organizations API.The API call correctly uses
sdk.forConsole.organizations.listInvoiceswith the new payload shape.
599-605: LGTM! URL helpers properly useresolve()for type-safe paths.The
upgradeURLderived store andhideBillingHeaderRoutescorrectly use SvelteKit'sresolve()function for path building.
607-610: LGTM! Type guard properly implemented.The
isPaymentAuthenticationRequiredtype guard correctly uses theinoperator to narrow the union type.
406-408: Undefined access concern is invalid.The
billingIdToPlan()function has a guaranteed return type ofModels.BillingPlanand cannot return undefined. It always returns either the mapped plan fromplansInfoStoreor falls back togetBasePlanFromGroup(BillingPlanGroup.Pro). Therefore, accessing.nameon the result is safe and will not cause an undefined access error.Likely an incorrect or invalid review comment.
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -n "billingIdToPlan" --type=ts --type=svelte -C2Repository: appwrite/console
Length of output: 90
🏁 Script executed:
rg -n "billingIdToPlan" --type=ts --type=js -C2Repository: appwrite/console
Length of output: 1989
🏁 Script executed:
rg -n "plansInfo" --type=ts --type=js -C1 | head -100Repository: appwrite/console
Length of output: 3972
🏁 Script executed:
rg -n "getNextTierBillingPlan|getPreviousTierBillingPlan" --type=ts --type=js -C2Repository: 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.tsRepository: appwrite/console
Length of output: 89
🏁 Script executed:
sed -n '102,145p' src/lib/stores/billing.tsRepository: appwrite/console
Length of output: 1431
🏁 Script executed:
rg -n "getNextTierBillingPlan|getPreviousTierBillingPlan" --type=ts --type=js --type=svelteRepository: appwrite/console
Length of output: 90
🏁 Script executed:
rg -n "getNextTierBillingPlan|getPreviousTierBillingPlan" --type=ts --type=jsRepository: appwrite/console
Length of output: 249
🏁 Script executed:
rg -n "getNextTierBillingPlan|getPreviousTierBillingPlan" --type=tsRepository: appwrite/console
Length of output: 249
🏁 Script executed:
git ls-files | xargs rg "getNextTierBillingPlan|getPreviousTierBillingPlan" 2>/dev/null | head -20Repository: appwrite/console
Length of output: 496
🏁 Script executed:
cat -n src/lib/components/emptyCardImageCloud.svelteRepository: appwrite/console
Length of output: 1652
🏁 Script executed:
rg -n "enum BillingPlan|class BillingPlan|type BillingPlan" --type=tsRepository: appwrite/console
Length of output: 199
🏁 Script executed:
sed -n '612,630p' src/lib/constants.tsRepository: appwrite/console
Length of output: 568
🏁 Script executed:
sed -n '20,90p' src/routes/\(console\)/+layout.tsRepository: appwrite/console
Length of output: 1680
🏁 Script executed:
sed -n '1,100p' src/routes/\(console\)/project-[region]-[project]/+layout.tsRepository: 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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!
src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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: DuplicateclientSecretquery parameter in return URL.
clientSecretis included in the URL string at lines 89-90 and then appended again at line 94. This results in a URL with duplicateclientSecretparameters.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
baseUrlis hardcoded tohttps://cloud.appwrite.io/console. This will cause issues in development, staging, or self-hosted environments where the domain differs.Consider using
window.location.originor an environment variable, similar to line 202 inconfirmPayment.
152-156: Throwing string instead of Error loses stack trace.At line 155,
throw e.messagethrows 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 whenorgIdis undefined and norouteis provided.When
orgIdis undefined androuteis not provided,resolve()at lines 198-200 will generate a URL withundefinedas 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: URLplanparameter 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.OrganizationCreatebut thisvalidatefunction is called during the upgrade flow. For consistency with line 226, consider usingSubmit.OrganizationUpgrade.Suggested fix
- trackError(e, Submit.OrganizationCreate); + trackError(e, Submit.OrganizationUpgrade);src/routes/(console)/create-organization/+page.svelte (1)
78-83: Guard against missing/emptyinvitesduring payment-confirmed flow.
get('invites')can benullor'', which makes.split(',')produce['']and can send invalid invites tovalidatePayment. 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 (orgvsid).You append
orgto the return URL but readidon mount, soorganizationIdcan be null after 3DS andvalidatePaymentfails. 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 settingisStripeInitializedafter 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 includenull.The variable is initialized to
null, but the type annotationPartial<Models.Coupon>doesn't includenull. 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: UnifyorganizationIdsource across SDK calls.
You mixpage.params.organizationandorganization.$id. If those ever diverge (e.g., slug vs ID), calls could target the wrong org. Consider using a single localorganizationId.♻️ 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 againstbillingIdToPlanreturningundefined.
If the mapping ever misses (new/unknown plan ID),.namewill throw. IfbillingIdToPlanis 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: Validateplanquery param before trusting it.
plancomes 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; + } + }
src/routes/(console)/organization-[organization]/billing/retryPaymentModal.svelte
Show resolved
Hide resolved
There was a problem hiding this 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 accessesinvoiceproperties without null guards.While
invoiceis typed asModels.Invoice | null(line 27), the template directly accessesinvoice.grossAmount,invoice.dueAt, andinvoice.$idwithout 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 forgetBasePlanFromGroupresult.Line 529 extracts
starterPlanbut doesn't check forundefinedbefore accessing.$idon line 536. If no Starter plans exist, this throws aTypeError.🐛 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 ifplansInfostore is empty.If
get(plansInfo)returnsundefined(e.g., during SSR or before store population), calling.get(billingPlanId)will throw aTypeError. 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:getBasePlanFromGroupcan returnundefined, causing runtime errors.When
plansInfoStoreis empty/undefined or no plans match the group,proPlans.sort(...)[0]returnsundefined. The return type declaresModels.BillingPlanbutundefinedis returned in practice. This causesTypeErrorwhen 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 unknownbillingIdvalues. The TODO on line 104 acknowledges this uncertainty. If there's a mismatch between server state and theplansInfocache, this could cause incorrect billing behavior or UI display.Additionally, if
getBasePlanFromGroup(BillingPlanGroup.Pro)returnsundefined, this function propagates thatundefinedwhile claiming to returnModels.BillingPlan.Consider either throwing an error for unknown IDs or updating the return type to
Models.BillingPlan | undefined.
568-570: SamegetBasePlanFromGroupundefined risk.Line 569 accesses
getBasePlanFromGroup(BillingPlanGroup.Starter).$idwithout 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.$idon potentially undefined value.Line 121 calls
getBasePlanFromGroup(BillingPlanGroup.Pro).$id. IfgetBasePlanFromGroupreturnsundefined(no Pro plans found), this throws aTypeError.🐛 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.messagedirectly on anunknowntype 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.
billingAddressPromiseis typed asPromise<Models.BillingAddress>, but the.catch(() => null)on line 38 means it actually resolves toModels.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 usingforEachfor cleaner iteration.The traditional for loop works correctly, but a
forEachwould 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:fallbackPlansInfoArrayis a Map, not an array.The variable is named
fallbackPlansInfoArraybut it holds aMap<string, Models.BillingPlan>returned bymakePlansMap. 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,
src/routes/(console)/organization-[organization]/billing/retryPaymentModal.svelte
Show resolved
Hide resolved
There was a problem hiding this 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 propagateundefinedifgetBasePlanFromGroupreturnsundefined. The TODO comment indicates this design decision needs verification.
113-141: Unsafe.idaccess on potentially undefined plan.Lines 125 and 140 access
.$idongetBasePlanFromGroup()results without null checks. If the function returnsundefined, 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: PotentialTypeErrorwhen accessing.nameonbillingIdToPlanresult.
billingIdToPlancan returnundefined(viagetBasePlanFromGroup), causing.nameaccess 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.$idaccess without null check.Line 540 accesses
starterPlan.$idwhich will throw ifgetBasePlanFromGroupreturnsundefined. 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: InlinegetBasePlanFromGroup().$idaccess without null safety.Line 573 directly accesses
.$idon the result ofgetBasePlanFromGroup(), which can throw if the function returnsundefined.src/routes/(console)/onboarding/create-project/+page.ts (1)
27-33: Potential null dereference when accessingstarterPlan.$id.
getBasePlanFromGroupcan returnundefinedwhen no plans match the group. Line 32 accessesstarterPlan.$iddirectly, which will throw ifstarterPlanis 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 });

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
Bug Fixes
Refactor
✏️ Tip: You can customize this high-level summary in your review settings.