@cosmicdrift/kumiko-bundled-features 0.396.0 → 0.398.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/dist/auth-mfa/schema/user-mfa.d.ts +4 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +2 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.js +11 -4
- package/dist/billing-foundation/consumer-protection/consent-text.d.ts +6 -1
- package/dist/billing-foundation/consumer-protection/consent-text.js +30 -5
- package/dist/billing-foundation/entities.d.ts +2 -0
- package/dist/billing-foundation/events.d.ts +1 -0
- package/dist/billing-foundation/events.js +1 -0
- package/dist/billing-foundation/handlers/billing-plans.query.d.ts +1 -1
- package/dist/billing-foundation/handlers/billing-plans.query.js +11 -9
- package/dist/billing-foundation/i18n.js +1 -0
- package/dist/billing-foundation/projection.d.ts +2 -0
- package/dist/billing-foundation/types.d.ts +7 -0
- package/dist/billing-foundation/validate-options.js +4 -1
- package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +1 -0
- package/dist/billing-foundation/web/checkout-consent-dialog.js +7 -15
- package/dist/billing-foundation/web/consent-fields.d.ts +28 -0
- package/dist/billing-foundation/web/consent-fields.js +43 -0
- package/dist/billing-foundation/web/plan-switch-confirm-dialog.js +9 -17
- package/dist/cap-counter/entity.d.ts +3 -0
- package/dist/cap-overview/handlers/caps-usage.query.js +3 -1
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +3 -3
- package/dist/cap-overview/index.d.ts +1 -1
- package/dist/cap-overview/types.d.ts +5 -1
- package/dist/compliance-profiles/schema/profile-selection.d.ts +2 -0
- package/dist/custom-fields/entity.d.ts +1 -0
- package/dist/inbound-mail-foundation/entities.d.ts +6 -0
- package/dist/inbound-mail-foundation/ingest-failures.d.ts +3 -0
- package/dist/inbound-mail-foundation/projection.d.ts +4 -0
- package/dist/inbound-mail-foundation/quarantine-entities.d.ts +3 -0
- package/dist/jobs/job-run-table.d.ts +4 -0
- package/dist/ledger/entity.d.ts +7 -0
- package/dist/managed-pages/table.d.ts +2 -0
- package/dist/personal-access-tokens/schema/api-token.d.ts +3 -0
- package/dist/readiness/schema/probe-run.d.ts +16 -0
- package/dist/secrets/table.d.ts +1 -0
- package/dist/sessions/schema/user-session.d.ts +4 -0
- package/dist/template-resolver/table.d.ts +12 -0
- package/dist/template-resolver/user-content-table.d.ts +4 -0
- package/dist/tenant/invitation-table.d.ts +6 -0
- package/dist/tenant/schema/tenant.d.ts +10 -0
- package/dist/tier-engine/entity.d.ts +1 -0
- package/dist/tier-engine/tier-resolver.d.ts +1 -0
- package/dist/user/schema/user.d.ts +4 -0
- package/dist/user-data-rights/schema/download-attempt.d.ts +2 -0
- package/dist/user-data-rights/schema/download-token.d.ts +8 -0
- package/dist/user-data-rights/schema/export-job.d.ts +12 -0
- package/dist/waitlist/entity.d.ts +8 -0
- package/package.json +14 -14
- package/src/billing-foundation/changes.json +6 -0
- package/src/cap-overview/changes.json +6 -0
|
@@ -13,6 +13,7 @@ export declare const userMfaEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
13
13
|
readonly description?: string;
|
|
14
14
|
readonly required?: boolean;
|
|
15
15
|
readonly sortable?: boolean;
|
|
16
|
+
readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
|
|
16
17
|
readonly filterable?: boolean;
|
|
17
18
|
readonly sensitive?: boolean;
|
|
18
19
|
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
@@ -28,6 +29,7 @@ export declare const userMfaEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
28
29
|
readonly description?: string;
|
|
29
30
|
readonly required?: boolean;
|
|
30
31
|
readonly sortable?: boolean;
|
|
32
|
+
readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
|
|
31
33
|
readonly filterable?: boolean;
|
|
32
34
|
readonly sensitive?: boolean;
|
|
33
35
|
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
@@ -54,6 +56,7 @@ export declare const userMfaTable: import("@cosmicdrift/kumiko-framework/db").En
|
|
|
54
56
|
readonly description?: string;
|
|
55
57
|
readonly required?: boolean;
|
|
56
58
|
readonly sortable?: boolean;
|
|
59
|
+
readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
|
|
57
60
|
readonly filterable?: boolean;
|
|
58
61
|
readonly sensitive?: boolean;
|
|
59
62
|
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
@@ -69,6 +72,7 @@ export declare const userMfaTable: import("@cosmicdrift/kumiko-framework/db").En
|
|
|
69
72
|
readonly description?: string;
|
|
70
73
|
readonly required?: boolean;
|
|
71
74
|
readonly sortable?: boolean;
|
|
75
|
+
readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
|
|
72
76
|
readonly filterable?: boolean;
|
|
73
77
|
readonly sensitive?: boolean;
|
|
74
78
|
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
@@ -5,6 +5,7 @@ export type CheckoutMode = "subscription" | "payment";
|
|
|
5
5
|
export declare const consentPayloadSchema: z.ZodObject<{
|
|
6
6
|
earlyPerformanceRequested: z.ZodBoolean;
|
|
7
7
|
withdrawalLossAcknowledged: z.ZodBoolean;
|
|
8
|
+
documentsAcknowledged: z.ZodOptional<z.ZodBoolean>;
|
|
8
9
|
consentTextVersion: z.ZodString;
|
|
9
10
|
locale: z.ZodString;
|
|
10
11
|
}, z.core.$strict>;
|
|
@@ -15,6 +16,7 @@ export type PreparedConsent = {
|
|
|
15
16
|
readonly locale: string;
|
|
16
17
|
readonly submitMessage: string;
|
|
17
18
|
readonly consentTextVersion: string;
|
|
19
|
+
readonly documentsAcknowledged: boolean;
|
|
18
20
|
readonly termsHash: string;
|
|
19
21
|
readonly termsTemplateVersion: number;
|
|
20
22
|
};
|
|
@@ -8,13 +8,14 @@ import * as z from "zod";
|
|
|
8
8
|
import { requireTemplateResolver, TEXT_BLOCK_KIND, TemplateNotFoundError, } from "../../template-resolver/index.js";
|
|
9
9
|
import { paymentAggregateId, subscriptionAggregateId } from "../aggregate-id.js";
|
|
10
10
|
import { CHECKOUT_CONSENT_RECORDED_EVENT_QN, checkoutConsentRecordedPayloadSchema, PAYMENT_AGGREGATE_TYPE, SUBSCRIPTION_AGGREGATE_TYPE, } from "../events.js";
|
|
11
|
-
import { consentTextVersion, resolveConsentLocale, submitMessageFor } from "./consent-text.js";
|
|
11
|
+
import { consentTextVersion, documentsAcknowledgementTextFor, resolveConsentLocale, submitMessageFor, } from "./consent-text.js";
|
|
12
12
|
// Optional at the schema level so a missing/unchecked consent reaches
|
|
13
13
|
// prepareConsent and fails as 422 consent_required, not as a zod 400.
|
|
14
14
|
export const consentPayloadSchema = z
|
|
15
15
|
.object({
|
|
16
16
|
earlyPerformanceRequested: z.boolean(),
|
|
17
17
|
withdrawalLossAcknowledged: z.boolean(),
|
|
18
|
+
documentsAcknowledged: z.boolean().optional(),
|
|
18
19
|
consentTextVersion: z.string().min(1).max(64),
|
|
19
20
|
locale: z.string().min(2).max(35),
|
|
20
21
|
})
|
|
@@ -39,14 +40,18 @@ export function assertValidOrderItem(item, priceId) {
|
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
export async function prepareConsent(ctx, options, consent, mode) {
|
|
42
|
-
|
|
43
|
+
const documentsRequired = options.documentsAcknowledgement === true;
|
|
44
|
+
if (!consent?.earlyPerformanceRequested ||
|
|
45
|
+
!consent.withdrawalLossAcknowledged ||
|
|
46
|
+
(documentsRequired && consent.documentsAcknowledged !== true)) {
|
|
43
47
|
throw new UnprocessableError("consent_required", {
|
|
44
48
|
i18nKey: "billing-foundation.errors.consentRequired",
|
|
45
|
-
message: "billing-foundation:
|
|
49
|
+
message: "billing-foundation: all consent flags must be true to start a checkout",
|
|
46
50
|
});
|
|
47
51
|
}
|
|
48
52
|
const locale = resolveConsentLocale(consent.locale);
|
|
49
|
-
|
|
53
|
+
const documentsText = documentsAcknowledgementTextFor(options, locale);
|
|
54
|
+
if (consent.consentTextVersion !== consentTextVersion(locale, documentsText)) {
|
|
50
55
|
throw new UnprocessableError("consent_text_outdated", {
|
|
51
56
|
i18nKey: "billing-foundation.errors.consentTextOutdated",
|
|
52
57
|
message: `billing-foundation: consent text version "${consent.consentTextVersion}" is not current for locale "${locale}"`,
|
|
@@ -78,6 +83,7 @@ export async function prepareConsent(ctx, options, consent, mode) {
|
|
|
78
83
|
locale,
|
|
79
84
|
submitMessage: submitMessageFor(locale, mode),
|
|
80
85
|
consentTextVersion: consent.consentTextVersion,
|
|
86
|
+
documentsAcknowledged: documentsRequired,
|
|
81
87
|
termsHash: createHash("sha256").update(terms.content).digest("hex"),
|
|
82
88
|
termsTemplateVersion: terms.version,
|
|
83
89
|
};
|
|
@@ -100,6 +106,7 @@ export async function recordConsent(ctx, prepared, details) {
|
|
|
100
106
|
interval: details.price?.interval ?? null,
|
|
101
107
|
intervalCount: details.price?.intervalCount ?? null,
|
|
102
108
|
consentTextVersion: prepared.consentTextVersion,
|
|
109
|
+
...(prepared.documentsAcknowledged && { documentsAcknowledged: true }),
|
|
103
110
|
termsHash: prepared.termsHash,
|
|
104
111
|
termsTemplateVersion: prepared.termsTemplateVersion,
|
|
105
112
|
locale: prepared.locale,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ConsumerProtectionOptions, type LegalLinkSet } from "../types.ts";
|
|
1
2
|
import type { ConsentLocale } from "../web/consumer-protection/consent-locale.ts";
|
|
2
3
|
export { CONSENT_LOCALES, type ConsentLocale, FALLBACK_CONSENT_LOCALE, resolveConsentLocale, } from "../web/consumer-protection/consent-locale.ts";
|
|
3
4
|
export type ConsentTexts = {
|
|
@@ -7,5 +8,9 @@ export type ConsentTexts = {
|
|
|
7
8
|
readonly paymentSubmitMessage: string;
|
|
8
9
|
};
|
|
9
10
|
export declare const CONSENT_TEXTS: Readonly<Record<ConsentLocale, ConsentTexts>>;
|
|
10
|
-
export declare function
|
|
11
|
+
export declare function legalLinksPerLocale(legalLinks: ConsumerProtectionOptions["legalLinks"]): Readonly<Record<ConsentLocale, LegalLinkSet>>;
|
|
12
|
+
/** The documents checkbox text for a locale, or undefined when the app did not
|
|
13
|
+
* opt in. The variant follows whether that locale has a refund link. */
|
|
14
|
+
export declare function documentsAcknowledgementTextFor(options: ConsumerProtectionOptions, locale: ConsentLocale): string | undefined;
|
|
15
|
+
export declare function consentTextVersion(locale: ConsentLocale, documentsAcknowledgementText?: string): string;
|
|
11
16
|
export declare function submitMessageFor(locale: ConsentLocale, mode: "subscription" | "payment"): string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { isLegalLinkSet } from "../types.js";
|
|
2
3
|
export { CONSENT_LOCALES, FALLBACK_CONSENT_LOCALE, resolveConsentLocale, } from "../web/consumer-protection/consent-locale.js";
|
|
3
4
|
export const CONSENT_TEXTS = {
|
|
4
5
|
de: {
|
|
@@ -14,13 +15,37 @@ export const CONSENT_TEXTS = {
|
|
|
14
15
|
paymentSubmitMessage: "Paid one-off payment: by clicking the button you commit to paying.",
|
|
15
16
|
},
|
|
16
17
|
};
|
|
18
|
+
const DOCUMENTS_ACKNOWLEDGEMENT_TEXTS = {
|
|
19
|
+
de: {
|
|
20
|
+
withRefund: "Ich habe die AGB, die Erstattungsrichtlinie und die Datenschutzerklärung gelesen. Mit den AGB und der Erstattungsrichtlinie bin ich einverstanden.",
|
|
21
|
+
withoutRefund: "Ich habe die AGB und die Datenschutzerklärung gelesen. Mit den AGB bin ich einverstanden.",
|
|
22
|
+
},
|
|
23
|
+
en: {
|
|
24
|
+
withRefund: "I have read the Terms, the Refund Policy and the Privacy Policy, and I agree to the Terms and the Refund Policy.",
|
|
25
|
+
withoutRefund: "I have read the Terms and the Privacy Policy, and I agree to the Terms.",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
export function legalLinksPerLocale(legalLinks) {
|
|
29
|
+
return isLegalLinkSet(legalLinks) ? { de: legalLinks, en: legalLinks } : legalLinks;
|
|
30
|
+
}
|
|
31
|
+
/** The documents checkbox text for a locale, or undefined when the app did not
|
|
32
|
+
* opt in. The variant follows whether that locale has a refund link. */
|
|
33
|
+
export function documentsAcknowledgementTextFor(options, locale) {
|
|
34
|
+
if (!options.documentsAcknowledgement)
|
|
35
|
+
return undefined;
|
|
36
|
+
const texts = DOCUMENTS_ACKNOWLEDGEMENT_TEXTS[locale];
|
|
37
|
+
return legalLinksPerLocale(options.legalLinks)[locale].refund !== undefined
|
|
38
|
+
? texts.withRefund
|
|
39
|
+
: texts.withoutRefund;
|
|
40
|
+
}
|
|
17
41
|
const CONSENT_TEXT_VERSION_LENGTH = 16;
|
|
18
|
-
|
|
42
|
+
// The documents text joins the hash only when shown, so apps without the opt-in keep their version.
|
|
43
|
+
export function consentTextVersion(locale, documentsAcknowledgementText) {
|
|
19
44
|
const { earlyPerformance, withdrawalLoss } = CONSENT_TEXTS[locale];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.
|
|
23
|
-
|
|
45
|
+
const hash = createHash("sha256").update(`${earlyPerformance}\n${withdrawalLoss}`);
|
|
46
|
+
if (documentsAcknowledgementText !== undefined)
|
|
47
|
+
hash.update(`\n${documentsAcknowledgementText}`);
|
|
48
|
+
return hash.digest("hex").slice(0, CONSENT_TEXT_VERSION_LENGTH);
|
|
24
49
|
}
|
|
25
50
|
export function submitMessageFor(locale, mode) {
|
|
26
51
|
const texts = CONSENT_TEXTS[locale];
|
|
@@ -19,6 +19,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
|
|
|
19
19
|
readonly description?: string;
|
|
20
20
|
readonly required?: boolean;
|
|
21
21
|
readonly sortable?: boolean;
|
|
22
|
+
readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
|
|
22
23
|
readonly filterable?: boolean;
|
|
23
24
|
readonly sensitive?: boolean;
|
|
24
25
|
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
@@ -34,6 +35,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
|
|
|
34
35
|
readonly description?: string;
|
|
35
36
|
readonly required?: boolean;
|
|
36
37
|
readonly sortable?: boolean;
|
|
38
|
+
readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
|
|
37
39
|
readonly filterable?: boolean;
|
|
38
40
|
readonly sensitive?: boolean;
|
|
39
41
|
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
@@ -63,6 +63,7 @@ export declare const checkoutConsentRecordedPayloadSchema: z.ZodObject<{
|
|
|
63
63
|
interval: z.ZodNullable<z.ZodString>;
|
|
64
64
|
intervalCount: z.ZodNullable<z.ZodNumber>;
|
|
65
65
|
consentTextVersion: z.ZodString;
|
|
66
|
+
documentsAcknowledged: z.ZodOptional<z.ZodBoolean>;
|
|
66
67
|
termsHash: z.ZodString;
|
|
67
68
|
termsTemplateVersion: z.ZodNumber;
|
|
68
69
|
locale: z.ZodString;
|
|
@@ -102,6 +102,7 @@ export const checkoutConsentRecordedPayloadSchema = z.object({
|
|
|
102
102
|
interval: z.string().nullable(),
|
|
103
103
|
intervalCount: z.number().nullable(),
|
|
104
104
|
consentTextVersion: z.string().min(1).max(64),
|
|
105
|
+
documentsAcknowledged: z.boolean().optional(),
|
|
105
106
|
termsHash: z.string().length(64),
|
|
106
107
|
termsTemplateVersion: z.number(),
|
|
107
108
|
locale: z.string().min(2).max(35),
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { QueryHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import {
|
|
2
|
+
import type { BillingPlanCatalog, ResolvedBillingFoundationOptions } from "../types.ts";
|
|
3
3
|
export declare function createBillingPlansQuery(options: ResolvedBillingFoundationOptions, catalog: BillingPlanCatalog): QueryHandlerDef;
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
// when `createBillingFoundationFeature` gets a `catalog`.
|
|
4
4
|
import * as z from "zod";
|
|
5
5
|
import { filterOfferedProviders, listCatalogProviders, } from "../checkout-core.js";
|
|
6
|
-
import { CONSENT_TEXTS, consentTextVersion, } from "../consumer-protection/consent-text.js";
|
|
6
|
+
import { CONSENT_TEXTS, consentTextVersion, documentsAcknowledgementTextFor, legalLinksPerLocale, } from "../consumer-protection/consent-text.js";
|
|
7
7
|
import { buildProviderPlanViews, loadBillingPlansBase, toBillingPlansResult, } from "../plan-catalog.js";
|
|
8
|
-
import { isLegalLinkSet, } from "../types.js";
|
|
9
8
|
const billingPlansSchema = z.object({}).strict();
|
|
10
9
|
export function createBillingPlansQuery(options, catalog) {
|
|
11
10
|
return {
|
|
@@ -23,8 +22,8 @@ export function createBillingPlansQuery(options, catalog) {
|
|
|
23
22
|
...result,
|
|
24
23
|
consumerProtection: {
|
|
25
24
|
consentTexts: {
|
|
26
|
-
de: consentTextView("de"),
|
|
27
|
-
en: consentTextView("en"),
|
|
25
|
+
de: consentTextView(consumerProtection, "de"),
|
|
26
|
+
en: consentTextView(consumerProtection, "en"),
|
|
28
27
|
},
|
|
29
28
|
legalLinks: legalLinksPerLocale(consumerProtection.legalLinks),
|
|
30
29
|
},
|
|
@@ -62,10 +61,13 @@ async function providerView(ctx, catalog, base, provider, providerTexts, known)
|
|
|
62
61
|
plans: views.plans,
|
|
63
62
|
};
|
|
64
63
|
}
|
|
65
|
-
function
|
|
66
|
-
return isLegalLinkSet(legalLinks) ? { de: legalLinks, en: legalLinks } : legalLinks;
|
|
67
|
-
}
|
|
68
|
-
function consentTextView(locale) {
|
|
64
|
+
function consentTextView(consumerProtection, locale) {
|
|
69
65
|
const { earlyPerformance, withdrawalLoss } = CONSENT_TEXTS[locale];
|
|
70
|
-
|
|
66
|
+
const documentsAcknowledgement = documentsAcknowledgementTextFor(consumerProtection, locale);
|
|
67
|
+
return {
|
|
68
|
+
earlyPerformance,
|
|
69
|
+
withdrawalLoss,
|
|
70
|
+
...(documentsAcknowledgement !== undefined && { documentsAcknowledgement }),
|
|
71
|
+
consentTextVersion: consentTextVersion(locale, documentsAcknowledgement),
|
|
72
|
+
};
|
|
71
73
|
}
|
|
@@ -78,6 +78,7 @@ export const BILLING_FOUNDATION_I18N = {
|
|
|
78
78
|
"billing-foundation.consent.link.terms": { en: "Terms" },
|
|
79
79
|
"billing-foundation.consent.link.withdrawal": { en: "Withdrawal policy" },
|
|
80
80
|
"billing-foundation.consent.link.privacy": { en: "Privacy policy" },
|
|
81
|
+
"billing-foundation.consent.link.refund": { en: "Refund policy" },
|
|
81
82
|
"billing-foundation.consent.order": { en: "Order with obligation to pay" },
|
|
82
83
|
"billing-foundation.plans.priceFrom": { en: "from" },
|
|
83
84
|
"billing-foundation.consent.chooseProvider": { en: "Payment provider" },
|
|
@@ -20,6 +20,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
|
|
|
20
20
|
readonly description?: string;
|
|
21
21
|
readonly required?: boolean;
|
|
22
22
|
readonly sortable?: boolean;
|
|
23
|
+
readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
|
|
23
24
|
readonly filterable?: boolean;
|
|
24
25
|
readonly sensitive?: boolean;
|
|
25
26
|
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
@@ -35,6 +36,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
|
|
|
35
36
|
readonly description?: string;
|
|
36
37
|
readonly required?: boolean;
|
|
37
38
|
readonly sortable?: boolean;
|
|
39
|
+
readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
|
|
38
40
|
readonly filterable?: boolean;
|
|
39
41
|
readonly sensitive?: boolean;
|
|
40
42
|
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
@@ -345,6 +345,8 @@ export type LegalLinkSet = {
|
|
|
345
345
|
readonly terms: string;
|
|
346
346
|
readonly withdrawal: string;
|
|
347
347
|
readonly privacy: string;
|
|
348
|
+
/** Link to the refund policy; shown next to the other links when set. */
|
|
349
|
+
readonly refund?: string;
|
|
348
350
|
};
|
|
349
351
|
export declare function isLegalLinkSet(links: LegalLinkSet | Readonly<Record<ConsentLocale, LegalLinkSet>>): links is LegalLinkSet;
|
|
350
352
|
/** Consumer-protection consent gate (§ 312j / § 356 BGB style). When set,
|
|
@@ -382,6 +384,9 @@ export type ConsumerProtectionOptions = {
|
|
|
382
384
|
/** Root-relative paths or absolute https URLs. One set for every locale, or
|
|
383
385
|
* one set per consent locale (`de` and `en` required). */
|
|
384
386
|
readonly legalLinks: LegalLinkSet | Readonly<Record<ConsentLocale, LegalLinkSet>>;
|
|
387
|
+
/** Adds a required first checkbox "documents read and accepted" (terms, privacy,
|
|
388
|
+
* and the refund policy when `legalLinks.refund` is set). Off by default. */
|
|
389
|
+
readonly documentsAcknowledgement?: boolean;
|
|
385
390
|
};
|
|
386
391
|
export type OneOffCheckoutRequest = {
|
|
387
392
|
readonly tenantId: TenantId;
|
|
@@ -476,6 +481,8 @@ export type BillingPlansResult = {
|
|
|
476
481
|
readonly consentTexts: Readonly<Record<"de" | "en", {
|
|
477
482
|
readonly earlyPerformance: string;
|
|
478
483
|
readonly withdrawalLoss: string;
|
|
484
|
+
/** Present only when `documentsAcknowledgement` is on. */
|
|
485
|
+
readonly documentsAcknowledgement?: string;
|
|
479
486
|
readonly consentTextVersion: string;
|
|
480
487
|
}>>;
|
|
481
488
|
readonly legalLinks: Readonly<Record<ConsentLocale, LegalLinkSet>>;
|
|
@@ -73,7 +73,10 @@ function validatePerLocaleLegalLinks(legalLinks) {
|
|
|
73
73
|
}
|
|
74
74
|
function validateLegalLinkSet(set, path) {
|
|
75
75
|
for (const [key, link] of Object.entries(set)) {
|
|
76
|
-
|
|
76
|
+
// skip: refund is optional
|
|
77
|
+
if (key === "refund" && link === undefined)
|
|
78
|
+
continue;
|
|
79
|
+
if (typeof link !== "string" || !isRootRelativeOrHttpsUrl(link)) {
|
|
77
80
|
throw new Error(`createBillingFoundationFeature: consumerProtection.${path}.${key} "${link}" must be a root-relative path ("/..." not "//") or an absolute https URL.`);
|
|
78
81
|
}
|
|
79
82
|
}
|
|
@@ -6,6 +6,7 @@ type ConsumerProtectionView = NonNullable<BillingPlansResult["consumerProtection
|
|
|
6
6
|
export type CheckoutConsentPayload = {
|
|
7
7
|
readonly earlyPerformanceRequested: true;
|
|
8
8
|
readonly withdrawalLossAcknowledged: true;
|
|
9
|
+
readonly documentsAcknowledged?: true;
|
|
9
10
|
readonly consentTextVersion: string;
|
|
10
11
|
readonly locale: string;
|
|
11
12
|
};
|
|
@@ -2,41 +2,33 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useLocale, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
3
3
|
import { ChoiceCards } from "@cosmicdrift/kumiko-renderer/web";
|
|
4
4
|
import { useState } from "react";
|
|
5
|
+
import { ConsentCheckboxes, ConsentLinks, useConsentFieldState } from "./consent-fields.js";
|
|
5
6
|
import { resolveConsentLocale } from "./consumer-protection/consent-locale.js";
|
|
6
7
|
export const CONSENT_TEXT_OUTDATED_CODE = "consent_text_outdated";
|
|
7
8
|
// kumiko-lint-ignore no-custom-primitives Uses usePrimitives().Modal internally, name is a billing domain action, not a primitive reimplementation
|
|
8
9
|
export function CheckoutConsentDialog({ open, onOpenChange, planName, price, consumerProtection, providerChoice, note, onOrder, onConsentTextOutdated, }) {
|
|
9
10
|
const t = useTranslation();
|
|
10
11
|
const uiLocale = useLocale().locale();
|
|
11
|
-
const { Modal, Text,
|
|
12
|
-
const [earlyPerformance, setEarlyPerformance] = useState(false);
|
|
13
|
-
const [withdrawalLoss, setWithdrawalLoss] = useState(false);
|
|
12
|
+
const { Modal, Text, Button, Banner } = usePrimitives();
|
|
14
13
|
const [submitting, setSubmitting] = useState(false);
|
|
15
14
|
const [errorText, setErrorText] = useState(null);
|
|
16
15
|
const consentLocale = resolveConsentLocale(uiLocale);
|
|
17
16
|
const texts = consumerProtection?.consentTexts[consentLocale];
|
|
18
17
|
const legalLinks = consumerProtection?.legalLinks[consentLocale];
|
|
19
|
-
const
|
|
18
|
+
const consentFields = useConsentFieldState(texts);
|
|
19
|
+
const consentGiven = consumerProtection === undefined || consentFields.allGiven;
|
|
20
20
|
async function order() {
|
|
21
21
|
setSubmitting(true);
|
|
22
22
|
setErrorText(null);
|
|
23
|
-
const result = await onOrder(
|
|
24
|
-
? undefined
|
|
25
|
-
: {
|
|
26
|
-
earlyPerformanceRequested: true,
|
|
27
|
-
withdrawalLossAcknowledged: true,
|
|
28
|
-
consentTextVersion: texts.consentTextVersion,
|
|
29
|
-
locale: consentLocale,
|
|
30
|
-
});
|
|
23
|
+
const result = await onOrder(consentFields.buildPayload(consentLocale));
|
|
31
24
|
if (result.isSuccess)
|
|
32
25
|
return;
|
|
33
26
|
setSubmitting(false);
|
|
34
27
|
setErrorText(t(result.error.i18nKey, result.error.i18nParams));
|
|
35
28
|
if (result.error.code === CONSENT_TEXT_OUTDATED_CODE) {
|
|
36
|
-
|
|
37
|
-
setWithdrawalLoss(false);
|
|
29
|
+
consentFields.reset();
|
|
38
30
|
onConsentTextOutdated();
|
|
39
31
|
}
|
|
40
32
|
}
|
|
41
|
-
return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, title: t("billing-foundation.consent.title"), testId: "checkout-consent-dialog", children: _jsxs("div", { className: "flex flex-col gap-4", children: [providerChoice !== undefined && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Text, { variant: "muted", children: t("billing-foundation.consent.chooseProvider") }), _jsx(ChoiceCards, { value: providerChoice.value, options: providerChoice.options, onChange: providerChoice.onChange, ariaLabel: t("billing-foundation.consent.chooseProvider"), testId: "checkout-provider-choice" })] })), _jsxs("div", { className: "flex flex-col gap-1", "data-testid": "checkout-consent-summary", children: [_jsxs(Text, { children: [planName, " \u2014 ", price.amount] }), price.renewalKey !== undefined && (_jsxs(_Fragment, { children: [_jsx(Text, { variant: "muted", children: t(price.renewalKey, price.count !== undefined ? { count: price.count } : undefined) }), _jsx(Text, { variant: "muted", children: t("billing-foundation.consent.cancelAnytime") })] })), note !== undefined && (_jsx(Text, { variant: "muted", testId: "checkout-consent-note", children: note }))] }), legalLinks !== undefined &&
|
|
33
|
+
return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, title: t("billing-foundation.consent.title"), testId: "checkout-consent-dialog", children: _jsxs("div", { className: "flex flex-col gap-4", children: [providerChoice !== undefined && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Text, { variant: "muted", children: t("billing-foundation.consent.chooseProvider") }), _jsx(ChoiceCards, { value: providerChoice.value, options: providerChoice.options, onChange: providerChoice.onChange, ariaLabel: t("billing-foundation.consent.chooseProvider"), testId: "checkout-provider-choice" })] })), _jsxs("div", { className: "flex flex-col gap-1", "data-testid": "checkout-consent-summary", children: [_jsxs(Text, { children: [planName, " \u2014 ", price.amount] }), price.renewalKey !== undefined && (_jsxs(_Fragment, { children: [_jsx(Text, { variant: "muted", children: t(price.renewalKey, price.count !== undefined ? { count: price.count } : undefined) }), _jsx(Text, { variant: "muted", children: t("billing-foundation.consent.cancelAnytime") })] })), note !== undefined && (_jsx(Text, { variant: "muted", testId: "checkout-consent-note", children: note }))] }), legalLinks !== undefined && _jsx(ConsentLinks, { legalLinks: legalLinks }), texts !== undefined && (_jsx(ConsentCheckboxes, { idPrefix: "consent", texts: texts, state: consentFields, disabled: submitting })), errorText !== null && (_jsx(Banner, { variant: "error", testId: "checkout-consent-error", children: errorText })), _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { variant: "secondary", disabled: submitting, onClick: () => onOpenChange(false), testId: "checkout-consent-cancel", children: t("billing-foundation.consent.back") }), _jsx(Button, { disabled: !consentGiven || submitting, loading: submitting, onClick: order, testId: "checkout-consent-order", children: t("billing-foundation.consent.order") })] })] }) }));
|
|
42
34
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { BillingPlansResult, LegalLinkSet } from "../types.ts";
|
|
3
|
+
import type { CheckoutConsentPayload } from "./checkout-consent-dialog.tsx";
|
|
4
|
+
type ConsumerProtectionView = NonNullable<BillingPlansResult["consumerProtection"]>;
|
|
5
|
+
export type ConsentTextView = ConsumerProtectionView["consentTexts"]["de"];
|
|
6
|
+
export type ConsentChecks = {
|
|
7
|
+
readonly documentsAcknowledged: boolean;
|
|
8
|
+
readonly earlyPerformance: boolean;
|
|
9
|
+
readonly withdrawalLoss: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type ConsentFieldState = {
|
|
12
|
+
readonly checks: ConsentChecks;
|
|
13
|
+
readonly setCheck: (key: keyof ConsentChecks, value: boolean) => void;
|
|
14
|
+
readonly reset: () => void;
|
|
15
|
+
readonly allGiven: boolean;
|
|
16
|
+
readonly buildPayload: (locale: string) => CheckoutConsentPayload | undefined;
|
|
17
|
+
};
|
|
18
|
+
export declare function useConsentFieldState(texts: ConsentTextView | undefined): ConsentFieldState;
|
|
19
|
+
export declare function ConsentLinks({ legalLinks }: {
|
|
20
|
+
readonly legalLinks: LegalLinkSet;
|
|
21
|
+
}): ReactNode;
|
|
22
|
+
export declare function ConsentCheckboxes({ idPrefix, texts, state, disabled, }: {
|
|
23
|
+
readonly idPrefix: string;
|
|
24
|
+
readonly texts: ConsentTextView;
|
|
25
|
+
readonly state: ConsentFieldState;
|
|
26
|
+
readonly disabled: boolean;
|
|
27
|
+
}): ReactNode;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
const NOTHING_CHECKED = {
|
|
5
|
+
documentsAcknowledged: false,
|
|
6
|
+
earlyPerformance: false,
|
|
7
|
+
withdrawalLoss: false,
|
|
8
|
+
};
|
|
9
|
+
export function useConsentFieldState(texts) {
|
|
10
|
+
const [checks, setChecks] = useState(NOTHING_CHECKED);
|
|
11
|
+
const documentsRequired = texts?.documentsAcknowledgement !== undefined;
|
|
12
|
+
return {
|
|
13
|
+
checks,
|
|
14
|
+
setCheck: (key, value) => setChecks((current) => ({ ...current, [key]: value })),
|
|
15
|
+
reset: () => setChecks(NOTHING_CHECKED),
|
|
16
|
+
allGiven: checks.earlyPerformance &&
|
|
17
|
+
checks.withdrawalLoss &&
|
|
18
|
+
(!documentsRequired || checks.documentsAcknowledged),
|
|
19
|
+
buildPayload: (locale) => texts === undefined
|
|
20
|
+
? undefined
|
|
21
|
+
: {
|
|
22
|
+
earlyPerformanceRequested: true,
|
|
23
|
+
withdrawalLossAcknowledged: true,
|
|
24
|
+
...(documentsRequired && { documentsAcknowledged: true }),
|
|
25
|
+
consentTextVersion: texts.consentTextVersion,
|
|
26
|
+
locale,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function ConsentLinks({ legalLinks }) {
|
|
31
|
+
const t = useTranslation();
|
|
32
|
+
const { Link } = usePrimitives();
|
|
33
|
+
return (_jsxs("div", { className: "flex flex-wrap gap-4", children: [_jsx(Link, { href: legalLinks.terms, target: "_blank", children: t("billing-foundation.consent.link.terms") }), _jsx(Link, { href: legalLinks.withdrawal, target: "_blank", children: t("billing-foundation.consent.link.withdrawal") }), _jsx(Link, { href: legalLinks.privacy, target: "_blank", children: t("billing-foundation.consent.link.privacy") }), legalLinks.refund !== undefined && (_jsx(Link, { href: legalLinks.refund, target: "_blank", children: t("billing-foundation.consent.link.refund") }))] }));
|
|
34
|
+
}
|
|
35
|
+
export function ConsentCheckboxes({ idPrefix, texts, state, disabled, }) {
|
|
36
|
+
const { Field, Input } = usePrimitives();
|
|
37
|
+
const boxes = [
|
|
38
|
+
{ key: "documentsAcknowledged", slug: "documents", label: texts.documentsAcknowledgement },
|
|
39
|
+
{ key: "earlyPerformance", slug: "early-performance", label: texts.earlyPerformance },
|
|
40
|
+
{ key: "withdrawalLoss", slug: "withdrawal-loss", label: texts.withdrawalLoss },
|
|
41
|
+
];
|
|
42
|
+
return (_jsx(_Fragment, { children: boxes.map(({ key, slug, label }) => label === undefined ? null : (_jsx(Field, { id: `${idPrefix}-${slug}`, label: label, layout: "inline", children: _jsx(Input, { kind: "boolean", id: `${idPrefix}-${slug}`, name: `${idPrefix}-${slug}`, value: state.checks[key], onChange: (value) => state.setCheck(key, value), disabled: disabled }) }, key))) }));
|
|
43
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useLocale, useMutation, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
3
3
|
import { formatMoney } from "@cosmicdrift/kumiko-renderer/web";
|
|
4
4
|
import { useState } from "react";
|
|
5
|
-
import { CONSENT_TEXT_OUTDATED_CODE
|
|
5
|
+
import { CONSENT_TEXT_OUTDATED_CODE } from "./checkout-consent-dialog.js";
|
|
6
|
+
import { ConsentCheckboxes, ConsentLinks, useConsentFieldState } from "./consent-fields.js";
|
|
6
7
|
import { SubscriptionFoundationHandlers, SubscriptionFoundationQueries } from "./constants.js";
|
|
7
8
|
import { resolveConsentLocale } from "./consumer-protection/consent-locale.js";
|
|
8
9
|
import { intervalKey } from "./plan-price.js";
|
|
@@ -11,28 +12,20 @@ import { recurringAmountsOf } from "./plan-switch-recurring.js";
|
|
|
11
12
|
export function PlanSwitchConfirmDialog({ open, onOpenChange, tier, fromPlanName, toPlanName, consumerProtection, onApplied, onConsentTextOutdated, }) {
|
|
12
13
|
const t = useTranslation();
|
|
13
14
|
const locale = useLocale().locale();
|
|
14
|
-
const { Modal, Text,
|
|
15
|
+
const { Modal, Text, Button, Banner } = usePrimitives();
|
|
15
16
|
const previewQuery = useQuery(SubscriptionFoundationQueries.planSwitchPreview, { tier });
|
|
16
17
|
const switchMutation = useMutation(SubscriptionFoundationHandlers.switchPlan);
|
|
17
|
-
const [earlyPerformance, setEarlyPerformance] = useState(false);
|
|
18
|
-
const [withdrawalLoss, setWithdrawalLoss] = useState(false);
|
|
19
18
|
const [submitting, setSubmitting] = useState(false);
|
|
20
19
|
const [errorText, setErrorText] = useState(null);
|
|
21
20
|
const consentLocale = resolveConsentLocale(locale);
|
|
22
21
|
const texts = consumerProtection?.consentTexts[consentLocale];
|
|
23
22
|
const legalLinks = consumerProtection?.legalLinks[consentLocale];
|
|
24
|
-
const
|
|
23
|
+
const consentFields = useConsentFieldState(texts);
|
|
24
|
+
const consentGiven = consumerProtection === undefined || consentFields.allGiven;
|
|
25
25
|
async function confirm() {
|
|
26
26
|
setSubmitting(true);
|
|
27
27
|
setErrorText(null);
|
|
28
|
-
const consent =
|
|
29
|
-
? undefined
|
|
30
|
-
: {
|
|
31
|
-
earlyPerformanceRequested: true,
|
|
32
|
-
withdrawalLossAcknowledged: true,
|
|
33
|
-
consentTextVersion: texts.consentTextVersion,
|
|
34
|
-
locale: consentLocale,
|
|
35
|
-
};
|
|
28
|
+
const consent = consentFields.buildPayload(consentLocale);
|
|
36
29
|
const result = await switchMutation.mutate({ tier, ...(consent && { consent }) });
|
|
37
30
|
if (result.isSuccess) {
|
|
38
31
|
onApplied();
|
|
@@ -41,8 +34,7 @@ export function PlanSwitchConfirmDialog({ open, onOpenChange, tier, fromPlanName
|
|
|
41
34
|
setSubmitting(false);
|
|
42
35
|
setErrorText(t(result.error.i18nKey, result.error.i18nParams));
|
|
43
36
|
if (result.error.code === CONSENT_TEXT_OUTDATED_CODE) {
|
|
44
|
-
|
|
45
|
-
setWithdrawalLoss(false);
|
|
37
|
+
consentFields.reset();
|
|
46
38
|
onConsentTextOutdated();
|
|
47
39
|
}
|
|
48
40
|
}
|
|
@@ -66,5 +58,5 @@ export function PlanSwitchConfirmDialog({ open, onOpenChange, tier, fromPlanName
|
|
|
66
58
|
amount: money(reply.preview.credit, reply.preview.currency),
|
|
67
59
|
}) }), _jsx(Text, { variant: "muted", children: t("billing-foundation.planSwitch.recurring", {
|
|
68
60
|
amount: recurringPrice,
|
|
69
|
-
}) }), _jsx(Text, { variant: "muted", children: t("billing-foundation.planSwitch.effective") }), _jsx(Text, { variant: "muted", children: t("billing-foundation.planSwitch.pendingNote") })] })), legalLinks !== undefined &&
|
|
61
|
+
}) }), _jsx(Text, { variant: "muted", children: t("billing-foundation.planSwitch.effective") }), _jsx(Text, { variant: "muted", children: t("billing-foundation.planSwitch.pendingNote") })] })), legalLinks !== undefined && _jsx(ConsentLinks, { legalLinks: legalLinks }), texts !== undefined && (_jsx(ConsentCheckboxes, { idPrefix: "plan-switch", texts: texts, state: consentFields, disabled: submitting })), errorText !== null && (_jsx(Banner, { variant: "error", testId: "plan-switch-confirm-error", children: errorText })), _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { variant: "secondary", disabled: submitting, onClick: () => onOpenChange(false), testId: "plan-switch-confirm-cancel", children: t("billing-foundation.consent.back") }), _jsx(Button, { disabled: !consentGiven || reply === null || submitting, loading: submitting, onClick: confirm, testId: "plan-switch-confirm-order", children: t("billing-foundation.planSwitch.order") })] })] }) }));
|
|
70
62
|
}
|
|
@@ -7,6 +7,7 @@ export declare const capCounterEntity: import("@cosmicdrift/kumiko-types/fields"
|
|
|
7
7
|
readonly description?: string;
|
|
8
8
|
readonly required?: boolean;
|
|
9
9
|
readonly sortable?: boolean;
|
|
10
|
+
readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
|
|
10
11
|
readonly filterable?: boolean;
|
|
11
12
|
readonly sensitive?: boolean;
|
|
12
13
|
readonly default?: number;
|
|
@@ -19,6 +20,7 @@ export declare const capCounterEntity: import("@cosmicdrift/kumiko-types/fields"
|
|
|
19
20
|
readonly description?: string;
|
|
20
21
|
readonly required?: boolean;
|
|
21
22
|
readonly sortable?: boolean;
|
|
23
|
+
readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
|
|
22
24
|
readonly filterable?: boolean;
|
|
23
25
|
readonly sensitive?: boolean;
|
|
24
26
|
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
@@ -34,6 +36,7 @@ export declare const capCounterEntity: import("@cosmicdrift/kumiko-types/fields"
|
|
|
34
36
|
readonly description?: string;
|
|
35
37
|
readonly required?: boolean;
|
|
36
38
|
readonly sortable?: boolean;
|
|
39
|
+
readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
|
|
37
40
|
readonly filterable?: boolean;
|
|
38
41
|
readonly sensitive?: boolean;
|
|
39
42
|
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
@@ -54,7 +54,9 @@ export function createCapsUsageQuery(caps, usageRoles) {
|
|
|
54
54
|
const config = override === undefined ? ctx.config : ctx.tenants.config?.(targetTenantId);
|
|
55
55
|
const rows = await Promise.all(caps.map(async (cap) => {
|
|
56
56
|
const used = await cap.usage(db, targetTenantId);
|
|
57
|
-
const limit = cap.showLimit === false
|
|
57
|
+
const limit = cap.showLimit === false
|
|
58
|
+
? null
|
|
59
|
+
: await cap.limit(tier, { config, tenantId: targetTenantId, db });
|
|
58
60
|
const base = {
|
|
59
61
|
id: cap.id,
|
|
60
62
|
label: cap.label,
|
|
@@ -31,9 +31,9 @@ function tenantCapKey(tenantId, capId) {
|
|
|
31
31
|
}
|
|
32
32
|
// One config accessor per tenant: limits may read tenant-scoped config keys, so
|
|
33
33
|
// a limit shared across tenants of the same tier would show the wrong value.
|
|
34
|
-
async function resolveLimitsByTenantAndCap(caps, tenants, configFor) {
|
|
34
|
+
async function resolveLimitsByTenantAndCap(caps, tenants, configFor, db) {
|
|
35
35
|
const entries = await Promise.all(tenants.flatMap(({ tenantId, tier }) => {
|
|
36
|
-
const context = { config: configFor?.(tenantId) };
|
|
36
|
+
const context = { config: configFor?.(tenantId), tenantId, db };
|
|
37
37
|
return caps.map(async (cap) => [tenantCapKey(tenantId, cap.id), await cap.limit(tier, context)]);
|
|
38
38
|
}));
|
|
39
39
|
return new Map(entries);
|
|
@@ -163,7 +163,7 @@ export function createTenantCapsListQuery(caps, listCaps) {
|
|
|
163
163
|
const perTenantEntries = await Promise.all(pageTenantIds.map(async (tenantId) => [tenantId, await cap.usage(db, tenantId)]));
|
|
164
164
|
usageByCap.set(cap.id, new Map(perTenantEntries));
|
|
165
165
|
}));
|
|
166
|
-
const limitByTenantAndCap = await resolveLimitsByTenantAndCap(listedCaps, page, ctx.tenants.config);
|
|
166
|
+
const limitByTenantAndCap = await resolveLimitsByTenantAndCap(listedCaps, page, ctx.tenants.config, db);
|
|
167
167
|
const rows = page.map((row) => {
|
|
168
168
|
const capFields = {};
|
|
169
169
|
for (const cap of listedCaps) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { DEFAULT_CAP_USAGE_ROLES } from "./access.ts";
|
|
2
2
|
export { type CreateCapOverviewOptions, createCapOverviewFeature } from "./feature.ts";
|
|
3
3
|
export { createMyCapsScreen, createTenantCapListScreen, platformTenantCapsScreen, } from "./screens.ts";
|
|
4
|
-
export type { CapSpec, CapUsage, CapUsageTone, CapUsageWithMeta } from "./types.ts";
|
|
4
|
+
export type { CapSpec, CapSpecLimitContext, CapUsage, CapUsageTone, CapUsageWithMeta, } from "./types.ts";
|
|
5
5
|
export { CAP_CARDS_PANEL_COMPONENT, CAP_OVERVIEW_FEATURE, CAP_USAGE_CELL_COMPONENT, CapOverviewQueries, capFieldName, MY_CAPS_SCREEN_ID, PLATFORM_TENANT_CAPS_SCREEN_ID, TENANT_CAP_LIST_SCREEN_ID, } from "./web/constants.ts";
|
|
6
6
|
export { computeFraction, computeTone } from "./web/usage-math.ts";
|
|
@@ -3,10 +3,14 @@ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
3
3
|
import type { CapLimitContext } from "../cap-counter/index.ts";
|
|
4
4
|
export type CapUnit = "count" | "mb" | "tokens";
|
|
5
5
|
export type CapIconKey = "file" | "hash" | "database" | "users" | "mail" | "gauge";
|
|
6
|
+
export type CapSpecLimitContext = CapLimitContext & {
|
|
7
|
+
readonly tenantId: TenantId;
|
|
8
|
+
readonly db: TenantDb;
|
|
9
|
+
};
|
|
6
10
|
export type CapSpec = {
|
|
7
11
|
readonly id: string;
|
|
8
12
|
readonly label: string;
|
|
9
|
-
readonly limit: (tier: string, context:
|
|
13
|
+
readonly limit: (tier: string, context: CapSpecLimitContext) => number | null | Promise<number | null>;
|
|
10
14
|
readonly usage: (db: TenantDb, tenantId: TenantId) => Promise<number | null>;
|
|
11
15
|
readonly usageBatch?: (db: TenantDb, tenantIds: readonly TenantId[]) => Promise<Map<TenantId, number | null>>;
|
|
12
16
|
readonly unit?: CapUnit;
|