Skip to content
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

[server] Fix UBP free tier edge case + tests #14557

Merged
merged 1 commit into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 22 additions & 82 deletions components/server/ee/src/billing/billing-mode.spec.db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,95 +40,25 @@ import Stripe from "stripe";
import { Config } from "../../../src/config";
import { BillingModes, BillingModesImpl } from "./billing-mode";
import * as deepEqualInAnyOrder from "deep-equal-in-any-order";
import {
UsageServiceDefinition,
UsageServiceClient,
GetCostCenterResponse,
CostCenter_BillingStrategy,
GetBalanceResponse,
SetCostCenterResponse,
ReconcileUsageResponse,
ListUsageRequest_Ordering,
ListUsageResponse,
ResetUsageResponse,
ResetUsageRequest,
} from "@gitpod/usage-api/lib/usage/v1/usage.pb";
import { CallOptions } from "nice-grpc-common";
import { CostCenter_BillingStrategy } from "@gitpod/usage-api/lib/usage/v1/usage.pb";
import { UsageService } from "../../../src/user/usage-service";
chai.use(deepEqualInAnyOrder);
const expect = chai.expect;

type StripeSubscription = Pick<Stripe.Subscription, "id"> & { customer: string };
class UsageServiceClientMock implements UsageServiceClient {
class UsageServiceMock implements UsageService {
constructor(protected readonly subscription?: StripeSubscription) {}

async getCostCenter(
request: { attributionId?: string | undefined },
options?: CallOptions | undefined,
): Promise<GetCostCenterResponse> {
if (!request.attributionId) {
return { costCenter: undefined };
}
async getCurrentBalance(attributionId: AttributionId): Promise<{ usedCredits: number; usageLimit: number }> {
throw new Error("Mock: not implemented");
}

async getCurrentBillingStategy(attributionId: AttributionId): Promise<CostCenter_BillingStrategy | undefined> {
let billingStrategy = CostCenter_BillingStrategy.BILLING_STRATEGY_OTHER;
if (this.subscription !== undefined) {
billingStrategy = CostCenter_BillingStrategy.BILLING_STRATEGY_STRIPE;
}

return {
costCenter: {
attributionId: request.attributionId,
billingStrategy,
nextBillingTime: new Date(), // does not matter here.
spendingLimit: 1234,
},
};
}

async getBalance(
request: { attributionId?: string | undefined },
options?: CallOptions | undefined,
): Promise<GetBalanceResponse> {
throw new Error("Mock: not implemented");
}

async setCostCenter(
request: {
costCenter?:
| {
attributionId?: string | undefined;
spendingLimit?: number | undefined;
billingStrategy?: CostCenter_BillingStrategy | undefined;
nextBillingTime?: Date | undefined;
}
| undefined;
},
options?: CallOptions | undefined,
): Promise<SetCostCenterResponse> {
throw new Error("Mock: not implemented");
}

async reconcileUsage(
request: { from?: Date | undefined; to?: Date | undefined },
options?: CallOptions | undefined,
): Promise<ReconcileUsageResponse> {
throw new Error("Mock: not implemented");
}

async listUsage(
request: {
attributionId?: string | undefined;
from?: Date | undefined;
to?: Date | undefined;
order?: ListUsageRequest_Ordering | undefined;
pagination?: { perPage?: number | undefined; page?: number | undefined } | undefined;
},
options?: CallOptions | undefined,
): Promise<ListUsageResponse> {
throw new Error("Mock: not implemented");
}

async resetUsage(request: ResetUsageRequest, options?: CallOptions | undefined): Promise<ResetUsageResponse> {
throw new Error("Mock: not implemented");
return billingStrategy;
}
}

Expand Down Expand Up @@ -366,6 +296,18 @@ class BillingModeSpec {
mode: "usage-based",
},
},
{
name: "user: UBP free tier, chargebee paid personal (inactive)",
subject: user(),
config: {
enablePayment: true,
usageBasedPricingEnabled: true,
subscriptions: [subscription(Plans.PERSONAL_EUR, cancellationDate, cancellationDate)],
},
expectation: {
mode: "usage-based",
},
},
{
name: "user: stripe paid, chargebee paid personal (inactive) + team seat (inactive)",
subject: user(),
Expand Down Expand Up @@ -451,6 +393,7 @@ class BillingModeSpec {
expectation: {
mode: "chargebee",
paid: true,
teamNames: ["team-123"],
},
},
{
Expand Down Expand Up @@ -479,7 +422,6 @@ class BillingModeSpec {
expectation: {
mode: "chargebee",
canUpgradeToUBB: true,
teamNames: ["team-123"],
},
},
{
Expand Down Expand Up @@ -558,9 +500,7 @@ class BillingModeSpec {
bind(SubscriptionService).toSelf().inSingletonScope();
bind(BillingModes).to(BillingModesImpl).inSingletonScope();

bind(UsageServiceDefinition.name).toConstantValue(
new UsageServiceClientMock(test.config.stripeSubscription),
);
bind(UsageService).toConstantValue(new UsageServiceMock(test.config.stripeSubscription));
bind(ConfigCatClientFactory).toConstantValue(
() =>
new ConfigCatClientMock({
Expand Down
4 changes: 2 additions & 2 deletions components/server/ee/src/billing/billing-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export class BillingModesImpl implements BillingModes {
await this.teamSubscriptionDb.findTeamSubscriptions({ userId: user.id })
).filter((ts) => TeamSubscription.isActive(ts, now.toISOString()));

let canUpgradeToUBB = false;
let canUpgradeToUBB: boolean | undefined = undefined;
if (cbPersonalSubscriptions.length > 0) {
if (cbPersonalSubscriptions.every((s) => Subscription.isCancelled(s, now.toISOString()))) {
// The user has one or more paid subscriptions, but all of them have already been cancelled
Expand Down Expand Up @@ -168,7 +168,7 @@ export class BillingModesImpl implements BillingModes {
// UBB is greedy: once a user has at least a paid team membership, they should benefit from it!
return usageBased();
}
if (hasCbPaidTeamMembership || hasCbPaidTeamSeat || canUpgradeToUBB) {
if (hasCbPaidTeamMembership || hasCbPaidTeamSeat) {
// Q: How to test the free-tier, then? A: Make sure you have no CB paid seats anymore
// For that we lists all Team Subscriptions/Team Memberships that are "blocking" you, and display them in the UI somewhere.
const result: BillingMode = { mode: "chargebee", canUpgradeToUBB }; // UBB is enabled, but no seat nor subscription yet.
Expand Down