@cosmicdrift/kumiko-bundled-features 0.338.0 → 0.339.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.
Files changed (153) hide show
  1. package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +2 -2
  2. package/dist/auth-email-password/handlers/invite-accept.write.js +2 -4
  3. package/dist/auth-email-password/handlers/invite-info.query.js +3 -1
  4. package/dist/auth-email-password/handlers/invite-signup-complete.write.js +2 -4
  5. package/dist/auth-email-password/handlers/signup-confirm.write.js +1 -1
  6. package/dist/auth-email-password/handlers/signup-request.write.js +1 -1
  7. package/dist/auth-email-password/web/auth-gate.d.ts +12 -2
  8. package/dist/auth-email-password/web/auth-gate.js +25 -1
  9. package/dist/auth-email-password/web/auth-redirect.d.ts +5 -0
  10. package/dist/auth-email-password/web/auth-redirect.js +58 -0
  11. package/dist/auth-email-password/web/client-plugin.d.ts +8 -1
  12. package/dist/auth-email-password/web/client-plugin.js +2 -0
  13. package/dist/auth-email-password/web/index.d.ts +2 -1
  14. package/dist/auth-email-password/web/index.js +1 -0
  15. package/dist/auth-email-password/web/session.d.ts +3 -1
  16. package/dist/auth-email-password/web/session.js +8 -2
  17. package/dist/auth-mfa/feature.js +1 -1
  18. package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -1
  19. package/dist/auth-mfa/handlers/enable-start-preauth.write.js +1 -1
  20. package/dist/auth-mfa/handlers/verify.write.js +1 -1
  21. package/dist/auth-mfa/mfa-code-verifier.js +1 -1
  22. package/dist/auth-mfa/mfa-status-checker.js +1 -1
  23. package/dist/billing-foundation/constants.d.ts +7 -0
  24. package/dist/billing-foundation/constants.js +8 -0
  25. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +3 -3
  26. package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +3 -3
  27. package/dist/billing-foundation/consumer-protection/consent-gate.js +10 -2
  28. package/dist/billing-foundation/consumer-protection/consent-text.js +2 -2
  29. package/dist/billing-foundation/consumer-protection/register-termination.d.ts +1 -1
  30. package/dist/billing-foundation/consumer-protection/register-termination.js +31 -3
  31. package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +3 -1
  32. package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -1
  33. package/dist/billing-foundation/consumer-protection/termination-notify.d.ts +18 -0
  34. package/dist/billing-foundation/consumer-protection/termination-notify.js +21 -0
  35. package/dist/billing-foundation/consumer-protection/termination-pages.js +31 -2
  36. package/dist/billing-foundation/consumer-protection/termination-record.d.ts +24 -2
  37. package/dist/billing-foundation/consumer-protection/termination-record.js +102 -34
  38. package/dist/billing-foundation/consumer-protection/termination-request.js +10 -42
  39. package/dist/billing-foundation/consumer-protection/termination-texts.js +20 -20
  40. package/dist/billing-foundation/events.d.ts +19 -0
  41. package/dist/billing-foundation/events.js +12 -0
  42. package/dist/billing-foundation/handlers/create-checkout-session.write.js +2 -2
  43. package/dist/billing-foundation/handlers/start-plan-checkout.write.js +4 -2
  44. package/dist/billing-foundation/i18n.js +3 -0
  45. package/dist/billing-foundation/tenant-destroy-hook.js +4 -4
  46. package/dist/billing-foundation/types.d.ts +6 -1
  47. package/dist/billing-foundation/validate-options.js +4 -0
  48. package/dist/channel-discord/discord-channel.d.ts +6 -0
  49. package/dist/channel-discord/discord-channel.js +19 -0
  50. package/dist/channel-discord/feature.d.ts +3 -0
  51. package/dist/channel-discord/feature.js +30 -0
  52. package/dist/channel-discord/index.d.ts +2 -0
  53. package/dist/channel-discord/index.js +2 -0
  54. package/dist/channel-slack/feature.d.ts +3 -0
  55. package/dist/channel-slack/feature.js +30 -0
  56. package/dist/channel-slack/index.d.ts +2 -0
  57. package/dist/channel-slack/index.js +2 -0
  58. package/dist/channel-slack/slack-channel.d.ts +5 -0
  59. package/dist/channel-slack/slack-channel.js +16 -0
  60. package/dist/channel-teams/feature.d.ts +3 -0
  61. package/dist/channel-teams/feature.js +34 -0
  62. package/dist/channel-teams/index.d.ts +2 -0
  63. package/dist/channel-teams/index.js +2 -0
  64. package/dist/channel-teams/teams-channel.d.ts +5 -0
  65. package/dist/channel-teams/teams-channel.js +38 -0
  66. package/dist/channel-telegram/feature.d.ts +3 -0
  67. package/dist/channel-telegram/feature.js +28 -0
  68. package/dist/channel-telegram/index.d.ts +2 -0
  69. package/dist/channel-telegram/index.js +2 -0
  70. package/dist/channel-telegram/telegram-channel.d.ts +8 -0
  71. package/dist/channel-telegram/telegram-channel.js +54 -0
  72. package/dist/crypto-shredding/handlers/forget-subject.write.js +6 -6
  73. package/dist/custom-fields/handlers/clear-custom-field.write.js +1 -1
  74. package/dist/custom-fields/handlers/define-tenant-field.write.js +1 -1
  75. package/dist/custom-fields/handlers/set-custom-field.write.js +1 -1
  76. package/dist/custom-fields/wire-user-data-rights.js +1 -1
  77. package/dist/data-retention/feature.js +1 -1
  78. package/dist/delivery/channel-context.d.ts +2 -1
  79. package/dist/delivery/channel-context.js +2 -2
  80. package/dist/delivery/chat-webhook-channel.d.ts +19 -0
  81. package/dist/delivery/chat-webhook-channel.js +48 -0
  82. package/dist/delivery/chat-webhook-sender.d.ts +21 -0
  83. package/dist/delivery/chat-webhook-sender.js +102 -0
  84. package/dist/delivery/delivery-service.js +3 -0
  85. package/dist/delivery/index.d.ts +2 -0
  86. package/dist/delivery/index.js +2 -0
  87. package/dist/delivery/jobs.js +1 -1
  88. package/dist/delivery/testing.d.ts +2 -0
  89. package/dist/delivery/types.d.ts +3 -1
  90. package/dist/delivery/types.js +1 -2
  91. package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
  92. package/dist/files-tenant-data/index.js +1 -1
  93. package/dist/form-draft/feature.js +1 -1
  94. package/dist/form-draft/handlers/discard.write.js +1 -1
  95. package/dist/form-draft/handlers/save.write.js +1 -1
  96. package/dist/inbound-mail-foundation/connect-routes.d.ts +7 -0
  97. package/dist/inbound-mail-foundation/connect-routes.js +13 -8
  98. package/dist/inbound-mail-foundation/feature.js +1 -1
  99. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +1 -1
  100. package/dist/inbound-mail-foundation/index.d.ts +1 -0
  101. package/dist/inbound-mail-foundation/index.js +1 -0
  102. package/dist/inbound-mail-foundation/oauth-access-token.d.ts +28 -0
  103. package/dist/inbound-mail-foundation/oauth-access-token.js +131 -0
  104. package/dist/inbound-mail-foundation/tenant-destroy-hook.js +1 -1
  105. package/dist/inbound-mail-foundation/types.d.ts +18 -1
  106. package/dist/inbound-mail-foundation/watch-supervisor.d.ts +5 -0
  107. package/dist/inbound-mail-foundation/watch-supervisor.js +42 -14
  108. package/dist/managed-pages/handlers/set.write.js +1 -1
  109. package/dist/notes-history/handlers/add-note.write.js +1 -1
  110. package/dist/renderer-simple/feature.d.ts +8 -2
  111. package/dist/renderer-simple/feature.js +8 -5
  112. package/dist/renderer-simple/index.d.ts +2 -2
  113. package/dist/renderer-simple/index.js +1 -1
  114. package/dist/renderer-simple/simple-renderer.d.ts +15 -0
  115. package/dist/renderer-simple/simple-renderer.js +74 -27
  116. package/dist/secrets/feature.js +1 -1
  117. package/dist/sessions/feature.js +1 -1
  118. package/dist/sessions/handlers/revoke-all-for-user.write.js +1 -1
  119. package/dist/tenant-handover/handlers/claim.write.js +1 -2
  120. package/dist/tenant-lifecycle/feature.js +1 -1
  121. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +3 -1
  122. package/dist/tenant-lifecycle/handlers/request-destruction.write.js +1 -1
  123. package/dist/tier-engine/feature.js +1 -1
  124. package/dist/tier-engine/handlers/get-tenant-tier.query.js +1 -1
  125. package/dist/tier-engine/handlers/set-tenant-tier.write.js +1 -1
  126. package/dist/user-data-rights/feature.js +2 -2
  127. package/dist/user-data-rights/handlers/cancel-deletion.write.js +1 -1
  128. package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
  129. package/dist/user-data-rights/handlers/download-by-job.query.js +1 -1
  130. package/dist/user-data-rights/handlers/download-by-token.query.js +1 -1
  131. package/dist/user-data-rights/handlers/export-status.query.js +1 -1
  132. package/dist/user-data-rights/handlers/lift-restriction.write.js +2 -4
  133. package/dist/user-data-rights/handlers/my-audit-log.query.js +1 -1
  134. package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +1 -1
  135. package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
  136. package/dist/user-data-rights/handlers/request-export.write.js +1 -1
  137. package/dist/user-data-rights/handlers/restrict-account.write.js +2 -2
  138. package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +1 -1
  139. package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
  140. package/dist/workflow-runner/event-subscriber.js +6 -2
  141. package/dist/workflow-runner/event-trigger.js +5 -0
  142. package/dist/workflow-runner/handlers/resume-run.write.js +147 -59
  143. package/dist/workflow-runner/pending-projection.js +1 -0
  144. package/dist/workflow-runner/runner.d.ts +7 -1
  145. package/dist/workflow-runner/runner.js +1 -1
  146. package/dist/workflow-runner/tables.js +8 -1
  147. package/package.json +41 -9
  148. package/src/auth-email-password/changes.json +7 -0
  149. package/src/billing-foundation/changes.json +34 -0
  150. package/src/delivery/changes.json +7 -0
  151. package/src/inbound-mail-foundation/changes.json +7 -0
  152. package/src/renderer-simple/changes.json +9 -1
  153. package/src/workflow-runner/changes.json +7 -0
@@ -0,0 +1,18 @@
1
+ import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { ProviderCancelOutcome } from "../events.js";
3
+ import type { ConsumerProtectionOptions } from "../types.js";
4
+ import type { ConsentLocale } from "./consent-text.js";
5
+ import { type OperatorNoticeDeclaration } from "./termination-mail.js";
6
+ import type { OperatorNoticeReason } from "./termination-texts.js";
7
+ export type OperatorNoticePlan = {
8
+ readonly locale: ConsentLocale;
9
+ readonly requestId: string;
10
+ readonly receivedAtIso: string;
11
+ readonly channel: "public" | "account";
12
+ readonly declaration: OperatorNoticeDeclaration;
13
+ readonly reasons: readonly OperatorNoticeReason[];
14
+ readonly tenantId?: string;
15
+ readonly providerCancel?: ProviderCancelOutcome;
16
+ };
17
+ export declare function requireNotify(ctx: HandlerContext): NonNullable<HandlerContext["notify"]>;
18
+ export declare function notifyOperator(ctx: HandlerContext, consumerProtection: ConsumerProtectionOptions, plan: OperatorNoticePlan): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
2
+ import { CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE } from "../constants.js";
3
+ import { renderOperatorNotice } from "./termination-mail.js";
4
+ export function requireNotify(ctx) {
5
+ if (!ctx.notify) {
6
+ throw new InternalError({
7
+ message: "billing-foundation:termination: ctx.notify unavailable — the delivery feature must be mounted",
8
+ });
9
+ }
10
+ return ctx.notify;
11
+ }
12
+ export async function notifyOperator(ctx, consumerProtection, plan) {
13
+ await requireNotify(ctx)(CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE, {
14
+ route: { email: consumerProtection.operatorEmail },
15
+ data: renderOperatorNotice({
16
+ ...plan,
17
+ operatorEmail: consumerProtection.operatorEmail,
18
+ }),
19
+ priority: "critical",
20
+ });
21
+ }
@@ -1,6 +1,7 @@
1
1
  // Public § 312k pages (/legal/kuendigen, /legal/cancel): form → review → confirm,
2
2
  // no JavaScript. Every echoed value goes through escapeHtml/escapeHtmlAttr.
3
3
  import { escapeHtml, escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
4
+ import { getCookie } from "hono/cookie";
4
5
  import { securePageHeaders, wrapInLayout } from "../../page-render/index.js";
5
6
  import { SubscriptionFoundationHandlers } from "../constants.js";
6
7
  import { CONTRACT_TERMINATION_DECLARATION_TYPES, CONTRACT_TERMINATION_KINDS } from "../events.js";
@@ -171,14 +172,42 @@ function resultPage(locale, receivedAtIso, requestId) {
171
172
  return htmlResponse(locale, page.resultTitle, body, 200);
172
173
  }
173
174
  const RATE_LIMITED_CODE = "rate_limited";
175
+ const TENANT_HEADER = "x-tenant";
176
+ const TENANT_COOKIE = "kumiko_tenant";
177
+ // Exactly what the anonymous tenant resolution reads. The resolver and its
178
+ // trust rules still decide; the re-entry only sees what a direct /api/write
179
+ // from this client would see. Never the session cookie or Authorization (the
180
+ // handler is anonymous) and never X-Forwarded-For (the resolved clientIp
181
+ // travels as env).
182
+ const FORWARDED_TENANT_HEADERS = [
183
+ "host",
184
+ "x-forwarded-host",
185
+ "x-forwarded-proto",
186
+ TENANT_HEADER,
187
+ ];
188
+ function reentryHeaders(c) {
189
+ const headers = new Headers({ "content-type": "application/json" });
190
+ for (const name of FORWARDED_TENANT_HEADERS) {
191
+ const value = c.req.header(name);
192
+ if (value !== undefined)
193
+ headers.set(name, value);
194
+ }
195
+ const tenantCookie = getCookie(c, TENANT_COOKIE);
196
+ if (tenantCookie !== undefined) {
197
+ headers.set("cookie", `${TENANT_COOKIE}=${encodeURIComponent(tenantCookie)}`);
198
+ }
199
+ return headers;
200
+ }
174
201
  async function submitDeclaration(c, app, clientIp, values, locale) {
175
202
  // deps.write only resolves a session user under /api/*; this page lives
176
203
  // outside it, so it re-enters the dispatcher through the app like the
177
204
  // user-export by-token route. The resolved clientIp as env keeps the
178
- // handler's per-IP rate limit per visitor instead of one shared bucket.
205
+ // handler's per-IP rate limit per visitor instead of one shared bucket;
206
+ // the forwarded host headers let a host-based tenant resolver see this
207
+ // visitor's host.
179
208
  const response = await app.fetch(new Request(`${new URL(c.req.url).origin}/api/write`, {
180
209
  method: "POST",
181
- headers: { "content-type": "application/json" },
210
+ headers: reentryHeaders(c),
182
211
  body: JSON.stringify({
183
212
  type: SubscriptionFoundationHandlers.requestContractTermination,
184
213
  payload: payloadOf(values, locale),
@@ -1,7 +1,7 @@
1
1
  import { type QueryHandlerDef, type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import * as z from "zod";
3
3
  import { type ProviderCancelOutcome } from "../events.js";
4
- import type { ResolvedBillingFoundationOptions } from "../types.js";
4
+ import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from "../types.js";
5
5
  import type { OperatorNoticeReason } from "./termination-texts.js";
6
6
  export declare const recordContractTerminationSchema: z.ZodObject<{
7
7
  requestId: z.ZodString;
@@ -22,6 +22,27 @@ export declare const recordContractTerminationSchema: z.ZodObject<{
22
22
  none: "none";
23
23
  "period-end": "period-end";
24
24
  }>;
25
+ receivedAtIso: z.ZodOptional<z.ZodString>;
26
+ locale: z.ZodOptional<z.ZodEnum<{
27
+ de: "de";
28
+ en: "en";
29
+ }>>;
30
+ }, z.core.$strict>;
31
+ export declare const declareContractTerminationSchema: z.ZodObject<{
32
+ requestId: z.ZodString;
33
+ declarationType: z.ZodEnum<{
34
+ termination: "termination";
35
+ withdrawal: "withdrawal";
36
+ }>;
37
+ terminationKind: z.ZodEnum<{
38
+ extraordinary: "extraordinary";
39
+ ordinary: "ordinary";
40
+ }>;
41
+ receivedAtIso: z.ZodString;
42
+ locale: z.ZodEnum<{
43
+ de: "de";
44
+ en: "en";
45
+ }>;
25
46
  }, z.core.$strict>;
26
47
  export type RecordContractTerminationResult = {
27
48
  readonly receivedAtIso: string;
@@ -47,5 +68,6 @@ export declare const recordUnmatchedContractTerminationSchema: z.ZodObject<{
47
68
  }, z.core.$strict>;
48
69
  export declare const terminableSubscriptionSchema: z.ZodObject<{}, z.core.$strict>;
49
70
  export declare function createTerminableSubscriptionQuery(): QueryHandlerDef;
50
- export declare function createRecordContractTerminationHandler(options: ResolvedBillingFoundationOptions): WriteHandlerDef;
71
+ export declare function createDeclareContractTerminationHandler(): WriteHandlerDef;
72
+ export declare function createRecordContractTerminationHandler(options: ResolvedBillingFoundationOptions, consumerProtection: ConsumerProtectionOptions): WriteHandlerDef;
51
73
  export declare function createRecordUnmatchedContractTerminationHandler(): WriteHandlerDef;
@@ -8,8 +8,10 @@ import * as z from "zod";
8
8
  import { subscriptionAggregateId, terminationUnmatchedAggregateId } from "../aggregate-id.js";
9
9
  import { resolveProviderPlugin } from "../checkout-core.js";
10
10
  import { isTerminalSubscriptionStatus, SubscriptionCancelTimings } from "../constants.js";
11
- import { CONTRACT_TERMINATION_CHANNELS, CONTRACT_TERMINATION_DECLARATION_TYPES, CONTRACT_TERMINATION_KINDS, CONTRACT_TERMINATION_REQUESTED_EVENT_QN, CONTRACT_TERMINATION_UNMATCHED_AGGREGATE_TYPE, CONTRACT_TERMINATION_UNMATCHED_EVENT_QN, SUBSCRIPTION_AGGREGATE_TYPE, } from "../events.js";
11
+ import { CONTRACT_TERMINATION_CHANNELS, CONTRACT_TERMINATION_DECLARATION_TYPES, CONTRACT_TERMINATION_DECLARED_EVENT_QN, CONTRACT_TERMINATION_KINDS, CONTRACT_TERMINATION_REQUESTED_EVENT_QN, CONTRACT_TERMINATION_UNMATCHED_AGGREGATE_TYPE, CONTRACT_TERMINATION_UNMATCHED_EVENT_QN, contractTerminationRequestedPayloadSchema, SUBSCRIPTION_AGGREGATE_TYPE, } from "../events.js";
12
12
  import { getSubscriptionForTenant } from "../get-subscription-for-tenant.js";
13
+ import { CONSENT_LOCALES, FALLBACK_CONSENT_LOCALE } from "./consent-locale.js";
14
+ import { notifyOperator } from "./termination-notify.js";
13
15
  const REQUESTED_CANCEL_TIMINGS = [
14
16
  SubscriptionCancelTimings.periodEnd,
15
17
  SubscriptionCancelTimings.immediately,
@@ -22,6 +24,19 @@ export const recordContractTerminationSchema = z
22
24
  terminationKind: z.enum(CONTRACT_TERMINATION_KINDS),
23
25
  channel: z.enum(CONTRACT_TERMINATION_CHANNELS),
24
26
  when: z.enum(REQUESTED_CANCEL_TIMINGS),
27
+ // The public job passes the time the declaration arrived and the declarant's
28
+ // locale, so the record and a provider-problem notice do not take the job's.
29
+ receivedAtIso: z.string().min(1).optional(),
30
+ locale: z.enum(CONSENT_LOCALES).optional(),
31
+ })
32
+ .strict();
33
+ export const declareContractTerminationSchema = z
34
+ .object({
35
+ requestId: z.string().min(1).max(100),
36
+ declarationType: z.enum(CONTRACT_TERMINATION_DECLARATION_TYPES),
37
+ terminationKind: z.enum(CONTRACT_TERMINATION_KINDS),
38
+ receivedAtIso: z.string().min(1),
39
+ locale: z.enum(CONSENT_LOCALES),
25
40
  })
26
41
  .strict();
27
42
  export const recordUnmatchedContractTerminationSchema = z
@@ -49,7 +64,56 @@ export function createTerminableSubscriptionQuery() {
49
64
  },
50
65
  };
51
66
  }
52
- export function createRecordContractTerminationHandler(options) {
67
+ async function cancelAtProvider(ctx, tenantId, when, receivedAtIso) {
68
+ // skip: a withdrawal over the public channel makes no provider call
69
+ if (when === "none")
70
+ return { providerCancel: "none", effectiveAtIso: null, noticeReason: null };
71
+ const subscription = await getSubscriptionForTenant(ctx, tenantId);
72
+ if (subscription === null || isTerminalSubscriptionStatus(subscription.status)) {
73
+ return { providerCancel: "none", effectiveAtIso: null, noticeReason: "no_active_subscription" };
74
+ }
75
+ const plugin = findPlugin(ctx, subscription.providerName);
76
+ if (!plugin?.cancelSubscription) {
77
+ return { providerCancel: "none", effectiveAtIso: null, noticeReason: "provider_cannot_cancel" };
78
+ }
79
+ try {
80
+ await plugin.cancelSubscription(ctx, {
81
+ providerSubscriptionId: subscription.providerSubscriptionId,
82
+ when,
83
+ });
84
+ }
85
+ catch (error) {
86
+ ctx.log?.warn(`[billing-foundation:record-contract-termination] provider cancel failed for tenant ${tenantId}: ${error instanceof Error ? error.message : String(error)}`);
87
+ return { providerCancel: "none", effectiveAtIso: null, noticeReason: "provider_error" };
88
+ }
89
+ return {
90
+ providerCancel: when,
91
+ effectiveAtIso: when === SubscriptionCancelTimings.immediately
92
+ ? receivedAtIso
93
+ : subscription.currentPeriodEnd.toString(),
94
+ noticeReason: null,
95
+ };
96
+ }
97
+ export function createDeclareContractTerminationHandler() {
98
+ return {
99
+ name: "declare-contract-termination",
100
+ agent: { expose: false },
101
+ schema: declareContractTerminationSchema,
102
+ access: { roles: access.system },
103
+ handler: async (event, ctx) => {
104
+ // @cast-boundary engine-payload — dispatcher-zod-validated payload
105
+ const payload = event.payload;
106
+ const stream = await ctx.fetchForWriting({
107
+ aggregateId: subscriptionAggregateId(event.user.tenantId),
108
+ aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
109
+ });
110
+ const declared = payload;
111
+ await stream.appendOne({ type: CONTRACT_TERMINATION_DECLARED_EVENT_QN, payload: declared });
112
+ return { isSuccess: true, data: { declared: true } };
113
+ },
114
+ };
115
+ }
116
+ export function createRecordContractTerminationHandler(options, consumerProtection) {
53
117
  return {
54
118
  name: "record-contract-termination",
55
119
  agent: { expose: false },
@@ -64,43 +128,30 @@ export function createRecordContractTerminationHandler(options) {
64
128
  message: "billing-foundation: the public channel never cancels at the provider immediately",
65
129
  });
66
130
  }
67
- const receivedAtIso = options.now().toString();
131
+ const receivedAtIso = payload.receivedAtIso ?? options.now().toString();
68
132
  const stream = await ctx.fetchForWriting({
69
133
  aggregateId: subscriptionAggregateId(tenantId),
70
134
  aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
71
135
  });
72
- let providerCancel = "none";
73
- let effectiveAtIso = null;
74
- let noticeReason = null;
75
- if (payload.when !== "none") {
76
- const subscription = await getSubscriptionForTenant(ctx, tenantId);
77
- if (subscription === null || isTerminalSubscriptionStatus(subscription.status)) {
78
- noticeReason = "no_active_subscription";
79
- }
80
- else {
81
- const plugin = findPlugin(ctx, subscription.providerName);
82
- if (!plugin?.cancelSubscription) {
83
- noticeReason = "provider_cannot_cancel";
84
- }
85
- else {
86
- try {
87
- await plugin.cancelSubscription(ctx, {
88
- providerSubscriptionId: subscription.providerSubscriptionId,
89
- when: payload.when,
90
- });
91
- providerCancel = payload.when;
92
- effectiveAtIso =
93
- payload.when === SubscriptionCancelTimings.immediately
94
- ? receivedAtIso
95
- : subscription.currentPeriodEnd.toString();
96
- }
97
- catch (error) {
98
- ctx.log?.warn(`[billing-foundation:record-contract-termination] provider cancel failed for tenant ${tenantId}: ${error instanceof Error ? error.message : String(error)}`);
99
- noticeReason = "provider_error";
100
- }
101
- }
102
- }
136
+ // The public job runs at least once: a second run must not call the
137
+ // provider or append again.
138
+ const alreadyRecorded = stream.events
139
+ .map((e) => e.type === CONTRACT_TERMINATION_REQUESTED_EVENT_QN
140
+ ? contractTerminationRequestedPayloadSchema.safeParse(e.payload).data
141
+ : undefined)
142
+ .find((recordedPayload) => recordedPayload?.requestId === payload.requestId);
143
+ if (alreadyRecorded) {
144
+ return {
145
+ isSuccess: true,
146
+ data: {
147
+ receivedAtIso: alreadyRecorded.receivedAtIso,
148
+ effectiveAtIso: alreadyRecorded.effectiveAtIso,
149
+ providerCancel: alreadyRecorded.providerCancel,
150
+ noticeReason: null,
151
+ },
152
+ };
103
153
  }
154
+ const { providerCancel, effectiveAtIso, noticeReason } = await cancelAtProvider(ctx, tenantId, payload.when, receivedAtIso);
104
155
  const recorded = {
105
156
  requestId: payload.requestId,
106
157
  declarationType: payload.declarationType,
@@ -114,6 +165,23 @@ export function createRecordContractTerminationHandler(options) {
114
165
  type: CONTRACT_TERMINATION_REQUESTED_EVENT_QN,
115
166
  payload: recorded,
116
167
  });
168
+ // The account path reports the provider outcome to its caller and the
169
+ // operator in its own mail; the public path has nobody else to tell.
170
+ if (payload.channel === "public" && noticeReason !== null) {
171
+ await notifyOperator(ctx, consumerProtection, {
172
+ locale: payload.locale ?? FALLBACK_CONSENT_LOCALE,
173
+ requestId: payload.requestId,
174
+ receivedAtIso,
175
+ channel: "public",
176
+ declaration: {
177
+ declarationType: payload.declarationType,
178
+ terminationKind: payload.terminationKind,
179
+ },
180
+ reasons: [noticeReason],
181
+ tenantId,
182
+ providerCancel,
183
+ });
184
+ }
117
185
  return {
118
186
  isSuccess: true,
119
187
  data: {
@@ -8,11 +8,12 @@ import { generateId } from "@cosmicdrift/kumiko-framework/utils";
8
8
  import * as z from "zod";
9
9
  import { TenantQueries } from "../../tenant/index.js";
10
10
  import { UserQueries } from "../../user/index.js";
11
- import { CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE, CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, SubscriptionCancelTimings, SubscriptionFoundationHandlers, SubscriptionFoundationQueries, } from "../constants.js";
11
+ import { CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, SubscriptionCancelTimings, SubscriptionFoundationHandlers, SubscriptionFoundationQueries, } from "../constants.js";
12
12
  import { CONTRACT_TERMINATION_DECLARATION_TYPES, CONTRACT_TERMINATION_KINDS, } from "../events.js";
13
13
  import { purchaseRolesOf } from "../plan-catalog.js";
14
14
  import { resolveConsentLocale } from "./consent-text.js";
15
- import { renderOperatorNotice, renderTerminationReceipt, } from "./termination-mail.js";
15
+ import { renderTerminationReceipt } from "./termination-mail.js";
16
+ import { notifyOperator, requireNotify } from "./termination-notify.js";
16
17
  const TENANT_ADMIN_ROLE = "TenantAdmin";
17
18
  const EXTRAORDINARY_REASON_MESSAGE = "reason is required for an extraordinary termination";
18
19
  const authUserRowSchema = z.object({
@@ -52,12 +53,7 @@ function needsOperatorNotice(declaration) {
52
53
  ];
53
54
  }
54
55
  async function sendTerminationMails(ctx, consumerProtection, plan) {
55
- if (!ctx.notify) {
56
- throw new InternalError({
57
- message: "billing-foundation:termination: ctx.notify unavailable — the delivery feature must be mounted",
58
- });
59
- }
60
- await ctx.notify(CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, {
56
+ await requireNotify(ctx)(CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, {
61
57
  route: { email: plan.declaration.email },
62
58
  data: renderTerminationReceipt({
63
59
  locale: plan.locale,
@@ -71,21 +67,7 @@ async function sendTerminationMails(ctx, consumerProtection, plan) {
71
67
  // skip: no operator notice needed for this declaration
72
68
  if (plan.reasons.length === 0)
73
69
  return;
74
- await ctx.notify(CONTRACT_TERMINATION_OPERATOR_NOTIFICATION_TYPE, {
75
- route: { email: consumerProtection.operatorEmail },
76
- data: renderOperatorNotice({
77
- locale: plan.locale,
78
- requestId: plan.requestId,
79
- receivedAtIso: plan.receivedAtIso,
80
- channel: plan.channel,
81
- declaration: plan.declaration,
82
- reasons: plan.reasons,
83
- operatorEmail: consumerProtection.operatorEmail,
84
- ...(plan.tenantId !== undefined && { tenantId: plan.tenantId }),
85
- ...(plan.providerCancel !== undefined && { providerCancel: plan.providerCancel }),
86
- }),
87
- priority: "critical",
88
- });
70
+ await notifyOperator(ctx, consumerProtection, plan);
89
71
  }
90
72
  // Tenants the entered email can terminate a contract for: tenants where the
91
73
  // user is TenantAdmin and the subscription is not terminal. Every identity
@@ -122,6 +104,7 @@ export function createRequestContractTerminationHandler(options, consumerProtect
122
104
  schema: requestContractTerminationSchema,
123
105
  access: { roles: ["anonymous"] },
124
106
  rateLimit: { per: "ip+handler", limit: 5, windowSeconds: 600 },
107
+ ...(consumerProtection.terminationScope === "platform" && { tenantlessAnonymous: true }),
125
108
  escapeHatch: {
126
109
  reason: "Anonymous declarant has no session — finds the contract by email via ctx.queryAs(system, user:find-for-auth) and tenant:memberships; the tenant written to comes only from those lookups.",
127
110
  },
@@ -132,29 +115,15 @@ export function createRequestContractTerminationHandler(options, consumerProtect
132
115
  const receivedAtIso = options.now().toString();
133
116
  const { declarationType, terminationKind } = payload;
134
117
  const reasons = [];
118
+ const locale = resolveConsentLocale(payload.locale);
135
119
  let matchedTenantId;
136
- let providerCancel;
137
120
  const candidates = await findTerminableTenantIds(ctx, event.user.tenantId, payload.email);
138
121
  const [onlyCandidate] = candidates;
139
122
  if (candidates.length === 1 && onlyCandidate !== undefined) {
140
123
  matchedTenantId = onlyCandidate;
141
- const recorded = await ctx.writeAs(createSystemUser(onlyCandidate), SubscriptionFoundationHandlers.recordContractTermination, {
142
- requestId,
143
- declarationType,
144
- terminationKind,
145
- channel: "public",
146
- when: declarationType === "withdrawal" ? "none" : SubscriptionCancelTimings.periodEnd,
147
- });
148
- if (recorded.isSuccess) {
149
- // @cast-boundary engine-payload — our own record handler's return shape
150
- const data = recorded.data;
151
- providerCancel = data.providerCancel;
152
- if (data.noticeReason !== null)
153
- reasons.push(data.noticeReason);
154
- }
155
- else {
124
+ const declared = await ctx.writeAs(createSystemUser(onlyCandidate), SubscriptionFoundationHandlers.declareContractTermination, { requestId, declarationType, terminationKind, receivedAtIso, locale });
125
+ if (!declared.isSuccess)
156
126
  reasons.push("recording_failed");
157
- }
158
127
  }
159
128
  else {
160
129
  reasons.push(candidates.length === 0 ? "unmatched" : "ambiguous");
@@ -170,7 +139,7 @@ export function createRequestContractTerminationHandler(options, consumerProtect
170
139
  }
171
140
  reasons.push(...needsOperatorNotice(payload));
172
141
  await sendTerminationMails(ctx, consumerProtection, {
173
- locale: resolveConsentLocale(payload.locale),
142
+ locale,
174
143
  requestId,
175
144
  receivedAtIso,
176
145
  channel: "public",
@@ -186,7 +155,6 @@ export function createRequestContractTerminationHandler(options, consumerProtect
186
155
  },
187
156
  reasons,
188
157
  ...(matchedTenantId !== undefined && { tenantId: matchedTenantId }),
189
- ...(providerCancel !== undefined && { providerCancel }),
190
158
  });
191
159
  return {
192
160
  isSuccess: true,
@@ -24,18 +24,18 @@ export const TERMINATION_TEXTS = {
24
24
  customerReference: "Kundennummer / Referenz",
25
25
  reasonFieldLabel: "Begründung",
26
26
  receiptSubject: {
27
- termination: "Eingangsbestätigung Ihrer Kündigung",
28
- withdrawal: "Eingangsbestätigung Ihres Widerrufs",
27
+ termination: "Eingangsbestätigung deiner Kündigung",
28
+ withdrawal: "Eingangsbestätigung deines Widerrufs",
29
29
  },
30
- receiptIntro: "wir bestätigen den Eingang Ihrer Erklärung mit folgendem Inhalt. Diese E-Mail ist eine Eingangsbestätigung, keine Bestätigung der Wirksamkeit.",
31
- receiptNextSteps: "Wir prüfen Ihre Erklärung und melden uns, sobald sie bearbeitet ist oder wir Rückfragen haben.",
32
- receiptRevoke: (operatorEmail) => `Haben Sie diese Erklärung nicht abgegeben oder möchten Sie sie zurücknehmen? Schreiben Sie uns bitte umgehend an ${operatorEmail}.`,
33
- contact: "Fragen richten Sie bitte an",
30
+ receiptIntro: "wir bestätigen den Eingang deiner Erklärung mit folgendem Inhalt. Diese E-Mail ist eine Eingangsbestätigung, keine Bestätigung der Wirksamkeit.",
31
+ receiptNextSteps: "Wir prüfen deine Erklärung und melden uns, sobald sie bearbeitet ist oder wir Rückfragen haben.",
32
+ receiptRevoke: (operatorEmail) => `Hast du diese Erklärung nicht abgegeben oder möchtest du sie zurücknehmen? Schreib uns bitte umgehend an ${operatorEmail}.`,
33
+ contact: "Fragen richtest du bitte an",
34
34
  operatorSubject: {
35
35
  termination: "Kündigung: Bearbeitung nötig",
36
36
  withdrawal: "Widerruf: Bearbeitung nötig",
37
37
  },
38
- operatorIntro: "Es ist eine Erklärung eingegangen, die Ihre Bearbeitung braucht.",
38
+ operatorIntro: "Es ist eine Erklärung eingegangen, die bearbeitet werden muss.",
39
39
  operatorReasons: {
40
40
  unmatched: "Kein passender Vertrag zur angegebenen E-Mail gefunden",
41
41
  ambiguous: "Die angegebene E-Mail passt zu mehreren Verträgen",
@@ -58,8 +58,8 @@ export const TERMINATION_TEXTS = {
58
58
  },
59
59
  page: {
60
60
  title: "Vertrag kündigen oder widerrufen",
61
- lead: "Mit diesem Formular erklären Sie die Kündigung oder den Widerruf Ihres Vertrags. Sie brauchen dafür kein Kundenkonto.",
62
- declarationTypeHeading: "Was möchten Sie erklären?",
61
+ lead: "Mit diesem Formular erklärst du die Kündigung oder den Widerruf deines Vertrags. Du brauchst dafür kein Kundenkonto.",
62
+ declarationTypeHeading: "Was möchtest du erklären?",
63
63
  kindHeading: "Art der Kündigung",
64
64
  reasonHint: "Pflicht bei außerordentlicher Kündigung.",
65
65
  earliestDate: "Die Kündigung gilt zum nächstmöglichen Zeitpunkt.",
@@ -68,23 +68,23 @@ export const TERMINATION_TEXTS = {
68
68
  customerReferenceLabel: "Kundennummer (optional)",
69
69
  reasonLabel: "Begründung",
70
70
  reviewButton: "Weiter zur Prüfung",
71
- reviewTitle: "Bitte prüfen Sie Ihre Angaben",
72
- reviewLead: "Erst mit dem Klick auf den Button unten wird Ihre Erklärung abgegeben.",
71
+ reviewTitle: "Bitte prüfe deine Angaben",
72
+ reviewLead: "Erst mit dem Klick auf den Button unten wird deine Erklärung abgegeben.",
73
73
  confirmButton: { termination: "Jetzt kündigen", withdrawal: "Jetzt widerrufen" },
74
74
  backButton: "Angaben ändern",
75
- resultTitle: "Ihre Erklärung ist eingegangen",
76
- resultLead: "Wir haben Ihre Erklärung erhalten und senden Ihnen eine Eingangsbestätigung.",
75
+ resultTitle: "Deine Erklärung ist eingegangen",
76
+ resultLead: "Wir haben deine Erklärung erhalten und senden dir eine Eingangsbestätigung.",
77
77
  resultReceivedAt: "Eingegangen am",
78
78
  resultRequestId: "Vorgangsnummer",
79
- resultMailNote: "Die Eingangsbestätigung geht an die von Ihnen angegebene E-Mail-Adresse.",
80
- errorsHeading: "Bitte korrigieren Sie Ihre Angaben",
81
- errorName: "Bitte geben Sie Ihren Namen an.",
82
- errorEmail: "Bitte geben Sie eine gültige E-Mail-Adresse an.",
79
+ resultMailNote: "Die Eingangsbestätigung geht an die von dir angegebene E-Mail-Adresse.",
80
+ errorsHeading: "Bitte korrigiere deine Angaben",
81
+ errorName: "Bitte gib deinen Namen an.",
82
+ errorEmail: "Bitte gib eine gültige E-Mail-Adresse an.",
83
83
  errorReason: "Bei einer außerordentlichen Kündigung ist eine Begründung nötig.",
84
- errorChoice: "Bitte wählen Sie eine Erklärung und die Art der Kündigung.",
85
- errorGeneric: "Ihre Erklärung konnte nicht verarbeitet werden. Bitte versuchen Sie es erneut.",
84
+ errorChoice: "Bitte wähle eine Erklärung und die Art der Kündigung.",
85
+ errorGeneric: "Deine Erklärung konnte nicht verarbeitet werden. Bitte versuch es erneut.",
86
86
  rateLimitedTitle: "Zu viele Anfragen",
87
- rateLimitedBody: "Von Ihrer Adresse kamen zu viele Anfragen. Bitte versuchen Sie es später erneut.",
87
+ rateLimitedBody: "Von deiner Adresse kamen zu viele Anfragen. Bitte versuch es später erneut.",
88
88
  },
89
89
  },
90
90
  en: {
@@ -108,6 +108,25 @@ export declare const contractTerminationRequestedPayloadSchema: z.ZodObject<{
108
108
  }>;
109
109
  }, z.core.$strip>;
110
110
  export type ContractTerminationRequestedPayload = z.infer<typeof contractTerminationRequestedPayloadSchema>;
111
+ export declare const CONTRACT_TERMINATION_DECLARED_EVENT_SHORT: "contract-termination-declared";
112
+ export declare const CONTRACT_TERMINATION_DECLARED_EVENT_QN: "billing-foundation:event:contract-termination-declared";
113
+ export declare const contractTerminationDeclaredPayloadSchema: z.ZodObject<{
114
+ requestId: z.ZodString;
115
+ declarationType: z.ZodEnum<{
116
+ termination: "termination";
117
+ withdrawal: "withdrawal";
118
+ }>;
119
+ terminationKind: z.ZodEnum<{
120
+ extraordinary: "extraordinary";
121
+ ordinary: "ordinary";
122
+ }>;
123
+ receivedAtIso: z.ZodString;
124
+ locale: z.ZodEnum<{
125
+ de: "de";
126
+ en: "en";
127
+ }>;
128
+ }, z.core.$strip>;
129
+ export type ContractTerminationDeclaredPayload = z.infer<typeof contractTerminationDeclaredPayloadSchema>;
111
130
  export declare const CONTRACT_TERMINATION_UNMATCHED_AGGREGATE_TYPE: "contract-termination-unmatched";
112
131
  export declare const CONTRACT_TERMINATION_UNMATCHED_EVENT_SHORT: "contract-termination-unmatched";
113
132
  export declare const CONTRACT_TERMINATION_UNMATCHED_EVENT_QN: "billing-foundation:event:contract-termination-unmatched";
@@ -11,6 +11,7 @@
11
11
  // type ohne payload-discriminator.
12
12
  import * as z from "zod";
13
13
  import { BILLING_FOUNDATION_FEATURE, SubscriptionStatuses } from "./constants.js";
14
+ import { CONSENT_LOCALES } from "./consumer-protection/consent-locale.js";
14
15
  // Aggregate-type für den event-store. Eine subscription pro Tenant ist
15
16
  // ein stream; der subscriptionAggregateId-helper liefert die stream-id.
16
17
  export const SUBSCRIPTION_AGGREGATE_TYPE = "subscription";
@@ -138,6 +139,17 @@ export const contractTerminationRequestedPayloadSchema = z.object({
138
139
  effectiveAtIso: z.string().min(1).nullable(),
139
140
  providerCancel: z.enum(PROVIDER_CANCEL_OUTCOMES),
140
141
  });
142
+ // A public declaration matched to this tenant. The provider cancel runs in a
143
+ // job triggered by this event, so the request itself only looks up and appends.
144
+ export const CONTRACT_TERMINATION_DECLARED_EVENT_SHORT = "contract-termination-declared";
145
+ export const CONTRACT_TERMINATION_DECLARED_EVENT_QN = `${BILLING_FOUNDATION_FEATURE}:event:${CONTRACT_TERMINATION_DECLARED_EVENT_SHORT}`;
146
+ export const contractTerminationDeclaredPayloadSchema = z.object({
147
+ requestId: z.string().min(1).max(100),
148
+ declarationType: z.enum(CONTRACT_TERMINATION_DECLARATION_TYPES),
149
+ terminationKind: z.enum(CONTRACT_TERMINATION_KINDS),
150
+ receivedAtIso: z.string().min(1),
151
+ locale: z.enum(CONSENT_LOCALES),
152
+ });
141
153
  export const CONTRACT_TERMINATION_UNMATCHED_AGGREGATE_TYPE = "contract-termination-unmatched";
142
154
  export const CONTRACT_TERMINATION_UNMATCHED_EVENT_SHORT = "contract-termination-unmatched";
143
155
  export const CONTRACT_TERMINATION_UNMATCHED_EVENT_QN = `${BILLING_FOUNDATION_FEATURE}:event:${CONTRACT_TERMINATION_UNMATCHED_EVENT_SHORT}`;
@@ -44,7 +44,7 @@ const createCheckoutSessionSchema = z.object({
44
44
  export function createCheckoutSessionHandler(options) {
45
45
  const consumerProtection = options.consumerProtection;
46
46
  const schema = consumerProtection
47
- ? createCheckoutSessionSchema.extend({ consent: consentPayloadSchema })
47
+ ? createCheckoutSessionSchema.extend({ consent: consentPayloadSchema.optional() })
48
48
  : createCheckoutSessionSchema;
49
49
  return {
50
50
  name: "create-checkout-session",
@@ -57,7 +57,7 @@ export function createCheckoutSessionHandler(options) {
57
57
  // @cast-boundary engine-payload — dispatcher-zod-validated payload
58
58
  const payload = event.payload;
59
59
  const mode = payload.mode ?? "subscription";
60
- const consent = consumerProtection && payload.consent
60
+ const consent = consumerProtection
61
61
  ? await prepareConsent(ctx, consumerProtection, payload.consent, mode)
62
62
  : undefined;
63
63
  const result = await openCheckout(ctx, { baseUrl: options.baseUrl, catalog: options.catalog, now: options.now }, {
@@ -14,7 +14,9 @@ export function createStartPlanCheckoutHandler(options, catalog) {
14
14
  }
15
15
  const consumerProtection = options.consumerProtection;
16
16
  const tierSchema = z.object({ tier: z.enum(plans) });
17
- const schema = (consumerProtection ? tierSchema.extend({ consent: consentPayloadSchema }) : tierSchema).strict();
17
+ const schema = (consumerProtection
18
+ ? tierSchema.extend({ consent: consentPayloadSchema.optional() })
19
+ : tierSchema).strict();
18
20
  return {
19
21
  name: "start-plan-checkout",
20
22
  description: "Starts a hosted checkout for a plan tier when the tenant has no active subscription; the server picks price and redirect URLs.",
@@ -34,7 +36,7 @@ export function createStartPlanCheckoutHandler(options, catalog) {
34
36
  message: `subscription-foundation: no live price resolved for tier "${payload.tier}"`,
35
37
  });
36
38
  }
37
- const consent = consumerProtection && payload.consent
39
+ const consent = consumerProtection
38
40
  ? await prepareConsent(ctx, consumerProtection, payload.consent, "subscription")
39
41
  : undefined;
40
42
  const baseUrl = options.baseUrl ?? "";
@@ -119,6 +119,9 @@ export const BILLING_FOUNDATION_I18N = {
119
119
  "billing-foundation.errors.priceUnavailable": {
120
120
  en: "This plan's price is temporarily unavailable.",
121
121
  },
122
+ "billing-foundation.errors.consentRequired": {
123
+ en: "Confirm both consent boxes before you continue.",
124
+ },
122
125
  "billing-foundation.errors.consentTextOutdated": {
123
126
  en: "The consent text has changed. Reload the page and confirm again.",
124
127
  },
@@ -20,7 +20,7 @@ export const PAYMENT_TENANT_DESTROY_ARCHIVE_REASON = "tenant-destroy looks up th
20
20
  export async function subscriptionTenantDestroyHook(ctx) {
21
21
  declareEscapeHatch({ reason: SUBSCRIPTION_TENANT_DESTROY_ARCHIVE_REASON });
22
22
  const { profile } = await resolveProfileForTenant({
23
- db: ctx.db.unsafeRaw(SUBSCRIPTION_TENANT_DESTROY_ARCHIVE_REASON),
23
+ db: ctx.db.unsafeRaw(),
24
24
  tenantId: ctx.tenantId,
25
25
  });
26
26
  const aggregateId = subscriptionAggregateId(ctx.tenantId);
@@ -30,7 +30,7 @@ export async function subscriptionTenantDestroyHook(ctx) {
30
30
  else {
31
31
  await ctx.db.deleteMany(subscriptionsProjectionTable, { id: aggregateId });
32
32
  }
33
- await archiveStream(ctx.db.unsafeRaw(SUBSCRIPTION_TENANT_DESTROY_ARCHIVE_REASON), {
33
+ await archiveStream(ctx.db.unsafeRaw(), {
34
34
  tenantId: ctx.tenantId,
35
35
  aggregateId,
36
36
  aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
@@ -46,7 +46,7 @@ export async function subscriptionTenantDestroyHook(ctx) {
46
46
  export async function paymentTenantDestroyHook(ctx) {
47
47
  declareEscapeHatch({ reason: PAYMENT_TENANT_DESTROY_ARCHIVE_REASON });
48
48
  const { profile } = await resolveProfileForTenant({
49
- db: ctx.db.unsafeRaw(PAYMENT_TENANT_DESTROY_ARCHIVE_REASON),
49
+ db: ctx.db.unsafeRaw(),
50
50
  tenantId: ctx.tenantId,
51
51
  });
52
52
  const aggregateId = paymentAggregateId(ctx.tenantId);
@@ -58,7 +58,7 @@ export async function paymentTenantDestroyHook(ctx) {
58
58
  tenantId: ctx.tenantId,
59
59
  });
60
60
  }
61
- await archiveStream(ctx.db.unsafeRaw(PAYMENT_TENANT_DESTROY_ARCHIVE_REASON), {
61
+ await archiveStream(ctx.db.unsafeRaw(), {
62
62
  tenantId: ctx.tenantId,
63
63
  aggregateId,
64
64
  aggregateType: PAYMENT_AGGREGATE_TYPE,