Contact Us!
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. Commercial Alternative to JupyterHub.

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