@cosmicdrift/kumiko-samples 0.363.0 → 0.365.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/LICENSE +1 -1
- package/package.json +1 -1
- package/packages/bundled-features/package.json +7 -1
- package/packages/bundled-features/src/audit/changes.json +18 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -1
- package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
- package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +63 -11
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +30 -11
- package/packages/bundled-features/src/auth-email-password/changes.json +55 -0
- package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
- package/packages/bundled-features/src/auth-email-password/feature.ts +4 -7
- package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -3
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +6 -4
- package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +2 -2
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +7 -3
- package/packages/bundled-features/src/auth-email-password/mail-i18n.ts +10 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
- package/packages/bundled-features/src/billing-foundation/changes.json +48 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +57 -5
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +4 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +9 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +18 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +35 -19
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +10 -4
- package/packages/bundled-features/src/billing-foundation/i18n.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +27 -7
- package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
- package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +20 -8
- package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +7 -2
- package/packages/bundled-features/src/cap-counter/changes.json +19 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
- package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +2 -0
- package/packages/bundled-features/src/cap-counter/index.ts +1 -0
- package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
- package/packages/bundled-features/src/cap-overview/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
- package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +2 -1
- package/packages/bundled-features/src/channel-discord/discord-channel.ts +4 -2
- package/packages/bundled-features/src/channel-email/changes.json +18 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +47 -24
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +7 -1
- package/packages/bundled-features/src/channel-slack/slack-channel.ts +4 -2
- package/packages/bundled-features/src/channel-teams/teams-channel.ts +4 -2
- package/packages/bundled-features/src/config/changes.json +6 -0
- package/packages/bundled-features/src/config/handlers/set.write.ts +2 -2
- package/packages/bundled-features/src/config/resolver.ts +15 -12
- package/packages/bundled-features/src/config/write-helpers.ts +23 -0
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +54 -27
- package/packages/bundled-features/src/custom-fields/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +4 -5
- package/packages/bundled-features/src/delivery/attachment-ref.ts +39 -1
- package/packages/bundled-features/src/delivery/changes.json +30 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +31 -8
- package/packages/bundled-features/src/delivery/delivery-service.ts +93 -92
- package/packages/bundled-features/src/delivery/index.ts +2 -0
- package/packages/bundled-features/src/delivery/jobs.ts +51 -14
- package/packages/bundled-features/src/delivery/types.ts +7 -1
- package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
- package/packages/bundled-features/src/feature-toggles/changes.json +7 -0
- package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +26 -12
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
- package/packages/bundled-features/src/file-foundation/changes.json +6 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +6 -3
- package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
- package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
- package/packages/bundled-features/src/files-tenant-data/index.ts +3 -1
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +12 -3
- package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +3 -1
- package/packages/bundled-features/src/jobs/changes.json +31 -0
- package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
- package/packages/bundled-features/src/jobs/feature.ts +7 -4
- package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
- package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
- package/packages/bundled-features/src/jobs/job-run-logger.ts +56 -18
- package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
- package/packages/bundled-features/src/ledger/changes.json +7 -0
- package/packages/bundled-features/src/ledger/entity.ts +3 -1
- package/packages/bundled-features/src/metrics/changes.json +6 -0
- package/packages/bundled-features/src/metrics/filters.ts +28 -12
- package/packages/bundled-features/src/personal-access-tokens/changes.json +6 -0
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +7 -8
- package/packages/bundled-features/src/renderer-foundation/types.ts +2 -0
- package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
- package/packages/bundled-features/src/secrets/changes.json +12 -0
- package/packages/bundled-features/src/secrets/feature.ts +4 -3
- package/packages/bundled-features/src/secrets/handlers/delete.write.ts +3 -1
- package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
- package/packages/bundled-features/src/secrets/write-gate.ts +8 -2
- package/packages/bundled-features/src/shared/changes.json +6 -0
- package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
- package/packages/bundled-features/src/shared/index.ts +8 -0
- package/packages/bundled-features/src/shared/mail-template-body.ts +17 -0
- package/packages/bundled-features/src/shared/resource-image-formats.ts +31 -0
- package/packages/bundled-features/src/shared/sharp-input-limits.ts +7 -8
- package/packages/bundled-features/src/step-dispatcher/webhook-runner.ts +2 -2
- package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
- package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
- package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
- package/packages/bundled-features/src/template-resolver/README.md +4 -2
- package/packages/bundled-features/src/template-resolver/api.ts +3 -2
- package/packages/bundled-features/src/template-resolver/changes.json +149 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +148 -18
- package/packages/bundled-features/src/template-resolver/compile-steps-block.ts +68 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +23 -3
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +33 -5
- package/packages/bundled-features/src/template-resolver/document-steps.ts +114 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +16 -1
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +81 -39
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +4 -3
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +121 -53
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +6 -44
- package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +56 -1
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +6 -3
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +25 -15
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +12 -1
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +73 -30
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +14 -11
- package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +102 -19
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +49 -10
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +39 -10
- package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +14 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +54 -4
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +96 -14
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +51 -24
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +16 -2
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +4 -1
- package/packages/bundled-features/src/template-resolver/index.ts +18 -2
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +56 -0
- package/packages/bundled-features/src/template-resolver/mail-constants.ts +4 -0
- package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +4 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +9 -16
- package/packages/bundled-features/src/template-resolver/seeding.ts +27 -4
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +390 -124
- package/packages/bundled-features/src/template-resolver/table.ts +13 -1
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +4 -0
- package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
- package/packages/bundled-features/src/template-resolver/template-meta.ts +95 -24
- package/packages/bundled-features/src/template-resolver/template-preview.ts +160 -57
- package/packages/bundled-features/src/template-resolver/template-resources.ts +3 -4
- package/packages/bundled-features/src/template-resolver/template-usage.ts +31 -11
- package/packages/bundled-features/src/template-resolver/template-validation.ts +16 -9
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +12 -8
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +97 -10
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +15 -5
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +405 -145
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +17 -3
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +32 -4
- package/packages/bundled-features/src/template-resolver/web/index.ts +8 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +23 -7
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +3 -8
- package/packages/bundled-features/src/tenant/seeding.ts +33 -27
- package/packages/bundled-features/src/tenant-handover/move-entity-graph.ts +14 -3
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +9 -1
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +3 -2
- package/packages/bundled-features/src/tier-engine/changes.json +18 -0
- package/packages/bundled-features/src/tier-engine/constants.ts +9 -0
- package/packages/bundled-features/src/tier-engine/entity.ts +9 -4
- package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +8 -5
- package/packages/bundled-features/src/tier-engine/index.ts +2 -0
- package/packages/bundled-features/src/user/changes.json +6 -0
- package/packages/bundled-features/src/user/constants.ts +1 -0
- package/packages/bundled-features/src/user/invited-global-roles.ts +2 -4
- package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
- package/packages/bundled-features/src/user/seeding.ts +19 -5
- package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +0 -1
- package/packages/bundled-features/src/user-data-rights/i18n.ts +5 -0
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +13 -2
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +7 -1
- package/packages/bundled-features/src/user-profile/changes.json +1 -1
- package/packages/bundled-features/src/waitlist/changes.json +18 -0
- package/packages/bundled-features/src/waitlist/feature.ts +1 -1
- package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +35 -3
- package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +2 -17
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +33 -9
- package/packages/bundled-features/src/waitlist/i18n.ts +9 -0
- package/packages/bundled-features/src/waitlist/lib.ts +20 -0
- package/packages/bundled-features/src/waitlist/options.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +2 -1
- package/packages/bundled-features/src/workflow-runner/feature.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
- package/samples/apps/ui-walkthrough/package.json +9 -0
- package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +87 -2
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +246 -30
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +4 -0
- package/samples/apps/ui-walkthrough/src/run-config.ts +18 -1
- package/samples/apps/use-all-bundled/README.md +3 -13
- package/samples/apps/use-all-bundled/feature-manifest.json +7 -5
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +34 -118
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +1 -1
- package/samples/apps/use-all-bundled/src/run-config.ts +0 -3
- package/samples/recipes/auth-mfa/README.md +2 -1
- package/samples/recipes/auth-mfa/src/feature.ts +1 -0
- package/samples/recipes/custom-handlers/README.md +4 -3
- package/samples/recipes/custom-handlers/src/feature.ts +21 -12
- package/samples/recipes/dashboard-stats/README.md +38 -0
- package/samples/recipes/dashboard-stats/package.json +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +104 -0
- package/samples/recipes/legal-pages/README.md +2 -2
- package/samples/recipes/managed-pages/README.md +2 -2
- package/samples/recipes/pdf-audit-export/README.md +2 -1
- package/samples/recipes/pdf-audit-export/package.json +1 -1
- package/samples/recipes/pdf-audit-export/src/feature.ts +68 -48
- package/samples/recipes/workflow-engine/src/db/queries/suspended-runs.ts +2 -2
- package/samples/apps/use-all-bundled/schema/generate.ts +0 -617
- package/samples/apps/use-all-bundled/schema/migrations/meta/0000_snapshot.json +0 -3055
- package/samples/apps/use-all-bundled/schema/migrations/meta/_journal.json +0 -13
- package/samples/apps/use-all-bundled/schema/schema.generated.ts +0 -94
- package/samples/apps/use-all-bundled/schema/schema.ts +0 -9
package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// hand-picked priceId.
|
|
24
24
|
|
|
25
25
|
import type { HandlerContext, WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
26
|
-
import { UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
26
|
+
import { InternalError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
27
27
|
import * as z from "zod";
|
|
28
28
|
import {
|
|
29
29
|
assertCheckoutAllowed,
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
startProviderCheckout,
|
|
33
33
|
} from "../checkout-core.js";
|
|
34
34
|
import {
|
|
35
|
+
assertValidOrderItem,
|
|
35
36
|
type ConsentPayload,
|
|
36
37
|
consentCheckoutFields,
|
|
37
38
|
consentPayloadSchema,
|
|
@@ -104,11 +105,12 @@ export function createCheckoutSessionHandler(
|
|
|
104
105
|
|
|
105
106
|
const mode = payload.mode ?? "subscription";
|
|
106
107
|
const plugin = await assertCheckoutAllowed(ctx, openOptions, checkoutInput);
|
|
107
|
-
const price = await lookupPrice(ctx, plugin, payload.priceId);
|
|
108
|
+
const price = await lookupPrice(ctx, plugin, payload.providerName, payload.priceId);
|
|
108
109
|
const orderItem =
|
|
109
110
|
mode === "payment"
|
|
110
111
|
? await consumerProtection.oneOffItemLabel?.(ctx, payload.priceId)
|
|
111
112
|
: undefined;
|
|
113
|
+
assertValidOrderItem(orderItem, payload.priceId);
|
|
112
114
|
const consent = await prepareConsent(ctx, consumerProtection, payload.consent, mode);
|
|
113
115
|
const result = await startProviderCheckout(ctx, plugin, {
|
|
114
116
|
...checkoutInput,
|
|
@@ -134,11 +136,17 @@ export function createCheckoutSessionHandler(
|
|
|
134
136
|
async function lookupPrice(
|
|
135
137
|
ctx: HandlerContext,
|
|
136
138
|
plugin: SubscriptionProviderPlugin,
|
|
139
|
+
providerName: string,
|
|
137
140
|
priceId: string,
|
|
138
141
|
): Promise<ProviderPrice> {
|
|
142
|
+
if (!plugin.retrievePrices) {
|
|
143
|
+
throw new InternalError({
|
|
144
|
+
message: `billing-foundation: provider "${providerName}" lacks retrievePrices, required by consumerProtection`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
139
147
|
let price: ProviderPrice | undefined;
|
|
140
148
|
try {
|
|
141
|
-
const prices =
|
|
149
|
+
const prices = await plugin.retrievePrices(ctx, [priceId]);
|
|
142
150
|
price = prices.find((p) => p.priceId === priceId);
|
|
143
151
|
} catch (cause) {
|
|
144
152
|
throw priceUnavailable(priceId, cause);
|
|
@@ -21,6 +21,7 @@ import * as z from "zod";
|
|
|
21
21
|
import { subscriptionAggregateId } from "../aggregate-id.js";
|
|
22
22
|
import {
|
|
23
23
|
isSubscriptionBlockingCheckout,
|
|
24
|
+
isSwitchableSubscriptionStatus,
|
|
24
25
|
SubscriptionEventTypes,
|
|
25
26
|
SubscriptionStatuses,
|
|
26
27
|
} from "../constants.js";
|
|
@@ -199,19 +200,24 @@ export async function appendSubscriptionEvent(
|
|
|
199
200
|
// Tenant-Admin direkt.
|
|
200
201
|
// One subscription aggregate exists per tenant, but a tenant can legitimately hold two provider
|
|
201
202
|
// subscriptions over time (a stale `incomplete` one superseded by a fresh checkout). A late or
|
|
202
|
-
// retried event of the old subscription must not overwrite the row of the live one, so
|
|
203
|
-
//
|
|
203
|
+
// retried event of the old subscription must not overwrite the row of the live one, so an event of a
|
|
204
|
+
// different provider subscription only replaces a row that no longer blocks a checkout. The exception
|
|
205
|
+
// is a `created` of a subscription that is itself live (a provider switch); a late `created` of a
|
|
206
|
+
// never-completed `incomplete` one stays stale, since delivery order is not guaranteed.
|
|
204
207
|
function isStaleSubscriptionEvent(
|
|
205
208
|
current: SubscriptionView | null,
|
|
206
209
|
payload: ProcessEventPayload,
|
|
207
210
|
now: Temporal.Instant,
|
|
208
211
|
): boolean {
|
|
209
212
|
if (current === null) return false;
|
|
210
|
-
if (payload.type === SubscriptionEventTypes.created) return false;
|
|
211
213
|
const sameSubscription =
|
|
212
214
|
current.providerName === payload.providerName &&
|
|
213
215
|
current.providerSubscriptionId === payload.providerSubscriptionId;
|
|
214
|
-
|
|
216
|
+
if (sameSubscription || !isSubscriptionBlockingCheckout(current, now)) return false;
|
|
217
|
+
const supersedesCurrent =
|
|
218
|
+
payload.type === SubscriptionEventTypes.created &&
|
|
219
|
+
isSwitchableSubscriptionStatus(payload.status);
|
|
220
|
+
return !supersedesCurrent;
|
|
215
221
|
}
|
|
216
222
|
|
|
217
223
|
export function createProcessEventHandler(now: () => Temporal.Instant): WriteHandlerDef {
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
type LocalizedString = { readonly en: string };
|
|
5
5
|
|
|
6
6
|
export const BILLING_FOUNDATION_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
7
|
+
"billing-foundation:feature.label": { en: "Billing" },
|
|
8
|
+
"billing-foundation:notify.contract-confirmation.label": { en: "Contract confirmation" },
|
|
9
|
+
"billing-foundation:notify.contract-termination-receipt.label": { en: "Termination receipt" },
|
|
10
|
+
"billing-foundation:notify.contract-termination-operator-notice.label": {
|
|
11
|
+
en: "Termination notice to operator",
|
|
12
|
+
},
|
|
7
13
|
"screen:billing-plans.title": { en: "Billing" },
|
|
8
14
|
"billing-foundation.plans.billingDisabled": {
|
|
9
15
|
en: "Billing is not active yet. Your current plan stays in place.",
|
|
@@ -95,6 +101,9 @@ export const BILLING_FOUNDATION_I18N: Readonly<Record<string, LocalizedString>>
|
|
|
95
101
|
"billing-foundation.errors.redirectOriginNotAllowed": {
|
|
96
102
|
en: "This redirect URL is not allowed for this app.",
|
|
97
103
|
},
|
|
104
|
+
"billing-foundation.errors.providerCustomerUnknown": {
|
|
105
|
+
en: "The billing account for this subscription no longer exists. Subscribe again to continue.",
|
|
106
|
+
},
|
|
98
107
|
"billing-foundation.errors.providerHasNoPriceCatalog": {
|
|
99
108
|
en: "This provider has no known prices configured.",
|
|
100
109
|
},
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// so it is now a factory parameter.
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
+
isTierAssignmentSource,
|
|
8
|
+
type TierAssignmentSource,
|
|
7
9
|
TierAssignmentSources,
|
|
8
10
|
TierEngineHandlers,
|
|
9
11
|
TierEngineQueries,
|
|
@@ -38,6 +40,10 @@ export type SubscriptionTierSyncDeps<TTier extends string> = {
|
|
|
38
40
|
readonly onSyncError?: "log" | "fail-webhook";
|
|
39
41
|
};
|
|
40
42
|
|
|
43
|
+
// A manual grant that differs from what billing would assign is reported with this
|
|
44
|
+
// code but never fails the webhook: a provider retry cannot resolve it.
|
|
45
|
+
export const TIER_SYNC_MANUAL_GRANT_SKIPPED = "tier_sync_manual_grant_skipped";
|
|
46
|
+
|
|
41
47
|
export function effectiveTierFromSubscription<TTier extends string>(
|
|
42
48
|
status: string | undefined,
|
|
43
49
|
tier: string | undefined,
|
|
@@ -63,7 +69,7 @@ type ExistingTierAssignment = {
|
|
|
63
69
|
readonly id: string;
|
|
64
70
|
readonly version: number;
|
|
65
71
|
readonly tier: unknown;
|
|
66
|
-
readonly source:
|
|
72
|
+
readonly source: TierAssignmentSource | null;
|
|
67
73
|
};
|
|
68
74
|
|
|
69
75
|
function findTierAssignment(
|
|
@@ -74,7 +80,13 @@ function findTierAssignment(
|
|
|
74
80
|
if (!row || typeof row["id"] !== "string" || typeof row["version"] !== "number") {
|
|
75
81
|
return undefined;
|
|
76
82
|
}
|
|
77
|
-
|
|
83
|
+
const source = row["source"];
|
|
84
|
+
return {
|
|
85
|
+
id: row["id"],
|
|
86
|
+
version: row["version"],
|
|
87
|
+
tier: row["tier"],
|
|
88
|
+
source: isTierAssignmentSource(source) ? source : null,
|
|
89
|
+
};
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
export function createSubscriptionTierSync<TTier extends string>(
|
|
@@ -82,7 +94,7 @@ export function createSubscriptionTierSync<TTier extends string>(
|
|
|
82
94
|
) {
|
|
83
95
|
async function syncTierFromSubscription(
|
|
84
96
|
tenantId: TenantId,
|
|
85
|
-
routeDeps: SignatureExtraRouteDeps,
|
|
97
|
+
routeDeps: Pick<SignatureExtraRouteDeps, "dispatchSystemQuery" | "dispatchSystemWrite">,
|
|
86
98
|
): Promise<{ code: string; message: string } | null> {
|
|
87
99
|
try {
|
|
88
100
|
const subscriptionId = subscriptionAggregateId(tenantId);
|
|
@@ -129,8 +141,13 @@ export function createSubscriptionTierSync<TTier extends string>(
|
|
|
129
141
|
}
|
|
130
142
|
return null;
|
|
131
143
|
}
|
|
132
|
-
if (assignment.source === TierAssignmentSources.manual) return null;
|
|
133
144
|
if (assignment.tier === effective) return null;
|
|
145
|
+
if (assignment.source === TierAssignmentSources.manual) {
|
|
146
|
+
return {
|
|
147
|
+
code: TIER_SYNC_MANUAL_GRANT_SKIPPED,
|
|
148
|
+
message: `manual tier grant "${String(assignment.tier)}" kept although the subscription yields "${effective}"`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
134
151
|
|
|
135
152
|
const result = await routeDeps.dispatchSystemWrite({
|
|
136
153
|
handlerQn: TierEngineHandlers.update,
|
|
@@ -169,9 +186,12 @@ export function createSubscriptionTierSync<TTier extends string>(
|
|
|
169
186
|
if (syncError) {
|
|
170
187
|
// biome-ignore lint/suspicious/noConsole: operator visibility for a post-commit sync failure
|
|
171
188
|
console.warn(
|
|
172
|
-
`[subscription-tier-sync] tier sync
|
|
189
|
+
`[subscription-tier-sync] tier sync for tenant ${tenantId} after successful webhook write: ${syncError.code} ${syncError.message}`,
|
|
173
190
|
);
|
|
174
|
-
if (
|
|
191
|
+
if (
|
|
192
|
+
deps.onSyncError === "fail-webhook" &&
|
|
193
|
+
syncError.code !== TIER_SYNC_MANUAL_GRANT_SKIPPED
|
|
194
|
+
) {
|
|
175
195
|
return {
|
|
176
196
|
isSuccess: false,
|
|
177
197
|
error: {
|
|
@@ -188,5 +208,5 @@ export function createSubscriptionTierSync<TTier extends string>(
|
|
|
188
208
|
});
|
|
189
209
|
}
|
|
190
210
|
|
|
191
|
-
return { createWebhookRoute };
|
|
211
|
+
return { createWebhookRoute, syncTierFromSubscription };
|
|
192
212
|
}
|
|
@@ -396,7 +396,10 @@ export function isLegalLinkSet(
|
|
|
396
396
|
|
|
397
397
|
/** Consumer-protection consent gate (§ 312j / § 356 BGB style). When set,
|
|
398
398
|
* `start-plan-checkout` and `create-checkout-session` require a `consent`
|
|
399
|
-
* payload and record it as an event before the buyer reaches the provider.
|
|
399
|
+
* payload and record it as an event before the buyer reaches the provider.
|
|
400
|
+
* Every provider used with `create-checkout-session` must implement
|
|
401
|
+
* `retrievePrices`: the live price is part of the consent record, and a
|
|
402
|
+
* provider without it fails the checkout with an InternalError. */
|
|
400
403
|
export type ConsumerProtectionOptions = {
|
|
401
404
|
/** Slug of the template-resolver `text-block` holding the terms text; its
|
|
402
405
|
* content hash is stored with every recorded consent. */
|
|
@@ -411,7 +414,9 @@ export type ConsumerProtectionOptions = {
|
|
|
411
414
|
readonly terminationScope?: TerminationScope;
|
|
412
415
|
/** Names the item of a one-off payment (mode "payment") for the consent record
|
|
413
416
|
* and the confirmation mail; the label key is resolved through the registry
|
|
414
|
-
* i18n, so it must be qualified with the registering feature.
|
|
417
|
+
* i18n, so it must be qualified with the registering feature. The label key
|
|
418
|
+
* is 1-200 characters, string params at most 100; a violation fails the
|
|
419
|
+
* checkout with an InternalError before the provider session is created. */
|
|
415
420
|
readonly oneOffItemLabel?: (
|
|
416
421
|
ctx: HandlerContext,
|
|
417
422
|
priceId: string,
|
|
@@ -280,6 +280,12 @@ export type ReadRollingCapUsageOptions = {
|
|
|
280
280
|
readonly windowDays: number;
|
|
281
281
|
};
|
|
282
282
|
|
|
283
|
+
// A release is written up to a reservation TTL after its increment; once the increment left the
|
|
284
|
+
// window the release must not subtract from other increments still inside it.
|
|
285
|
+
function isBefore(isoInstant: string, cutoff: Temporal.Instant): boolean {
|
|
286
|
+
return Temporal.Instant.compare(Temporal.Instant.from(isoInstant), cutoff) < 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
283
289
|
async function sumRollingEventAmounts(
|
|
284
290
|
db: TenantDb,
|
|
285
291
|
tenantId: TenantId,
|
|
@@ -289,20 +295,26 @@ async function sumRollingEventAmounts(
|
|
|
289
295
|
const aggregateId = rollingCapAggregateId(tenantId, options.capName);
|
|
290
296
|
const cutoff = Temporal.Now.instant().subtract({ hours: options.windowDays * 24 });
|
|
291
297
|
|
|
292
|
-
const rows = await db.selectMany<{ payload: { amount?: number } }>(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
298
|
+
const rows = await db.selectMany<{ payload: { amount?: number; incrementedAt?: string } }>(
|
|
299
|
+
eventsTable,
|
|
300
|
+
{
|
|
301
|
+
tenantId,
|
|
302
|
+
aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
|
|
303
|
+
aggregateId,
|
|
304
|
+
type,
|
|
305
|
+
createdAt: { gte: cutoff },
|
|
306
|
+
},
|
|
307
|
+
);
|
|
299
308
|
|
|
300
309
|
let total = 0;
|
|
301
310
|
for (const row of rows) {
|
|
302
311
|
// @cast-boundary engine-payload — events.payload is jsonb (typed as
|
|
303
312
|
// unknown by drizzle's $type<Record<string,unknown>>); narrowing the
|
|
304
313
|
// shape here mirrors enforceRollingCap's own read-side contract.
|
|
305
|
-
const payload = row["payload"] as { amount?: number };
|
|
314
|
+
const payload = row["payload"] as { amount?: number; incrementedAt?: string };
|
|
315
|
+
if (typeof payload.incrementedAt === "string" && isBefore(payload.incrementedAt, cutoff)) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
306
318
|
if (typeof payload.amount === "number") {
|
|
307
319
|
total += payload.amount;
|
|
308
320
|
}
|
|
@@ -44,6 +44,8 @@ export async function reserveEnforcedCalendarCap(
|
|
|
44
44
|
profile: cap.profile,
|
|
45
45
|
notify: cap.notify,
|
|
46
46
|
amount: cap.amount,
|
|
47
|
+
...(cap.periodEndIso !== undefined && { periodEndIso: cap.periodEndIso }),
|
|
48
|
+
...(cap.i18nKey !== undefined && { i18nKey: cap.i18nKey }),
|
|
47
49
|
markSoftWarnedOutsideTransaction: outsideTransaction,
|
|
48
50
|
});
|
|
49
51
|
}
|
|
@@ -37,6 +37,7 @@ type CapReservationRow = {
|
|
|
37
37
|
readonly periodStartIso: string | null;
|
|
38
38
|
readonly amount: number;
|
|
39
39
|
readonly expiresAt: Temporal.Instant;
|
|
40
|
+
readonly createdAt: Temporal.Instant;
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
const NOTHING_TO_RELEASE: WriteResult = { isSuccess: true, data: {} };
|
|
@@ -136,13 +137,14 @@ function rollingEvent(
|
|
|
136
137
|
amount: number,
|
|
137
138
|
expectedVersion: number,
|
|
138
139
|
type: string,
|
|
140
|
+
extraPayload: { readonly incrementedAt?: string } = {},
|
|
139
141
|
) {
|
|
140
142
|
return {
|
|
141
143
|
aggregateId: rollingCapAggregateId(user.tenantId, capName),
|
|
142
144
|
aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
|
|
143
145
|
expectedVersion,
|
|
144
146
|
type,
|
|
145
|
-
payload: { capName, amount },
|
|
147
|
+
payload: { capName, amount, ...extraPayload },
|
|
146
148
|
metadata: { userId: user.id },
|
|
147
149
|
};
|
|
148
150
|
}
|
|
@@ -213,7 +215,10 @@ async function giveBackRolling(
|
|
|
213
215
|
);
|
|
214
216
|
await appendEventInTenantDb(
|
|
215
217
|
txDb,
|
|
216
|
-
|
|
218
|
+
// The window filter must use the increment's time: a sweep releases up to a TTL later.
|
|
219
|
+
rollingEvent(user, row.capName, row.amount, version, ROLLING_RELEASED_EVENT_QN, {
|
|
220
|
+
incrementedAt: row.createdAt.toString(),
|
|
221
|
+
}),
|
|
217
222
|
);
|
|
218
223
|
return NOTHING_TO_RELEASE;
|
|
219
224
|
}
|
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "Handlers wrapped with withCapEnforcement or withRollingCapEnforcement are rejected when called through a nested ctx.write",
|
|
6
|
+
"detail": "Since the wrappers reserve through reserveBeforeTransaction, the dispatcher rejects a nested ctx.write, ctx.writeAs or handler-to-handler call of such a handler with an internal error (\"has a pre-transaction phase ... cannot run as a nested write\") instead of running it. Before the reservation lifecycle change the wrapped handler ran normally when called nested.",
|
|
7
|
+
"migration": "Dispatch rolling- and calendar-capped handlers only as top-level commands (HTTP write or batch). A handler that calls one through ctx.write declares the cap on itself instead, or the call moves out of the handler into a client or job that dispatches a top-level command."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.365.0",
|
|
11
|
+
"type": "fix",
|
|
12
|
+
"title": "Rolling cap release remembers the original booking time",
|
|
13
|
+
"detail": "rolling-released events now carry the incrementedAt of the booking they return, so readRollingCapUsage no longer subtracts a release whose increment already left the rolling window."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"version": "0.364.0",
|
|
17
|
+
"type": "breaking",
|
|
18
|
+
"title": "CapExceededError carries resetAt and retryAfterSeconds, API sets Retry-After",
|
|
19
|
+
"migration": "Periodic caps no longer need RateLimitError({ windowSeconds: 0, ... }). Where you catch CapExceededError from enforceCap and rethrow it as RateLimitError, drop the catch and let CapExceededError pass: the response stays 429 with a correct Retry-After, error.code becomes cap_exceeded instead of rate_limited, and error.details carries resetAt and retryAfterSeconds. Keep a feature-specific message with enforceCap({ ..., i18nKey }) and pass a period end other than the calendar month with periodEndIso (e.g. a budget's resetsAtIso). Clients matching on rate_limited for these caps must also accept cap_exceeded. Any KumikoError with retryAfterSeconds (including RateLimitError thrown in a handler) now gets a Retry-After header from /api/write, /api/batch and /api/query."
|
|
20
|
+
},
|
|
2
21
|
{
|
|
3
22
|
"version": "0.361.0",
|
|
4
23
|
"type": "improvement",
|
|
@@ -66,14 +66,38 @@ export function assertBelowHardCap(
|
|
|
66
66
|
readonly capName: string;
|
|
67
67
|
readonly limit: number;
|
|
68
68
|
readonly profile: CapToleranceProfileName;
|
|
69
|
+
// Rolling caps have no fixed period, so they pass neither period field.
|
|
70
|
+
readonly periodStartIso?: string;
|
|
71
|
+
readonly periodEndIso?: string;
|
|
72
|
+
readonly i18nKey?: string;
|
|
69
73
|
},
|
|
70
74
|
): void {
|
|
71
75
|
const tolerance = CAP_TOLERANCES[cap.profile];
|
|
72
76
|
if (valueSeenByLastUnit >= cap.limit * tolerance.hard) {
|
|
73
|
-
|
|
77
|
+
const resetAt = resolvePeriodEndIso(cap.periodStartIso, cap.periodEndIso);
|
|
78
|
+
throw new CapExceededError(cap.capName, cap.limit, valueSeenByLastUnit, tolerance, {
|
|
79
|
+
...(resetAt !== undefined && { resetAt }),
|
|
80
|
+
...(cap.i18nKey !== undefined && { i18nKey: cap.i18nKey }),
|
|
81
|
+
});
|
|
74
82
|
}
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
function resolvePeriodEndIso(
|
|
86
|
+
periodStartIso: string | undefined,
|
|
87
|
+
periodEndIso: string | undefined,
|
|
88
|
+
): string | undefined {
|
|
89
|
+
if (periodStartIso === undefined) return periodEndIso;
|
|
90
|
+
if (periodEndIso === undefined) return calendarMonthPeriodEndIso(periodStartIso);
|
|
91
|
+
const start = Temporal.Instant.from(periodStartIso);
|
|
92
|
+
const end = Temporal.Instant.from(periodEndIso);
|
|
93
|
+
if (Temporal.Instant.compare(end, start) <= 0) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`cap-counter: periodEndIso (${periodEndIso}) must be after periodStartIso (${periodStartIso})`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return end.toString();
|
|
99
|
+
}
|
|
100
|
+
|
|
77
101
|
// =============================================================================
|
|
78
102
|
// Enforce-Cap helper
|
|
79
103
|
// =============================================================================
|
|
@@ -109,6 +133,9 @@ export async function enforceCap(
|
|
|
109
133
|
// Units the caller is about to book in one go (default 1), checked with
|
|
110
134
|
// the same outcome as booking them one by one.
|
|
111
135
|
readonly amount?: number;
|
|
136
|
+
// Default: end of the calendar month that starts at periodStartIso.
|
|
137
|
+
readonly periodEndIso?: string;
|
|
138
|
+
readonly i18nKey?: string;
|
|
112
139
|
},
|
|
113
140
|
): Promise<EnforceCapResult> {
|
|
114
141
|
const { user, readDb } = resolveCapCounterScope(ctx, false);
|
|
@@ -211,23 +238,54 @@ export async function enforceRollingCap(
|
|
|
211
238
|
* Thrown by enforceCap/enforceRollingCap when value ≥ hard-threshold.
|
|
212
239
|
* Extends `KumikoError` — the dispatcher auto-maps this to HTTP 429
|
|
213
240
|
* with `code: "cap_exceeded"`, no caller-side catch needed.
|
|
241
|
+
*
|
|
242
|
+
* Calendar caps carry `resetAt` (end of the period) and `retryAfterSeconds`, which the API
|
|
243
|
+
* answers as Retry-After. Rolling caps have no fixed reset instant, so both stay undefined.
|
|
214
244
|
*/
|
|
215
245
|
export class CapExceededError extends KumikoError {
|
|
216
246
|
readonly code = "cap_exceeded" as const;
|
|
217
247
|
readonly httpStatus = 429;
|
|
248
|
+
readonly resetAt: string | undefined;
|
|
218
249
|
|
|
219
250
|
constructor(
|
|
220
251
|
readonly capName: string,
|
|
221
252
|
readonly limit: number,
|
|
222
253
|
readonly currentValue: number,
|
|
223
254
|
readonly tolerance: CapToleranceProfile,
|
|
255
|
+
options?: {
|
|
256
|
+
readonly resetAt?: string;
|
|
257
|
+
readonly i18nKey?: string;
|
|
258
|
+
readonly cause?: Error;
|
|
259
|
+
// Deterministic tests only.
|
|
260
|
+
readonly now?: Temporal.Instant;
|
|
261
|
+
},
|
|
224
262
|
) {
|
|
263
|
+
const resetAt = options?.resetAt;
|
|
264
|
+
const retryAfterSeconds =
|
|
265
|
+
resetAt === undefined
|
|
266
|
+
? undefined
|
|
267
|
+
: Math.max(
|
|
268
|
+
0,
|
|
269
|
+
Math.ceil(
|
|
270
|
+
Temporal.Instant.from(resetAt).epochMilliseconds / 1000 -
|
|
271
|
+
(options?.now ?? Temporal.Now.instant()).epochMilliseconds / 1000,
|
|
272
|
+
),
|
|
273
|
+
);
|
|
225
274
|
super({
|
|
226
|
-
message: `Cap "${capName}" exceeded: current=${currentValue}, limit=${limit}, hard-threshold=${limit * tolerance.hard}. Upgrade tier or wait for next period reset
|
|
227
|
-
i18nKey: "errors.cap.exceeded",
|
|
228
|
-
i18nParams: { capName, limit, currentValue },
|
|
229
|
-
details: {
|
|
275
|
+
message: `Cap "${capName}" exceeded: current=${currentValue}, limit=${limit}, hard-threshold=${limit * tolerance.hard}. Upgrade tier or wait for next period reset.${resetAt === undefined ? "" : ` Resets at ${resetAt}.`}`,
|
|
276
|
+
i18nKey: options?.i18nKey ?? "errors.cap.exceeded",
|
|
277
|
+
i18nParams: { capName, limit, currentValue, ...(resetAt !== undefined && { resetAt }) },
|
|
278
|
+
details: {
|
|
279
|
+
capName,
|
|
280
|
+
limit,
|
|
281
|
+
currentValue,
|
|
282
|
+
tolerance,
|
|
283
|
+
...(resetAt !== undefined && { resetAt, retryAfterSeconds }),
|
|
284
|
+
},
|
|
285
|
+
...(retryAfterSeconds !== undefined && { retryAfterSeconds }),
|
|
286
|
+
...(options?.cause && { cause: options.cause }),
|
|
230
287
|
});
|
|
288
|
+
this.resetAt = resetAt;
|
|
231
289
|
}
|
|
232
290
|
}
|
|
233
291
|
|
|
@@ -257,6 +315,14 @@ export function currentCalendarMonthStartIso(
|
|
|
257
315
|
return start.toInstant().toString();
|
|
258
316
|
}
|
|
259
317
|
|
|
318
|
+
export function calendarMonthPeriodEndIso(periodStartIso: string): string {
|
|
319
|
+
return Temporal.Instant.from(periodStartIso)
|
|
320
|
+
.toZonedDateTimeISO("UTC")
|
|
321
|
+
.add({ months: 1 })
|
|
322
|
+
.toInstant()
|
|
323
|
+
.toString();
|
|
324
|
+
}
|
|
325
|
+
|
|
260
326
|
// =============================================================================
|
|
261
327
|
// Notification-Wiring helpers — convenience-wrapper für enforceCap +
|
|
262
328
|
// enforceRollingCap, die einen Caller-supplied delivery-emit beim
|
|
@@ -299,6 +365,8 @@ export async function enforceCapAndMaybeNotify(
|
|
|
299
365
|
readonly profile: CapToleranceProfileName;
|
|
300
366
|
readonly notify: SoftHitNotifier;
|
|
301
367
|
readonly amount?: number;
|
|
368
|
+
readonly periodEndIso?: string;
|
|
369
|
+
readonly i18nKey?: string;
|
|
302
370
|
readonly markSoftWarnedOutsideTransaction?: boolean;
|
|
303
371
|
},
|
|
304
372
|
): Promise<EnforceCapResult> {
|
|
@@ -308,6 +376,8 @@ export async function enforceCapAndMaybeNotify(
|
|
|
308
376
|
limit: options.limit,
|
|
309
377
|
profile: options.profile,
|
|
310
378
|
...(options.amount !== undefined && { amount: options.amount }),
|
|
379
|
+
...(options.periodEndIso !== undefined && { periodEndIso: options.periodEndIso }),
|
|
380
|
+
...(options.i18nKey !== undefined && { i18nKey: options.i18nKey }),
|
|
311
381
|
});
|
|
312
382
|
|
|
313
383
|
if (result.state === "soft-hit" && result.crossed) {
|
|
@@ -40,6 +40,8 @@ export const rollingIncrementedSchema = incrementRollingSchema;
|
|
|
40
40
|
/** Payload of the event that gives a reserved rolling amount back. */
|
|
41
41
|
export const rollingReleasedSchema = incrementRollingSchema.extend({
|
|
42
42
|
amount: z.number().int().positive(),
|
|
43
|
+
/** When the released increment was booked; absent on events written before it was recorded. */
|
|
44
|
+
incrementedAt: z.string().optional(),
|
|
43
45
|
});
|
|
44
46
|
|
|
45
47
|
// Rolling-Increment-Handler — append-only. Race-frei: zwei parallele
|
|
@@ -53,6 +53,9 @@ import { chainReservations } from "./reservation-chain.js";
|
|
|
53
53
|
export type CalendarCapDef = {
|
|
54
54
|
readonly capName: string;
|
|
55
55
|
readonly periodStartIso: string;
|
|
56
|
+
// Default: end of the calendar month that starts at periodStartIso.
|
|
57
|
+
readonly periodEndIso?: string;
|
|
58
|
+
readonly i18nKey?: string;
|
|
56
59
|
readonly limit: number;
|
|
57
60
|
readonly profile: CapToleranceProfileName;
|
|
58
61
|
/** Increment-amount post-success. Default 1. */
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Cap overview lists no destroyed tenants",
|
|
6
|
+
"detail": "tenant-options and tenant-caps-list now filter destroyed tenants and keep reading tenant names through the PII-aware path."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.338.0",
|
|
4
10
|
"type": "breaking",
|
|
@@ -176,7 +176,8 @@ export function createTenantCapsListQuery(caps: readonly CapSpec[], listCaps: re
|
|
|
176
176
|
);
|
|
177
177
|
|
|
178
178
|
const tenants = await decryptTenantNames(
|
|
179
|
-
|
|
179
|
+
// Destroyed tenants decrypt to the erased sentinel, which a picker or list would show verbatim.
|
|
180
|
+
await selectMany<TenantRow>(db, tenantTable, { destroyedAt: null }),
|
|
180
181
|
"cap-overview:tenant-caps:list",
|
|
181
182
|
);
|
|
182
183
|
const assignments = await selectMany<TierAssignmentRow>(db, tierAssignmentTable, {});
|
|
@@ -23,7 +23,8 @@ export const tenantOptionsQuery = defineQueryHandler({
|
|
|
23
23
|
"cap-overview:tenant-options — SystemAdmin dashboard tenant-filter options",
|
|
24
24
|
);
|
|
25
25
|
const tenants = await decryptTenantNames(
|
|
26
|
-
|
|
26
|
+
// Destroyed tenants decrypt to the erased sentinel, which the picker would show verbatim.
|
|
27
|
+
await selectMany<TenantRow>(db, tenantTable, { destroyedAt: null }),
|
|
27
28
|
"cap-overview:tenant-options",
|
|
28
29
|
);
|
|
29
30
|
const sorted = [...tenants].sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
|
|
9
9
|
export type DiscordChannelOptions = ChatWebhookChannelOptions;
|
|
10
10
|
|
|
11
|
+
const DISCORD_WEBHOOK_PREFIX = "channel-discord:webhooks.";
|
|
12
|
+
|
|
11
13
|
export const DISCORD_SECRET_KEYS = {
|
|
12
|
-
webhookPrefix:
|
|
13
|
-
webhookKeyFor: (connection: string) =>
|
|
14
|
+
webhookPrefix: DISCORD_WEBHOOK_PREFIX,
|
|
15
|
+
webhookKeyFor: (connection: string) => `${DISCORD_WEBHOOK_PREFIX}${connection}`,
|
|
14
16
|
} as const;
|
|
15
17
|
|
|
16
18
|
export const DISCORD_DEFAULT_ALLOWED_HOSTS = ["discord.com", "discordapp.com"] as const;
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "SMTP display names drop address-like characters, inline images get matching extensions",
|
|
6
|
+
"detail": "Control characters, angle brackets and at signs in a From display name are replaced by spaces so it cannot smuggle a second address; inline image file names derive their extension from the shared image format list."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.365.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "List-Unsubscribe accepts an app-owned unsubscribe URL",
|
|
12
|
+
"detail": "When unsubscribeUrl in the mail data points to an http(s) URL outside the framework unsubscribe route, the email channel sets List-Unsubscribe to that URL without the one-click List-Unsubscribe-Post header, because the framework cannot vouch for a foreign endpoint accepting POST. Other schemes set no header."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.365.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "A mail whose file attachment is missing fails at once instead of retrying",
|
|
18
|
+
"detail": "A missing or foreign-tenant file ref ends the delivery attempt as failed (send_failed) without a retry. The send job no longer throws, so the idempotency key is released and the caller can resend. A template test mail whose template file is missing now answers not_sent instead of channel_error."
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"version": "0.363.0",
|
|
4
22
|
"type": "breaking",
|