@copilotz/billing 0.0.0-stage → 1.0.0-rc.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +48 -2
- package/contract.d.ts +7 -0
- package/contract.js +7 -0
- package/credits/actions/check.d.ts +5 -0
- package/credits/actions/check.js +97 -0
- package/credits/actions/manage.d.ts +9 -0
- package/credits/actions/manage.js +266 -0
- package/credits/actions/state.d.ts +2 -0
- package/credits/actions/state.js +172 -0
- package/credits/collections/credit.d.ts +110 -0
- package/credits/collections/credit.js +31 -0
- package/credits/collections/payment.d.ts +139 -0
- package/credits/collections/payment.js +32 -0
- package/credits/collections/plan.d.ts +428 -0
- package/credits/collections/plan.js +97 -0
- package/credits/collections/room.d.ts +85 -0
- package/credits/collections/room.js +24 -0
- package/credits/contract.d.ts +19 -0
- package/credits/contract.js +12 -0
- package/credits/mod.d.ts +38 -0
- package/credits/mod.js +72 -0
- package/credits/shared/apply.d.ts +66 -0
- package/credits/shared/apply.js +106 -0
- package/credits/shared/settings.d.ts +70 -0
- package/credits/shared/settings.js +28 -0
- package/credits/shared/standing.d.ts +71 -0
- package/credits/shared/standing.js +235 -0
- package/credits/shared/walk.d.ts +50 -0
- package/credits/shared/walk.js +127 -0
- package/credits/views/web/add.d.ts +25 -0
- package/credits/views/web/add.js +92 -0
- package/credits/views/web/budget.d.ts +16 -0
- package/credits/views/web/budget.js +62 -0
- package/credits/views/web/composer.d.ts +13 -0
- package/credits/views/web/composer.js +65 -0
- package/credits/views/web/history.d.ts +12 -0
- package/credits/views/web/history.js +55 -0
- package/credits/views/web/numbers.d.ts +256 -0
- package/credits/views/web/numbers.js +291 -0
- package/credits/views/web/org.d.ts +31 -0
- package/credits/views/web/org.js +238 -0
- package/credits/views/web/plan.d.ts +28 -0
- package/credits/views/web/plan.js +84 -0
- package/credits/views/web/staff.d.ts +20 -0
- package/credits/views/web/staff.js +265 -0
- package/credits/views/web/style.d.ts +9 -0
- package/credits/views/web/style.js +190 -0
- package/credits/views/web/types.d.ts +17 -0
- package/credits/views/web/types.js +32 -0
- package/credits/web.d.ts +12 -0
- package/credits/web.js +33 -0
- package/mod.d.ts +26 -0
- package/mod.js +25 -0
- package/package.json +61 -4
- package/stripe/actions/checkout.d.ts +4 -0
- package/stripe/actions/checkout.js +188 -0
- package/stripe/actions/fact.d.ts +1 -0
- package/stripe/actions/fact.js +39 -0
- package/stripe/mod.d.ts +30 -0
- package/stripe/mod.js +52 -0
- package/stripe/routes/webhook.d.ts +16 -0
- package/stripe/routes/webhook.js +56 -0
- package/stripe/shared/api.d.ts +14 -0
- package/stripe/shared/api.js +42 -0
- package/stripe/shared/events.d.ts +42 -0
- package/stripe/shared/events.js +226 -0
- package/stripe/shared/settings.d.ts +39 -0
- package/stripe/shared/settings.js +10 -0
- package/web.d.ts +13 -0
- package/web.js +13 -0
package/credits/mod.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credits: what an org has to spend, and the check that stops it at its limit.
|
|
3
|
+
* Built on the usage ledger, whose prices it counts.
|
|
4
|
+
*
|
|
5
|
+
* - **The org pays.** Its plan (`billingPlan`), credits (`credit`) and payments
|
|
6
|
+
* (`payment`) are its own rows. A plan's terms are data, so a contract is a
|
|
7
|
+
* record, not code. Nothing asks a payment provider what an org has.
|
|
8
|
+
* - **Balance:** credits less what the ledger charged since its plan began,
|
|
9
|
+
* walked day by day (shared/walk.ts), so usage from before never counts, and
|
|
10
|
+
* credits lapse on their day. Own keys and subscriptions are charged nothing.
|
|
11
|
+
* - **The check** (`billing.check`, named by the `spending` resource, which
|
|
12
|
+
* whatever spends reads): `room` is the smaller of balance and budget left,
|
|
13
|
+
* in the owner's memory, so asking costs no database read, and each use takes
|
|
14
|
+
* its price off. Memory can only say yes: before anyone's stopped, it's worked
|
|
15
|
+
* out again from what's kept.
|
|
16
|
+
* - **By hand now, by provider later:** staff change plans, give credits, waive
|
|
17
|
+
* and record payments (actions/manage.ts); a provider's webhook calls
|
|
18
|
+
* `billing.apply` with its own event id, which lands in the same place.
|
|
19
|
+
* - **`enforce`** off: nothing starts and nobody's stopped; staff can prepare.
|
|
20
|
+
* - **Who a plan is for** (`for`: personal or team orgs): an org of the other
|
|
21
|
+
* kind isn't offered it and can't be moved to it, by anyone. A plan can name
|
|
22
|
+
* the model tier its agents use while the org hasn't chosen one (`models`,
|
|
23
|
+
* read through `billing.tier`, the `planTier` resource).
|
|
24
|
+
*
|
|
25
|
+
* plugins: [models, usage, credits, …]
|
|
26
|
+
* resources: {
|
|
27
|
+
* billing: {
|
|
28
|
+
* plans: { team: { title: "Team", credits: { perSeat: 10 }, for: "team", models: "advanced" } },
|
|
29
|
+
* },
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
import { type Plugin } from "copilotz";
|
|
33
|
+
export type { Credit } from "./collections/credit.js";
|
|
34
|
+
export type { Payment } from "./collections/payment.js";
|
|
35
|
+
export type { Plan } from "./collections/plan.js";
|
|
36
|
+
export type { CreditsSettings, PlanTerms } from "./shared/settings.js";
|
|
37
|
+
export declare const credits: Plugin;
|
|
38
|
+
export default credits;
|
package/credits/mod.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credits: what an org has to spend, and the check that stops it at its limit.
|
|
3
|
+
* Built on the usage ledger, whose prices it counts.
|
|
4
|
+
*
|
|
5
|
+
* - **The org pays.** Its plan (`billingPlan`), credits (`credit`) and payments
|
|
6
|
+
* (`payment`) are its own rows. A plan's terms are data, so a contract is a
|
|
7
|
+
* record, not code. Nothing asks a payment provider what an org has.
|
|
8
|
+
* - **Balance:** credits less what the ledger charged since its plan began,
|
|
9
|
+
* walked day by day (shared/walk.ts), so usage from before never counts, and
|
|
10
|
+
* credits lapse on their day. Own keys and subscriptions are charged nothing.
|
|
11
|
+
* - **The check** (`billing.check`, named by the `spending` resource, which
|
|
12
|
+
* whatever spends reads): `room` is the smaller of balance and budget left,
|
|
13
|
+
* in the owner's memory, so asking costs no database read, and each use takes
|
|
14
|
+
* its price off. Memory can only say yes: before anyone's stopped, it's worked
|
|
15
|
+
* out again from what's kept.
|
|
16
|
+
* - **By hand now, by provider later:** staff change plans, give credits, waive
|
|
17
|
+
* and record payments (actions/manage.ts); a provider's webhook calls
|
|
18
|
+
* `billing.apply` with its own event id, which lands in the same place.
|
|
19
|
+
* - **`enforce`** off: nothing starts and nobody's stopped; staff can prepare.
|
|
20
|
+
* - **Who a plan is for** (`for`: personal or team orgs): an org of the other
|
|
21
|
+
* kind isn't offered it and can't be moved to it, by anyone. A plan can name
|
|
22
|
+
* the model tier its agents use while the org hasn't chosen one (`models`,
|
|
23
|
+
* read through `billing.tier`, the `planTier` resource).
|
|
24
|
+
*
|
|
25
|
+
* plugins: [models, usage, credits, …]
|
|
26
|
+
* resources: {
|
|
27
|
+
* billing: {
|
|
28
|
+
* plans: { team: { title: "Team", credits: { perSeat: 10 }, for: "team", models: "advanced" } },
|
|
29
|
+
* },
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
import { definePlugin } from "copilotz";
|
|
33
|
+
import { checkAction, mayAction, priceListAction, spentAction, tierAction, } from "./actions/check.js";
|
|
34
|
+
import { applyAction, budgetAction, grantAction, payAction, planAction, redeemAction, setPlanAction, settleAction, waiveAction, } from "./actions/manage.js";
|
|
35
|
+
import { noticeAction, stateAction } from "./actions/state.js";
|
|
36
|
+
import { credit } from "./collections/credit.js";
|
|
37
|
+
import { payment } from "./collections/payment.js";
|
|
38
|
+
import { billingPlan } from "./collections/plan.js";
|
|
39
|
+
import { billingRoom } from "./collections/room.js";
|
|
40
|
+
import { DEFAULTS } from "./shared/settings.js";
|
|
41
|
+
export const credits = definePlugin({
|
|
42
|
+
name: "credits",
|
|
43
|
+
requires: ["usage"],
|
|
44
|
+
collections: { billingPlan, credit, payment, billingRoom },
|
|
45
|
+
actions: {
|
|
46
|
+
"billing.check": checkAction,
|
|
47
|
+
"billing.may": mayAction,
|
|
48
|
+
"billing.spent": spentAction,
|
|
49
|
+
"billing.priceList": priceListAction,
|
|
50
|
+
"billing.tier": tierAction,
|
|
51
|
+
"billing.state": stateAction,
|
|
52
|
+
"billing.notice": noticeAction,
|
|
53
|
+
"billing.setPlan": setPlanAction,
|
|
54
|
+
"billing.grant": grantAction,
|
|
55
|
+
"billing.waive": waiveAction,
|
|
56
|
+
"billing.pay": payAction,
|
|
57
|
+
"billing.budget": budgetAction,
|
|
58
|
+
"billing.redeem": redeemAction,
|
|
59
|
+
"billing.apply": applyAction,
|
|
60
|
+
"billing.plan": planAction,
|
|
61
|
+
"billing.settle": settleAction,
|
|
62
|
+
},
|
|
63
|
+
schedules: { "billing.settle": { cron: "20 0 * * *", call: "billing.settle", input: {} } },
|
|
64
|
+
resources: {
|
|
65
|
+
billing: DEFAULTS,
|
|
66
|
+
usage: { priceList: "billing.priceList", spent: "billing.spent" },
|
|
67
|
+
spending: { check: "billing.check", may: "billing.may" },
|
|
68
|
+
// Which model tier the org's plan gives its agents, for the tiers plugin.
|
|
69
|
+
planTier: { action: "billing.tier" },
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
export default credits;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What comes from outside, whoever collected it: a payment, a plan changed, a
|
|
3
|
+
* period paid for. A person acting by hand and a payment provider's webhook both
|
|
4
|
+
* land here, so the rest never knows the difference.
|
|
5
|
+
*
|
|
6
|
+
* News comes late and out of order, and an org can have had more than one
|
|
7
|
+
* subscription, so news of a subscription (`of`) changes the org only while that
|
|
8
|
+
* one is its own, what's older than what it knows changes nothing (`at`), and
|
|
9
|
+
* one that ended (`ended`) never comes back.
|
|
10
|
+
*/
|
|
11
|
+
import { type Context } from "copilotz";
|
|
12
|
+
import type { Plan } from "../collections/plan.js";
|
|
13
|
+
export type PaymentFact = {
|
|
14
|
+
channel: string;
|
|
15
|
+
ref: string;
|
|
16
|
+
seller?: string;
|
|
17
|
+
amount: number;
|
|
18
|
+
currency: string;
|
|
19
|
+
rate?: number;
|
|
20
|
+
fee?: number;
|
|
21
|
+
purpose: "seats" | "credits" | "contract" | "refund";
|
|
22
|
+
credits?: number;
|
|
23
|
+
note?: string;
|
|
24
|
+
by?: string;
|
|
25
|
+
sandbox?: boolean;
|
|
26
|
+
at?: string;
|
|
27
|
+
};
|
|
28
|
+
/** Keeps a payment, and the credits it bought (taken back, for a refund). Once per `channel:ref`. */
|
|
29
|
+
export declare function pay(ctx: Context, fact: PaymentFact): Promise<void>;
|
|
30
|
+
export type PlanFact = {
|
|
31
|
+
/** One of the app's plans, or a contract's own name with its `terms`. */
|
|
32
|
+
name?: string;
|
|
33
|
+
terms?: Plan["terms"];
|
|
34
|
+
seats?: number | null;
|
|
35
|
+
channel?: string;
|
|
36
|
+
seller?: string;
|
|
37
|
+
customer?: string;
|
|
38
|
+
/** Null: it has none now. */
|
|
39
|
+
subscription?: string | null;
|
|
40
|
+
/** Null: nothing to say of its payments, as on a contract. */
|
|
41
|
+
status?: string | null;
|
|
42
|
+
/** Where its periods start (now, when its plan changes). */
|
|
43
|
+
anchor?: string;
|
|
44
|
+
/** The subscription this is news of, and when it happened. */
|
|
45
|
+
of?: string;
|
|
46
|
+
at?: string;
|
|
47
|
+
/** A subscription that has ended. */
|
|
48
|
+
ended?: string;
|
|
49
|
+
sandbox?: boolean;
|
|
50
|
+
};
|
|
51
|
+
/** Changes the org's plan. What it had stays until it lapses. */
|
|
52
|
+
export declare function changePlan(ctx: Context, fact: PlanFact): Promise<void>;
|
|
53
|
+
/** A period someone paid for. */
|
|
54
|
+
export type PeriodFact = {
|
|
55
|
+
plan: string;
|
|
56
|
+
/** `YYYY-MM-DD`: its first day, and the first day its credits no longer count. */
|
|
57
|
+
start: string;
|
|
58
|
+
expires: string;
|
|
59
|
+
seats: number;
|
|
60
|
+
sandbox?: boolean;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Its credits, as many as it paid for. For a plan a provider collects for, this
|
|
64
|
+
* is when they come: the money first, so a card that fails gets none.
|
|
65
|
+
*/
|
|
66
|
+
export declare function grantPeriod(ctx: Context, fact: PeriodFact): Promise<void>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What comes from outside, whoever collected it: a payment, a plan changed, a
|
|
3
|
+
* period paid for. A person acting by hand and a payment provider's webhook both
|
|
4
|
+
* land here, so the rest never knows the difference.
|
|
5
|
+
*
|
|
6
|
+
* News comes late and out of order, and an org can have had more than one
|
|
7
|
+
* subscription, so news of a subscription (`of`) changes the org only while that
|
|
8
|
+
* one is its own, what's older than what it knows changes nothing (`at`), and
|
|
9
|
+
* one that ended (`ended`) never comes back.
|
|
10
|
+
*/
|
|
11
|
+
import { invalid } from "copilotz";
|
|
12
|
+
import { orgKind, settingsOf, unfit } from "./settings.js";
|
|
13
|
+
import { ensurePlan, forget, termsFor } from "./standing.js";
|
|
14
|
+
const round = (usd) => Math.round(usd * 1e8) / 1e8;
|
|
15
|
+
/** Keeps a payment, and the credits it bought (taken back, for a refund). Once per `channel:ref`. */
|
|
16
|
+
export async function pay(ctx, fact) {
|
|
17
|
+
const at = fact.at ?? new Date().toISOString();
|
|
18
|
+
const id = `${fact.channel}:${fact.ref}`;
|
|
19
|
+
const system = ctx.as();
|
|
20
|
+
await system.db.collection("payment").insertOne({ ...fact, _id: id, at });
|
|
21
|
+
if (fact.credits) {
|
|
22
|
+
const back = fact.credits < 0;
|
|
23
|
+
await system.db.collection("credit").insertOne({
|
|
24
|
+
_id: `purchase:${id}`,
|
|
25
|
+
kind: back ? "adjustment" : "purchase",
|
|
26
|
+
...(back ? { takes: "purchase" } : {}),
|
|
27
|
+
amount: round(fact.credits),
|
|
28
|
+
at,
|
|
29
|
+
...(fact.note ? { note: fact.note } : {}),
|
|
30
|
+
...(fact.by ? { by: fact.by } : {}),
|
|
31
|
+
...(fact.sandbox ? { sandbox: true } : {}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
await forget(ctx);
|
|
35
|
+
}
|
|
36
|
+
/** Changes the org's plan. What it had stays until it lapses. */
|
|
37
|
+
export async function changePlan(ctx, fact) {
|
|
38
|
+
const now = new Date().toISOString();
|
|
39
|
+
const plan = await ensurePlan(ctx, now);
|
|
40
|
+
const plans = ctx.as().db.collection("billingPlan");
|
|
41
|
+
if (fact.ended) {
|
|
42
|
+
await plans.updateOne({ _id: "plan" }, { $addToSet: { ended: fact.ended } });
|
|
43
|
+
}
|
|
44
|
+
const ended = new Set([...(plan.ended ?? []), ...(fact.ended ? [fact.ended] : [])]);
|
|
45
|
+
// News of a subscription that isn't its own now, or one that's over, leaves it as it is.
|
|
46
|
+
const elsewhere = fact.of !== undefined && fact.of !== plan.subscription;
|
|
47
|
+
const back = typeof fact.subscription === "string" && ended.has(fact.subscription);
|
|
48
|
+
if (elsewhere || back) {
|
|
49
|
+
await forget(ctx);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const stale = !!fact.at && !!plan.statusAt && fact.at < plan.statusAt && !fact.ended;
|
|
53
|
+
const named = fact.name ? termsFor(ctx, fact.name) : undefined;
|
|
54
|
+
const terms = fact.terms ?? named;
|
|
55
|
+
if (fact.name && !terms)
|
|
56
|
+
throw invalid(`There's no plan "${fact.name}".`);
|
|
57
|
+
// Whoever moves it, a personal org takes no team's plan, nor a team a person's.
|
|
58
|
+
const why = terms && fact.name !== plan.name ? unfit(terms, orgKind(ctx)) : undefined;
|
|
59
|
+
if (why)
|
|
60
|
+
throw invalid(why);
|
|
61
|
+
const moved = !!fact.name && fact.name !== plan.name;
|
|
62
|
+
const unset = {
|
|
63
|
+
...(fact.seats === null ? { seats: "" } : {}),
|
|
64
|
+
...(fact.subscription === null ? { subscription: "" } : {}),
|
|
65
|
+
...(fact.status === null ? { status: "", statusAt: "" } : {}),
|
|
66
|
+
};
|
|
67
|
+
await plans.updateOne({ _id: "plan" }, {
|
|
68
|
+
$set: {
|
|
69
|
+
...(fact.name ? { name: fact.name } : {}),
|
|
70
|
+
...(terms ? { terms } : {}),
|
|
71
|
+
...(fact.channel ? { channel: fact.channel } : {}),
|
|
72
|
+
...(fact.seller ? { seller: fact.seller } : {}),
|
|
73
|
+
...(fact.customer ? { customer: fact.customer } : {}),
|
|
74
|
+
...(fact.subscription ? { subscription: fact.subscription } : {}),
|
|
75
|
+
...(fact.status && !stale
|
|
76
|
+
? { status: fact.status, ...(fact.at ? { statusAt: fact.at } : {}) }
|
|
77
|
+
: {}),
|
|
78
|
+
...(typeof fact.seats === "number" ? { seats: fact.seats } : {}),
|
|
79
|
+
...(fact.anchor || moved ? { anchor: fact.anchor ?? now } : {}),
|
|
80
|
+
...(fact.sandbox !== undefined ? { sandbox: fact.sandbox } : {}),
|
|
81
|
+
},
|
|
82
|
+
...(Object.keys(unset).length ? { $unset: unset } : {}),
|
|
83
|
+
});
|
|
84
|
+
await forget(ctx);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Its credits, as many as it paid for. For a plan a provider collects for, this
|
|
88
|
+
* is when they come: the money first, so a card that fails gets none.
|
|
89
|
+
*/
|
|
90
|
+
export async function grantPeriod(ctx, fact) {
|
|
91
|
+
const terms = settingsOf(ctx).plans[fact.plan];
|
|
92
|
+
const credits = terms?.credits;
|
|
93
|
+
const amount = (credits?.monthly ?? 0) + (credits?.perSeat ?? 0) * fact.seats;
|
|
94
|
+
if (!amount)
|
|
95
|
+
return;
|
|
96
|
+
await ctx.as().db.collection("credit").insertOne({
|
|
97
|
+
_id: `plan:${fact.plan}:${fact.start}`,
|
|
98
|
+
kind: "plan",
|
|
99
|
+
amount,
|
|
100
|
+
at: new Date().toISOString(),
|
|
101
|
+
expires: fact.expires,
|
|
102
|
+
note: terms.title,
|
|
103
|
+
...(fact.sandbox ? { sandbox: true } : {}),
|
|
104
|
+
});
|
|
105
|
+
await forget(ctx);
|
|
106
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** Credits' settings: the `billing` resource, which an app patches. */
|
|
2
|
+
import type { Resources } from "copilotz";
|
|
3
|
+
/** A plan: the terms an org on it gets. */
|
|
4
|
+
export type PlanTerms = {
|
|
5
|
+
title: string;
|
|
6
|
+
seat?: {
|
|
7
|
+
price: number;
|
|
8
|
+
currency: string;
|
|
9
|
+
};
|
|
10
|
+
credits?: {
|
|
11
|
+
once?: number;
|
|
12
|
+
monthly?: number;
|
|
13
|
+
perSeat?: number;
|
|
14
|
+
};
|
|
15
|
+
prices?: string;
|
|
16
|
+
/** The month's limit it starts with, per seat; none: the app's `budget`. */
|
|
17
|
+
budget?: {
|
|
18
|
+
perSeat: number;
|
|
19
|
+
};
|
|
20
|
+
/** Which orgs may be on it: a person's own, or a team's; none: any. */
|
|
21
|
+
for?: "personal" | "team";
|
|
22
|
+
/** The model tier its orgs' agents use while the org hasn't chosen one (`billing.tier`). */
|
|
23
|
+
models?: string;
|
|
24
|
+
};
|
|
25
|
+
/** What kind of org this is (`personal` or `team`), where the app serves many. */
|
|
26
|
+
export declare const orgKind: (ctx: {
|
|
27
|
+
resources: Resources;
|
|
28
|
+
}) => string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Why an org of this kind can't be on a plan, in words, or nothing when it
|
|
31
|
+
* can: a plan for personal orgs isn't a team's, and the other way round. An
|
|
32
|
+
* org whose kind isn't known (an app that serves one) can be on any.
|
|
33
|
+
*/
|
|
34
|
+
export declare function unfit(terms: Pick<PlanTerms, "title" | "for">, kind?: string): string | undefined;
|
|
35
|
+
export type CreditsSettings = {
|
|
36
|
+
/**
|
|
37
|
+
* Whether limits apply. Off, nothing starts: no plans, no trials, no
|
|
38
|
+
* balances, and nobody's stopped, though staff can prepare orgs.
|
|
39
|
+
*/
|
|
40
|
+
enforce: boolean;
|
|
41
|
+
/** The plans there are, and the one an org starts on. */
|
|
42
|
+
plans: Record<string, PlanTerms>;
|
|
43
|
+
start: string;
|
|
44
|
+
/** The month's limit an org starts with (USD); none: no limit. */
|
|
45
|
+
budget?: number;
|
|
46
|
+
/** The share of a budget or of its credits used when it's said to be running low. */
|
|
47
|
+
warnAt: number;
|
|
48
|
+
/** What's never stopped, by a call's `purpose`: the app's own upkeep, which follows turns. */
|
|
49
|
+
free: string[];
|
|
50
|
+
/** Amounts offered for buying credits (USD), and the least and most. */
|
|
51
|
+
topUps: number[];
|
|
52
|
+
topUpMin: number;
|
|
53
|
+
topUpMax: number;
|
|
54
|
+
/**
|
|
55
|
+
* How an org pays, when something lets it (the Actions `stripe` names):
|
|
56
|
+
* checking out, managing what it pays, and which plans can be sold to it
|
|
57
|
+
* now (`sellable`: `{}` → plan names; none: every plan with a seat price).
|
|
58
|
+
*/
|
|
59
|
+
pay?: {
|
|
60
|
+
checkout: string;
|
|
61
|
+
portal: string;
|
|
62
|
+
sellable?: string;
|
|
63
|
+
};
|
|
64
|
+
/** Real money: what came from a test mode stops counting. */
|
|
65
|
+
live?: boolean;
|
|
66
|
+
};
|
|
67
|
+
export declare const DEFAULTS: CreditsSettings;
|
|
68
|
+
export declare const settingsOf: (ctx: {
|
|
69
|
+
resources: Resources;
|
|
70
|
+
}) => CreditsSettings;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** What kind of org this is (`personal` or `team`), where the app serves many. */
|
|
2
|
+
export const orgKind = (ctx) => ctx.resources.org?.kind;
|
|
3
|
+
/**
|
|
4
|
+
* Why an org of this kind can't be on a plan, in words, or nothing when it
|
|
5
|
+
* can: a plan for personal orgs isn't a team's, and the other way round. An
|
|
6
|
+
* org whose kind isn't known (an app that serves one) can be on any.
|
|
7
|
+
*/
|
|
8
|
+
export function unfit(terms, kind) {
|
|
9
|
+
if (!terms.for || !kind || terms.for === kind)
|
|
10
|
+
return undefined;
|
|
11
|
+
return terms.for === "team"
|
|
12
|
+
? `${terms.title} is for team orgs, and this is a personal one. Make a team org to move to it.`
|
|
13
|
+
: `${terms.title} is for personal orgs, and this one is a team's.`;
|
|
14
|
+
}
|
|
15
|
+
export const DEFAULTS = {
|
|
16
|
+
enforce: true,
|
|
17
|
+
plans: { free: { title: "Free", credits: { once: 5 } } },
|
|
18
|
+
start: "free",
|
|
19
|
+
warnAt: 0.8,
|
|
20
|
+
free: ["history", "memory"],
|
|
21
|
+
topUps: [10, 25, 50, 100],
|
|
22
|
+
topUpMin: 5,
|
|
23
|
+
topUpMax: 2000,
|
|
24
|
+
};
|
|
25
|
+
export const settingsOf = (ctx) => ({
|
|
26
|
+
...DEFAULTS,
|
|
27
|
+
...ctx.resources.billing,
|
|
28
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an org stands: its plan, credits and spending worked out from what's
|
|
3
|
+
* kept, what's due written, and `room`, the short form every check reads.
|
|
4
|
+
*
|
|
5
|
+
* `settle` writes what's come due, and only it does: the trial, this period's
|
|
6
|
+
* credits, a waiver that ended. Each has an `_id` of its own, so running it
|
|
7
|
+
* twice changes nothing.
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from "copilotz";
|
|
10
|
+
import type { Credit } from "../collections/credit.js";
|
|
11
|
+
import type { Plan } from "../collections/plan.js";
|
|
12
|
+
import type { Room } from "../collections/room.js";
|
|
13
|
+
import { type PlanTerms } from "./settings.js";
|
|
14
|
+
import { type Walked } from "./walk.js";
|
|
15
|
+
/** What stands for "no limit" in `room`: taking prices off it never gets near zero. */
|
|
16
|
+
export declare const NO_LIMIT = 1000000000000000;
|
|
17
|
+
export declare const rooms: (ctx: Context) => import("copilotz").Collection<Room>;
|
|
18
|
+
export declare const planOf: (ctx: Context) => Promise<import("copilotz").WithId<Plan> | null>;
|
|
19
|
+
/**
|
|
20
|
+
* A plan's terms for this org. A trial is one a person: the personal org they
|
|
21
|
+
* get once. A team's starts without it, and doesn't gain it by falling back.
|
|
22
|
+
*/
|
|
23
|
+
export declare function termsFor(ctx: Context, name: string): PlanTerms | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* The org's plan, started on the app's first one the first time it's needed.
|
|
26
|
+
* While limits are off, its clock doesn't start (`counting`).
|
|
27
|
+
*/
|
|
28
|
+
export declare function ensurePlan(ctx: Context, now: string): Promise<Plan>;
|
|
29
|
+
/** People who count as seats: the org's, guests apart. An app without org roles has one. */
|
|
30
|
+
export declare const seatsOf: (ctx: Context, plan: Plan) => Promise<number>;
|
|
31
|
+
/**
|
|
32
|
+
* A usage waiver's end: what it let through is given back, so it doesn't count
|
|
33
|
+
* afterwards. Not what was charged, but what the balance lost by it: credits that
|
|
34
|
+
* would have lapsed anyway aren't given back for good.
|
|
35
|
+
*/
|
|
36
|
+
export declare function endWaiver(ctx: Context, plan: Plan, at: string): Promise<void>;
|
|
37
|
+
/** Writes what's come due. */
|
|
38
|
+
export declare function settle(ctx: Context, now: string): Promise<Plan>;
|
|
39
|
+
export type Standing = {
|
|
40
|
+
plan: Plan | null;
|
|
41
|
+
walked: Walked;
|
|
42
|
+
history: Credit[];
|
|
43
|
+
/** Spent this month, and the month's limit (none: no limit). */
|
|
44
|
+
month: {
|
|
45
|
+
start: string;
|
|
46
|
+
end: string;
|
|
47
|
+
spent: number;
|
|
48
|
+
limit?: number;
|
|
49
|
+
};
|
|
50
|
+
/** This period, on a plan with periods. */
|
|
51
|
+
period?: {
|
|
52
|
+
start: string;
|
|
53
|
+
end: string;
|
|
54
|
+
};
|
|
55
|
+
waivedUsage: boolean;
|
|
56
|
+
};
|
|
57
|
+
/** Where the org stands now, from what's kept. */
|
|
58
|
+
export declare function standingOf(ctx: Context, now: string): Promise<Standing>;
|
|
59
|
+
/** Settles, works out `room` again, and keeps it. */
|
|
60
|
+
export declare function refresh(ctx: Context, now: string): Promise<Room>;
|
|
61
|
+
/** Something changed what's left: the next check works it out again. */
|
|
62
|
+
export declare const forget: (ctx: Context) => Promise<import("copilotz").DeleteResult>;
|
|
63
|
+
export type Verdict = {
|
|
64
|
+
ok: true;
|
|
65
|
+
} | {
|
|
66
|
+
ok: false;
|
|
67
|
+
why: "credits" | "budget";
|
|
68
|
+
message: string;
|
|
69
|
+
};
|
|
70
|
+
/** What a check answers for `room`, in the words people see when it stops them. */
|
|
71
|
+
export declare function verdictOf(room: Room): Verdict;
|