Skip to content

Commit

Permalink
Implement realm server /_user endpoint for credits balance (#1756)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Buck Doyle <[email protected]>
  • Loading branch information
FadhlanR and backspace authored Nov 11, 2024
1 parent e6f9585 commit 953143e
Show file tree
Hide file tree
Showing 17 changed files with 1,346 additions and 711 deletions.
63 changes: 62 additions & 1 deletion packages/billing/billing-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,24 @@ export async function getPlanByStripeId(
} as Plan;
}

export async function getPlan(dbAdapter: DBAdapter, id: string): Promise<Plan> {
let results = await query(dbAdapter, [
`SELECT * FROM plans WHERE id = `,
param(id),
]);

if (results.length !== 1) {
throw new Error(`No plan found with id: ${id}`);
}

return {
id: results[0].id,
name: results[0].name,
monthlyPrice: results[0].monthly_price,
creditsIncluded: results[0].credits_included,
} as Plan;
}

export async function updateUserStripeCustomerId(
dbAdapter: DBAdapter,
userId: string,
Expand Down Expand Up @@ -140,6 +158,26 @@ export async function getUserByStripeId(
} as User;
}

export async function getUserByMatrixUserId(
dbAdapter: DBAdapter,
matrixUserId: string,
): Promise<User | null> {
let results = await query(dbAdapter, [
`SELECT * FROM users WHERE matrix_user_id = `,
param(matrixUserId),
]);

if (results.length !== 1) {
return null;
}

return {
id: results[0].id,
matrixUserId: results[0].matrix_user_id,
stripeCustomerId: results[0].stripe_customer_id,
} as User;
}

export async function insertSubscriptionCycle(
dbAdapter: DBAdapter,
subscriptionCycle: {
Expand Down Expand Up @@ -283,7 +321,7 @@ export async function sumUpCreditsLedger(

let results = await query(dbAdapter, ledgerQuery);

return parseInt(results[0].sum as string);
return results[0].sum != null ? parseInt(results[0].sum as string) : 0;
}

export async function getCurrentActiveSubscription(
Expand Down Expand Up @@ -315,6 +353,29 @@ export async function getCurrentActiveSubscription(
} as Subscription;
}

export async function getMostRecentSubscription(
dbAdapter: DBAdapter,
userId: string,
) {
let results = await query(dbAdapter, [
`SELECT * FROM subscriptions WHERE user_id = `,
param(userId),
`ORDER BY started_at DESC`,
]);
if (results.length === 0) {
return null;
}

return {
id: results[0].id,
userId: results[0].user_id,
planId: results[0].plan_id,
startedAt: results[0].started_at,
status: results[0].status,
stripeSubscriptionId: results[0].stripe_subscription_id,
} as Subscription;
}

export async function getMostRecentSubscriptionCycle(
dbAdapter: DBAdapter,
subscriptionId: string,
Expand Down
61 changes: 40 additions & 21 deletions packages/host/app/components/operator-mode/profile-info-popover.gts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Component from '@glimmer/component';

import { trackedFunction } from 'ember-resources/util/function';

import { BoxelButton } from '@cardstack/boxel-ui/components';
import { BoxelButton, LoadingIndicator } from '@cardstack/boxel-ui/components';
import { cn } from '@cardstack/boxel-ui/helpers';
import { IconHexagon } from '@cardstack/boxel-ui/icons';

Expand Down Expand Up @@ -83,6 +83,7 @@ export default class ProfileInfoPopover extends Component<ProfileInfoPopoverSign
gap: var(--boxel-sp-4xs);
--icon-color: var(--boxel-teal);
--boxel-loading-indicator-size: var(--boxel-icon-xs);
}
.info-group .value.out-of-credit {
--icon-color: #ff0000;
Expand Down Expand Up @@ -139,14 +140,18 @@ export default class ProfileInfoPopover extends Component<ProfileInfoPopoverSign
<div class='credit-info' data-test-credit-info>
<div class='info-group'>
<span class='label'>Membership Tier</span>
<span
class='value'
data-test-membership-tier
>{{this.plan.name}}</span>
<span class='value' data-test-membership-tier>
{{#if this.isLoading}}
<LoadingIndicator />
{{else}}
{{this.plan}}
{{/if}}
</span>
</div>
<BoxelButton
@kind='secondary-light'
@size='small'
@disabled={{this.isLoading}}
data-test-upgrade-plan-button
{{on 'click' @toggleProfileSettings}}
>Upgrade Plan</BoxelButton>
Expand All @@ -155,16 +160,26 @@ export default class ProfileInfoPopover extends Component<ProfileInfoPopoverSign
<span
class={{cn 'value' out-of-credit=this.isOutOfPlanCreditAllowance}}
data-test-monthly-credit
><IconHexagon width='16px' height='16px' />
{{this.monthlyCreditText}}</span>
>
{{#if this.isLoading}}
<LoadingIndicator />
{{else}}
<IconHexagon width='16px' height='16px' />
{{this.monthlyCreditText}}
{{/if}}
</span>
</div>
<div class='info-group additional-credit'>
<span class='label'>Additional Credit</span>
<span
class={{cn 'value' out-of-credit=this.isOutOfCredit}}
data-test-additional-credit
><IconHexagon width='16px' height='16px' />
{{this.extraCreditsAvailableInBalance}}</span>
>{{#if this.isLoading}}
<LoadingIndicator />
{{else}}
<IconHexagon width='16px' height='16px' />
{{this.extraCreditsAvailableInBalance}}
{{/if}}</span>
</div>
<div
class={{cn 'buy-more-credits' out-of-credit=this.isOutOfCredit}}
Expand All @@ -173,6 +188,7 @@ export default class ProfileInfoPopover extends Component<ProfileInfoPopoverSign
<BoxelButton
@kind={{if this.isOutOfCredit 'primary' 'secondary-light'}}
@size={{if this.isOutOfCredit 'base' 'small'}}
@disabled={{this.isLoading}}
{{on 'click' @toggleProfileSettings}}
>Buy more credits</BoxelButton>
</div>
Expand All @@ -192,6 +208,10 @@ export default class ProfileInfoPopover extends Component<ProfileInfoPopoverSign
return await this.realmServer.fetchCreditInfo();
});

private get isLoading() {
return this.fetchCreditInfo.isLoading;
}

private get plan() {
return this.fetchCreditInfo.value?.plan;
}
Expand All @@ -200,35 +220,34 @@ export default class ProfileInfoPopover extends Component<ProfileInfoPopoverSign
return this.fetchCreditInfo.value?.creditsIncludedInPlanAllowance;
}

private get creditsUsedInPlanAllowance() {
return this.fetchCreditInfo.value?.creditsUsedInPlanAllowance;
private get creditsAvailableInPlanAllowance() {
return this.fetchCreditInfo.value?.creditsAvailableInPlanAllowance;
}

private get extraCreditsAvailableInBalance() {
return this.fetchCreditInfo.value?.extraCreditsAvailableInBalance;
}

private get monthlyCreditText() {
return this.creditsUsedInPlanAllowance != undefined &&
this.creditsIncludedInPlanAllowance != undefined
? `${
this.creditsIncludedInPlanAllowance - this.creditsUsedInPlanAllowance
} of ${this.creditsIncludedInPlanAllowance} left`
: '';
return this.creditsAvailableInPlanAllowance != null &&
this.creditsIncludedInPlanAllowance != null
? `${this.creditsAvailableInPlanAllowance} of ${this.creditsIncludedInPlanAllowance} left`
: null;
}

private get isOutOfCredit() {
return (
this.isOutOfPlanCreditAllowance &&
this.extraCreditsAvailableInBalance == 0
(this.extraCreditsAvailableInBalance == null ||
this.extraCreditsAvailableInBalance == 0)
);
}

private get isOutOfPlanCreditAllowance() {
return (
this.creditsUsedInPlanAllowance &&
this.creditsIncludedInPlanAllowance &&
this.creditsUsedInPlanAllowance >= this.creditsIncludedInPlanAllowance
this.creditsAvailableInPlanAllowance == null ||
this.creditsIncludedInPlanAllowance == null ||
this.creditsAvailableInPlanAllowance <= 0
);
}
}
Expand Down
48 changes: 30 additions & 18 deletions packages/host/app/services/realm-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,10 @@ interface AvailableRealm {
}

interface CreditInfo {
plan: {
name: string;
monthlyPrice: number;
};
creditsIncludedInPlanAllowance: number;
creditsUsedInPlanAllowance: number;
extraCreditsAvailableInBalance: number;
plan: string | null;
creditsAvailableInPlanAllowance: number | null;
creditsIncludedInPlanAllowance: number | null;
extraCreditsAvailableInBalance: number | null;
}

export default class RealmServerService extends Service {
Expand Down Expand Up @@ -177,25 +174,40 @@ export default class RealmServerService extends Service {
}

async fetchCreditInfo(): Promise<CreditInfo> {
let response = await this.network.authedFetch(`${this.url.origin}/_user`);
if (!this.client) {
throw new Error(`Cannot fetch credit info without matrix client`);
}
await this.login();
if (this.auth.type !== 'logged-in') {
throw new Error('Could not login to realm server');
}

let response = await this.network.fetch(`${this.url.origin}/_user`, {
headers: {
Accept: SupportedMimeType.JSONAPI,
'Content-Type': 'application/json',
Authorization: `Bearer ${this.token}`,
},
});
if (response.status !== 200) {
throw new Error(
`Failed to fetch user for realm server ${this.url.origin}: ${response.status}`,
);
}
let json = await response.json();
let plan = json.included.find((i: { type: string }) => i.type === 'plan');
let creditsUsedInPlanAllowance =
json.data.attributes.creditsUsedInPlanAllowance;
let plan =
json.included?.find((i: { type: string }) => i.type === 'plan')
?.attributes?.name ?? null;
let creditsAvailableInPlanAllowance =
json.data?.attributes?.creditsAvailableInPlanAllowance ?? null;
let creditsIncludedInPlanAllowance =
json.data?.attributes?.creditsIncludedInPlanAllowance ?? null;
let extraCreditsAvailableInBalance =
json.data.attributes.extraCreditsAvailableInBalance;
json.data?.attributes?.extraCreditsAvailableInBalance ?? null;
return {
plan: {
name: plan.attributes.name,
monthlyPrice: plan.attributes.monthlyPrice,
},
creditsIncludedInPlanAllowance: plan.attributes.creditsIncluded,
creditsUsedInPlanAllowance,
plan,
creditsAvailableInPlanAllowance,
creditsIncludedInPlanAllowance,
extraCreditsAvailableInBalance,
};
}
Expand Down
Loading

0 comments on commit 953143e

Please sign in to comment.