gitpod/components/gitpod-protocol/src/accounting-protocol.ts
2023-02-22 11:02:52 +01:00

254 lines
8.1 KiB
TypeScript

/**
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/
import { v4 as uuidv4 } from "uuid";
import { Plans } from "./plans";
import { User } from "./protocol";
import { oneMonthLater } from "./util/timeutil";
/*
* Subscription and acocunting data
*/
export interface AccountEntry {
uid: string;
userId: string;
/** [hours] */
amount: number;
/**
* credit: start of validity,
* session: end of (split-) session
*/
date: string;
/**
* debits (session, expiry, loss): relation to credit
*/
creditId?: string;
/**
* credit: end of validity
*/
expiryDate?: string; // exclusive
kind: AccountEntryKind;
/**
* credit: amount - accounted debits
* [hours]
*/
remainingAmount?: number;
description?: object;
}
export namespace AccountEntry {
export function create<T extends AccountEntry>(entry: Omit<T, "uid">): T {
const result = entry as T;
result.uid = uuidv4();
return result;
}
}
export type DebitAccountEntryKind = "session" | "expiry" | "loss";
export type AccountEntryKind = "credit" | DebitAccountEntryKind | "carry" | "open";
export interface Credit extends AccountEntry {
kind: "credit";
expiryDate: string;
}
export type Debit = LossDebit | ExpiryDebit | SessionDebit;
export interface LossDebit extends AccountEntry {
kind: "loss";
}
export interface ExpiryDebit extends AccountEntry {
kind: "expiry";
creditId: undefined;
}
export interface SessionDebit extends AccountEntry {
kind: DebitAccountEntryKind;
creditId: string;
}
export type AccountEntryDescription = SessionDescription | CreditDescription;
export interface CreditDescription {
subscriptionId: string;
planId: string;
}
export namespace CreditDescription {
export function is(obj: any): obj is CreditDescription {
return !!obj && obj.hasOwnProperty("subscriptionId") && obj.hasOwnProperty("planId");
}
}
export interface SessionDescription {
contextTitle: string;
contextUrl: string;
workspaceId: string;
workspaceInstanceId: string;
}
export namespace SessionDescription {
export function is(obj: any): obj is SessionDescription {
return (
!!obj &&
obj.hasOwnProperty("contextTitle") &&
obj.hasOwnProperty("contextUrl") &&
obj.hasOwnProperty("workspaceId") &&
obj.hasOwnProperty("workspaceInstanceId")
);
}
}
/**
* - The earliest subscription may start with User.creationDate
* - There may be multiple Gitpod subscriptions for a user at any given time
* - The dates form an interval of the form: [startDate, endDate)
* - Subscriptions that directly map to a Chargebee plan have their paymentReference set and MAY carry additional paymentData (UserPaidSubscription)
* - Subscriptions that are assigned to a user through a Team Subscription carry a teamSubscriptionSlotId (AssignedTeamSubscription)
*/
export interface Subscription {
uid: string;
userId: string;
startDate: string; // inclusive
/** When the subscription will end (must be >= cancellationDate!) */
endDate?: string; // exclusive
/** When the subscription was cancelled */
cancellationDate?: string; // exclusive
/** Number of granted hours */
amount: number;
/** Number of granted hours for the first month: If this is set, use this value for the first month */
firstMonthAmount?: number;
planId?: string;
paymentReference?: string;
paymentData?: PaymentData;
teamSubscriptionSlotId?: string;
teamMembershipId?: string;
/** marks the subscription as deleted */
deleted?: boolean;
}
export interface SubscriptionAndUser extends Subscription {
user: User;
}
export interface PaymentData {
/** Marks the date as of which the _switch_ is effective. */
downgradeDate?: string;
/** Determines the new plan the dowgrade is targeted against (optional for backwards compatibility) */
newPlan?: string;
}
export interface UserPaidSubscription extends Subscription {
paymentReference: string;
paymentData?: PaymentData;
}
export namespace UserPaidSubscription {
export function is(data: any): data is UserPaidSubscription {
return !!data && data.hasOwnProperty("paymentReference");
}
}
export interface AssignedTeamSubscription extends Subscription {
teamSubscriptionSlotId: string;
}
export namespace AssignedTeamSubscription {
export function is(data: any): data is AssignedTeamSubscription {
return !!data && data.hasOwnProperty("teamSubscriptionSlotId");
}
}
export interface AssignedTeamSubscription2 extends Subscription {
teamMembershipId: string;
}
export namespace AssignedTeamSubscription2 {
export function is(data: any): data is AssignedTeamSubscription2 {
return typeof data === "object" && data.hasOwnProperty("teamMembershipId");
}
}
export namespace Subscription {
export function create(newSubscription: Omit<Subscription, "uid">) {
const subscription = newSubscription as Subscription;
subscription.uid = uuidv4();
return subscription;
}
export function cancelSubscription(s: Subscription, cancellationDate: string, endDate?: string) {
s.endDate = endDate || cancellationDate;
s.cancellationDate = cancellationDate;
}
export function isSame(s1: Subscription | undefined, s2: Subscription | undefined): boolean {
return (
!!s1 &&
!!s2 &&
s1.userId === s2.userId &&
s1.planId === s2.planId &&
s1.startDate === s2.startDate &&
s1.endDate === s2.endDate &&
s1.amount === s2.amount &&
s1.cancellationDate === s2.cancellationDate &&
s1.deleted === s2.deleted &&
((s1.paymentData === undefined && s2.paymentData === undefined) ||
(!!s1.paymentData &&
!!s2.paymentData &&
s1.paymentData.downgradeDate === s2.paymentData.downgradeDate &&
s1.paymentData.newPlan === s2.paymentData.newPlan))
);
}
export function isActive(s: Subscription, date: string): boolean {
return s.startDate <= date && (s.endDate === undefined || date < s.endDate);
}
export function isCancelled(s: Subscription, date: string): boolean {
return (!!s.cancellationDate && s.cancellationDate < date) || (!!s.endDate && s.endDate < date); // This edge case is meant to handle bad data: If for whatever reason cancellationDate has not been set: treat endDate as such
}
export function isDowngraded(s: Subscription) {
return s.paymentData && s.paymentData.downgradeDate;
}
export function calculateCurrentPeriod(startDate: string, now: Date) {
let nextStartDate = startDate;
do {
startDate = nextStartDate;
nextStartDate = oneMonthLater(startDate, new Date(startDate).getDate());
} while (nextStartDate < now.toISOString());
return { startDate, endDate: nextStartDate };
}
export function isOldTeamSubscription(s: Subscription): boolean {
return Plans.isTeamPlan(s.planId) && !s.teamMembershipId;
}
export function isTeamSubscription2(s: Subscription): boolean {
return Plans.isTeamPlan(s.planId) && !!s.teamMembershipId;
}
}
export type MaybeSubscription = Subscription | undefined;
export interface Period {
startDate: string; // inclusive
endDate: string; // exclusive
}
export type MaybePeriod = Period | undefined;
export type AccountEntryFixedPeriod = Omit<AccountEntry, "uid"> & { expiryDate: string };
export interface AccountStatement extends Period {
userId: string;
/**
* The subscriptions that have not been cancelled yet at the end of the period
*/
subscriptions: Subscription[];
credits: Credit[];
debits: Debit[];
/** Remaining valid hours (accumulated from credits) */
remainingHours: RemainingHours;
}
export type RemainingHours = number | "unlimited";
export interface CreditAlert {
userId: string;
remainingUsageHours: number;
}