Skip to content

Commit b7d230a

Browse files
felixkobcursoragent
andcommitted
feat(accounts/billing): getSubscription reads a real subscription endpoint
Replace the listMine()-derived shim with a dedicated call to GET /accounts/{id}/billing/subscription, returning richer detail: billing_provider, current_period_end (renewal), cancel_at_period_end, canceled_at, started_at — alongside plan_id/billing_status/plan. The id still auto-resolves to the active account when omitted. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent feced91 commit b7d230a

3 files changed

Lines changed: 35 additions & 38 deletions

File tree

src/modules/accounts.ts

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -128,28 +128,8 @@ export function createAccountsModule(
128128
},
129129

130130
async getSubscription(accountId?: string): Promise<AccountSubscription> {
131-
// /me is needed for the account's plan_id/billing_status, and also
132-
// resolves the active account id — fetch it once and reuse it.
133-
const mine: MyAccountsResponse = await axios.get(`${base}/me`);
134-
const id =
135-
accountId ??
136-
getStoredActiveAccountId(appId) ??
137-
mine.active_account_id ??
138-
undefined;
139-
if (!id) {
140-
throw new Error(
141-
"No active account: pass an accountId, or have the user select or create an account first."
142-
);
143-
}
144-
const plans: AccountPlan[] = await axios.get(`${base}/${id}/billing/plans`);
145-
const account = mine.accounts.find((a) => a.id === id) ?? null;
146-
const planId = account?.plan_id ?? null;
147-
return {
148-
account_id: id,
149-
plan_id: planId,
150-
billing_status: account?.billing_status ?? "none",
151-
plan: planId ? plans.find((p) => p.id === planId) ?? null : null,
152-
};
131+
const id = await resolveAccountId(accountId);
132+
return axios.get(`${base}/${id}/billing/subscription`);
153133
},
154134

155135
async startCheckout(

src/modules/accounts.types.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,18 @@ export interface AccountSubscription {
7878
plan_id: string | null;
7979
/** Lifecycle status: "none" | "active" | "past_due" | "canceled". */
8080
billing_status: string;
81-
/** The resolved plan (matched from the account's available plans), or `null`. */
81+
/** The payment rail backing the subscription, or `null`. */
82+
billing_provider: string | null;
83+
/** The current plan, or `null` when the account has no subscription. */
8284
plan: AccountPlan | null;
85+
/** When the current paid period ends / renews (ISO 8601), or `null`. */
86+
current_period_end: string | null;
87+
/** True when the subscription will not renew at period end. */
88+
cancel_at_period_end: boolean;
89+
/** When the subscription was canceled (ISO 8601), or `null`. */
90+
canceled_at: string | null;
91+
/** When the subscription started (ISO 8601), or `null`. */
92+
started_at: string | null;
8393
}
8494

8595
/**

tests/unit/accounts.test.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,26 +56,33 @@ describe("Accounts module", () => {
5656
expect(scope.isDone()).toBe(true);
5757
});
5858

59-
test("billing.getSubscription derives plan + status from /me and plans", async () => {
60-
const plan = { id: "p1", name: "Pro", price_amount: 1000, currency: "usd", interval: "month", is_active: true };
61-
scope.get(`/api/apps/${appId}/accounts/me`).reply(200, {
62-
accounts: [{ id: ACCT, name: "Acme", plan_id: "p1", billing_status: "active" }],
63-
active_account_id: ACCT,
64-
});
65-
scope.get(`/api/apps/${appId}/accounts/${ACCT}/billing/plans`).reply(200, [plan]);
59+
test("billing.getSubscription GETs the subscription endpoint (resolving active account)", async () => {
60+
const payload = {
61+
account_id: ACCT,
62+
plan_id: "p1",
63+
billing_status: "active",
64+
billing_provider: "stripe_connect",
65+
plan: { id: "p1", name: "Pro", price_amount: 1000, currency: "usd", interval: "month", is_active: true },
66+
current_period_end: "2026-07-01T00:00:00+00:00",
67+
cancel_at_period_end: false,
68+
canceled_at: null,
69+
started_at: "2026-06-01T00:00:00+00:00",
70+
};
71+
scope.get(`/api/apps/${appId}/accounts/me`).reply(200, { accounts: [], active_account_id: ACCT });
72+
scope.get(`/api/apps/${appId}/accounts/${ACCT}/billing/subscription`).reply(200, payload);
6673
const sub = await base44.accounts.billing.getSubscription();
67-
expect(sub).toEqual({ account_id: ACCT, plan_id: "p1", billing_status: "active", plan });
74+
expect(sub).toEqual(payload);
6875
expect(scope.isDone()).toBe(true);
6976
});
7077

71-
test("billing.getSubscription returns null plan/none status when unsubscribed", async () => {
72-
scope.get(`/api/apps/${appId}/accounts/me`).reply(200, {
73-
accounts: [{ id: ACCT, name: "Acme", plan_id: null }],
74-
active_account_id: ACCT,
75-
});
76-
scope.get(`/api/apps/${appId}/accounts/${ACCT}/billing/plans`).reply(200, []);
78+
test("billing.getSubscription(accountId) uses the explicit id (no /me)", async () => {
79+
const payload = {
80+
account_id: ACCT, plan_id: null, billing_status: "none", billing_provider: null,
81+
plan: null, current_period_end: null, cancel_at_period_end: false, canceled_at: null, started_at: null,
82+
};
83+
scope.get(`/api/apps/${appId}/accounts/${ACCT}/billing/subscription`).reply(200, payload);
7784
const sub = await base44.accounts.billing.getSubscription(ACCT);
78-
expect(sub).toEqual({ account_id: ACCT, plan_id: null, billing_status: "none", plan: null });
85+
expect(sub).toEqual(payload);
7986
expect(scope.isDone()).toBe(true);
8087
});
8188

0 commit comments

Comments
 (0)