@cosmicdrift/kumiko-bundled-features 0.337.1 → 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.
- package/dist/agent-tools/tool-catalog.js +37 -17
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +3 -2
- package/dist/auth-email-password/handlers/invite-accept.write.js +3 -4
- package/dist/auth-email-password/handlers/invite-info.query.js +4 -1
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +3 -4
- package/dist/auth-email-password/handlers/signup-confirm.write.js +2 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +2 -2
- package/dist/auth-email-password/web/auth-gate.d.ts +12 -2
- package/dist/auth-email-password/web/auth-gate.js +25 -1
- package/dist/auth-email-password/web/auth-redirect.d.ts +5 -0
- package/dist/auth-email-password/web/auth-redirect.js +58 -0
- package/dist/auth-email-password/web/client-plugin.d.ts +8 -1
- package/dist/auth-email-password/web/client-plugin.js +2 -0
- package/dist/auth-email-password/web/index.d.ts +2 -1
- package/dist/auth-email-password/web/index.js +1 -0
- package/dist/auth-email-password/web/session.d.ts +3 -1
- package/dist/auth-email-password/web/session.js +8 -2
- package/dist/auth-mfa/feature.js +1 -1
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -1
- package/dist/auth-mfa/handlers/enable-start-preauth.write.js +2 -1
- package/dist/auth-mfa/handlers/verify.write.js +1 -1
- package/dist/auth-mfa/mfa-code-verifier.js +1 -1
- package/dist/auth-mfa/mfa-status-checker.js +1 -1
- package/dist/billing-foundation/aggregate-id.d.ts +2 -0
- package/dist/billing-foundation/aggregate-id.js +8 -0
- package/dist/billing-foundation/checkout-core.d.ts +3 -0
- package/dist/billing-foundation/checkout-core.js +3 -0
- package/dist/billing-foundation/constants.d.ts +38 -0
- package/dist/billing-foundation/constants.js +43 -0
- package/dist/billing-foundation/consumer-protection/confirmation-mail.d.ts +21 -0
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +125 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +32 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.js +96 -0
- package/dist/billing-foundation/consumer-protection/consent-locale.d.ts +5 -0
- package/dist/billing-foundation/consumer-protection/consent-locale.js +17 -0
- package/dist/billing-foundation/consumer-protection/consent-text.d.ts +11 -0
- package/dist/billing-foundation/consumer-protection/consent-text.js +28 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.d.ts +15 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +149 -0
- package/dist/billing-foundation/consumer-protection/register-consent.d.ts +3 -0
- package/dist/billing-foundation/consumer-protection/register-consent.js +63 -0
- package/dist/billing-foundation/consumer-protection/register-termination.d.ts +3 -0
- package/dist/billing-foundation/consumer-protection/register-termination.js +40 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +45 -0
- package/dist/billing-foundation/consumer-protection/termination-mail.js +76 -0
- package/dist/billing-foundation/consumer-protection/termination-notify.d.ts +18 -0
- package/dist/billing-foundation/consumer-protection/termination-notify.js +21 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.d.ts +8 -0
- package/dist/billing-foundation/consumer-protection/termination-pages.js +268 -0
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +73 -0
- package/dist/billing-foundation/consumer-protection/termination-record.js +230 -0
- package/dist/billing-foundation/consumer-protection/termination-request.d.ts +45 -0
- package/dist/billing-foundation/consumer-protection/termination-request.js +231 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +62 -0
- package/dist/billing-foundation/consumer-protection/termination-texts.js +168 -0
- package/dist/billing-foundation/events.d.ts +105 -2
- package/dist/billing-foundation/events.js +88 -2
- package/dist/billing-foundation/feature.js +6 -2
- package/dist/billing-foundation/handlers/billing-plans.query.js +19 -1
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +33 -2
- package/dist/billing-foundation/handlers/process-event.write.d.ts +2 -2
- package/dist/billing-foundation/handlers/process-event.write.js +43 -15
- package/dist/billing-foundation/handlers/process-payment-event.write.d.ts +2 -1
- package/dist/billing-foundation/handlers/process-payment-event.write.js +6 -2
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +17 -1
- package/dist/billing-foundation/handlers/sync-subscription.write.js +0 -1
- package/dist/billing-foundation/i18n.js +61 -0
- package/dist/billing-foundation/index.d.ts +3 -2
- package/dist/billing-foundation/index.js +2 -1
- package/dist/billing-foundation/tenant-destroy-hook.js +4 -4
- package/dist/billing-foundation/types.d.ts +54 -9
- package/dist/billing-foundation/validate-options.js +51 -10
- package/dist/billing-foundation/web/billing-plans-panel.js +40 -3
- package/dist/billing-foundation/web/cancel-contract-dialog.d.ts +7 -0
- package/dist/billing-foundation/web/cancel-contract-dialog.js +57 -0
- package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +28 -0
- package/dist/billing-foundation/web/checkout-consent-dialog.js +38 -0
- package/dist/billing-foundation/webhook-handler.js +5 -2
- package/dist/cap-counter/with-cap-enforcement.js +7 -3
- package/dist/cap-overview/access.d.ts +2 -1
- package/dist/cap-overview/access.js +26 -5
- package/dist/cap-overview/feature.d.ts +4 -0
- package/dist/cap-overview/feature.js +8 -6
- package/dist/cap-overview/handlers/caps-usage.query.d.ts +1 -1
- package/dist/cap-overview/handlers/caps-usage.query.js +6 -4
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +12 -7
- package/dist/cap-overview/index.d.ts +2 -2
- package/dist/cap-overview/index.js +2 -2
- package/dist/cap-overview/screens.d.ts +1 -1
- package/dist/cap-overview/screens.js +17 -16
- package/dist/channel-discord/discord-channel.d.ts +6 -0
- package/dist/channel-discord/discord-channel.js +19 -0
- package/dist/channel-discord/feature.d.ts +3 -0
- package/dist/channel-discord/feature.js +30 -0
- package/dist/channel-discord/index.d.ts +2 -0
- package/dist/channel-discord/index.js +2 -0
- package/dist/channel-slack/feature.d.ts +3 -0
- package/dist/channel-slack/feature.js +30 -0
- package/dist/channel-slack/index.d.ts +2 -0
- package/dist/channel-slack/index.js +2 -0
- package/dist/channel-slack/slack-channel.d.ts +5 -0
- package/dist/channel-slack/slack-channel.js +16 -0
- package/dist/channel-teams/feature.d.ts +3 -0
- package/dist/channel-teams/feature.js +34 -0
- package/dist/channel-teams/index.d.ts +2 -0
- package/dist/channel-teams/index.js +2 -0
- package/dist/channel-teams/teams-channel.d.ts +5 -0
- package/dist/channel-teams/teams-channel.js +38 -0
- package/dist/channel-telegram/feature.d.ts +3 -0
- package/dist/channel-telegram/feature.js +28 -0
- package/dist/channel-telegram/index.d.ts +2 -0
- package/dist/channel-telegram/index.js +2 -0
- package/dist/channel-telegram/telegram-channel.d.ts +8 -0
- package/dist/channel-telegram/telegram-channel.js +54 -0
- package/dist/config/handlers/set.write.js +18 -2
- package/dist/crypto-shredding/feature.js +3 -2
- package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -1
- package/dist/crypto-shredding/handlers/forget-subject.write.js +30 -19
- package/dist/custom-fields/feature.js +9 -6
- package/dist/custom-fields/handlers/clear-custom-field.write.js +2 -1
- package/dist/custom-fields/handlers/define-tenant-field.write.js +2 -1
- package/dist/custom-fields/handlers/set-custom-field.write.js +2 -1
- package/dist/custom-fields/wire-user-data-rights.js +1 -1
- package/dist/data-retention/feature.js +1 -1
- package/dist/delivery/channel-context.d.ts +2 -1
- package/dist/delivery/channel-context.js +2 -2
- package/dist/delivery/chat-webhook-channel.d.ts +19 -0
- package/dist/delivery/chat-webhook-channel.js +48 -0
- package/dist/delivery/chat-webhook-sender.d.ts +21 -0
- package/dist/delivery/chat-webhook-sender.js +102 -0
- package/dist/delivery/delivery-service.js +12 -7
- package/dist/delivery/index.d.ts +2 -0
- package/dist/delivery/index.js +2 -0
- package/dist/delivery/jobs.js +1 -1
- package/dist/delivery/testing.d.ts +2 -0
- package/dist/delivery/types.d.ts +3 -1
- package/dist/delivery/types.js +1 -2
- package/dist/document-ingest-foundation/feature.js +3 -2
- package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
- package/dist/files-tenant-data/index.js +1 -1
- package/dist/form-draft/feature.js +1 -1
- package/dist/form-draft/handlers/discard.write.js +2 -1
- package/dist/form-draft/handlers/save.write.js +2 -1
- package/dist/inbound-mail-foundation/connect-routes.d.ts +7 -0
- package/dist/inbound-mail-foundation/connect-routes.js +13 -8
- package/dist/inbound-mail-foundation/feature.js +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +2 -1
- package/dist/inbound-mail-foundation/index.d.ts +1 -0
- package/dist/inbound-mail-foundation/index.js +1 -0
- package/dist/inbound-mail-foundation/oauth-access-token.d.ts +28 -0
- package/dist/inbound-mail-foundation/oauth-access-token.js +131 -0
- package/dist/inbound-mail-foundation/tenant-destroy-hook.js +1 -1
- package/dist/inbound-mail-foundation/types.d.ts +18 -1
- package/dist/inbound-mail-foundation/watch-supervisor.d.ts +5 -0
- package/dist/inbound-mail-foundation/watch-supervisor.js +42 -14
- package/dist/managed-pages/handlers/set.write.js +2 -1
- package/dist/notes-history/executor.d.ts +2 -0
- package/dist/notes-history/executor.js +9 -1
- package/dist/notes-history/feature.d.ts +2 -3
- package/dist/notes-history/feature.js +2 -1
- package/dist/notes-history/handlers/add-note.write.d.ts +2 -1
- package/dist/notes-history/handlers/add-note.write.js +21 -4
- package/dist/notes-history-user-data/hooks.js +26 -8
- package/dist/renderer-simple/feature.d.ts +8 -2
- package/dist/renderer-simple/feature.js +8 -5
- package/dist/renderer-simple/index.d.ts +2 -2
- package/dist/renderer-simple/index.js +1 -1
- package/dist/renderer-simple/simple-renderer.d.ts +15 -0
- package/dist/renderer-simple/simple-renderer.js +74 -27
- package/dist/secrets/feature.js +1 -1
- package/dist/sessions/feature.js +1 -1
- package/dist/sessions/handlers/revoke-all-for-user.write.js +2 -1
- package/dist/subscription-mollie/plugin-methods.js +1 -0
- package/dist/subscription-mollie/verify-webhook.js +5 -3
- package/dist/subscription-stripe/plugin-methods.d.ts +3 -1
- package/dist/subscription-stripe/plugin-methods.js +78 -13
- package/dist/subscription-stripe/verify-webhook.js +7 -3
- package/dist/tags/executor.d.ts +2 -0
- package/dist/tags/executor.js +9 -1
- package/dist/tags/feature.d.ts +4 -3
- package/dist/tags/feature.js +4 -2
- package/dist/tags/handlers/assign-tag.write.d.ts +2 -1
- package/dist/tags/handlers/assign-tag.write.js +4 -4
- package/dist/tags/handlers/remove-tag.write.d.ts +2 -1
- package/dist/tags/handlers/remove-tag.write.js +3 -3
- package/dist/tenant-handover/handlers/claim.write.js +2 -2
- package/dist/tenant-lifecycle/feature.js +1 -1
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +4 -1
- package/dist/tenant-lifecycle/handlers/request-destruction.write.js +2 -1
- package/dist/tier-engine/feature.js +1 -1
- package/dist/tier-engine/handlers/get-tenant-tier.query.js +2 -1
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +2 -1
- package/dist/user/handlers/detail.query.js +1 -0
- package/dist/user/handlers/list.query.js +1 -0
- package/dist/user-data-rights/feature.js +2 -2
- package/dist/user-data-rights/handlers/cancel-deletion.write.js +2 -1
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +1 -1
- package/dist/user-data-rights/handlers/download-attempt-list.query.js +1 -0
- package/dist/user-data-rights/handlers/download-by-job.query.js +2 -1
- package/dist/user-data-rights/handlers/download-by-token.query.js +2 -1
- package/dist/user-data-rights/handlers/export-status.query.js +2 -1
- package/dist/user-data-rights/handlers/lift-restriction.write.js +3 -4
- package/dist/user-data-rights/handlers/my-audit-log.query.js +2 -1
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.js +2 -1
- package/dist/user-data-rights/handlers/request-deletion.write.js +1 -1
- package/dist/user-data-rights/handlers/request-export.write.js +2 -1
- package/dist/user-data-rights/handlers/restrict-account.write.js +2 -2
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +2 -1
- package/dist/user-data-rights/lib/update-user-lifecycle.js +1 -1
- package/dist/user-data-rights/run-forget-cleanup.js +30 -15
- package/dist/workflow-runner/event-subscriber.js +6 -2
- package/dist/workflow-runner/event-trigger.js +5 -0
- package/dist/workflow-runner/handlers/resume-run.write.js +147 -59
- package/dist/workflow-runner/pending-projection.js +1 -0
- package/dist/workflow-runner/runner.d.ts +7 -1
- package/dist/workflow-runner/runner.js +1 -1
- package/dist/workflow-runner/tables.js +8 -1
- package/package.json +41 -9
- package/src/agent-tools/changes.json +14 -0
- package/src/auth-email-password/changes.json +7 -0
- package/src/billing-foundation/changes.json +83 -0
- package/src/cap-counter/changes.json +7 -0
- package/src/cap-overview/changes.json +14 -0
- package/src/config/changes.json +7 -0
- package/src/crypto-shredding/changes.json +13 -0
- package/src/custom-fields/changes.json +7 -0
- package/src/delivery/changes.json +14 -0
- package/src/inbound-mail-foundation/changes.json +7 -0
- package/src/notes-history/changes.json +7 -0
- package/src/renderer-simple/changes.json +9 -1
- package/src/subscription-mollie/changes.json +13 -0
- package/src/subscription-stripe/changes.json +13 -0
- package/src/tags/changes.json +7 -0
- package/src/user-data-rights/changes.json +6 -0
- package/src/workflow-runner/changes.json +7 -0
|
@@ -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,40 @@
|
|
|
1
|
+
import { createSystemUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { SubscriptionCancelTimings, SubscriptionFoundationHandlers } from "../constants.js";
|
|
3
|
+
import { CONTRACT_TERMINATION_DECLARED_EVENT_QN, CONTRACT_TERMINATION_DECLARED_EVENT_SHORT, CONTRACT_TERMINATION_REQUESTED_EVENT_SHORT, CONTRACT_TERMINATION_UNMATCHED_EVENT_SHORT, contractTerminationDeclaredPayloadSchema, contractTerminationRequestedPayloadSchema, contractTerminationUnmatchedPayloadSchema, } from "../events.js";
|
|
4
|
+
import { createDeclareContractTerminationHandler, createRecordContractTerminationHandler, createRecordUnmatchedContractTerminationHandler, createTerminableSubscriptionQuery, } from "./termination-record.js";
|
|
5
|
+
import { createRequestContractTerminationHandler, createTerminateContractHandler, } from "./termination-request.js";
|
|
6
|
+
export function registerContractTermination(r, options, consumerProtection) {
|
|
7
|
+
r.defineEvent(CONTRACT_TERMINATION_REQUESTED_EVENT_SHORT, contractTerminationRequestedPayloadSchema, { piiFields: "none" });
|
|
8
|
+
r.defineEvent(CONTRACT_TERMINATION_DECLARED_EVENT_SHORT, contractTerminationDeclaredPayloadSchema, { piiFields: "none" });
|
|
9
|
+
r.defineEvent(CONTRACT_TERMINATION_UNMATCHED_EVENT_SHORT, contractTerminationUnmatchedPayloadSchema, { piiFields: "none" });
|
|
10
|
+
r.queryHandler(createTerminableSubscriptionQuery());
|
|
11
|
+
r.writeHandler(createDeclareContractTerminationHandler());
|
|
12
|
+
r.writeHandler(createRecordContractTerminationHandler(options, consumerProtection));
|
|
13
|
+
r.writeHandler(createRecordUnmatchedContractTerminationHandler());
|
|
14
|
+
r.writeHandler(createRequestContractTerminationHandler(options, consumerProtection));
|
|
15
|
+
r.writeHandler(createTerminateContractHandler(options, consumerProtection));
|
|
16
|
+
// The provider cancel of a public declaration runs here, not in the
|
|
17
|
+
// request: matched and unmatched requests then do the same work and the
|
|
18
|
+
// response time does not tell whether the email belongs to a customer.
|
|
19
|
+
// The tenant comes from the stored trigger event (the job's system user).
|
|
20
|
+
r.job("cancel-on-public-termination-declared", { trigger: { on: CONTRACT_TERMINATION_DECLARED_EVENT_QN }, runIn: "worker" }, async (payload, ctx) => {
|
|
21
|
+
const declared = contractTerminationDeclaredPayloadSchema.safeParse(payload);
|
|
22
|
+
// skip: trigger event carries no usable declaration
|
|
23
|
+
if (!declared.success)
|
|
24
|
+
return;
|
|
25
|
+
const result = await ctx.writeAs(createSystemUser(ctx.systemUser.tenantId), SubscriptionFoundationHandlers.recordContractTermination, {
|
|
26
|
+
requestId: declared.data.requestId,
|
|
27
|
+
declarationType: declared.data.declarationType,
|
|
28
|
+
terminationKind: declared.data.terminationKind,
|
|
29
|
+
channel: "public",
|
|
30
|
+
when: declared.data.declarationType === "withdrawal"
|
|
31
|
+
? "none"
|
|
32
|
+
: SubscriptionCancelTimings.periodEnd,
|
|
33
|
+
receivedAtIso: declared.data.receivedAtIso,
|
|
34
|
+
locale: declared.data.locale,
|
|
35
|
+
});
|
|
36
|
+
if (!result.isSuccess) {
|
|
37
|
+
throw new Error(`billing-foundation:cancel-on-public-termination-declared: record-contract-termination failed: ${JSON.stringify(result.error)}`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
/** The operator notice for a provider problem has no declarant data at all. */
|
|
22
|
+
export type OperatorNoticeDeclaration = Pick<TerminationDeclaration, "declarationType" | "terminationKind"> & Partial<Omit<TerminationDeclaration, "declarationType" | "terminationKind">>;
|
|
23
|
+
export declare function formatReceivedAt(iso: string, locale: ConsentLocale): string;
|
|
24
|
+
export type RenderTerminationReceiptArgs = {
|
|
25
|
+
readonly locale: ConsentLocale;
|
|
26
|
+
readonly requestId: string;
|
|
27
|
+
readonly receivedAtIso: string;
|
|
28
|
+
readonly declaration: TerminationDeclaration;
|
|
29
|
+
readonly operatorEmail: string;
|
|
30
|
+
};
|
|
31
|
+
/** Built from the declarant's own input only: the content must not depend on
|
|
32
|
+
* whether (or how) the declaration matched a contract. */
|
|
33
|
+
export declare function renderTerminationReceipt(args: RenderTerminationReceiptArgs): TerminationMailContent;
|
|
34
|
+
export type RenderOperatorNoticeArgs = {
|
|
35
|
+
readonly locale: ConsentLocale;
|
|
36
|
+
readonly requestId: string;
|
|
37
|
+
readonly receivedAtIso: string;
|
|
38
|
+
readonly channel: "public" | "account";
|
|
39
|
+
readonly declaration: OperatorNoticeDeclaration;
|
|
40
|
+
readonly reasons: readonly OperatorNoticeReason[];
|
|
41
|
+
readonly tenantId?: string;
|
|
42
|
+
readonly providerCancel?: ProviderCancelOutcome;
|
|
43
|
+
readonly operatorEmail: string;
|
|
44
|
+
};
|
|
45
|
+
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
|
+
...(declaration.email !== undefined ? [`${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,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
|
+
}
|
|
@@ -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[];
|