CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/util/db-schema/purchases.ts
Views: 687
1
/*
2
Purchases
3
4
NOTES:
5
6
7
- cost is by definition how much the thing costs the customer, e.g., -10 means a credit of $10.
8
- amount is by definition the negative of cost.
9
10
We typically *show* user the amount, but we do absolutely all internal accounting
11
and storage with cost. Why? Because I wrote all the code and tests that way, and it was
12
too late to change t use amount internally. That's the only reason.
13
*/
14
15
import { PurchaseInfo } from "@cocalc/util/licenses/purchase/types";
16
import * as computeServers from "./compute-servers";
17
import { CREATED_BY, ID } from "./crm";
18
import { SCHEMA as schema } from "./index";
19
import { LanguageServiceCore } from "./llm-utils";
20
import type { CourseInfo } from "./projects";
21
import { Table } from "./types";
22
23
export type Reason =
24
| "duplicate"
25
| "fraudulent"
26
| "requested_by_customer"
27
| "other";
28
29
// The general categories of services we offer. These must
30
// be at most 127 characters, and users can set an individual
31
// monthly quota on each one in purchase-quotas.
32
// The service names for openai are of the form "openai-[model name]"
33
34
export type ComputeService =
35
| "credit"
36
| "refund"
37
| "project-upgrade"
38
| "compute-server"
39
| "compute-server-network-usage"
40
| "compute-server-storage"
41
| "license"
42
| "voucher"
43
| "edit-license";
44
45
export type Service = LanguageServiceCore | ComputeService;
46
47
export interface LLMDescription {
48
type: LanguageServiceCore;
49
prompt_tokens: number;
50
completion_tokens: number;
51
amount?: number; // appears in purchses/close.ts
52
last_updated?: number; // also in purchases/close.ts, a timestamp (Date.valueOf())
53
}
54
55
export interface ProjectUpgrade {
56
type: "project-upgrade";
57
project_id: string;
58
start: number; // ms since epoch
59
stop?: number; // ms since epoch
60
quota: {
61
cost: number; // dollars per hour
62
cores?: number;
63
memory?: number;
64
network?: number;
65
mintime?: number;
66
cpu_shares?: number;
67
disk_quota?: number;
68
member_host?: number;
69
always_running?: number;
70
memory_request?: number;
71
};
72
}
73
74
export interface ComputeServer {
75
type: "compute-server";
76
state: computeServers.State;
77
compute_server_id: number;
78
configuration: computeServers.Configuration;
79
}
80
81
export interface ComputeServerNetworkUsage {
82
type: "compute-server-network-usage";
83
cost?: number;
84
compute_server_id: number;
85
amount: number; // amount of data used in GB
86
last_updated?: number;
87
}
88
89
// describes how the charges for GCS for a period time break down
90
// into components. Of course there is much more detail than this
91
// in billing data, e.g., exactly how much of each kind of network.
92
// But at least this breakdown is probably helpful as a start to
93
// better understand charges.
94
export interface GoogleCloudStorageBucketCost {
95
network: number;
96
storage: number;
97
classA: number;
98
classB: number;
99
autoclass: number;
100
other: number;
101
}
102
103
// This is used to support cloud file systems; however, it's generic
104
// enough it could be for any bucket storage.
105
export interface ComputeServerStorage {
106
type: "compute-server-storage";
107
cloud: "google-cloud"; // only google-cloud currently supported
108
bucket: string; // SUPER important -- the name of the bucket
109
cloud_filesystem_id: number;
110
// once the purchase is done and finalized, we put the final cost here:
111
cost?: number;
112
// this is a breakdown of the cost, which is cloud-specific
113
cost_breakdown?: GoogleCloudStorageBucketCost;
114
// filesystem the bucket is used for.
115
// an estimated cost for the given period of time -- we try to make this
116
// based on collected metrics, and it may or may not be close to the
117
// actual cost.
118
estimated_cost?: { min: number; max: number };
119
// when the estimated cost was set.
120
last_updated?: number;
121
}
122
123
export interface License {
124
type: "license";
125
info: PurchaseInfo;
126
license_id: string;
127
item?; // item in shopping cart
128
course?: CourseInfo;
129
}
130
131
export interface Voucher {
132
type: "voucher";
133
quantity: number;
134
cost: number; // per voucher
135
title: string;
136
voucher_id: number;
137
}
138
139
export interface EditLicense {
140
type: "edit-license";
141
license_id: string;
142
origInfo: PurchaseInfo;
143
modifiedInfo: PurchaseInfo;
144
note: string; // not explaining the cost
145
}
146
147
export interface Credit {
148
type: "credit";
149
voucher_code?: string; // if credit is the result of redeeming a voucher code
150
}
151
152
export interface Refund {
153
type: "refund";
154
purchase_id: number; // id of entry in purchases table of the credit that this is refunding back from
155
refund_id?: string; // stripe Refund object id for the refund
156
reason: Reason;
157
notes: string;
158
}
159
160
export type Description =
161
| LLMDescription
162
| ProjectUpgrade
163
| ComputeServer
164
| ComputeServerNetworkUsage
165
| ComputeServerStorage
166
| Credit
167
| Refund
168
| License
169
| Voucher
170
| EditLicense;
171
172
// max number of purchases a user can get in one query.
173
export const MAX_API_LIMIT = 500;
174
175
// maximum for any single purchase ever. Any frontend
176
// ui or api should use this constant to define a check.
177
export const MAX_COST = 99999;
178
179
export function getAmountStyle(amount: number) {
180
return {
181
fontWeight: "bold",
182
color: amount >= 0 ? "#126bc5" : "#414042",
183
} as const;
184
}
185
186
export interface Purchase {
187
id: number;
188
time: Date;
189
account_id: string;
190
cost?: number;
191
cost_per_hour?: number; // for purchases with a specific rate (e.g., an upgrade)
192
cost_so_far?: number; // for purchases that accumulate (e.g., data transfer)
193
period_start?: Date;
194
period_end?: Date;
195
pending?: boolean;
196
service: Service;
197
description: Description;
198
invoice_id?: string;
199
project_id?: string;
200
tag?: string;
201
day_statement_id?: number;
202
month_statement_id?: number;
203
notes?: string;
204
}
205
206
Table({
207
name: "purchases",
208
fields: {
209
id: ID,
210
time: { type: "timestamp", desc: "When this purchase was logged." },
211
account_id: CREATED_BY,
212
cost: {
213
title: "Cost ($)",
214
desc: "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.",
215
type: "number",
216
pg_type: "real",
217
},
218
pending: {
219
type: "boolean",
220
desc: "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.).",
221
},
222
cost_per_hour: {
223
title: "Cost Per Hour",
224
desc: "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.",
225
type: "number",
226
pg_type: "real",
227
},
228
cost_so_far: {
229
title: "Cost So Far",
230
desc: "The cost so far in US dollars for a metered purchase that accumulates. This is used, e.g., for data transfer charges.",
231
type: "number",
232
pg_type: "real",
233
},
234
period_start: {
235
title: "Period Start",
236
type: "timestamp",
237
desc: "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)",
238
},
239
period_end: {
240
title: "Period End",
241
type: "timestamp",
242
desc: "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.",
243
},
244
invoice_id: {
245
title: "Invoice Id",
246
desc: "The id of the stripe invoice that was sent that included this item. Legacy: if paid via a payment intent, this will be the id of a payment intent instead, and it will start with pi_.",
247
type: "string",
248
},
249
project_id: {
250
title: "Project Id",
251
desc: "The id of the project where this purchase happened. Not all purchases necessarily involve a project.",
252
type: "uuid",
253
render: { type: "project_link" },
254
},
255
service: {
256
title: "Service Category",
257
desc: "The service being charged for, e.g., openai-gpt-4, etc.",
258
type: "string",
259
pg_type: "varchar(127)",
260
},
261
description: {
262
title: "Description",
263
desc: "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.",
264
type: "map",
265
pg_type: "jsonb",
266
},
267
tag: {
268
type: "string",
269
pg_type: "varchar(127)",
270
desc: "Optional string that can be useful in analytics to understand where and how this purchase was made.",
271
},
272
day_statement_id: {
273
type: "integer",
274
desc: "id of the daily statement that includes this purchase",
275
},
276
month_statement_id: {
277
type: "integer",
278
desc: "id of the monthly statement that includes this purchase",
279
},
280
notes: {
281
type: "string",
282
desc: "Non-private notes about this purchase. The user CAN see but not edit them.",
283
render: {
284
type: "markdown",
285
editable: true,
286
},
287
},
288
},
289
rules: {
290
desc: "Purchase Log",
291
primary_key: "id",
292
pg_indexes: ["account_id", "time", "project_id"],
293
user_query: {
294
get: {
295
pg_where: [{ "account_id = $::UUID": "account_id" }],
296
fields: {
297
id: null,
298
time: null,
299
period_start: null,
300
period_end: null,
301
account_id: null,
302
cost: null,
303
pending: null,
304
cost_per_hour: null,
305
cost_so_far: null,
306
service: null,
307
description: null,
308
invoice_id: null,
309
project_id: null,
310
tag: null,
311
notes: null,
312
},
313
},
314
},
315
},
316
});
317
318
Table({
319
name: "crm_purchases",
320
rules: {
321
virtual: "purchases",
322
primary_key: "id",
323
user_query: {
324
get: {
325
pg_where: [],
326
admin: true,
327
fields: {
328
id: null,
329
time: null,
330
period_start: null,
331
period_end: null,
332
account_id: null,
333
cost: null,
334
pending: null,
335
cost_per_hour: null,
336
cost_so_far: null,
337
service: null,
338
description: null,
339
invoice_id: null,
340
project_id: null,
341
tag: null,
342
notes: null,
343
},
344
},
345
set: {
346
admin: true,
347
fields: {
348
id: true,
349
tag: true,
350
notes: true,
351
},
352
},
353
},
354
},
355
fields: schema.purchases.fields,
356
});
357
358