@cosmicdrift/kumiko-bundled-features 0.337.1 → 0.338.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 (139) hide show
  1. package/dist/agent-tools/tool-catalog.js +37 -17
  2. package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +1 -0
  3. package/dist/auth-email-password/handlers/invite-accept.write.js +1 -0
  4. package/dist/auth-email-password/handlers/invite-info.query.js +1 -0
  5. package/dist/auth-email-password/handlers/invite-signup-complete.write.js +1 -0
  6. package/dist/auth-email-password/handlers/signup-confirm.write.js +1 -0
  7. package/dist/auth-email-password/handlers/signup-request.write.js +1 -1
  8. package/dist/auth-mfa/handlers/enable-start-preauth.write.js +1 -0
  9. package/dist/billing-foundation/aggregate-id.d.ts +2 -0
  10. package/dist/billing-foundation/aggregate-id.js +8 -0
  11. package/dist/billing-foundation/checkout-core.d.ts +3 -0
  12. package/dist/billing-foundation/checkout-core.js +3 -0
  13. package/dist/billing-foundation/constants.d.ts +31 -0
  14. package/dist/billing-foundation/constants.js +35 -0
  15. package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +21 -0
  16. package/dist/billing-foundation/consumer-protection/confirmation-mail.js +125 -0
  17. package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +32 -0
  18. package/dist/billing-foundation/consumer-protection/consent-gate.js +88 -0
  19. package/dist/billing-foundation/consumer-protection/consent-locale.d.ts +5 -0
  20. package/dist/billing-foundation/consumer-protection/consent-locale.js +17 -0
  21. package/dist/billing-foundation/consumer-protection/consent-text.d.ts +11 -0
  22. package/dist/billing-foundation/consumer-protection/consent-text.js +28 -0
  23. package/dist/billing-foundation/consumer-protection/issue-confirmation.d.ts +15 -0
  24. package/dist/billing-foundation/consumer-protection/issue-confirmation.js +149 -0
  25. package/dist/billing-foundation/consumer-protection/register-consent.d.ts +3 -0
  26. package/dist/billing-foundation/consumer-protection/register-consent.js +63 -0
  27. package/dist/billing-foundation/consumer-protection/register-termination.d.ts +3 -0
  28. package/dist/billing-foundation/consumer-protection/register-termination.js +12 -0
  29. package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +43 -0
  30. package/dist/billing-foundation/consumer-protection/termination-mail.js +76 -0
  31. package/dist/billing-foundation/consumer-protection/termination-pages.d.ts +8 -0
  32. package/dist/billing-foundation/consumer-protection/termination-pages.js +239 -0
  33. package/dist/billing-foundation/consumer-protection/termination-record.d.ts +51 -0
  34. package/dist/billing-foundation/consumer-protection/termination-record.js +162 -0
  35. package/dist/billing-foundation/consumer-protection/termination-request.d.ts +45 -0
  36. package/dist/billing-foundation/consumer-protection/termination-request.js +263 -0
  37. package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +62 -0
  38. package/dist/billing-foundation/consumer-protection/termination-texts.js +168 -0
  39. package/dist/billing-foundation/events.d.ts +86 -2
  40. package/dist/billing-foundation/events.js +76 -2
  41. package/dist/billing-foundation/feature.js +6 -2
  42. package/dist/billing-foundation/handlers/billing-plans.query.js +19 -1
  43. package/dist/billing-foundation/handlers/create-checkout-session.write.js +33 -2
  44. package/dist/billing-foundation/handlers/process-event.write.d.ts +2 -2
  45. package/dist/billing-foundation/handlers/process-event.write.js +43 -15
  46. package/dist/billing-foundation/handlers/process-payment-event.write.d.ts +2 -1
  47. package/dist/billing-foundation/handlers/process-payment-event.write.js +6 -2
  48. package/dist/billing-foundation/handlers/start-plan-checkout.write.js +15 -1
  49. package/dist/billing-foundation/handlers/sync-subscription.write.js +0 -1
  50. package/dist/billing-foundation/i18n.js +58 -0
  51. package/dist/billing-foundation/index.d.ts +3 -2
  52. package/dist/billing-foundation/index.js +2 -1
  53. package/dist/billing-foundation/types.d.ts +49 -9
  54. package/dist/billing-foundation/validate-options.js +47 -10
  55. package/dist/billing-foundation/web/billing-plans-panel.js +40 -3
  56. package/dist/billing-foundation/web/cancel-contract-dialog.d.ts +7 -0
  57. package/dist/billing-foundation/web/cancel-contract-dialog.js +57 -0
  58. package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +28 -0
  59. package/dist/billing-foundation/web/checkout-consent-dialog.js +38 -0
  60. package/dist/billing-foundation/webhook-handler.js +5 -2
  61. package/dist/cap-counter/with-cap-enforcement.js +7 -3
  62. package/dist/cap-overview/access.d.ts +2 -1
  63. package/dist/cap-overview/access.js +26 -5
  64. package/dist/cap-overview/feature.d.ts +4 -0
  65. package/dist/cap-overview/feature.js +8 -6
  66. package/dist/cap-overview/handlers/caps-usage.query.d.ts +1 -1
  67. package/dist/cap-overview/handlers/caps-usage.query.js +6 -4
  68. package/dist/cap-overview/handlers/tenant-caps-list.query.js +12 -7
  69. package/dist/cap-overview/index.d.ts +2 -2
  70. package/dist/cap-overview/index.js +2 -2
  71. package/dist/cap-overview/screens.d.ts +1 -1
  72. package/dist/cap-overview/screens.js +17 -16
  73. package/dist/config/handlers/set.write.js +18 -2
  74. package/dist/crypto-shredding/feature.js +3 -2
  75. package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -1
  76. package/dist/crypto-shredding/handlers/forget-subject.write.js +24 -13
  77. package/dist/custom-fields/feature.js +9 -6
  78. package/dist/custom-fields/handlers/clear-custom-field.write.js +1 -0
  79. package/dist/custom-fields/handlers/define-tenant-field.write.js +1 -0
  80. package/dist/custom-fields/handlers/set-custom-field.write.js +1 -0
  81. package/dist/delivery/delivery-service.js +9 -7
  82. package/dist/document-ingest-foundation/feature.js +3 -2
  83. package/dist/form-draft/handlers/discard.write.js +1 -0
  84. package/dist/form-draft/handlers/save.write.js +1 -0
  85. package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +1 -0
  86. package/dist/managed-pages/handlers/set.write.js +1 -0
  87. package/dist/notes-history/executor.d.ts +2 -0
  88. package/dist/notes-history/executor.js +9 -1
  89. package/dist/notes-history/feature.d.ts +2 -3
  90. package/dist/notes-history/feature.js +2 -1
  91. package/dist/notes-history/handlers/add-note.write.d.ts +2 -1
  92. package/dist/notes-history/handlers/add-note.write.js +20 -3
  93. package/dist/notes-history-user-data/hooks.js +26 -8
  94. package/dist/sessions/handlers/revoke-all-for-user.write.js +1 -0
  95. package/dist/subscription-mollie/plugin-methods.js +1 -0
  96. package/dist/subscription-mollie/verify-webhook.js +5 -3
  97. package/dist/subscription-stripe/plugin-methods.d.ts +3 -1
  98. package/dist/subscription-stripe/plugin-methods.js +78 -13
  99. package/dist/subscription-stripe/verify-webhook.js +7 -3
  100. package/dist/tags/executor.d.ts +2 -0
  101. package/dist/tags/executor.js +9 -1
  102. package/dist/tags/feature.d.ts +4 -3
  103. package/dist/tags/feature.js +4 -2
  104. package/dist/tags/handlers/assign-tag.write.d.ts +2 -1
  105. package/dist/tags/handlers/assign-tag.write.js +4 -4
  106. package/dist/tags/handlers/remove-tag.write.d.ts +2 -1
  107. package/dist/tags/handlers/remove-tag.write.js +3 -3
  108. package/dist/tenant-handover/handlers/claim.write.js +1 -0
  109. package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +1 -0
  110. package/dist/tenant-lifecycle/handlers/request-destruction.write.js +1 -0
  111. package/dist/tier-engine/handlers/get-tenant-tier.query.js +1 -0
  112. package/dist/tier-engine/handlers/set-tenant-tier.write.js +1 -0
  113. package/dist/user/handlers/detail.query.js +1 -0
  114. package/dist/user/handlers/list.query.js +1 -0
  115. package/dist/user-data-rights/handlers/cancel-deletion.write.js +1 -0
  116. package/dist/user-data-rights/handlers/download-attempt-list.query.js +1 -0
  117. package/dist/user-data-rights/handlers/download-by-job.query.js +1 -0
  118. package/dist/user-data-rights/handlers/download-by-token.query.js +1 -0
  119. package/dist/user-data-rights/handlers/export-status.query.js +1 -0
  120. package/dist/user-data-rights/handlers/lift-restriction.write.js +1 -0
  121. package/dist/user-data-rights/handlers/my-audit-log.query.js +1 -0
  122. package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +1 -0
  123. package/dist/user-data-rights/handlers/request-export.write.js +1 -0
  124. package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +1 -0
  125. package/dist/user-data-rights/run-forget-cleanup.js +30 -15
  126. package/package.json +9 -9
  127. package/src/agent-tools/changes.json +14 -0
  128. package/src/billing-foundation/changes.json +49 -0
  129. package/src/cap-counter/changes.json +7 -0
  130. package/src/cap-overview/changes.json +14 -0
  131. package/src/config/changes.json +7 -0
  132. package/src/crypto-shredding/changes.json +13 -0
  133. package/src/custom-fields/changes.json +7 -0
  134. package/src/delivery/changes.json +7 -0
  135. package/src/notes-history/changes.json +7 -0
  136. package/src/subscription-mollie/changes.json +13 -0
  137. package/src/subscription-stripe/changes.json +13 -0
  138. package/src/tags/changes.json +7 -0
  139. package/src/user-data-rights/changes.json +6 -0
@@ -0,0 +1,17 @@
1
+ // @runtime client
2
+ // Crypto-free so the browser panel can share the locale rule with the server.
3
+ export const CONSENT_LOCALES = ["de", "en"];
4
+ export const FALLBACK_CONSENT_LOCALE = "de";
5
+ function isConsentLocale(value) {
6
+ return CONSENT_LOCALES.some((locale) => locale === value);
7
+ }
8
+ /** Exact match, else the language part ("de-AT" → "de"), else the fallback. */
9
+ export function resolveConsentLocale(input) {
10
+ if (input === undefined)
11
+ return FALLBACK_CONSENT_LOCALE;
12
+ const lower = input.toLowerCase();
13
+ if (isConsentLocale(lower))
14
+ return lower;
15
+ const language = lower.split("-")[0] ?? "";
16
+ return isConsentLocale(language) ? language : FALLBACK_CONSENT_LOCALE;
17
+ }
@@ -0,0 +1,11 @@
1
+ import type { ConsentLocale } from "./consent-locale.js";
2
+ export { CONSENT_LOCALES, type ConsentLocale, FALLBACK_CONSENT_LOCALE, resolveConsentLocale, } from "./consent-locale.js";
3
+ export type ConsentTexts = {
4
+ readonly earlyPerformance: string;
5
+ readonly withdrawalLoss: string;
6
+ readonly subscriptionSubmitMessage: string;
7
+ readonly paymentSubmitMessage: string;
8
+ };
9
+ export declare const CONSENT_TEXTS: Readonly<Record<ConsentLocale, ConsentTexts>>;
10
+ export declare function consentTextVersion(locale: ConsentLocale): string;
11
+ export declare function submitMessageFor(locale: ConsentLocale, mode: "subscription" | "payment"): string;
@@ -0,0 +1,28 @@
1
+ import { createHash } from "node:crypto";
2
+ export { CONSENT_LOCALES, FALLBACK_CONSENT_LOCALE, resolveConsentLocale, } from "./consent-locale.js";
3
+ export const CONSENT_TEXTS = {
4
+ de: {
5
+ earlyPerformance: "Ich verlange ausdrücklich, dass der Anbieter vor Ablauf der Widerrufsfrist mit der Ausführung des Vertrags beginnt.",
6
+ withdrawalLoss: "Ich nehme zur Kenntnis, dass ich mein Widerrufsrecht verliere, sobald der Vertrag von dem Anbieter vollständig erfüllt ist, bei digitalen Inhalten sobald die Ausführung begonnen hat, nachdem ich ausdrücklich zugestimmt habe und bestätigt habe, dass ich dadurch mein Widerrufsrecht verliere.",
7
+ subscriptionSubmitMessage: "Kostenpflichtiges Abonnement: Es verlängert sich zum Ende jedes Abrechnungszeitraums automatisch, bis Sie es kündigen. Sie können jederzeit zum Ende des laufenden Zeitraums kündigen.",
8
+ paymentSubmitMessage: "Kostenpflichtige Einmalzahlung: Mit Klick auf den Button verpflichten Sie sich zur Zahlung.",
9
+ },
10
+ en: {
11
+ earlyPerformance: "I expressly request that the provider begins performing the contract before the withdrawal period has expired.",
12
+ withdrawalLoss: "I acknowledge that I lose my right of withdrawal once the provider has fully performed the contract, and for digital content once performance has begun, after I have expressly consented and confirmed that I lose my right of withdrawal as a result.",
13
+ subscriptionSubmitMessage: "Paid subscription: it renews automatically at the end of each billing period until you cancel. You can cancel at any time, effective at the end of the current period.",
14
+ paymentSubmitMessage: "Paid one-off payment: by clicking the button you commit to paying.",
15
+ },
16
+ };
17
+ const CONSENT_TEXT_VERSION_LENGTH = 16;
18
+ export function consentTextVersion(locale) {
19
+ const { earlyPerformance, withdrawalLoss } = CONSENT_TEXTS[locale];
20
+ return createHash("sha256")
21
+ .update(`${earlyPerformance}\n${withdrawalLoss}`)
22
+ .digest("hex")
23
+ .slice(0, CONSENT_TEXT_VERSION_LENGTH);
24
+ }
25
+ export function submitMessageFor(locale, mode) {
26
+ const texts = CONSENT_TEXTS[locale];
27
+ return mode === "subscription" ? texts.subscriptionSubmitMessage : texts.paymentSubmitMessage;
28
+ }
@@ -0,0 +1,15 @@
1
+ import { type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
+ import * as z from "zod";
3
+ import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from "../types.js";
4
+ export declare const issueContractConfirmationSchema: z.ZodObject<{
5
+ consentId: z.ZodString;
6
+ sourceAggregateId: z.ZodUUID;
7
+ currentPeriodEndIso: z.ZodOptional<z.ZodString>;
8
+ }, z.core.$strict>;
9
+ export type IssueContractConfirmationResult = {
10
+ readonly issued: true;
11
+ } | {
12
+ readonly issued: false;
13
+ readonly reason: "consent_not_found" | "already_issued" | "recipient_not_found";
14
+ };
15
+ export declare function createIssueContractConfirmationHandler(options: ResolvedBillingFoundationOptions, consumerProtection: ConsumerProtectionOptions): WriteHandlerDef;
@@ -0,0 +1,149 @@
1
+ // § 312f contract confirmation: one mail per recorded consent, sent by the
2
+ // system-only issue-contract-confirmation handler that the event-triggered
3
+ // jobs (register-consent.ts) call.
4
+ import { access, createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
5
+ import { InternalError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
6
+ import * as z from "zod";
7
+ import { requireTemplateResolver, TEXT_BLOCK_KIND } from "../../template-resolver/index.js";
8
+ import { UserQueries } from "../../user/index.js";
9
+ import { paymentAggregateId, subscriptionAggregateId } from "../aggregate-id.js";
10
+ import { CONTRACT_CONFIRMATION_NOTIFICATION_TYPE } from "../constants.js";
11
+ import { CHECKOUT_CONSENT_RECORDED_EVENT_QN, CONTRACT_CONFIRMATION_ISSUED_EVENT_QN, checkoutConsentRecordedPayloadSchema, contractConfirmationIssuedPayloadSchema, PAYMENT_AGGREGATE_TYPE, SUBSCRIPTION_AGGREGATE_TYPE, } from "../events.js";
12
+ import { renderContractConfirmation } from "./confirmation-mail.js";
13
+ import { resolveConsentLocale } from "./consent-text.js";
14
+ export const issueContractConfirmationSchema = z
15
+ .object({
16
+ consentId: z.string().min(1).max(100),
17
+ sourceAggregateId: z.uuid(),
18
+ // Only set when the triggering subscription event carries it.
19
+ currentPeriodEndIso: z.string().min(1).optional(),
20
+ })
21
+ .strict();
22
+ const recipientRowSchema = z.object({
23
+ email: z.string().min(1),
24
+ isDeleted: z.boolean().optional(),
25
+ });
26
+ export function createIssueContractConfirmationHandler(options, consumerProtection) {
27
+ return {
28
+ name: "issue-contract-confirmation",
29
+ agent: { expose: false },
30
+ schema: issueContractConfirmationSchema,
31
+ access: { roles: access.system },
32
+ escapeHatch: {
33
+ reason: "reads the consenting buyer's email via UserQueries.findForAuth as a tenant system user; the mail goes only to the user recorded in the consent event",
34
+ },
35
+ // kumiko-lint-ignore complexity-budget one linear pass: stream lookup, consent match, idempotency check, recipient lookup, mail, marker append
36
+ handler: async (event, ctx) => {
37
+ // @cast-boundary engine-payload — dispatcher-zod-validated payload
38
+ const payload = event.payload;
39
+ const tenantId = event.user.tenantId;
40
+ // The stream is derived from the caller's tenant: no read of another
41
+ // tenant's stream and no arbitrary aggregate id.
42
+ const isSubscription = payload.sourceAggregateId === subscriptionAggregateId(tenantId);
43
+ if (!isSubscription && payload.sourceAggregateId !== paymentAggregateId(tenantId)) {
44
+ throw new UnprocessableError("invalid_source_aggregate", {
45
+ message: `billing-foundation: ${payload.sourceAggregateId} is not the subscription or payment stream of tenant ${tenantId}`,
46
+ });
47
+ }
48
+ const stream = await ctx.fetchForWriting({
49
+ aggregateId: payload.sourceAggregateId,
50
+ aggregateType: isSubscription ? SUBSCRIPTION_AGGREGATE_TYPE : PAYMENT_AGGREGATE_TYPE,
51
+ });
52
+ let consentEvent;
53
+ let consent;
54
+ for (const e of stream.events) {
55
+ if (e.type !== CHECKOUT_CONSENT_RECORDED_EVENT_QN)
56
+ continue;
57
+ const parsed = checkoutConsentRecordedPayloadSchema.safeParse(e.payload);
58
+ if (parsed.success && parsed.data.consentId === payload.consentId) {
59
+ consentEvent = e;
60
+ consent = parsed.data;
61
+ break;
62
+ }
63
+ }
64
+ if (!consentEvent || !consent) {
65
+ ctx.log?.warn(`[billing-foundation:issue-contract-confirmation] no recorded consent ${payload.consentId} on the stream; no mail sent`);
66
+ return {
67
+ isSuccess: true,
68
+ data: {
69
+ issued: false,
70
+ reason: "consent_not_found",
71
+ },
72
+ };
73
+ }
74
+ const alreadyIssued = stream.events.some((e) => e.type === CONTRACT_CONFIRMATION_ISSUED_EVENT_QN &&
75
+ contractConfirmationIssuedPayloadSchema.safeParse(e.payload).data?.consentId ===
76
+ payload.consentId);
77
+ if (alreadyIssued) {
78
+ return {
79
+ isSuccess: true,
80
+ data: {
81
+ issued: false,
82
+ reason: "already_issued",
83
+ },
84
+ };
85
+ }
86
+ // Everything that can fail runs before the append, so a failed run
87
+ // leaves no marker behind and the job retry starts clean.
88
+ const recipient = recipientRowSchema.safeParse(await ctx.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, {
89
+ id: consent.actorUserId,
90
+ }));
91
+ if (!recipient.success || recipient.data.isDeleted === true) {
92
+ ctx.log?.warn(`[billing-foundation:issue-contract-confirmation] recipient of consent ${payload.consentId} not found; no mail sent`);
93
+ return {
94
+ isSuccess: true,
95
+ data: {
96
+ issued: false,
97
+ reason: "recipient_not_found",
98
+ },
99
+ };
100
+ }
101
+ const locale = resolveConsentLocale(consent.locale);
102
+ const terms = await requireTemplateResolver(ctx, "billing-foundation").resolveTemplate({
103
+ tenantId,
104
+ slug: consumerProtection.termsTextBlock,
105
+ kind: TEXT_BLOCK_KIND,
106
+ locale,
107
+ });
108
+ if (terms.version !== consent.termsTemplateVersion) {
109
+ ctx.log?.warn(`[billing-foundation:issue-contract-confirmation] terms text block changed since consent ${payload.consentId} (version ${consent.termsTemplateVersion} -> ${terms.version}); the mail carries the current text`);
110
+ }
111
+ const issuedAtIso = options.now().toString();
112
+ const content = renderContractConfirmation({
113
+ consent,
114
+ consentGivenAtIso: consentEvent.createdAt.toString(),
115
+ contractStartIso: issuedAtIso,
116
+ ...(payload.currentPeriodEndIso !== undefined && {
117
+ currentPeriodEndIso: payload.currentPeriodEndIso,
118
+ }),
119
+ vatNote: consumerProtection.vatNote,
120
+ operatorEmail: consumerProtection.operatorEmail,
121
+ termsContent: terms.content,
122
+ });
123
+ if (!ctx.notify) {
124
+ throw new InternalError({
125
+ message: "billing-foundation:issue-contract-confirmation: ctx.notify unavailable — the delivery feature must be mounted",
126
+ });
127
+ }
128
+ // Append BEFORE notifying: appendOne carries the fetched stream version
129
+ // as expectedVersion, so of two concurrent runs only one insert wins
130
+ // (the other fails with a version conflict) and only the winner mails.
131
+ const issued = {
132
+ consentId: payload.consentId,
133
+ issuedAtIso,
134
+ locale,
135
+ termsTemplateVersion: terms.version,
136
+ };
137
+ await stream.appendOne({ type: CONTRACT_CONFIRMATION_ISSUED_EVENT_QN, payload: issued });
138
+ await ctx.notify(CONTRACT_CONFIRMATION_NOTIFICATION_TYPE, {
139
+ route: { email: recipient.data.email },
140
+ data: content,
141
+ priority: "critical",
142
+ });
143
+ return {
144
+ isSuccess: true,
145
+ data: { issued: true },
146
+ };
147
+ },
148
+ };
149
+ }
@@ -0,0 +1,3 @@
1
+ import type { FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from "../types.js";
3
+ export declare function registerConsumerProtection(r: FeatureRegistrar, options: ResolvedBillingFoundationOptions, consumerProtection: ConsumerProtectionOptions): void;
@@ -0,0 +1,63 @@
1
+ import { createSystemUser } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { paymentAggregateId, subscriptionAggregateId } from "../aggregate-id.js";
3
+ import { SubscriptionFoundationHandlers, SubscriptionStatuses } from "../constants.js";
4
+ import { CHECKOUT_CONSENT_RECORDED_EVENT_SHORT, CONTRACT_CONFIRMATION_ISSUED_EVENT_SHORT, checkoutConsentRecordedPayloadSchema, contractConfirmationIssuedPayloadSchema, INVOICE_PAID_EVENT_QN, PAYMENT_RECEIVED_EVENT_QN, SUBSCRIPTION_CREATED_EVENT_QN, SUBSCRIPTION_UPDATED_EVENT_QN, } from "../events.js";
5
+ import { createIssueContractConfirmationHandler } from "./issue-confirmation.js";
6
+ import { registerContractTermination } from "./register-termination.js";
7
+ const CONFIRMATION_TRIGGERS = [
8
+ {
9
+ eventQn: SUBSCRIPTION_CREATED_EVENT_QN,
10
+ jobName: "confirm-on-subscription-created",
11
+ stream: "subscription",
12
+ },
13
+ {
14
+ eventQn: SUBSCRIPTION_UPDATED_EVENT_QN,
15
+ jobName: "confirm-on-subscription-updated",
16
+ stream: "subscription",
17
+ },
18
+ { eventQn: INVOICE_PAID_EVENT_QN, jobName: "confirm-on-invoice-paid", stream: "subscription" },
19
+ { eventQn: PAYMENT_RECEIVED_EVENT_QN, jobName: "confirm-on-payment-received", stream: "payment" },
20
+ ];
21
+ function isConfirmableSubscriptionStatus(status) {
22
+ return status === SubscriptionStatuses.active || status === SubscriptionStatuses.trialing;
23
+ }
24
+ export function registerConsumerProtection(r, options, consumerProtection) {
25
+ r.requires("template-resolver", "delivery", "user", "tenant");
26
+ registerContractTermination(r, options, consumerProtection);
27
+ r.defineEvent(CHECKOUT_CONSENT_RECORDED_EVENT_SHORT, checkoutConsentRecordedPayloadSchema, {
28
+ piiFields: "none",
29
+ });
30
+ r.defineEvent(CONTRACT_CONFIRMATION_ISSUED_EVENT_SHORT, contractConfirmationIssuedPayloadSchema, {
31
+ piiFields: "none",
32
+ });
33
+ r.writeHandler(createIssueContractConfirmationHandler(options, consumerProtection));
34
+ // The consent is recorded before the buyer pays; the mail goes out once a
35
+ // later event for the same consentId shows the contract is live. The jobs
36
+ // are at-least-once, the handler's append makes the mail once-only.
37
+ for (const trigger of CONFIRMATION_TRIGGERS) {
38
+ r.job(trigger.jobName, { trigger: { on: trigger.eventQn }, runIn: "worker" }, async (payload, ctx) => {
39
+ const consentId = payload["consentId"];
40
+ // skip: trigger event carries no consent to confirm
41
+ if (typeof consentId !== "string" || consentId.length === 0)
42
+ return;
43
+ const isSubscriptionEvent = trigger.stream === "subscription";
44
+ // skip: subscription not live yet, a later event confirms
45
+ if (isSubscriptionEvent && !isConfirmableSubscriptionStatus(payload["status"]))
46
+ return;
47
+ // The tenant comes from the stored trigger event (the job's system
48
+ // user), never from payload data.
49
+ const tenantId = ctx.systemUser.tenantId;
50
+ const currentPeriodEndIso = payload["currentPeriodEndIso"];
51
+ const result = await ctx.writeAs(createSystemUser(tenantId), SubscriptionFoundationHandlers.issueContractConfirmation, {
52
+ consentId,
53
+ sourceAggregateId: isSubscriptionEvent
54
+ ? subscriptionAggregateId(tenantId)
55
+ : paymentAggregateId(tenantId),
56
+ ...(typeof currentPeriodEndIso === "string" && { currentPeriodEndIso }),
57
+ });
58
+ if (!result.isSuccess) {
59
+ throw new Error(`billing-foundation:${trigger.jobName}: issue-contract-confirmation failed: ${JSON.stringify(result.error)}`);
60
+ }
61
+ });
62
+ }
63
+ }
@@ -0,0 +1,3 @@
1
+ import type { FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from "../types.js";
3
+ export declare function registerContractTermination(r: FeatureRegistrar, options: ResolvedBillingFoundationOptions, consumerProtection: ConsumerProtectionOptions): void;
@@ -0,0 +1,12 @@
1
+ import { CONTRACT_TERMINATION_REQUESTED_EVENT_SHORT, CONTRACT_TERMINATION_UNMATCHED_EVENT_SHORT, contractTerminationRequestedPayloadSchema, contractTerminationUnmatchedPayloadSchema, } from "../events.js";
2
+ import { createRecordContractTerminationHandler, createRecordUnmatchedContractTerminationHandler, createTerminableSubscriptionQuery, } from "./termination-record.js";
3
+ import { createRequestContractTerminationHandler, createTerminateContractHandler, } from "./termination-request.js";
4
+ export function registerContractTermination(r, options, consumerProtection) {
5
+ r.defineEvent(CONTRACT_TERMINATION_REQUESTED_EVENT_SHORT, contractTerminationRequestedPayloadSchema, { piiFields: "none" });
6
+ r.defineEvent(CONTRACT_TERMINATION_UNMATCHED_EVENT_SHORT, contractTerminationUnmatchedPayloadSchema, { piiFields: "none" });
7
+ r.queryHandler(createTerminableSubscriptionQuery());
8
+ r.writeHandler(createRecordContractTerminationHandler(options));
9
+ r.writeHandler(createRecordUnmatchedContractTerminationHandler());
10
+ r.writeHandler(createRequestContractTerminationHandler(options, consumerProtection));
11
+ r.writeHandler(createTerminateContractHandler(options, consumerProtection));
12
+ }
@@ -0,0 +1,43 @@
1
+ import type { ContractTerminationDeclarationType, ContractTerminationKind, ProviderCancelOutcome } from "../events.js";
2
+ import type { ConsentLocale } from "./consent-text.js";
3
+ import { type OperatorNoticeReason } from "./termination-texts.js";
4
+ export type TerminationMailContent = {
5
+ readonly subject: string;
6
+ readonly header: string;
7
+ readonly sections: readonly {
8
+ readonly text: string;
9
+ }[];
10
+ readonly footer: string;
11
+ };
12
+ /** What the declarant entered; the only input of the receipt. */
13
+ export type TerminationDeclaration = {
14
+ readonly declarationType: ContractTerminationDeclarationType;
15
+ readonly terminationKind: ContractTerminationKind;
16
+ readonly name?: string;
17
+ readonly email: string;
18
+ readonly customerReference?: string;
19
+ readonly reason?: string;
20
+ };
21
+ export declare function formatReceivedAt(iso: string, locale: ConsentLocale): string;
22
+ export type RenderTerminationReceiptArgs = {
23
+ readonly locale: ConsentLocale;
24
+ readonly requestId: string;
25
+ readonly receivedAtIso: string;
26
+ readonly declaration: TerminationDeclaration;
27
+ readonly operatorEmail: string;
28
+ };
29
+ /** Built from the declarant's own input only: the content must not depend on
30
+ * whether (or how) the declaration matched a contract. */
31
+ export declare function renderTerminationReceipt(args: RenderTerminationReceiptArgs): TerminationMailContent;
32
+ export type RenderOperatorNoticeArgs = {
33
+ readonly locale: ConsentLocale;
34
+ readonly requestId: string;
35
+ readonly receivedAtIso: string;
36
+ readonly channel: "public" | "account";
37
+ readonly declaration: TerminationDeclaration;
38
+ readonly reasons: readonly OperatorNoticeReason[];
39
+ readonly tenantId?: string;
40
+ readonly providerCancel?: ProviderCancelOutcome;
41
+ readonly operatorEmail: string;
42
+ };
43
+ export declare function renderOperatorNotice(args: RenderOperatorNoticeArgs): TerminationMailContent;
@@ -0,0 +1,76 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import { TERMINATION_TEXTS } from "./termination-texts.js";
3
+ const TIME_ZONE_BY_LOCALE = {
4
+ de: "Europe/Berlin",
5
+ en: "UTC",
6
+ };
7
+ export function formatReceivedAt(iso, locale) {
8
+ let epochMilliseconds;
9
+ try {
10
+ epochMilliseconds = Temporal.Instant.from(iso).epochMilliseconds;
11
+ }
12
+ catch {
13
+ return iso;
14
+ }
15
+ return new Intl.DateTimeFormat(locale, {
16
+ dateStyle: "long",
17
+ timeStyle: "long",
18
+ timeZone: TIME_ZONE_BY_LOCALE[locale],
19
+ }).format(epochMilliseconds);
20
+ }
21
+ function declarationLines(declaration, locale) {
22
+ const texts = TERMINATION_TEXTS[locale];
23
+ const lines = [
24
+ `${texts.declarationType}: ${texts.declarationTypeLabel[declaration.declarationType]}`,
25
+ `${texts.terminationKind}: ${texts.terminationKindLabel[declaration.terminationKind]}`,
26
+ ...(declaration.name !== undefined ? [`${texts.name}: ${declaration.name}`] : []),
27
+ `${texts.email}: ${declaration.email}`,
28
+ ...(declaration.customerReference
29
+ ? [`${texts.customerReference}: ${declaration.customerReference}`]
30
+ : []),
31
+ ...(declaration.reason ? [`${texts.reasonFieldLabel}: ${declaration.reason}`] : []),
32
+ ];
33
+ return lines.map((text) => ({ text }));
34
+ }
35
+ /** Built from the declarant's own input only: the content must not depend on
36
+ * whether (or how) the declaration matched a contract. */
37
+ export function renderTerminationReceipt(args) {
38
+ const texts = TERMINATION_TEXTS[args.locale];
39
+ const subject = texts.receiptSubject[args.declaration.declarationType];
40
+ return {
41
+ subject,
42
+ header: subject,
43
+ sections: [
44
+ { text: texts.receiptIntro },
45
+ { text: `${texts.receivedAt}: ${formatReceivedAt(args.receivedAtIso, args.locale)}` },
46
+ { text: `${texts.requestId}: ${args.requestId}` },
47
+ ...declarationLines(args.declaration, args.locale),
48
+ { text: texts.receiptNextSteps },
49
+ { text: texts.receiptRevoke(args.operatorEmail) },
50
+ ],
51
+ footer: `${texts.contact} ${args.operatorEmail}`,
52
+ };
53
+ }
54
+ export function renderOperatorNotice(args) {
55
+ const texts = TERMINATION_TEXTS[args.locale];
56
+ const subject = texts.operatorSubject[args.declaration.declarationType];
57
+ return {
58
+ subject,
59
+ header: subject,
60
+ sections: [
61
+ { text: texts.operatorIntro },
62
+ ...args.reasons.map((reason) => ({
63
+ text: `${texts.noticeReasons}: ${texts.operatorReasons[reason]}`,
64
+ })),
65
+ { text: `${texts.receivedAt}: ${formatReceivedAt(args.receivedAtIso, args.locale)}` },
66
+ { text: `${texts.requestId}: ${args.requestId}` },
67
+ { text: `${texts.channel}: ${texts.channelLabel[args.channel]}` },
68
+ ...(args.tenantId !== undefined ? [{ text: `${texts.tenant}: ${args.tenantId}` }] : []),
69
+ ...(args.providerCancel !== undefined
70
+ ? [{ text: `${texts.providerResult}: ${texts.providerResultLabel[args.providerCancel]}` }]
71
+ : []),
72
+ ...declarationLines(args.declaration, args.locale),
73
+ ],
74
+ footer: args.operatorEmail,
75
+ };
76
+ }
@@ -0,0 +1,8 @@
1
+ import type { AnonymousExtraRoute } from "@cosmicdrift/kumiko-framework/api";
2
+ import type { ConsentLocale } from "./consent-text.js";
3
+ export type ContractTerminationRoutesOptions = {
4
+ readonly paths?: Readonly<Partial<Record<ConsentLocale, string>>>;
5
+ };
6
+ /** GET + POST pages for de (`/legal/kuendigen`) and en (`/legal/cancel`);
7
+ * mount via the app's `extraRoutes`. */
8
+ export declare function createContractTerminationRoutes(options?: ContractTerminationRoutesOptions): AnonymousExtraRoute[];