@cosmicdrift/kumiko-samples 0.363.0 → 0.365.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/LICENSE +1 -1
  2. package/package.json +1 -1
  3. package/packages/bundled-features/package.json +7 -1
  4. package/packages/bundled-features/src/audit/changes.json +18 -0
  5. package/packages/bundled-features/src/audit/feature.ts +1 -1
  6. package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
  7. package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +63 -11
  8. package/packages/bundled-features/src/auth-email-password/bootstrap.ts +30 -11
  9. package/packages/bundled-features/src/auth-email-password/changes.json +55 -0
  10. package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
  11. package/packages/bundled-features/src/auth-email-password/feature.ts +4 -7
  12. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
  13. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -3
  14. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +6 -4
  15. package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
  16. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
  17. package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +2 -2
  18. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +2 -1
  19. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +7 -3
  20. package/packages/bundled-features/src/auth-email-password/mail-i18n.ts +10 -0
  21. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
  22. package/packages/bundled-features/src/billing-foundation/changes.json +48 -0
  23. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +57 -5
  24. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
  25. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +4 -1
  26. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
  27. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +9 -3
  28. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
  29. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +18 -2
  30. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +35 -19
  31. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
  32. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
  33. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
  34. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +10 -4
  35. package/packages/bundled-features/src/billing-foundation/i18n.ts +9 -0
  36. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +27 -7
  37. package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
  38. package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +20 -8
  39. package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
  40. package/packages/bundled-features/src/cap-counter/cap-reservation.ts +7 -2
  41. package/packages/bundled-features/src/cap-counter/changes.json +19 -0
  42. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
  43. package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +2 -0
  44. package/packages/bundled-features/src/cap-counter/index.ts +1 -0
  45. package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
  46. package/packages/bundled-features/src/cap-overview/changes.json +6 -0
  47. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  48. package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +2 -1
  49. package/packages/bundled-features/src/channel-discord/discord-channel.ts +4 -2
  50. package/packages/bundled-features/src/channel-email/changes.json +18 -0
  51. package/packages/bundled-features/src/channel-email/email-channel.ts +47 -24
  52. package/packages/bundled-features/src/channel-email/smtp-transport.ts +7 -1
  53. package/packages/bundled-features/src/channel-slack/slack-channel.ts +4 -2
  54. package/packages/bundled-features/src/channel-teams/teams-channel.ts +4 -2
  55. package/packages/bundled-features/src/config/changes.json +6 -0
  56. package/packages/bundled-features/src/config/handlers/set.write.ts +2 -2
  57. package/packages/bundled-features/src/config/resolver.ts +15 -12
  58. package/packages/bundled-features/src/config/write-helpers.ts +23 -0
  59. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  60. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +54 -27
  61. package/packages/bundled-features/src/custom-fields/changes.json +6 -0
  62. package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +2 -2
  63. package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
  64. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +4 -5
  65. package/packages/bundled-features/src/delivery/attachment-ref.ts +39 -1
  66. package/packages/bundled-features/src/delivery/changes.json +30 -0
  67. package/packages/bundled-features/src/delivery/channel-context.ts +31 -8
  68. package/packages/bundled-features/src/delivery/delivery-service.ts +93 -92
  69. package/packages/bundled-features/src/delivery/index.ts +2 -0
  70. package/packages/bundled-features/src/delivery/jobs.ts +51 -14
  71. package/packages/bundled-features/src/delivery/types.ts +7 -1
  72. package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
  73. package/packages/bundled-features/src/feature-toggles/changes.json +7 -0
  74. package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +26 -12
  75. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
  76. package/packages/bundled-features/src/file-foundation/changes.json +6 -0
  77. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +6 -3
  78. package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
  79. package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
  80. package/packages/bundled-features/src/files-tenant-data/index.ts +3 -1
  81. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +12 -3
  82. package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
  83. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
  84. package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
  85. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +3 -1
  86. package/packages/bundled-features/src/jobs/changes.json +31 -0
  87. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
  88. package/packages/bundled-features/src/jobs/feature.ts +7 -4
  89. package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
  90. package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
  91. package/packages/bundled-features/src/jobs/job-run-logger.ts +56 -18
  92. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
  93. package/packages/bundled-features/src/ledger/changes.json +7 -0
  94. package/packages/bundled-features/src/ledger/entity.ts +3 -1
  95. package/packages/bundled-features/src/metrics/changes.json +6 -0
  96. package/packages/bundled-features/src/metrics/filters.ts +28 -12
  97. package/packages/bundled-features/src/personal-access-tokens/changes.json +6 -0
  98. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +7 -8
  99. package/packages/bundled-features/src/renderer-foundation/types.ts +2 -0
  100. package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
  101. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
  102. package/packages/bundled-features/src/secrets/changes.json +12 -0
  103. package/packages/bundled-features/src/secrets/feature.ts +4 -3
  104. package/packages/bundled-features/src/secrets/handlers/delete.write.ts +3 -1
  105. package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
  106. package/packages/bundled-features/src/secrets/write-gate.ts +8 -2
  107. package/packages/bundled-features/src/shared/changes.json +6 -0
  108. package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
  109. package/packages/bundled-features/src/shared/index.ts +8 -0
  110. package/packages/bundled-features/src/shared/mail-template-body.ts +17 -0
  111. package/packages/bundled-features/src/shared/resource-image-formats.ts +31 -0
  112. package/packages/bundled-features/src/shared/sharp-input-limits.ts +7 -8
  113. package/packages/bundled-features/src/step-dispatcher/webhook-runner.ts +2 -2
  114. package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
  115. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
  116. package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
  117. package/packages/bundled-features/src/template-resolver/README.md +4 -2
  118. package/packages/bundled-features/src/template-resolver/api.ts +3 -2
  119. package/packages/bundled-features/src/template-resolver/changes.json +149 -0
  120. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +148 -18
  121. package/packages/bundled-features/src/template-resolver/compile-steps-block.ts +68 -0
  122. package/packages/bundled-features/src/template-resolver/constants.ts +23 -3
  123. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -0
  124. package/packages/bundled-features/src/template-resolver/document-schema.ts +33 -5
  125. package/packages/bundled-features/src/template-resolver/document-steps.ts +114 -0
  126. package/packages/bundled-features/src/template-resolver/feature.ts +16 -1
  127. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +81 -39
  128. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +4 -3
  129. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +121 -53
  130. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +6 -44
  131. package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
  132. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +56 -1
  133. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +6 -3
  134. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +25 -15
  135. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +12 -1
  136. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +73 -30
  137. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +14 -11
  138. package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
  139. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +102 -19
  140. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +49 -10
  141. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +39 -10
  142. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +14 -2
  143. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +54 -4
  144. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +96 -14
  145. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +51 -24
  146. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +1 -1
  147. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +16 -2
  148. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +4 -1
  149. package/packages/bundled-features/src/template-resolver/index.ts +18 -2
  150. package/packages/bundled-features/src/template-resolver/layout-appearance.ts +56 -0
  151. package/packages/bundled-features/src/template-resolver/mail-constants.ts +4 -0
  152. package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
  153. package/packages/bundled-features/src/template-resolver/qualified-names.ts +4 -0
  154. package/packages/bundled-features/src/template-resolver/resource-image.ts +9 -16
  155. package/packages/bundled-features/src/template-resolver/seeding.ts +27 -4
  156. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +390 -124
  157. package/packages/bundled-features/src/template-resolver/table.ts +13 -1
  158. package/packages/bundled-features/src/template-resolver/template-bundle.ts +4 -0
  159. package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
  160. package/packages/bundled-features/src/template-resolver/template-meta.ts +95 -24
  161. package/packages/bundled-features/src/template-resolver/template-preview.ts +160 -57
  162. package/packages/bundled-features/src/template-resolver/template-resources.ts +3 -4
  163. package/packages/bundled-features/src/template-resolver/template-usage.ts +31 -11
  164. package/packages/bundled-features/src/template-resolver/template-validation.ts +16 -9
  165. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +12 -8
  166. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +97 -10
  167. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +15 -5
  168. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +405 -145
  169. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +17 -3
  170. package/packages/bundled-features/src/template-resolver/web/i18n.ts +32 -4
  171. package/packages/bundled-features/src/template-resolver/web/index.ts +8 -0
  172. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +23 -7
  173. package/packages/bundled-features/src/tenant/changes.json +6 -0
  174. package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +3 -8
  175. package/packages/bundled-features/src/tenant/seeding.ts +33 -27
  176. package/packages/bundled-features/src/tenant-handover/move-entity-graph.ts +14 -3
  177. package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
  178. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +9 -1
  179. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +3 -2
  180. package/packages/bundled-features/src/tier-engine/changes.json +18 -0
  181. package/packages/bundled-features/src/tier-engine/constants.ts +9 -0
  182. package/packages/bundled-features/src/tier-engine/entity.ts +9 -4
  183. package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
  184. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +8 -5
  185. package/packages/bundled-features/src/tier-engine/index.ts +2 -0
  186. package/packages/bundled-features/src/user/changes.json +6 -0
  187. package/packages/bundled-features/src/user/constants.ts +1 -0
  188. package/packages/bundled-features/src/user/invited-global-roles.ts +2 -4
  189. package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
  190. package/packages/bundled-features/src/user/seeding.ts +19 -5
  191. package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
  192. package/packages/bundled-features/src/user-data-rights/email-templates.ts +0 -1
  193. package/packages/bundled-features/src/user-data-rights/i18n.ts +5 -0
  194. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +13 -2
  195. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +7 -1
  196. package/packages/bundled-features/src/user-profile/changes.json +1 -1
  197. package/packages/bundled-features/src/waitlist/changes.json +18 -0
  198. package/packages/bundled-features/src/waitlist/feature.ts +1 -1
  199. package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +35 -3
  200. package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +2 -17
  201. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +33 -9
  202. package/packages/bundled-features/src/waitlist/i18n.ts +9 -0
  203. package/packages/bundled-features/src/waitlist/lib.ts +20 -0
  204. package/packages/bundled-features/src/waitlist/options.ts +1 -1
  205. package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
  206. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +2 -1
  207. package/packages/bundled-features/src/workflow-runner/feature.ts +1 -1
  208. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
  209. package/samples/apps/ui-walkthrough/package.json +9 -0
  210. package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
  211. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +87 -2
  212. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +246 -30
  213. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +4 -0
  214. package/samples/apps/ui-walkthrough/src/run-config.ts +18 -1
  215. package/samples/apps/use-all-bundled/README.md +3 -13
  216. package/samples/apps/use-all-bundled/feature-manifest.json +7 -5
  217. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +34 -118
  218. package/samples/apps/use-all-bundled/src/app/notes-feature.ts +1 -1
  219. package/samples/apps/use-all-bundled/src/run-config.ts +0 -3
  220. package/samples/recipes/auth-mfa/README.md +2 -1
  221. package/samples/recipes/auth-mfa/src/feature.ts +1 -0
  222. package/samples/recipes/custom-handlers/README.md +4 -3
  223. package/samples/recipes/custom-handlers/src/feature.ts +21 -12
  224. package/samples/recipes/dashboard-stats/README.md +38 -0
  225. package/samples/recipes/dashboard-stats/package.json +8 -0
  226. package/samples/recipes/dashboard-stats/src/feature.ts +104 -0
  227. package/samples/recipes/legal-pages/README.md +2 -2
  228. package/samples/recipes/managed-pages/README.md +2 -2
  229. package/samples/recipes/pdf-audit-export/README.md +2 -1
  230. package/samples/recipes/pdf-audit-export/package.json +1 -1
  231. package/samples/recipes/pdf-audit-export/src/feature.ts +68 -48
  232. package/samples/recipes/workflow-engine/src/db/queries/suspended-runs.ts +2 -2
  233. package/samples/apps/use-all-bundled/schema/generate.ts +0 -617
  234. package/samples/apps/use-all-bundled/schema/migrations/meta/0000_snapshot.json +0 -3055
  235. package/samples/apps/use-all-bundled/schema/migrations/meta/_journal.json +0 -13
  236. package/samples/apps/use-all-bundled/schema/schema.generated.ts +0 -94
  237. package/samples/apps/use-all-bundled/schema/schema.ts +0 -9
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.364.0",
4
+ "type": "fix",
5
+ "title": "Workflow run-failed and retry logs carry the original cause",
6
+ "detail": "The ops log used to print the generic `workflow step N failed` wrapper. It now prints the underlying error, while the persisted event still carries the error class only."
7
+ },
2
8
  {
3
9
  "version": "0.353.0",
4
10
  "type": "fix",
@@ -22,6 +22,7 @@ import {
22
22
  describeWorkflowStepError,
23
23
  getStep,
24
24
  SYSTEM_TENANT_ID,
25
+ unwrapWorkflowStepError,
25
26
  WORKFLOW_AGGREGATE_TYPE,
26
27
  WORKFLOW_RUN_FAILED_TYPE,
27
28
  WorkflowStepError,
@@ -167,7 +168,7 @@ export function registerEventTrigger(r: FeatureRegistrar, workflow: WorkflowDefi
167
168
  runId,
168
169
  workflowName: workflow.name,
169
170
  stepIndex,
170
- error: String(error),
171
+ error: String(unwrapWorkflowStepError(error)),
171
172
  });
172
173
  const failedPayload: WorkflowRunFailedPayload = {
173
174
  workflowName: workflow.name,
@@ -32,7 +32,7 @@ import { workflowRunPendingTableMeta } from "./tables.js";
32
32
 
33
33
  // Kebab-case matching the "./workflow-runner" export subpath — every other
34
34
  // bundled feature's runtime name follows this convention, and
35
- // FEATURE_IMPORT_REGISTRY (use-all-bundled) keys off it.
35
+ // use-all-bundled's check-coverage matches exports to mounted features by it.
36
36
  const FEATURE_NAME = "workflow-runner";
37
37
 
38
38
  export const workflowRunnerFeature = defineFeature(FEATURE_NAME, (r) => {
@@ -40,6 +40,7 @@ import {
40
40
  runStepList,
41
41
  type StepInstance,
42
42
  SYSTEM_ROLE,
43
+ unwrapWorkflowStepError,
43
44
  WORKFLOW_AGGREGATE_TYPE,
44
45
  WORKFLOW_RESUMED_TYPE,
45
46
  WORKFLOW_RETRY_SCHEDULED_TYPE,
@@ -392,7 +393,12 @@ async function runResumedPipeline(ctx: HandlerContext, input: ResumedPipelineInp
392
393
  });
393
394
  return { isSuccess: true as const, data: { outcome: "completed" as const } };
394
395
  } catch (error) {
395
- log.warn("workflow run failed", { runId, workflowName, stepIndex, error: String(error) });
396
+ log.warn("workflow run failed", {
397
+ runId,
398
+ workflowName,
399
+ stepIndex,
400
+ error: String(unwrapWorkflowStepError(error)),
401
+ });
396
402
  // The input stepIndex is the suspended step; the failure happened at the step the resumed pass reached.
397
403
  const failedPayload: WorkflowRunFailedPayload = {
398
404
  workflowName,
@@ -15,6 +15,7 @@
15
15
  "@cosmicdrift/kumiko-framework": "workspace:*",
16
16
  "@cosmicdrift/kumiko-headless": "workspace:*",
17
17
  "@cosmicdrift/kumiko-renderer-mail-html": "workspace:*",
18
+ "@cosmicdrift/kumiko-renderer-puppeteer-client": "workspace:*",
18
19
  "@cosmicdrift/kumiko-renderer-web": "workspace:*",
19
20
  "@cosmicdrift/kumiko-server-runtime": "workspace:*",
20
21
  "react": "^19.2.6",
@@ -24,5 +25,13 @@
24
25
  },
25
26
  "devDependencies": {
26
27
  "@cosmicdrift/kumiko-testing": "workspace:*"
28
+ },
29
+ "kumiko": {
30
+ "assets": [
31
+ {
32
+ "name": "logo-nordlicht.png",
33
+ "source": "src/app/assets/logo-nordlicht.png"
34
+ }
35
+ ]
27
36
  }
28
37
  }
@@ -7,18 +7,51 @@
7
7
  // setupTestStack-Default ist ephemeral (fresh kumiko_test_<random> DB),
8
8
  // deshalb braucht's keine DB-Reset-Logik hier.
9
9
 
10
- import { defineAppE2eConfig, PLAYWRIGHT_DEMO_ENV } from "@cosmicdrift/kumiko-testing/e2e";
10
+ import {
11
+ defineAppE2eConfig,
12
+ E2E_TIMEOUT_MS,
13
+ PLAYWRIGHT_DEMO_ENV,
14
+ } from "@cosmicdrift/kumiko-testing/e2e";
15
+ import { chromium } from "@playwright/test";
11
16
  import { E2E_PORTS } from "../../e2e/e2e-ports";
12
17
 
13
18
  const PORT = E2E_PORTS["framework/ui-walkthrough"];
19
+ const PRINT_SERVICE_PORT = E2E_PORTS["framework/ui-walkthrough-puppeteer-service"];
20
+ // Both sides of the print service handshake; any value of 16+ characters will do for a local run.
21
+ const PRINT_SERVICE_TOKEN = "ui-walkthrough-e2e-print-service";
14
22
 
23
+ const appConfig = defineAppE2eConfig({
24
+ port: PORT,
25
+ serverEntry: "src/app/server.ts",
26
+ locale: "en-US",
27
+ env: {
28
+ ...PLAYWRIGHT_DEMO_ENV,
29
+ PUPPETEER_SERVICE_URL: `http://127.0.0.1:${PRINT_SERVICE_PORT}`,
30
+ PUPPETEER_AUTH_TOKEN: PRINT_SERVICE_TOKEN,
31
+ },
32
+ });
33
+
34
+ // The PDF preview of the template editor prints through the real puppeteer-service,
35
+ // on the Chromium that `playwright install chromium` provides.
15
36
  export default {
16
- ...defineAppE2eConfig({
17
- port: PORT,
18
- serverEntry: "src/app/server.ts",
19
- locale: "en-US",
20
- env: PLAYWRIGHT_DEMO_ENV,
21
- }),
37
+ ...appConfig,
38
+ webServer: [
39
+ {
40
+ command: "bun run src/index.ts",
41
+ cwd: "../../../apps/puppeteer-service",
42
+ url: `http://127.0.0.1:${PRINT_SERVICE_PORT}/healthz`,
43
+ env: {
44
+ PORT: String(PRINT_SERVICE_PORT),
45
+ PUPPETEER_AUTH_TOKEN: PRINT_SERVICE_TOKEN,
46
+ PUPPETEER_EXECUTABLE_PATH: chromium.executablePath(),
47
+ },
48
+ reuseExistingServer: false,
49
+ timeout: E2E_TIMEOUT_MS.webServer,
50
+ stdout: "pipe",
51
+ stderr: "pipe",
52
+ },
53
+ ...[appConfig.webServer ?? []].flat(),
54
+ ],
22
55
  // globalSetup spawnt vor allen Tests einen bun-Subprozess der die
23
56
  // Registry auswertet und `e2e/.e2e-data.json` schreibt. Der eigent-
24
57
  // liche generated.spec.ts-Runner liest nur die JSON — framework-
@@ -12,6 +12,7 @@ import {
12
12
  import type { SeedFn } from "@cosmicdrift/kumiko-dev-server";
13
13
  import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
14
14
  import { DEFAULT_MAIL_LAYOUT_MJML } from "@cosmicdrift/kumiko-renderer-mail-html";
15
+ import { readBundledAsset } from "@cosmicdrift/kumiko-server-runtime";
15
16
  import {
16
17
  NORDLICHT_TENANT_ID,
17
18
  NORDLICHT_TENANT_NAME,
@@ -23,6 +24,7 @@ import {
23
24
  import {
24
25
  INVOICE_CORRECTED_DRAFT,
25
26
  INVOICE_ISSUED_DRAFT_VERSIONS,
27
+ LOGO_RESOURCE_KEY,
26
28
  PAYMENT_REMINDER_TEMPLATES,
27
29
  type SeedTemplate,
28
30
  SYSTEM_AUTH_MAILS,
@@ -30,12 +32,18 @@ import {
30
32
  SYSTEM_INVOICE_ISSUED,
31
33
  SYSTEM_PDF_LAYOUT,
32
34
  SYSTEM_TENANCY_CERTIFICATE,
35
+ TENANT_INVOICE_OVERRIDES,
33
36
  TENANT_MAIL_LAYOUT_VERSIONS,
34
37
  TENANT_UTILITY_STATEMENT,
35
38
  } from "./template-seed-data";
36
39
 
37
40
  const NORDLICHT = NORDLICHT_TENANT_ID as TenantId;
38
41
 
42
+ // Declared under `kumiko.assets` in this sample's package.json; looked up from the
43
+ // package dir so the seed does not depend on the cwd the dev server was started in.
44
+ const LOGO_ASSET_NAME = "logo-nordlicht.png";
45
+ const SAMPLE_APP_DIR = new URL("../../", import.meta.url).pathname;
46
+
39
47
  const BRANDING = [
40
48
  [BRANDING_QN.title, "Nordlicht Mieterportal"],
41
49
  [BRANDING_QN.accentColor, "#205265"],
@@ -108,6 +116,35 @@ export const seedTemplates: SeedFn = async (stack) => {
108
116
  },
109
117
  user,
110
118
  );
119
+ // Real multipart upload; every resource link and the branding logo consume their own file.
120
+ const uploadLogo = async (user: SessionUser): Promise<string> => {
121
+ const logoPng = await readBundledAsset(LOGO_ASSET_NAME, { cwd: SAMPLE_APP_DIR });
122
+ const form = new FormData();
123
+ form.append(
124
+ "file",
125
+ new File([new Uint8Array(logoPng)], LOGO_RESOURCE_KEY, { type: "image/png" }),
126
+ );
127
+ const response = await stack.app.request("/api/files", {
128
+ method: "POST",
129
+ headers: {
130
+ Authorization: `Bearer ${await stack.jwt.sign(user)}`,
131
+ },
132
+ body: form,
133
+ });
134
+ if (response.status !== 201) throw new Error(`logo upload failed: HTTP ${response.status}`);
135
+ return ((await response.json()) as { id: string }).id;
136
+ };
137
+ const linkInvoicePdf = (templateId: string): Promise<unknown> =>
138
+ stack.http.writeOk(
139
+ TemplateResolverHandlers.resourceLinkPdf,
140
+ {
141
+ templateId,
142
+ key: "rechnung",
143
+ templateSlug: "billing:invoice",
144
+ fileName: "rechnung-{{invoiceNumber}}.pdf",
145
+ },
146
+ tenantAdmin,
147
+ );
111
148
  const upsertSystem = (template: SeedTemplate) =>
112
149
  upsert(TemplateResolverHandlers.upsertSystem, template, platformAdmin);
113
150
  const upsertTenant = (template: SeedTemplate, status: "draft" | "active") =>
@@ -139,16 +176,64 @@ export const seedTemplates: SeedFn = async (stack) => {
139
176
  for (const [key, value] of BRANDING) {
140
177
  await stack.http.writeOk("config:write:set", { key, value }, tenantAdmin);
141
178
  }
179
+ // The built-in "logo" placeholder of the layouts resolves to this file.
180
+ await stack.http.writeOk(
181
+ "config:write:set",
182
+ { key: BRANDING_QN.logoFile, value: await uploadLogo(tenantAdmin) },
183
+ tenantAdmin,
184
+ );
142
185
  // Each save is a new version of the same row; the second one makes the
143
186
  // layout "customised" for the overview and the first leaves history to diff.
144
187
  for (const content of TENANT_MAIL_LAYOUT_VERSIONS) {
145
188
  await upsertTenant(
146
- { slug: "default", kind: "mail-layout", locale: "en", title: "Mail-Layout", content },
189
+ {
190
+ slug: "default",
191
+ kind: "mail-layout",
192
+ locale: "en",
193
+ title: "Mail-Layout",
194
+ content,
195
+ meta: { accentLine: "branding" },
196
+ },
147
197
  "active",
148
198
  );
149
199
  }
150
- for (const draft of INVOICE_ISSUED_DRAFT_VERSIONS) await upsertTenant(draft, "draft");
200
+ let invoiceDraftId: string | null = null;
201
+ for (const draft of INVOICE_ISSUED_DRAFT_VERSIONS) {
202
+ const saved = await upsertTenant(draft, "draft");
203
+ if (invoiceDraftId === null) {
204
+ invoiceDraftId = saved.id;
205
+ await linkInvoicePdf(saved.id);
206
+ }
207
+ }
151
208
  await upsertTenant(INVOICE_CORRECTED_DRAFT, "draft");
152
209
  for (const reminder of PAYMENT_REMINDER_TEMPLATES) await upsertTenant(reminder, "active");
153
210
  await upsertTenant(TENANT_UTILITY_STATEMENT, "active");
211
+
212
+ // Other tenants with their own invoice copy: what the SystemAdmin sees in the overrides drawer.
213
+ for (const override of TENANT_INVOICE_OVERRIDES) {
214
+ const account = await seedAdmin(stack.db, {
215
+ email: override.adminEmail,
216
+ password: TENANT_ADMIN_PASSWORD,
217
+ displayName: override.adminName,
218
+ emailVerified: true,
219
+ memberships: [
220
+ {
221
+ tenantId: override.tenantId as TenantId,
222
+ tenantKey: override.tenantKey,
223
+ tenantName: override.tenantName,
224
+ roles: ["TenantAdmin"],
225
+ },
226
+ ],
227
+ });
228
+ const admin: SessionUser = {
229
+ id: account.id as SessionUser["id"],
230
+ tenantId: override.tenantId as TenantId,
231
+ roles: ["TenantAdmin"],
232
+ };
233
+ for (const version of override.versions) {
234
+ await upsert(TemplateResolverHandlers.upsertTenant, version, admin, {
235
+ status: override.status,
236
+ });
237
+ }
238
+ }
154
239
  };
@@ -16,6 +16,9 @@ export type SeedTemplate = {
16
16
  // Document HTML marks a variable as an empty span; only link targets take {{name}}.
17
17
  const v = (name: string): string => `<span data-variable="${name}"></span>`;
18
18
 
19
+ // Resource key of the logo image the invoice mail embeds (uploaded by seed-templates.ts).
20
+ export const LOGO_RESOURCE_KEY = "logo-nordlicht.png";
21
+
19
22
  const AUTH_VARIABLE_SCHEMA = {
20
23
  appName: { type: "text", required: true },
21
24
  actionUrl: { type: "link", required: true },
@@ -44,16 +47,134 @@ const DOCUMENT_LINES_SCHEMA = {
44
47
  },
45
48
  } as const;
46
49
 
50
+ // Same variable names the billing notifications declare; labels follow the template language.
51
+ type InvoiceDocumentLabels = {
52
+ readonly recipientAddress: string;
53
+ readonly invoiceNumber: string;
54
+ readonly invoiceDate: string;
55
+ readonly invoiceTotal: string;
56
+ readonly netTotal: string;
57
+ readonly vatAmount: string;
58
+ readonly dueDate: string;
59
+ readonly lines: string;
60
+ readonly position: string;
61
+ readonly description: string;
62
+ readonly period: string;
63
+ readonly net: string;
64
+ readonly paymentReference: string;
65
+ readonly iban: string;
66
+ };
67
+
68
+ function invoiceDocumentSchema(labels: InvoiceDocumentLabels) {
69
+ return {
70
+ recipientAddress: {
71
+ type: "text",
72
+ required: true,
73
+ label: labels.recipientAddress,
74
+ example: "Familie Yilmaz, Lindenstraße 14, 24103 Kiel",
75
+ },
76
+ invoiceNumber: {
77
+ type: "text",
78
+ required: true,
79
+ label: labels.invoiceNumber,
80
+ example: "RE-2026-0142",
81
+ },
82
+ invoiceDate: { type: "date", required: true, label: labels.invoiceDate, example: "2026-10-01" },
83
+ dueDate: { type: "date", required: true, label: labels.dueDate, example: "2026-10-15" },
84
+ netTotal: {
85
+ type: "money",
86
+ label: labels.netTotal,
87
+ example: { amount: 107900, currency: "EUR" },
88
+ },
89
+ vatAmount: {
90
+ type: "money",
91
+ label: labels.vatAmount,
92
+ example: { amount: 20501, currency: "EUR" },
93
+ },
94
+ invoiceTotal: {
95
+ type: "money",
96
+ required: true,
97
+ label: labels.invoiceTotal,
98
+ example: { amount: 128401, currency: "EUR" },
99
+ },
100
+ lines: {
101
+ type: "list",
102
+ required: true,
103
+ label: labels.lines,
104
+ item: {
105
+ position: { type: "text", label: labels.position },
106
+ description: { type: "text", label: labels.description },
107
+ period: { type: "text", label: labels.period },
108
+ net: { type: "money", label: labels.net },
109
+ },
110
+ example: [
111
+ {
112
+ position: "1",
113
+ description: "Kaltmiete Lindenstraße 14, WE 3",
114
+ period: "Okt 2026",
115
+ net: { amount: 98000, currency: "EUR" },
116
+ },
117
+ {
118
+ position: "2",
119
+ description: "Stellplatz 7",
120
+ period: "Okt 2026",
121
+ net: { amount: 9900, currency: "EUR" },
122
+ },
123
+ ],
124
+ },
125
+ paymentReference: {
126
+ type: "text",
127
+ label: labels.paymentReference,
128
+ example: "RE-2026-0142 / Yilmaz",
129
+ },
130
+ iban: { type: "text", label: labels.iban, example: "DE12 2105 0170 0012 3456 78" },
131
+ } as const;
132
+ }
133
+
134
+ const INVOICE_DOCUMENT_SCHEMA_DE = invoiceDocumentSchema({
135
+ recipientAddress: "Empfängeradresse",
136
+ invoiceNumber: "Rechnungsnummer",
137
+ invoiceDate: "Rechnungsdatum",
138
+ invoiceTotal: "Rechnungsbetrag",
139
+ netTotal: "Nettobetrag",
140
+ vatAmount: "Umsatzsteuer",
141
+ dueDate: "Fälligkeitsdatum",
142
+ lines: "Rechnungspositionen",
143
+ position: "Nr.",
144
+ description: "Bezeichnung",
145
+ period: "Zeitraum",
146
+ net: "Netto",
147
+ paymentReference: "Zahlungsreferenz",
148
+ iban: "IBAN",
149
+ });
150
+
151
+ const INVOICE_DOCUMENT_SCHEMA_EN = invoiceDocumentSchema({
152
+ recipientAddress: "Recipient address",
153
+ invoiceNumber: "Invoice number",
154
+ invoiceDate: "Invoice date",
155
+ invoiceTotal: "Invoice total",
156
+ netTotal: "Net amount",
157
+ vatAmount: "VAT",
158
+ dueDate: "Due date",
159
+ lines: "Invoice lines",
160
+ position: "No.",
161
+ description: "Description",
162
+ period: "Period",
163
+ net: "Net",
164
+ paymentReference: "Payment reference",
165
+ iban: "IBAN",
166
+ });
167
+
47
168
  export const SYSTEM_PDF_LAYOUT: SeedTemplate = {
48
169
  slug: "default",
49
170
  kind: "pdf-layout",
50
171
  locale: "en",
51
172
  title: "Briefpapier",
52
173
  content: [
53
- `<div data-header><p><strong>${v("tenantName")}</strong></p></div>`,
174
+ `<div data-header><table><tbody><tr><td><img data-resource="logo" alt="Nordlicht"></td><td><p data-align="right" data-size="small">Am Hafen 12 · 24103 Kiel</p></td></tr></tbody></table></div>`,
54
175
  "<div data-content></div>",
55
176
  "<div data-signature></div>",
56
- `<div data-footer><p>${v("tenantName")} · Seite ${v("page")} von ${v("pages")}</p></div>`,
177
+ `<div data-footer><p>Seite ${v("page")} von ${v("pages")}</p><p>${v("tenantName")} · HRB 18422 Kiel · IBAN DE12 2105 0170 0012 3456 78</p></div>`,
57
178
  ].join(""),
58
179
  };
59
180
 
@@ -65,14 +186,14 @@ export const TENANT_MAIL_LAYOUT_VERSIONS: readonly string[] = [
65
186
  `<div data-footer><p>${v("tenantName")}</p></div>`,
66
187
  ].join(""),
67
188
  [
68
- `<div data-header><p><strong>${v("tenantName")}</strong></p><p>Ihre Hausverwaltung in Kiel</p></div>`,
189
+ '<div data-header><img data-resource="logo" alt="Nordlicht"></div>',
69
190
  "<div data-content></div>",
70
191
  `<div data-signature><p>Mit freundlichen Grüßen</p><p>${v("signature")}</p><p>${v("tenantName")}</p></div>`,
71
- `<div data-footer><p>${v("imprint")}</p><p><a href="https://nordlicht-immobilien.de/datenschutz">Datenschutz</a> · <a href="{{unsubscribeUrl}}">Abmelden</a></p></div>`,
192
+ `<div data-footer><p>${v("imprint")}</p><p><a href="https://nordlicht-immobilien.de/datenschutz">Datenschutz</a> · <a href="{{unsubscribeUrl}}">Abmeldelink</a></p></div>`,
72
193
  ].join(""),
73
194
  ];
74
195
 
75
- const LINES_TABLE = `<table><tbody><tr><th><p>Position</p></th><th><p>Betrag</p></th></tr><tr data-each="lines" data-repeat-header=""><td><p>${v("description")}</p></td><td><p>${v("amount")}</p></td></tr></tbody></table>`;
196
+ const LINES_TABLE = `<table><tbody><tr><th><p>Position</p></th><th><p data-align="right">Betrag</p></th></tr><tr data-each="lines" data-repeat-header=""><td><p>${v("description")}</p></td><td><p data-align="right">${v("amount")}</p></td></tr><tr><td><p><strong>Gesamt</strong></p></td><td><p data-align="right">${v("invoiceTotal")}</p></td></tr></tbody></table>`;
76
197
 
77
198
  // The registry of the billing notifications declares the variables, so these
78
199
  // templates carry no variableSchema of their own.
@@ -82,14 +203,14 @@ function invoiceIssuedBody(args: {
82
203
  }): string {
83
204
  const due = args.withDueDate ? ` Bitte überweisen Sie den Betrag bis zum ${v("dueDate")}.` : "";
84
205
  return [
85
- "<h2>Ihre Rechnung ist da</h2>",
206
+ `<h2>Ihre Rechnung ${v("invoiceNumber")}</h2>`,
86
207
  `<p>Guten Tag ${v("recipientName")},</p>`,
87
- `<p>${args.intro} <strong>${v("invoiceNumber")}</strong> über <strong>${v("invoiceTotal")}</strong>.${due}</p>`,
88
- '<a data-button href="{{invoiceUrl}}">Rechnung öffnen</a>',
208
+ `<p>${args.intro} ${v("invoiceNumber")} über ${v("invoiceTotal")}.${due} Die Rechnung finden Sie als PDF im Anhang und jederzeit online.</p>`,
209
+ '<a data-button href="{{invoiceUrl}}">Rechnung online ansehen</a>',
89
210
  LINES_TABLE,
90
211
  "<hr>",
91
- "<p>Mit freundlichen Grüßen</p>",
92
- `<p>${v("tenantName")}</p>`,
212
+ "<p>Bei Fragen zur Rechnung antworten Sie einfach auf diese E-Mail.</p>",
213
+ `<p>${v("signature")}</p>`,
93
214
  ].join("");
94
215
  }
95
216
 
@@ -101,6 +222,12 @@ const invoiceIssuedMeta = (subject: string) => ({
101
222
  previewText: "Rechnung {{invoiceNumber}} über {{invoiceTotal}}, online abrufbar",
102
223
  });
103
224
 
225
+ const tenantInvoiceMeta = (subject: string) => ({
226
+ ...invoiceIssuedMeta(subject),
227
+ senderName: "Nordlicht Immobilien GmbH",
228
+ replyTo: "buchhaltung@nordlicht-immobilien.de",
229
+ });
230
+
104
231
  // The platform default has a due date; the tenant drafts below drop it, which
105
232
  // is what blocks publishing in the walkthrough.
106
233
  export const SYSTEM_INVOICE_ISSUED: SeedTemplate = {
@@ -123,7 +250,7 @@ export const INVOICE_ISSUED_DRAFT_VERSIONS: readonly SeedTemplate[] = [
123
250
  intro: "anbei senden wir Ihnen die Rechnung",
124
251
  withDueDate: false,
125
252
  }),
126
- meta: invoiceIssuedMeta("Ihre Rechnung {{invoiceNumber}}"),
253
+ meta: tenantInvoiceMeta("Ihre Rechnung {{invoiceNumber}}"),
127
254
  },
128
255
  {
129
256
  slug: INVOICE_ISSUED_SLUG,
@@ -131,7 +258,7 @@ export const INVOICE_ISSUED_DRAFT_VERSIONS: readonly SeedTemplate[] = [
131
258
  locale: "de",
132
259
  title: "Rechnung versendet",
133
260
  content: invoiceIssuedBody({ intro: "anbei erhalten Sie die Rechnung", withDueDate: false }),
134
- meta: invoiceIssuedMeta("Ihre Rechnung {{invoiceNumber}} von {{tenantName}}"),
261
+ meta: tenantInvoiceMeta("Ihre Rechnung {{invoiceNumber}} von {{tenantName}}"),
135
262
  },
136
263
  ];
137
264
 
@@ -279,7 +406,22 @@ export const SYSTEM_AUTH_MAILS: readonly SeedTemplate[] = [
279
406
  ]),
280
407
  ];
281
408
 
282
- const INVOICE_DOCUMENT_ROWS = `<table><tbody><tr><th><p>Leistung</p></th><th><p>Betrag</p></th></tr><tr data-each="lines" data-repeat-header=""><td><p>${v("description")}</p></td><td><p>${v("amount")}</p></td></tr></tbody></table>`;
409
+ const UTILITY_ROWS = `<table><tbody><tr><th><p>Leistung</p></th><th><p>Betrag</p></th></tr><tr data-each="lines" data-repeat-header=""><td><p>${v("description")}</p></td><td><p>${v("amount")}</p></td></tr></tbody></table>`;
410
+
411
+ const right = (inner: string): string => `<p data-align="right">${inner}</p>`;
412
+
413
+ function invoiceRows(labels: {
414
+ readonly service: string;
415
+ readonly period: string;
416
+ readonly net: string;
417
+ }): string {
418
+ return [
419
+ "<table><tbody>",
420
+ `<tr><th><p>#</p></th><th><p>${labels.service}</p></th><th><p>${labels.period}</p></th><th><p data-align="right">${labels.net}</p></th></tr>`,
421
+ `<tr data-each="lines" data-repeat-header=""><td><p>${v("position")}</p></td><td><p>${v("description")}</p></td><td><p>${v("period")}</p></td><td><p data-align="right">${v("net")}</p></td></tr>`,
422
+ "</tbody></table>",
423
+ ].join("");
424
+ }
283
425
 
284
426
  export const SYSTEM_INVOICE_DOCUMENTS: readonly SeedTemplate[] = [
285
427
  {
@@ -288,16 +430,29 @@ export const SYSTEM_INVOICE_DOCUMENTS: readonly SeedTemplate[] = [
288
430
  locale: "de",
289
431
  title: "Rechnung",
290
432
  content: [
433
+ `<p data-size="small"><u>Nordlicht Immobilien GmbH · Am Hafen 12 · 24103 Kiel</u></p>`,
434
+ `<p>${v("recipientAddress")}</p>`,
435
+ right(`Rechnungsnummer: ${v("invoiceNumber")}`),
436
+ right(`Rechnungsdatum: ${v("invoiceDate")}`),
437
+ right(`Fällig am: ${v("dueDate")}`),
291
438
  `<h1>Rechnung ${v("invoiceNumber")}</h1>`,
292
- `<p>Rechnungsdatum: ${v("invoiceDate")}</p>`,
293
- `<p>Guten Tag ${v("customerName")},</p>`,
294
- "<p>für unsere Leistungen stellen wir Ihnen folgenden Betrag in Rechnung.</p>",
295
- INVOICE_DOCUMENT_ROWS,
296
- `<p><strong>Gesamtbetrag: ${v("total")}</strong></p>`,
297
- "<p>Bitte überweisen Sie den Betrag innerhalb von 14 Tagen auf unser Konto.</p>",
439
+ "<p>Sehr geehrte Damen und Herren,</p>",
440
+ "<p>wir stellen Ihnen die folgenden Leistungen in Rechnung:</p>",
441
+ invoiceRows({ service: "Leistung", period: "Zeitraum", net: "Netto" }),
442
+ right(`Nettobetrag ${v("netTotal")}`),
443
+ right(`Umsatzsteuer 19 % ${v("vatAmount")}`),
444
+ right(`<strong>Rechnungsbetrag ${v("invoiceTotal")}</strong>`),
445
+ `<p>Bitte überweisen Sie den Rechnungsbetrag bis zum ${v("dueDate")} unter Angabe der Referenz ${v("paymentReference")} auf ${v("iban")}.</p>`,
446
+ "<div data-page-break></div>",
447
+ "<p>Zahlungsbedingungen und Hinweise auf Seite 2 …</p>",
298
448
  ].join(""),
299
- meta: { layoutSlug: "default", pageFormat: "A4", fileName: "Rechnung.pdf" },
300
- variableSchema: DOCUMENT_LINES_SCHEMA,
449
+ meta: {
450
+ layoutSlug: "default",
451
+ pageFormat: "A4",
452
+ fileName: "rechnung-{{invoiceNumber}}.pdf",
453
+ metadata: { title: "Rechnung {{invoiceNumber}}" },
454
+ },
455
+ variableSchema: INVOICE_DOCUMENT_SCHEMA_DE,
301
456
  },
302
457
  {
303
458
  slug: "billing:invoice",
@@ -305,16 +460,29 @@ export const SYSTEM_INVOICE_DOCUMENTS: readonly SeedTemplate[] = [
305
460
  locale: "en",
306
461
  title: "Invoice",
307
462
  content: [
463
+ `<p data-size="small"><u>Nordlicht Immobilien GmbH · Am Hafen 12 · 24103 Kiel</u></p>`,
464
+ `<p>${v("recipientAddress")}</p>`,
465
+ right(`Invoice number: ${v("invoiceNumber")}`),
466
+ right(`Invoice date: ${v("invoiceDate")}`),
467
+ right(`Due on: ${v("dueDate")}`),
308
468
  `<h1>Invoice ${v("invoiceNumber")}</h1>`,
309
- `<p>Invoice date: ${v("invoiceDate")}</p>`,
310
- `<p>Dear ${v("customerName")},</p>`,
311
- "<p>we charge you the following amount for our services.</p>",
312
- INVOICE_DOCUMENT_ROWS.replace("Leistung", "Service").replace("Betrag", "Amount"),
313
- `<p><strong>Total: ${v("total")}</strong></p>`,
314
- "<p>Please transfer the amount to our account within 14 days.</p>",
469
+ "<p>Dear Sir or Madam,</p>",
470
+ "<p>we charge you the following services:</p>",
471
+ invoiceRows({ service: "Service", period: "Period", net: "Net" }),
472
+ right(`Net amount ${v("netTotal")}`),
473
+ right(`VAT 19 % ${v("vatAmount")}`),
474
+ right(`<strong>Total ${v("invoiceTotal")}</strong>`),
475
+ `<p>Please transfer the amount by ${v("dueDate")} quoting the reference ${v("paymentReference")} to ${v("iban")}.</p>`,
476
+ "<div data-page-break></div>",
477
+ "<p>Terms of payment and notes on page 2 …</p>",
315
478
  ].join(""),
316
- meta: { layoutSlug: "default", pageFormat: "A4", fileName: "Invoice.pdf" },
317
- variableSchema: DOCUMENT_LINES_SCHEMA,
479
+ meta: {
480
+ layoutSlug: "default",
481
+ pageFormat: "A4",
482
+ fileName: "invoice-{{invoiceNumber}}.pdf",
483
+ metadata: { title: "Invoice {{invoiceNumber}}" },
484
+ },
485
+ variableSchema: INVOICE_DOCUMENT_SCHEMA_EN,
318
486
  },
319
487
  ];
320
488
 
@@ -328,7 +496,7 @@ export const TENANT_UTILITY_STATEMENT: SeedTemplate = {
328
496
  `<p>Abrechnungsdatum: ${v("invoiceDate")}</p>`,
329
497
  `<p>Guten Tag ${v("customerName")},</p>`,
330
498
  "<p>nachfolgend erhalten Sie die Abrechnung der Betriebskosten für den Abrechnungszeitraum.</p>",
331
- INVOICE_DOCUMENT_ROWS,
499
+ UTILITY_ROWS,
332
500
  `<p><strong>Ergebnis: ${v("total")}</strong></p>`,
333
501
  "<p>Ein Guthaben überweisen wir Ihnen innerhalb von 30 Tagen, eine Nachzahlung ist zum Monatsende fällig.</p>",
334
502
  ].join(""),
@@ -352,3 +520,51 @@ export const SYSTEM_TENANCY_CERTIFICATE: SeedTemplate = {
352
520
  invoiceDate: { type: "date", required: true },
353
521
  },
354
522
  };
523
+
524
+ // Two other tenants customise the invoice for the "Anpassungen der Mandanten" drawer: one
525
+ // published copy with a second save, one draft.
526
+ export type TenantInvoiceOverride = {
527
+ readonly tenantId: string;
528
+ readonly tenantKey: string;
529
+ readonly tenantName: string;
530
+ readonly adminEmail: string;
531
+ readonly adminName: string;
532
+ readonly status: "active" | "draft";
533
+ readonly versions: readonly SeedTemplate[];
534
+ };
535
+
536
+ const SYSTEM_INVOICE_DE = SYSTEM_INVOICE_DOCUMENTS[0] as SeedTemplate;
537
+ const INVOICE_INTRO = "<p>wir stellen Ihnen die folgenden Leistungen in Rechnung:</p>";
538
+
539
+ function invoiceWithIntro(sentence: string): SeedTemplate {
540
+ return {
541
+ ...SYSTEM_INVOICE_DE,
542
+ content: SYSTEM_INVOICE_DE.content.replace(INVOICE_INTRO, `<p>${sentence}</p>`),
543
+ };
544
+ }
545
+
546
+ export const TENANT_INVOICE_OVERRIDES: readonly TenantInvoiceOverride[] = [
547
+ {
548
+ tenantId: "00000000-0000-4000-8000-000000000004",
549
+ tenantKey: "kuestenwohnen",
550
+ tenantName: "Küstenwohnen Kiel GmbH",
551
+ adminEmail: "verwaltung@kuestenwohnen-kiel.de",
552
+ adminName: "M. Petersen",
553
+ status: "active",
554
+ versions: [
555
+ invoiceWithIntro("für den genannten Zeitraum stellen wir Ihnen Folgendes in Rechnung:"),
556
+ invoiceWithIntro(
557
+ "für den genannten Zeitraum stellen wir Ihnen Folgendes in Rechnung. Die Nebenkosten rechnen wir jährlich ab.",
558
+ ),
559
+ ],
560
+ },
561
+ {
562
+ tenantId: "00000000-0000-4000-8000-000000000005",
563
+ tenantKey: "hansa-wohnbau",
564
+ tenantName: "Hansa Wohnbau eG",
565
+ adminEmail: "rechnungswesen@hansa-wohnbau.de",
566
+ adminName: "K. Lorenzen",
567
+ status: "draft",
568
+ versions: [invoiceWithIntro("nachfolgend die Abrechnung für Ihre Wohnung:")],
569
+ },
570
+ ];
@@ -10,6 +10,10 @@ const INVOICE_ISSUED_VARIABLES = {
10
10
  required: true,
11
11
  example: "Familie Yilmaz",
12
12
  }),
13
+ recipientEmail: templateVariable.text({
14
+ label: "Empfänger-Adresse",
15
+ example: "yilmaz.familie@example.org",
16
+ }),
13
17
  invoiceNumber: templateVariable.text({
14
18
  label: "Rechnungsnummer",
15
19
  required: true,