gitpod/components/gitpod-protocol/src/accounting-protocol.ts
2021-03-04 10:50:27 +01:00

236 lines
7.4 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 * as uuidv4 from 'uuid/v4';
import { Without } from "./util/without";
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: Without<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;
private: boolean;
}
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')
&& obj.hasOwnProperty('private');
}
}
/**
* - 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;
/** 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 namespace Subscription {
export function create(newSubscription: Without<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 isDowngraded(s: Subscription) {
return s.paymentData && s.paymentData.downgradeDate;
};
export function isCancelled(s: Subscription) {
return !!s.cancellationDate;
};
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 = Without<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
}