@thejob/schema 2.1.8 → 2.2.0
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/CLAUDE.md +17 -0
- package/dist/index.cjs +2174 -722
- package/dist/index.d.cts +1992 -534
- package/dist/index.d.ts +1992 -534
- package/dist/index.js +2075 -696
- package/package.json +1 -1
- package/src/billing/billing.constant.ts +179 -0
- package/src/billing/billing.schema.ts +280 -0
- package/src/common/facet.schema.ts +342 -0
- package/src/feedback/feedback-filter.schema.ts +39 -0
- package/src/geo/city-filter.schema.ts +41 -0
- package/src/geo/city.schema.ts +104 -0
- package/src/geo/country-filter.schema.ts +47 -0
- package/src/geo/country.schema.ts +148 -0
- package/src/geo/geo-sync.schema.ts +116 -0
- package/src/geo/geo.constant.ts +78 -0
- package/src/geo/state-filter.schema.ts +57 -0
- package/src/geo/state.schema.ts +101 -0
- package/src/group/group-filter.schema.ts +81 -0
- package/src/group/group.constant.ts +15 -0
- package/src/index.ts +35 -0
- package/src/job/job-filter.schema.ts +266 -0
- package/src/language/language-filter.schema.ts +40 -0
- package/src/marketing/marketing.constant.ts +105 -30
- package/src/marketing/marketing.schema.ts +118 -403
- package/src/page/page-filter.schema.ts +67 -0
- package/src/skill/skill-filter.schema.ts +61 -0
- package/src/user/general-detail.schema.ts +7 -0
- package/src/user/push-subscription.schema.ts +86 -0
- package/src/user/user-filter.schema.ts +438 -0
- package/src/user/user-interest.schema.ts +34 -2
- package/src/user/user.constant.ts +20 -8
- package/src/user/user.schema.ts +45 -0
- package/src/user/user.types.ts +35 -0
package/package.json
CHANGED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing vocabulary (Paddle).
|
|
3
|
+
*
|
|
4
|
+
* Two conventions in this module differ from the rest of the package, both
|
|
5
|
+
* deliberately and both load-bearing. Read them before editing:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Paddle's wire values win.** `SubscriptionStatus` and `TransactionStatus`
|
|
8
|
+
* mirror Paddle verbatim, underscores included (`past_due`). The house style
|
|
9
|
+
* is lowercase-hyphen, but these values arrive in webhooks and are compared
|
|
10
|
+
* as literals; normalising them would silently stop matching live events.
|
|
11
|
+
* Never invent a member Paddle does not send.
|
|
12
|
+
*
|
|
13
|
+
* 2. **Money is in MINOR units.** Every other amount in this package (salary) is
|
|
14
|
+
* a bare integer in the currency's MAJOR unit. Paddle is the opposite. See
|
|
15
|
+
* `MoneySchema` in `billing.schema.ts`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ─── Plans ───────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/** Whether buying this plan starts a recurring subscription or is a one-time purchase. */
|
|
21
|
+
export enum PlanKind {
|
|
22
|
+
Subscription = "subscription",
|
|
23
|
+
OneOff = "one-off",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const SupportedPlanKinds = Object.values(PlanKind);
|
|
27
|
+
|
|
28
|
+
/** Whether a plan can currently be bought. Archived plans still grant to existing owners. */
|
|
29
|
+
export enum PlanStatus {
|
|
30
|
+
Draft = "draft",
|
|
31
|
+
Active = "active",
|
|
32
|
+
Archived = "archived",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const SupportedPlanStatuses = Object.values(PlanStatus);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* How a grant gives access to a feature.
|
|
39
|
+
*
|
|
40
|
+
* - `boolean` — plain on/off for as long as the plan is held.
|
|
41
|
+
* - `quota` — N uses per period, refreshed each period.
|
|
42
|
+
* - `credit` — N uses that do not expire, from a one-off purchase.
|
|
43
|
+
*
|
|
44
|
+
* Quota is spent before credit, so an included monthly allowance is used up
|
|
45
|
+
* before a purchased pack is touched.
|
|
46
|
+
*/
|
|
47
|
+
export enum GrantType {
|
|
48
|
+
Boolean = "boolean",
|
|
49
|
+
Quota = "quota",
|
|
50
|
+
Credit = "credit",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const SupportedGrantTypes = Object.values(GrantType);
|
|
54
|
+
|
|
55
|
+
/** The window a quota refreshes on, anchored to the subscription's billing date. */
|
|
56
|
+
export enum QuotaPeriod {
|
|
57
|
+
Month = "month",
|
|
58
|
+
Year = "year",
|
|
59
|
+
/** Never refreshes: N uses for the lifetime of the subscription. */
|
|
60
|
+
Lifetime = "lifetime",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const SupportedQuotaPeriods = Object.values(QuotaPeriod);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* How often a subscription plan BILLS.
|
|
67
|
+
*
|
|
68
|
+
* Deliberately NOT `QuotaPeriod`, though the two share `month` and `year`. They
|
|
69
|
+
* answer different questions: `QuotaPeriod` is how often a grant refreshes,
|
|
70
|
+
* this is how often the customer is charged, and a plan may bill yearly while
|
|
71
|
+
* refreshing its quota monthly (`employer-annual` does exactly that). Reusing
|
|
72
|
+
* `QuotaPeriod` here would also admit `lifetime`, which is not a billing
|
|
73
|
+
* interval for a subscription.
|
|
74
|
+
*
|
|
75
|
+
* Absent on one-off plans, which are not billed on an interval at all.
|
|
76
|
+
*/
|
|
77
|
+
export enum BillingPeriod {
|
|
78
|
+
Month = "month",
|
|
79
|
+
Year = "year",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const SupportedBillingPeriods = Object.values(BillingPeriod);
|
|
83
|
+
|
|
84
|
+
// ─── Paddle mirrors ──────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Paddle subscription status, verbatim.
|
|
88
|
+
*
|
|
89
|
+
* `active`, `trialing` and `past_due` all currently GRANT access: dunning is a
|
|
90
|
+
* payment problem, and cutting a paying customer off mid-retry is worse than
|
|
91
|
+
* carrying them for a few days. `paused` and `canceled` do not grant.
|
|
92
|
+
*/
|
|
93
|
+
export enum SubscriptionStatus {
|
|
94
|
+
Active = "active",
|
|
95
|
+
Trialing = "trialing",
|
|
96
|
+
PastDue = "past_due",
|
|
97
|
+
Paused = "paused",
|
|
98
|
+
Canceled = "canceled",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const SupportedSubscriptionStatuses = Object.values(SubscriptionStatus);
|
|
102
|
+
|
|
103
|
+
/** Subscription statuses that currently entitle the holder to their plan's grants. */
|
|
104
|
+
export const EntitlingSubscriptionStatuses = [
|
|
105
|
+
SubscriptionStatus.Active,
|
|
106
|
+
SubscriptionStatus.Trialing,
|
|
107
|
+
SubscriptionStatus.PastDue,
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
/** Paddle transaction status, verbatim. `completed` is where a one-off is fulfilled. */
|
|
111
|
+
export enum TransactionStatus {
|
|
112
|
+
Draft = "draft",
|
|
113
|
+
Ready = "ready",
|
|
114
|
+
Billed = "billed",
|
|
115
|
+
Paid = "paid",
|
|
116
|
+
Completed = "completed",
|
|
117
|
+
Canceled = "canceled",
|
|
118
|
+
PastDue = "past_due",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const SupportedTransactionStatuses = Object.values(TransactionStatus);
|
|
122
|
+
|
|
123
|
+
/** Which Paddle environment an identifier belongs to. Sandbox and live IDs never match. */
|
|
124
|
+
export enum PaddleEnvironment {
|
|
125
|
+
Sandbox = "sandbox",
|
|
126
|
+
Production = "production",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const SupportedPaddleEnvironments = Object.values(PaddleEnvironment);
|
|
130
|
+
|
|
131
|
+
// ─── Features ────────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Feature keys currently known to the platform.
|
|
135
|
+
*
|
|
136
|
+
* INFORMATIONAL ONLY — for autocomplete and for grepping who gates what.
|
|
137
|
+
*
|
|
138
|
+
* Do NOT constrain a stored field with `.oneOf(SupportedFeatureKeys)`. Feature
|
|
139
|
+
* keys are data owned by billing-service, and the entire point of the design is
|
|
140
|
+
* that putting a feature behind a paywall is a database change. Validating
|
|
141
|
+
* against this enum would make every new paywall a release of this package plus
|
|
142
|
+
* a redeploy of every service that holds it — exactly what the design avoids.
|
|
143
|
+
*
|
|
144
|
+
* Keys are dot-namespaced `<domain>.<capability>` and are permanent once sold:
|
|
145
|
+
* renaming one orphans the grants already stored against it.
|
|
146
|
+
*/
|
|
147
|
+
export enum FeatureKey {
|
|
148
|
+
JobPost = "job.post",
|
|
149
|
+
JobAlerts = "job.alerts",
|
|
150
|
+
PageCreate = "page.create",
|
|
151
|
+
GroupCreate = "group.create",
|
|
152
|
+
CandidateFinder = "candidate.finder",
|
|
153
|
+
SearchAdvanced = "search.advanced",
|
|
154
|
+
GetDiscovered = "get-discovered",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const SupportedFeatureKeys = Object.values(FeatureKey);
|
|
158
|
+
|
|
159
|
+
/** Why a gate refused, so the UI can tell "buy this" apart from "wait". */
|
|
160
|
+
export enum EntitlementDenialReason {
|
|
161
|
+
/** No plan grants this feature at all. */
|
|
162
|
+
NotEntitled = "not-entitled",
|
|
163
|
+
/** Entitled, but the period's quota is spent. */
|
|
164
|
+
QuotaExhausted = "quota-exhausted",
|
|
165
|
+
/** Entitled, but the credit balance is zero. */
|
|
166
|
+
NoCredits = "no-credits",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const SupportedEntitlementDenialReasons = Object.values(
|
|
170
|
+
EntitlementDenialReason,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// ─── Bounds ──────────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/** A single grant cannot hand out more than this per period. Guards a typo'd plan. */
|
|
176
|
+
export const GRANT_MAX_LIMIT = 100_000;
|
|
177
|
+
|
|
178
|
+
/** Ceiling on units consumed in one call. A gate spends one unit in the normal case. */
|
|
179
|
+
export const CONSUME_MAX_UNITS = 1_000;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { array, boolean, number, object, string } from "yup";
|
|
2
|
+
import {
|
|
3
|
+
CONSUME_MAX_UNITS,
|
|
4
|
+
GRANT_MAX_LIMIT,
|
|
5
|
+
GrantType,
|
|
6
|
+
PlanKind,
|
|
7
|
+
PlanStatus,
|
|
8
|
+
QuotaPeriod,
|
|
9
|
+
SupportedBillingPeriods,
|
|
10
|
+
SupportedEntitlementDenialReasons,
|
|
11
|
+
SupportedGrantTypes,
|
|
12
|
+
SupportedPlanKinds,
|
|
13
|
+
SupportedPlanStatuses,
|
|
14
|
+
SupportedQuotaPeriods,
|
|
15
|
+
SupportedSubscriptionStatuses,
|
|
16
|
+
SupportedTransactionStatuses,
|
|
17
|
+
} from "./billing.constant.js";
|
|
18
|
+
|
|
19
|
+
// ─── Money ───────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* An amount of money as Paddle expresses it.
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ MINOR UNITS. `999` here is $9.99, not $999. This is the OPPOSITE of every
|
|
25
|
+
* other amount in this package: `salaryRange.min`, `minSalary` and friends are
|
|
26
|
+
* bare integers in the currency's MAJOR unit. Mixing the two is a 100x error in
|
|
27
|
+
* either direction, which is why the field is named `amountMinor` rather than
|
|
28
|
+
* `amount` — a bare `amount` next to a bare `currency` would read exactly like
|
|
29
|
+
* the salary fields and be wrong.
|
|
30
|
+
*
|
|
31
|
+
* Paddle transmits this as a STRING of minor units; billing-service parses it to
|
|
32
|
+
* a number on ingest and keeps the unit. Never render it without dividing by the
|
|
33
|
+
* currency's exponent.
|
|
34
|
+
*/
|
|
35
|
+
export const MoneySchema = object({
|
|
36
|
+
amountMinor: number()
|
|
37
|
+
.integer()
|
|
38
|
+
.min(0)
|
|
39
|
+
.required()
|
|
40
|
+
.label("Amount (minor units)"),
|
|
41
|
+
/**
|
|
42
|
+
* ISO-4217, as returned by Paddle. Deliberately NOT constrained to
|
|
43
|
+
* `SupportedSalaryCurrencies`: that list is the job-salary vocabulary, and
|
|
44
|
+
* Paddle bills in currencies it does not contain. Constraining it here would
|
|
45
|
+
* reject real payments.
|
|
46
|
+
*/
|
|
47
|
+
currencyCode: string().trim().uppercase().required().label("Currency"),
|
|
48
|
+
}).label("Money");
|
|
49
|
+
|
|
50
|
+
// ─── Plans ───────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* One thing a plan gives you.
|
|
54
|
+
*
|
|
55
|
+
* `limit` applies to quota grants, `amount` to credit grants, and neither to
|
|
56
|
+
* boolean. They are optional at the schema level and enforced by `.when`, so a
|
|
57
|
+
* malformed plan is caught when an operator saves it rather than when a user's
|
|
58
|
+
* entitlements silently resolve to nothing.
|
|
59
|
+
*/
|
|
60
|
+
export const PlanGrantSchema = object({
|
|
61
|
+
/**
|
|
62
|
+
* The gated capability, e.g. `job.post`. A free string on purpose: see the
|
|
63
|
+
* note on `FeatureKey` in `billing.constant.ts`. Adding a feature to the
|
|
64
|
+
* paywall must not require releasing this package.
|
|
65
|
+
*/
|
|
66
|
+
feature: string().trim().required().min(1).max(64).label("Feature"),
|
|
67
|
+
type: string()
|
|
68
|
+
.oneOf(SupportedGrantTypes)
|
|
69
|
+
.required()
|
|
70
|
+
.default(GrantType.Boolean)
|
|
71
|
+
.label("Grant Type"),
|
|
72
|
+
/** Uses per period. Required for `quota`, meaningless otherwise. */
|
|
73
|
+
limit: number()
|
|
74
|
+
.integer()
|
|
75
|
+
.min(1)
|
|
76
|
+
.max(GRANT_MAX_LIMIT)
|
|
77
|
+
.optional()
|
|
78
|
+
.label("Limit")
|
|
79
|
+
.when("type", {
|
|
80
|
+
is: GrantType.Quota,
|
|
81
|
+
then: (s) => s.required("A quota grant needs a limit"),
|
|
82
|
+
}),
|
|
83
|
+
period: string()
|
|
84
|
+
.oneOf(SupportedQuotaPeriods)
|
|
85
|
+
.optional()
|
|
86
|
+
.default(QuotaPeriod.Month)
|
|
87
|
+
.label("Period")
|
|
88
|
+
.when("type", {
|
|
89
|
+
is: GrantType.Quota,
|
|
90
|
+
then: (s) => s.required("A quota grant needs a period"),
|
|
91
|
+
}),
|
|
92
|
+
/** Non-expiring uses granted per purchase. Required for `credit`. */
|
|
93
|
+
amount: number()
|
|
94
|
+
.integer()
|
|
95
|
+
.min(1)
|
|
96
|
+
.max(GRANT_MAX_LIMIT)
|
|
97
|
+
.optional()
|
|
98
|
+
.label("Amount")
|
|
99
|
+
.when("type", {
|
|
100
|
+
is: GrantType.Credit,
|
|
101
|
+
then: (s) => s.required("A credit grant needs an amount"),
|
|
102
|
+
}),
|
|
103
|
+
}).label("Plan Grant");
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Paddle price identifiers, split by environment.
|
|
107
|
+
*
|
|
108
|
+
* Keyed rather than flat because sandbox and live price IDs are different
|
|
109
|
+
* strings for the same product. Shipping sandbox IDs to production does not fail
|
|
110
|
+
* at checkout — it fails later, when a webhook arrives carrying a price nothing
|
|
111
|
+
* maps to, and reads as "plan not found" rather than "wrong environment".
|
|
112
|
+
*/
|
|
113
|
+
export const PaddlePriceIdsSchema = object({
|
|
114
|
+
sandbox: array().of(string().required()).optional().default([]).label("Sandbox Price IDs"),
|
|
115
|
+
live: array().of(string().required()).optional().default([]).label("Live Price IDs"),
|
|
116
|
+
}).label("Paddle Price IDs");
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* What can be bought, and what it grants.
|
|
120
|
+
*
|
|
121
|
+
* This document is the paywall. Putting a feature behind it, taking one out, or
|
|
122
|
+
* changing a quota is an edit here plus a cache bust — no service redeploys.
|
|
123
|
+
*/
|
|
124
|
+
export const PlanSchema = object({
|
|
125
|
+
/** Stable slug, e.g. `employer-pro`. The identifier everything else references. */
|
|
126
|
+
key: string()
|
|
127
|
+
.trim()
|
|
128
|
+
.lowercase()
|
|
129
|
+
.required()
|
|
130
|
+
.min(2)
|
|
131
|
+
.max(64)
|
|
132
|
+
.label("Key"),
|
|
133
|
+
name: string().required().min(1).max(120).label("Name"),
|
|
134
|
+
description: string().optional().max(500).label("Description"),
|
|
135
|
+
kind: string()
|
|
136
|
+
.oneOf(SupportedPlanKinds)
|
|
137
|
+
.required()
|
|
138
|
+
.default(PlanKind.Subscription)
|
|
139
|
+
.label("Kind"),
|
|
140
|
+
status: string()
|
|
141
|
+
.oneOf(SupportedPlanStatuses)
|
|
142
|
+
.required()
|
|
143
|
+
.default(PlanStatus.Draft)
|
|
144
|
+
.label("Status"),
|
|
145
|
+
paddlePriceIds: PaddlePriceIdsSchema.default(() =>
|
|
146
|
+
PaddlePriceIdsSchema.getDefault(),
|
|
147
|
+
),
|
|
148
|
+
grants: array()
|
|
149
|
+
.of(PlanGrantSchema.required())
|
|
150
|
+
.required()
|
|
151
|
+
.min(1, "A plan must grant at least one feature")
|
|
152
|
+
.label("Grants"),
|
|
153
|
+
/** Display price for the pricing page. Billing is always Paddle's own figure. */
|
|
154
|
+
price: MoneySchema.optional().default(undefined).label("Price"),
|
|
155
|
+
/**
|
|
156
|
+
* How often this plan bills, for the pricing page's monthly/yearly toggle and
|
|
157
|
+
* for rendering `price` with the right suffix.
|
|
158
|
+
*
|
|
159
|
+
* Display only: Paddle owns the real billing interval, and this never drives a
|
|
160
|
+
* charge. It is optional because one-off plans have no interval, and because
|
|
161
|
+
* plans seeded before this field existed must keep validating. Treat absent on
|
|
162
|
+
* a subscription as "monthly" at the use site rather than defaulting it here,
|
|
163
|
+
* so an unset value stays visible as unset.
|
|
164
|
+
*/
|
|
165
|
+
billingPeriod: string()
|
|
166
|
+
.oneOf(SupportedBillingPeriods)
|
|
167
|
+
.optional()
|
|
168
|
+
.label("Billing Period"),
|
|
169
|
+
/** Ordering on the pricing page, ascending. */
|
|
170
|
+
sortOrder: number().integer().optional().default(0).label("Sort Order"),
|
|
171
|
+
createdAt: number().optional().label("Created At"),
|
|
172
|
+
updatedAt: number().optional().label("Updated At"),
|
|
173
|
+
}).label("Plan");
|
|
174
|
+
|
|
175
|
+
// ─── Subscriptions and transactions ──────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A user's Paddle subscription, mirrored locally.
|
|
179
|
+
*
|
|
180
|
+
* Paddle is the source of truth; this is a cache kept current by webhooks and
|
|
181
|
+
* repaired by the reconciler. `lastEventAt` is what makes that safe: Paddle
|
|
182
|
+
* guarantees no ordering, so every write is conditional on this advancing, and a
|
|
183
|
+
* late `subscription.updated` cannot resurrect a canceled subscription.
|
|
184
|
+
*/
|
|
185
|
+
export const BillingSubscriptionSchema = object({
|
|
186
|
+
userId: string().required().label("User ID"),
|
|
187
|
+
paddleSubscriptionId: string().required().label("Paddle Subscription ID"),
|
|
188
|
+
paddleCustomerId: string().required().label("Paddle Customer ID"),
|
|
189
|
+
planKey: string().required().label("Plan Key"),
|
|
190
|
+
priceIds: array().of(string().required()).required().label("Price IDs"),
|
|
191
|
+
status: string()
|
|
192
|
+
.oneOf(SupportedSubscriptionStatuses)
|
|
193
|
+
.required()
|
|
194
|
+
.label("Status"),
|
|
195
|
+
/** Start of the period the quota is anchored to. */
|
|
196
|
+
currentPeriodStart: number().optional().label("Current Period Start"),
|
|
197
|
+
currentPeriodEnd: number().optional().label("Current Period End"),
|
|
198
|
+
/** Set when a cancellation is scheduled; access continues until the period ends. */
|
|
199
|
+
canceledAt: number().optional().label("Canceled At"),
|
|
200
|
+
/** `occurred_at` of the newest Paddle event applied. Guards out-of-order writes. */
|
|
201
|
+
lastEventAt: number().required().default(0).label("Last Event At"),
|
|
202
|
+
createdAt: number().optional().label("Created At"),
|
|
203
|
+
updatedAt: number().optional().label("Updated At"),
|
|
204
|
+
}).label("Billing Subscription");
|
|
205
|
+
|
|
206
|
+
/** A completed Paddle payment. One-off purchases are fulfilled from these. */
|
|
207
|
+
export const BillingTransactionSchema = object({
|
|
208
|
+
userId: string().required().label("User ID"),
|
|
209
|
+
paddleTransactionId: string().required().label("Paddle Transaction ID"),
|
|
210
|
+
paddleCustomerId: string().optional().label("Paddle Customer ID"),
|
|
211
|
+
paddleSubscriptionId: string().optional().label("Paddle Subscription ID"),
|
|
212
|
+
planKey: string().optional().label("Plan Key"),
|
|
213
|
+
priceIds: array().of(string().required()).required().label("Price IDs"),
|
|
214
|
+
status: string()
|
|
215
|
+
.oneOf(SupportedTransactionStatuses)
|
|
216
|
+
.required()
|
|
217
|
+
.label("Status"),
|
|
218
|
+
total: MoneySchema.optional().default(undefined).label("Total"),
|
|
219
|
+
billedAt: number().optional().label("Billed At"),
|
|
220
|
+
lastEventAt: number().required().default(0).label("Last Event At"),
|
|
221
|
+
createdAt: number().optional().label("Created At"),
|
|
222
|
+
updatedAt: number().optional().label("Updated At"),
|
|
223
|
+
}).label("Billing Transaction");
|
|
224
|
+
|
|
225
|
+
// ─── Entitlements ────────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The resolved state of one feature for one user.
|
|
229
|
+
*
|
|
230
|
+
* Both halves of the answer travel together: `allowed` is what a gate checks,
|
|
231
|
+
* and the counts are what the UI and the 402 body need. Only `allowed` becomes a
|
|
232
|
+
* CASL rule — see `defineEntitlementRules` in `@thejob/permission` for why counts
|
|
233
|
+
* cannot.
|
|
234
|
+
*/
|
|
235
|
+
export const FeatureEntitlementSchema = object({
|
|
236
|
+
feature: string().required().label("Feature"),
|
|
237
|
+
type: string().oneOf(SupportedGrantTypes).required().label("Grant Type"),
|
|
238
|
+
/** Whether the feature may be used AT ALL right now. */
|
|
239
|
+
allowed: boolean().required().default(false).label("Allowed"),
|
|
240
|
+
/** Quota ceiling for the period. Absent for boolean grants. */
|
|
241
|
+
limit: number().integer().optional().label("Limit"),
|
|
242
|
+
used: number().integer().optional().label("Used"),
|
|
243
|
+
/** Quota remaining plus any credit balance. Absent for boolean grants. */
|
|
244
|
+
remaining: number().integer().optional().label("Remaining"),
|
|
245
|
+
/** Non-expiring balance from one-off purchases. */
|
|
246
|
+
credits: number().integer().optional().label("Credits"),
|
|
247
|
+
/** When the quota next refreshes. */
|
|
248
|
+
resetsAt: number().optional().label("Resets At"),
|
|
249
|
+
}).label("Feature Entitlement");
|
|
250
|
+
|
|
251
|
+
/** Why a gate refused, with the numbers behind it. The body of a 402. */
|
|
252
|
+
export const EntitlementDenialSchema = object({
|
|
253
|
+
feature: string().required().label("Feature"),
|
|
254
|
+
reason: string()
|
|
255
|
+
.oneOf(SupportedEntitlementDenialReasons)
|
|
256
|
+
.required()
|
|
257
|
+
.label("Reason"),
|
|
258
|
+
limit: number().integer().optional().label("Limit"),
|
|
259
|
+
used: number().integer().optional().label("Used"),
|
|
260
|
+
resetsAt: number().optional().label("Resets At"),
|
|
261
|
+
upgradeUrl: string().optional().label("Upgrade URL"),
|
|
262
|
+
}).label("Entitlement Denial");
|
|
263
|
+
|
|
264
|
+
/** What a service asks for when spending a unit of a metered feature. */
|
|
265
|
+
export const EntitlementConsumeSchema = object({
|
|
266
|
+
userId: string().required().label("User ID"),
|
|
267
|
+
feature: string().trim().required().min(1).max(64).label("Feature"),
|
|
268
|
+
units: number()
|
|
269
|
+
.integer()
|
|
270
|
+
.min(1)
|
|
271
|
+
.max(CONSUME_MAX_UNITS)
|
|
272
|
+
.required()
|
|
273
|
+
.default(1)
|
|
274
|
+
.label("Units"),
|
|
275
|
+
/**
|
|
276
|
+
* Caller-supplied de-duplication key. Makes a spend exactly-once across the
|
|
277
|
+
* retries that at-least-once delivery and ordinary network failures produce.
|
|
278
|
+
*/
|
|
279
|
+
idempotencyKey: string().trim().required().min(8).max(128).label("Idempotency Key"),
|
|
280
|
+
}).label("Entitlement Consume");
|