/** * 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 { 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(entry: Omit): 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) { 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 type MaybeSubscription = Subscription | undefined; export interface Period { startDate: string; // inclusive endDate: string; // exclusive } export type MaybePeriod = Period | undefined; export type AccountEntryFixedPeriod = Omit & { 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; }