Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Path: blob/master/src/packages/util/db-schema/purchases.ts
Views: 1116
/*1Purchases23NOTES:45- cost is by definition how much the thing costs the customer, e.g., -10 means a credit of $10.6- amount is by definition the negative of cost.78We typically *show* user the amount, but we do absolutely all internal accounting9and storage with cost. Why? Because I wrote all the code and tests that way, and it was10too late to change t use amount internally. That's the only reason.11*/1213import { PurchaseInfo } from "@cocalc/util/licenses/purchase/types";14import * as computeServers from "./compute-servers";15import { CREATED_BY, ID } from "./crm";16import { SCHEMA as schema } from "./index";17import { LanguageServiceCore } from "./llm-utils";18import type { CourseInfo } from "./projects";19import { Table } from "./types";20import type { LineItem } from "@cocalc/util/stripe/types";2122// various specific payment purposes2324// buying items in the shopping cart25export const SHOPPING_CART_CHECKOUT = "shopping-cart-checkout";2627// automatic balance top up28export const AUTO_CREDIT = "auto-credit";2930// paying for a class31export const STUDENT_PAY = "student-pay";3233// month-to-month payment for active subscription34export const SUBSCRIPTION_RENEWAL = "subscription-renewal";3536// resuming a canceled subscription that has expired:37export const RESUME_SUBSCRIPTION = "resume-subscription";3839// for paying a statement the purpose is `statement-${statement_id}`40// (Maybe we should be usig metadata for this though?)41424344export type Reason =45| "duplicate"46| "fraudulent"47| "requested_by_customer"48| "other";4950// The general categories of services we offer. These must51// be at most 127 characters, and users can set an individual52// monthly quota on each one in purchase-quotas.53// The service names for openai are of the form "openai-[model name]"5455// todo: why is this "compute"? makes no sense.56export type ComputeService =57| "credit"58| "auto-credit"59| "refund"60| "project-upgrade"61| "compute-server"62| "compute-server-network-usage"63| "compute-server-storage"64| "license"65| "voucher"66| "edit-license";6768export type Service = LanguageServiceCore | ComputeService;6970export interface LLMDescription {71type: LanguageServiceCore;72prompt_tokens: number;73completion_tokens: number;74amount?: number; // appears in purchses/close.ts75last_updated?: number; // also in purchases/close.ts, a timestamp (Date.valueOf())76}7778export interface ProjectUpgrade {79type: "project-upgrade";80project_id: string;81start: number; // ms since epoch82stop?: number; // ms since epoch83quota: {84cost: number; // dollars per hour85cores?: number;86memory?: number;87network?: number;88mintime?: number;89cpu_shares?: number;90disk_quota?: number;91member_host?: number;92always_running?: number;93memory_request?: number;94};95}9697export interface ComputeServer {98type: "compute-server";99state: computeServers.State;100compute_server_id: number;101configuration: computeServers.Configuration;102}103104export interface ComputeServerNetworkUsage {105type: "compute-server-network-usage";106cost?: number;107compute_server_id: number;108amount: number; // amount of data used in GB109last_updated?: number;110}111112// describes how the charges for GCS for a period time break down113// into components. Of course there is much more detail than this114// in billing data, e.g., exactly how much of each kind of network.115// But at least this breakdown is probably helpful as a start to116// better understand charges.117export interface GoogleCloudStorageBucketCost {118network: number;119storage: number;120classA: number;121classB: number;122autoclass: number;123other: number;124}125126// This is used to support cloud file systems; however, it's generic127// enough it could be for any bucket storage.128export interface ComputeServerStorage {129type: "compute-server-storage";130cloud: "google-cloud"; // only google-cloud currently supported131bucket: string; // SUPER important -- the name of the bucket132cloud_filesystem_id: number;133// once the purchase is done and finalized, we put the final cost here:134cost?: number;135// this is a breakdown of the cost, which is cloud-specific136cost_breakdown?: GoogleCloudStorageBucketCost;137// filesystem the bucket is used for.138// an estimated cost for the given period of time -- we try to make this139// based on collected metrics, and it may or may not be close to the140// actual cost.141estimated_cost?: { min: number; max: number };142// when the estimated cost was set.143last_updated?: number;144}145146export interface License {147type: "license";148info: PurchaseInfo;149license_id: string;150item?; // item in shopping cart151course?: CourseInfo;152// if this license was bought using credit that was added, then record the id of that transaction here.153// it's mainly "psychological", but often money is added specifically to buy a license, and it is good154// to keep track of that flow.155credit_id?: number;156}157158export interface Voucher {159type: "voucher";160quantity: number;161cost: number; // per voucher162title: string;163voucher_id: number;164credit_id?: number;165}166167export interface EditLicense {168type: "edit-license";169license_id: string;170origInfo: PurchaseInfo;171modifiedInfo: PurchaseInfo;172note: string; // not explaining the cost173}174175export interface Credit {176type: "credit";177voucher_code?: string; // if credit is the result of redeeming a voucher code178line_items?: LineItem[];179description?: string;180purpose?: string;181}182183export interface AutoCredit {184type: "auto-credit";185line_items?: LineItem[];186description?: string;187}188189export interface Refund {190type: "refund";191purchase_id: number; // id of entry in purchases table of the credit that this is refunding back from192refund_id?: string; // stripe Refund object id for the refund193reason: Reason;194notes: string;195}196197export type Description =198| LLMDescription199| ProjectUpgrade200| ComputeServer201| ComputeServerNetworkUsage202| ComputeServerStorage203| Credit204| Refund205| License206| Voucher207| EditLicense;208209// max number of purchases a user can get in one query.210export const MAX_API_LIMIT = 500;211212// maximum for any single purchase ever. Any frontend213// ui or api should use this constant to define a check.214export const MAX_COST = 99999;215216export function getAmountStyle(amount: number) {217return {218fontWeight: "bold",219color: amount >= 0 ? "#126bc5" : "#414042",220whiteSpace: "nowrap",221} as const;222}223224export interface Purchase {225id: number;226time: Date;227account_id: string;228cost?: number;229cost_per_hour?: number; // for purchases with a specific rate (e.g., an upgrade)230cost_so_far?: number; // for purchases that accumulate (e.g., data transfer)231period_start?: Date;232period_end?: Date;233pending?: boolean;234service: Service;235description: Description;236invoice_id?: string;237payment_intent_id?: string;238project_id?: string;239tag?: string;240day_statement_id?: number;241month_statement_id?: number;242notes?: string;243}244245Table({246name: "purchases",247fields: {248id: ID,249time: { type: "timestamp", desc: "When this purchase was logged." },250account_id: CREATED_BY,251cost: {252title: "Cost ($)",253desc: "The cost in US dollars. Not set if the purchase isn't finished, e.g., when upgrading a project this is only set when project stops or purchase is finalized. This takes precedence over the cost_per_hour times the length of the period when active.",254type: "number",255pg_type: "real",256},257pending: {258type: "boolean",259desc: "**DEPRECATED** -- not used anywhere; do NOT use! If true, then this transaction is considered pending, which means that for a few days it doesn't count against the user's quotas for the purposes of deciding whether or not a purchase is allowed. This is needed so we can charge a user for their subscriptions, then collect the money from them, without all of the running pay-as-you-go project upgrades suddenly breaking (etc.).",260},261cost_per_hour: {262title: "Cost Per Hour",263desc: "The cost in US dollars per hour. This is used to compute the cost so far for metered purchases when the cost field isn't set yet. The cost so far is the number of hours since period_start times the cost_per_hour. The description field may also contain redundant cost per hour information, but this cost_per_hour field is the definitive source of truth. Once the cost field is set, this cost_per_hour is just useful for display purposes.",264type: "number",265pg_type: "real",266},267cost_so_far: {268title: "Cost So Far",269desc: "The cost so far in US dollars for a metered purchase that accumulates. This is used, e.g., for data transfer charges.",270type: "number",271pg_type: "real",272},273period_start: {274title: "Period Start",275type: "timestamp",276desc: "When the purchase starts being active (e.g., a 1 week license starts and ends on specific days; for metered purchases it is when the purchased started charging)",277},278period_end: {279title: "Period End",280type: "timestamp",281desc: "When the purchase stops being active. For metered purchases, it's when the purchase finished being charged, in which case the cost field should be equal to the length of the period times the cost_per_hour.",282},283invoice_id: {284title: "Stripe Invoice Id or Payment Intent Id",285desc: "The id of the stripe invoice that was sent that included this item. If paid via a payment intent, this will be the id of a payment intent instead, and it will start with pi_.",286type: "string",287},288project_id: {289title: "Project Id",290desc: "The id of the project where this purchase happened. Not all purchases necessarily involve a project.",291type: "uuid",292render: { type: "project_link" },293},294service: {295title: "Service Category",296desc: "The service being charged for, e.g., openai-gpt-4, etc.",297type: "string",298pg_type: "varchar(127)",299},300description: {301title: "Description",302desc: "An object that provides additional details about what was purchased and can have an arbitrary format. This is mainly used to provide extra insight when rendering this purchase for users, and its content should not be relied on for queries.",303type: "map",304pg_type: "jsonb",305},306tag: {307type: "string",308pg_type: "varchar(127)",309desc: "Optional string that can be useful in analytics to understand where and how this purchase was made.",310},311day_statement_id: {312type: "integer",313desc: "id of the daily statement that includes this purchase",314},315month_statement_id: {316type: "integer",317desc: "id of the monthly statement that includes this purchase",318},319notes: {320type: "string",321desc: "Non-private notes about this purchase. The user CAN see but not edit them.",322render: {323type: "markdown",324editable: true,325},326},327},328rules: {329desc: "Purchase Log",330primary_key: "id",331pg_indexes: ["account_id", "time", "project_id"],332pg_unique_indexes: [333// having two entries with same invoice_id or id would be very bad, since that334// would mean user got money twice for one payment!335// Existence of this unique index is assumed in src/packages/server/purchases/stripe/process-payment-intents.ts336"invoice_id",337],338user_query: {339get: {340pg_where: [{ "account_id = $::UUID": "account_id" }],341fields: {342id: null,343time: null,344period_start: null,345period_end: null,346account_id: null,347cost: null,348pending: null,349cost_per_hour: null,350cost_so_far: null,351service: null,352description: null,353invoice_id: null,354project_id: null,355tag: null,356notes: null,357},358},359},360},361});362363Table({364name: "crm_purchases",365rules: {366virtual: "purchases",367primary_key: "id",368user_query: {369get: {370pg_where: [],371admin: true,372fields: {373id: null,374time: null,375period_start: null,376period_end: null,377account_id: null,378cost: null,379pending: null,380cost_per_hour: null,381cost_so_far: null,382service: null,383description: null,384invoice_id: null,385project_id: null,386tag: null,387notes: null,388},389},390set: {391admin: true,392fields: {393id: true,394tag: true,395notes: true,396},397},398},399},400fields: schema.purchases.fields,401});402403404