@cosmicdrift/kumiko-samples 0.387.0 → 0.389.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/package.json +1 -1
- package/packages/bundled-features/package.json +8 -1
- package/packages/bundled-features/src/agent-tools/changes.json +12 -0
- package/packages/bundled-features/src/agent-tools/index.ts +1 -1
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +48 -32
- package/packages/bundled-features/src/audit/changes.json +12 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -0
- package/packages/bundled-features/src/audit/handlers/audit-events.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +12 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +4 -0
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +8 -0
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +16 -3
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +8 -2
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +23 -21
- package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +6 -3
- package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +11 -1
- package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
- package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +16 -5
- package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +7 -1
- package/packages/bundled-features/src/auth-email-password/web/return-path.ts +35 -0
- package/packages/bundled-features/src/auth-email-password/web/session.tsx +6 -1
- package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -3
- package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +12 -2
- package/packages/bundled-features/src/auth-mfa/changes.json +12 -0
- package/packages/bundled-features/src/auth-mfa/config.ts +3 -7
- package/packages/bundled-features/src/auth-mfa/feature-exports.ts +2 -2
- package/packages/bundled-features/src/auth-mfa/feature.ts +6 -0
- package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +8 -5
- package/packages/bundled-features/src/auth-mfa/web/i18n.ts +6 -1
- package/packages/bundled-features/src/auth-mfa/web/mfa-code-or-recovery-field.tsx +68 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-disable-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +15 -6
- package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +65 -18
- package/packages/bundled-features/src/billing-foundation/changes.json +55 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +32 -42
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +8 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +14 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +3 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +12 -6
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +35 -19
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +12 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +14 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +5 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +8 -10
- package/packages/bundled-features/src/billing-foundation/events.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +3 -1
- package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +71 -6
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/handlers/plan-switch-preview.query.ts +65 -0
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +44 -8
- package/packages/bundled-features/src/billing-foundation/handlers/switch-plan.write.ts +72 -82
- package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +14 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +22 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +62 -27
- package/packages/bundled-features/src/billing-foundation/plan-switch.ts +84 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +80 -1
- package/packages/bundled-features/src/billing-foundation/validate-options.ts +19 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +119 -47
- package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +89 -42
- package/packages/bundled-features/src/billing-foundation/web/constants.ts +4 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-price.ts +55 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +224 -0
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +25 -8
- package/packages/bundled-features/src/cap-counter/changes.json +30 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +2 -1
- package/packages/bundled-features/src/cap-counter/feature.ts +1 -0
- package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +8 -1
- package/packages/bundled-features/src/cap-overview/feature.ts +1 -0
- package/packages/bundled-features/src/channel-email/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/feature.ts +1 -1
- package/packages/bundled-features/src/config/changes.json +18 -0
- package/packages/bundled-features/src/config/handlers/cascade.query.ts +4 -2
- package/packages/bundled-features/src/config/handlers/set.write.ts +19 -2
- package/packages/bundled-features/src/config/handlers/values.query.ts +4 -2
- package/packages/bundled-features/src/config/write-helpers.ts +27 -8
- package/packages/bundled-features/src/crypto-shredding/changes.json +18 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +11 -10
- package/packages/bundled-features/src/custom-fields/lib/parse-serialized-field.ts +1 -1
- package/packages/bundled-features/src/delivery/changes.json +42 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +9 -3
- package/packages/bundled-features/src/delivery/db/queries/preferences.ts +2 -2
- package/packages/bundled-features/src/delivery/delivery-service.ts +35 -5
- package/packages/bundled-features/src/delivery/feature.ts +1 -0
- package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +20 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-page-chrome.query.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +5 -0
- package/packages/bundled-features/src/delivery/system-channel-context.ts +33 -0
- package/packages/bundled-features/src/delivery/unsubscribe-link.ts +3 -1
- package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +1 -0
- package/packages/bundled-features/src/delivery/unsubscribe-page.ts +6 -0
- package/packages/bundled-features/src/delivery/unsubscribe.ts +36 -15
- package/packages/bundled-features/src/document-ingest-foundation/append-missing-extract-event.ts +84 -0
- package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
- package/packages/bundled-features/src/document-ingest-foundation/events.ts +15 -0
- package/packages/bundled-features/src/document-ingest-foundation/index.ts +11 -0
- package/packages/bundled-features/src/feature-toggles/feature.ts +1 -0
- package/packages/bundled-features/src/file-foundation/index.ts +1 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -1
- package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +0 -2
- package/packages/bundled-features/src/files/README.md +1 -1
- package/packages/bundled-features/src/form-draft/changes.json +6 -0
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/form-draft/decrypt-draft.ts +4 -1
- package/packages/bundled-features/src/host-routing/README.md +1 -1
- package/packages/bundled-features/src/host-routing/changes.json +12 -0
- package/packages/bundled-features/src/host-routing/dispatch.ts +16 -17
- package/packages/bundled-features/src/host-routing/options.ts +2 -1
- package/packages/bundled-features/src/host-routing/validate-options.ts +5 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +17 -5
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +0 -1
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +0 -3
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -3
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +32 -16
- package/packages/bundled-features/src/jobs/changes.json +13 -0
- package/packages/bundled-features/src/jobs/feature.ts +15 -4
- package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +45 -40
- package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +63 -59
- package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -4
- package/packages/bundled-features/src/ledger/web/index.ts +1 -1
- package/packages/bundled-features/src/legal-pages/README.md +1 -1
- package/packages/bundled-features/src/legal-pages/seed.ts +5 -2
- package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/mail-foundation/feature.ts +5 -3
- package/packages/bundled-features/src/mail-transport-smtp/changes.json +6 -0
- package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +1 -1
- package/packages/bundled-features/src/mail-transport-smtp/feature.ts +9 -11
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/constants.ts +1 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +3 -1
- package/packages/bundled-features/src/managed-pages/index.ts +1 -0
- package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +5 -1
- package/packages/bundled-features/src/renderer-pdf/changes.json +6 -0
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +27 -10
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/write-gate.ts +9 -2
- package/packages/bundled-features/src/sessions/changes.json +7 -0
- package/packages/bundled-features/src/sessions/feature.ts +8 -2
- package/packages/bundled-features/src/sessions/index.ts +1 -1
- package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -9
- package/packages/bundled-features/src/shared/decrypt-stored-pii.ts +10 -0
- package/packages/bundled-features/src/shared/index.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +10 -3
- package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +24 -2
- package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +5 -5
- package/packages/bundled-features/src/step-dispatcher/changes.json +12 -0
- package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +17 -3
- package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -0
- package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +2 -1
- package/packages/bundled-features/src/subscription-dodo/changes.json +14 -0
- package/packages/bundled-features/src/subscription-dodo/client.ts +45 -0
- package/packages/bundled-features/src/subscription-dodo/constants.ts +24 -0
- package/packages/bundled-features/src/subscription-dodo/feature.ts +207 -0
- package/packages/bundled-features/src/subscription-dodo/index.ts +16 -0
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +345 -0
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +126 -0
- package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +262 -0
- package/packages/bundled-features/src/subscription-stripe/changes.json +12 -0
- package/packages/bundled-features/src/subscription-stripe/feature.ts +1 -0
- package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -6
- package/packages/bundled-features/src/tags/feature.ts +1 -0
- package/packages/bundled-features/src/template-resolver/api.ts +31 -4
- package/packages/bundled-features/src/template-resolver/changes.json +84 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +1 -0
- package/packages/bundled-features/src/template-resolver/document-ops.ts +20 -1
- package/packages/bundled-features/src/template-resolver/feature.ts +4 -4
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +27 -5
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +36 -11
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +18 -19
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +45 -10
- package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +22 -19
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts +9 -5
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +8 -6
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +44 -5
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +37 -26
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +7 -2
- package/packages/bundled-features/src/template-resolver/seeding.ts +42 -17
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +74 -12
- package/packages/bundled-features/src/template-resolver/template-delivery-policy.ts +0 -11
- package/packages/bundled-features/src/template-resolver/template-meta.ts +12 -4
- package/packages/bundled-features/src/template-resolver/template-preview.ts +11 -3
- package/packages/bundled-features/src/template-resolver/web/constants.ts +7 -5
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +6 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/mail-layout-constants.ts +8 -0
- package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/feature.ts +1 -0
- package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +9 -3
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -3
- package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +7 -4
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +12 -0
- package/packages/bundled-features/src/tenant-lifecycle/constants.ts +5 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +5 -1
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +35 -1
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +4 -1
- package/packages/bundled-features/src/tier-engine/web/constants.ts +3 -3
- package/packages/bundled-features/src/user/feature.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
- package/packages/bundled-features/src/user-data-rights/feature.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/index.ts +6 -1
- package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +3 -4
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +2 -1
- package/packages/bundled-features/src/user-data-rights/web/constants.ts +6 -0
- package/packages/bundled-features/src/user-data-rights/web/index.ts +1 -0
- package/packages/bundled-features/src/waitlist/changes.json +36 -0
- package/packages/bundled-features/src/waitlist/feature.ts +1 -0
- package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +2 -1
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +28 -9
- package/packages/bundled-features/src/waitlist/import.ts +28 -3
- package/packages/bundled-features/src/waitlist/legacy-migrate.ts +24 -4
- package/packages/bundled-features/src/waitlist/link-on-signup.ts +20 -10
- package/packages/bundled-features/src/waitlist/options.ts +5 -1
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +1 -1
- package/samples/apps/styleguide/src/app/client.tsx +2 -0
- package/samples/apps/styleguide/src/app/seed.ts +6 -4
- package/samples/apps/styleguide/src/app/server.ts +4 -1
- package/samples/apps/styleguide/src/features/widgets/feature.ts +15 -4
- package/samples/apps/styleguide/src/features/widgets/i18n.ts +11 -0
- package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +24 -0
- package/samples/apps/styleguide/src/features/workboard/feature.ts +185 -0
- package/samples/apps/styleguide/src/features/workboard/i18n.ts +27 -0
- package/samples/apps/styleguide/src/features/workboard/seed.ts +121 -0
- package/samples/apps/styleguide/src/features/workboard/web.ts +13 -0
- package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +9 -0
- package/samples/apps/use-all-bundled/bin/main.ts +2 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +5 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +90 -15
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +16 -6
- package/samples/apps/use-all-bundled/feature-manifest.json +136 -3
- package/samples/apps/use-all-bundled/kumiko/schema.ts +2 -0
- package/samples/apps/use-all-bundled/kumiko.config.ts +2 -0
- package/samples/apps/use-all-bundled/package.json +2 -1
- package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -0
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +2 -0
- package/samples/apps/use-all-bundled/src/bundled-lists.ts +15 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +9 -3
- package/samples/recipes/auth-mfa/README.md +5 -0
- package/samples/recipes/auth-mfa/package.json +5 -2
- package/samples/recipes/auth-mfa/src/web/confirmation-code-step.tsx +49 -0
- package/samples/recipes/auth-mfa/tsconfig.json +8 -0
- package/samples/recipes/env-composition/README.md +3 -1
- package/samples/recipes/env-composition/src/env.ts +2 -0
- package/samples/recipes/golden-path/src/feature.ts +2 -1
- package/samples/recipes/legal-pages/README.md +1 -1
- package/samples/recipes/lifecycle-hooks/README.md +4 -0
- package/samples/recipes/lifecycle-hooks/src/feature.ts +26 -1
- package/samples/recipes/list-table-options/README.md +50 -0
- package/samples/recipes/list-table-options/package.json +11 -0
- package/samples/recipes/list-table-options/src/feature.ts +221 -0
- package/samples/recipes/pdf-audit-export/src/feature.ts +2 -1
- package/samples/recipes/rate-limiting/README.md +18 -0
- package/samples/recipes/rate-limiting/src/feature.ts +12 -0
- package/samples/recipes/state-machine/src/events.ts +3 -2
- package/samples/recipes/tenant-isolation/README.md +1 -0
- package/samples/recipes/tenant-isolation/src/feature.ts +36 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts +0 -12
|
@@ -28,13 +28,21 @@ const loanMonthMs = (monthIndex: number): number => Date.UTC(2020, monthIndex, 1
|
|
|
28
28
|
// screenshots stay stable.
|
|
29
29
|
const growth = (1 + LOAN_MONTHLY_RATE) ** LOAN_MONTHS;
|
|
30
30
|
const annuityMinor = (LOAN_PRINCIPAL_MINOR * LOAN_MONTHLY_RATE * growth) / (growth - 1);
|
|
31
|
+
// Month index -> extra repayment, which shortens the term below LOAN_MONTHS.
|
|
32
|
+
const EXTRA_REPAYMENTS_MINOR: ReadonlyMap<number, number> = new Map([
|
|
33
|
+
[84, 3_000_000],
|
|
34
|
+
[150, 2_000_000],
|
|
35
|
+
]);
|
|
31
36
|
let balanceMinor: number = LOAN_PRINCIPAL_MINOR;
|
|
32
37
|
let interestPaidMinor = 0;
|
|
38
|
+
let payoffMonth: number | undefined;
|
|
33
39
|
const LOAN_REMAINING_POINTS = [{ atMs: loanMonthMs(0), value: LOAN_PRINCIPAL_MINOR }];
|
|
34
40
|
const LOAN_INTEREST_POINTS = [{ atMs: loanMonthMs(0), value: 0 }];
|
|
35
41
|
for (let i = 1; i <= LOAN_MONTHS; i++) {
|
|
36
42
|
const interest = balanceMinor * LOAN_MONTHLY_RATE;
|
|
37
|
-
|
|
43
|
+
const extraRepayment = EXTRA_REPAYMENTS_MINOR.get(i) ?? 0;
|
|
44
|
+
balanceMinor = Math.max(0, balanceMinor - (annuityMinor - interest) - extraRepayment);
|
|
45
|
+
if (balanceMinor === 0) payoffMonth ??= i;
|
|
38
46
|
interestPaidMinor += interest;
|
|
39
47
|
LOAN_REMAINING_POINTS.push({ atMs: loanMonthMs(i), value: Math.round(balanceMinor) });
|
|
40
48
|
LOAN_INTEREST_POINTS.push({ atMs: loanMonthMs(i), value: Math.round(interestPaidMinor) });
|
|
@@ -44,10 +52,13 @@ const LOAN_SERIES = [
|
|
|
44
52
|
{ key: "interest", label: "widgets:dashboard:loan-interest", points: LOAN_INTEREST_POINTS },
|
|
45
53
|
];
|
|
46
54
|
const LOAN_MARKERS = [
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
...[...EXTRA_REPAYMENTS_MINOR.keys()].map((monthIndex, i) => ({
|
|
56
|
+
atMs: loanMonthMs(monthIndex),
|
|
57
|
+
label: { i18nKey: `widgets:dashboard:marker-extra-${i + 1}` },
|
|
58
|
+
kind: "extra",
|
|
59
|
+
})),
|
|
49
60
|
{
|
|
50
|
-
atMs: loanMonthMs(LOAN_MONTHS),
|
|
61
|
+
atMs: loanMonthMs(payoffMonth ?? LOAN_MONTHS),
|
|
51
62
|
label: { i18nKey: "widgets:dashboard:marker-payoff" },
|
|
52
63
|
kind: "payoff",
|
|
53
64
|
},
|
|
@@ -119,6 +119,17 @@ export const WIDGETS_I18N = {
|
|
|
119
119
|
"widgets:catalog:status-maintenance": { de: "maintenance", en: "maintenance" },
|
|
120
120
|
"widgets:catalog:repayment-model": { de: "Tilgungsmodell", en: "Repayment model" },
|
|
121
121
|
"widgets:catalog:mode-annuity": { de: "Annuität", en: "Annuity" },
|
|
122
|
+
"widgets:catalog:choice-cards": { de: "Zahlungsanbieter", en: "Payment provider" },
|
|
123
|
+
"widgets:catalog:choice-direct": { de: "Direkt", en: "Direct" },
|
|
124
|
+
"widgets:catalog:choice-direct-description": {
|
|
125
|
+
de: "Rechnung vom Anbieter der App",
|
|
126
|
+
en: "Invoiced by the app operator",
|
|
127
|
+
},
|
|
128
|
+
"widgets:catalog:choice-reseller": { de: "Reseller", en: "Reseller" },
|
|
129
|
+
"widgets:catalog:choice-reseller-description": {
|
|
130
|
+
de: "Verkäufer ist der Reseller, inklusive Umsatzsteuer",
|
|
131
|
+
en: "Sold by the reseller, VAT included",
|
|
132
|
+
},
|
|
122
133
|
"widgets:catalog:mode-fixed": { de: "Feste Rate", en: "Fixed rate" },
|
|
123
134
|
"widgets:catalog:model": { de: "Modell", en: "Model" },
|
|
124
135
|
"widgets:catalog:nominal-rate": { de: "Sollzins", en: "Nominal rate" },
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
AiTextArea,
|
|
7
7
|
AiTextField,
|
|
8
8
|
BooleanField,
|
|
9
|
+
ChoiceCards,
|
|
9
10
|
CollapsibleSection,
|
|
10
11
|
ComparisonTable,
|
|
11
12
|
DateField,
|
|
@@ -50,6 +51,7 @@ export function Widgets(): ReactNode {
|
|
|
50
51
|
const t = useTranslation();
|
|
51
52
|
const locale = useLocale().locale();
|
|
52
53
|
const [mode, setMode] = useState<"annuity" | "fixed">("annuity");
|
|
54
|
+
const [provider, setProvider] = useState<"direct" | "reseller">("direct");
|
|
53
55
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
54
56
|
const [belowHeaderDrawerOpen, setBelowHeaderDrawerOpen] = useState(false);
|
|
55
57
|
const [floatingPanelOpen, setFloatingPanelOpen] = useState(false);
|
|
@@ -181,6 +183,28 @@ export function Widgets(): ReactNode {
|
|
|
181
183
|
/>
|
|
182
184
|
</SectionCard>
|
|
183
185
|
|
|
186
|
+
<SectionCard title={t("widgets:catalog:choice-cards")}>
|
|
187
|
+
<ChoiceCards
|
|
188
|
+
value={provider}
|
|
189
|
+
onChange={setProvider}
|
|
190
|
+
ariaLabel={t("widgets:catalog:choice-cards")}
|
|
191
|
+
options={[
|
|
192
|
+
{
|
|
193
|
+
value: "direct",
|
|
194
|
+
title: t("widgets:catalog:choice-direct"),
|
|
195
|
+
description: t("widgets:catalog:choice-direct-description"),
|
|
196
|
+
aside: "9,00 €",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
value: "reseller",
|
|
200
|
+
title: t("widgets:catalog:choice-reseller"),
|
|
201
|
+
description: t("widgets:catalog:choice-reseller-description"),
|
|
202
|
+
aside: "9,90 €",
|
|
203
|
+
},
|
|
204
|
+
]}
|
|
205
|
+
/>
|
|
206
|
+
</SectionCard>
|
|
207
|
+
|
|
184
208
|
<CollapsibleSection title={t("widgets:catalog:advanced-settings")}>
|
|
185
209
|
<EmptyState
|
|
186
210
|
title={t("widgets:catalog:no-extra-repayments-title")}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDateField,
|
|
3
|
+
createEntity,
|
|
4
|
+
createNumberField,
|
|
5
|
+
createSelectField,
|
|
6
|
+
createTextField,
|
|
7
|
+
defineFeature,
|
|
8
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
9
|
+
|
|
10
|
+
import { WORKBOARD_I18N } from "./i18n";
|
|
11
|
+
|
|
12
|
+
export const taskEntity = createEntity({
|
|
13
|
+
table: "read_workboard_tasks",
|
|
14
|
+
fields: {
|
|
15
|
+
title: createTextField({
|
|
16
|
+
personal: false,
|
|
17
|
+
reason: "is_business_data",
|
|
18
|
+
required: true,
|
|
19
|
+
searchable: true,
|
|
20
|
+
sortable: true,
|
|
21
|
+
}),
|
|
22
|
+
status: createSelectField({
|
|
23
|
+
options: ["open", "inProgress", "blocked", "done"] as const,
|
|
24
|
+
default: "open",
|
|
25
|
+
sortable: true,
|
|
26
|
+
filterable: true,
|
|
27
|
+
}),
|
|
28
|
+
priority: createSelectField({
|
|
29
|
+
options: ["low", "medium", "high"] as const,
|
|
30
|
+
default: "medium",
|
|
31
|
+
sortable: true,
|
|
32
|
+
filterable: true,
|
|
33
|
+
}),
|
|
34
|
+
dueDate: createDateField({ sortable: true }),
|
|
35
|
+
assignee: createTextField({ personal: false, reason: "is_business_data", sortable: true }),
|
|
36
|
+
estimateHours: createNumberField({ sortable: true, filterable: true }),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const open = {
|
|
41
|
+
access: {
|
|
42
|
+
openToAll: {
|
|
43
|
+
reason: "demo app: any signed-in user manages every task; there is no per-user ownership",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
// All of this round's list options on one screen: remembered state, one reset
|
|
49
|
+
// button, the column picker (estimateHours starts hidden) and the density choice.
|
|
50
|
+
export const workboardFeature = defineFeature("workboard", (r) => {
|
|
51
|
+
r.translations({ keys: WORKBOARD_I18N });
|
|
52
|
+
r.crud("task", taskEntity, { write: open, read: open });
|
|
53
|
+
|
|
54
|
+
r.screen({
|
|
55
|
+
id: "task-list",
|
|
56
|
+
type: "entityList",
|
|
57
|
+
entity: "task",
|
|
58
|
+
columns: [
|
|
59
|
+
"title",
|
|
60
|
+
"status",
|
|
61
|
+
"priority",
|
|
62
|
+
{ field: "dueDate", hideOnNarrow: true },
|
|
63
|
+
{ field: "assignee", hideOnNarrow: true },
|
|
64
|
+
{ field: "estimateHours", hidden: true, hideOnNarrow: true },
|
|
65
|
+
],
|
|
66
|
+
pagination: "pages",
|
|
67
|
+
pageSize: 25,
|
|
68
|
+
defaultSort: { field: "dueDate", dir: "asc" },
|
|
69
|
+
searchable: true,
|
|
70
|
+
facets: { status: { display: "chips" }, priority: {} },
|
|
71
|
+
table: {
|
|
72
|
+
rememberState: true,
|
|
73
|
+
resetAll: true,
|
|
74
|
+
columnPicker: true,
|
|
75
|
+
density: { default: "comfortable", userChoice: true },
|
|
76
|
+
export: { xlsx: true },
|
|
77
|
+
},
|
|
78
|
+
rowActions: [
|
|
79
|
+
{ kind: "navigate", id: "edit", label: "Edit", screen: "task-edit", rowClick: true },
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// The same tasks as a spreadsheet-like board: fixed column widths, a pinned title,
|
|
84
|
+
// keyboard navigation between rows, row checkboxes with a bulk delete.
|
|
85
|
+
r.screen({
|
|
86
|
+
id: "task-board",
|
|
87
|
+
type: "entityList",
|
|
88
|
+
entity: "task",
|
|
89
|
+
columns: [
|
|
90
|
+
{ field: "title", pinned: "left", width: 260, minWidth: 140 },
|
|
91
|
+
{ field: "status", width: 180, minWidth: 100 },
|
|
92
|
+
{ field: "priority", width: 180, minWidth: 100 },
|
|
93
|
+
{ field: "dueDate", width: 180, minWidth: 100, hideOnNarrow: true },
|
|
94
|
+
{ field: "assignee", width: 220, minWidth: 100, hideOnNarrow: true },
|
|
95
|
+
{ field: "estimateHours", width: 180, minWidth: 100, hideOnNarrow: true },
|
|
96
|
+
],
|
|
97
|
+
pagination: "pages",
|
|
98
|
+
pageSize: 25,
|
|
99
|
+
defaultSort: { field: "dueDate", dir: "asc" },
|
|
100
|
+
searchable: true,
|
|
101
|
+
table: {
|
|
102
|
+
rememberState: true,
|
|
103
|
+
columnPicker: true,
|
|
104
|
+
pinning: true,
|
|
105
|
+
resizing: true,
|
|
106
|
+
keyboardNavigation: true,
|
|
107
|
+
density: { default: "comfortable", userChoice: true },
|
|
108
|
+
},
|
|
109
|
+
rowActions: [
|
|
110
|
+
{ kind: "navigate", id: "edit", label: "Edit", screen: "task-edit", rowClick: true },
|
|
111
|
+
{
|
|
112
|
+
kind: "writeHandler",
|
|
113
|
+
id: "delete",
|
|
114
|
+
label: "Delete",
|
|
115
|
+
handler: "workboard:write:task:delete",
|
|
116
|
+
style: "danger",
|
|
117
|
+
visible: { field: "status", ne: "done" },
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
bulkActions: [{ id: "delete", rowAction: "delete" }],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// The same tasks grouped by status on the server: each header carries the
|
|
124
|
+
// group's count and estimate sum, a footer row the sum over all filtered tasks.
|
|
125
|
+
r.screen({
|
|
126
|
+
id: "task-estimates",
|
|
127
|
+
type: "entityList",
|
|
128
|
+
entity: "task",
|
|
129
|
+
columns: ["title", "status", "priority", "assignee", "estimateHours"],
|
|
130
|
+
pagination: "pages",
|
|
131
|
+
pageSize: 3,
|
|
132
|
+
defaultSort: { field: "title", dir: "asc" },
|
|
133
|
+
searchable: true,
|
|
134
|
+
facets: { estimateHours: { display: "range" } },
|
|
135
|
+
table: {
|
|
136
|
+
grouping: { field: "status", collapsedWhen: "done", userChoice: true, fields: ["priority"] },
|
|
137
|
+
totals: [{ field: "estimateHours", kind: "sum" }],
|
|
138
|
+
},
|
|
139
|
+
rowActions: [
|
|
140
|
+
{ kind: "navigate", id: "edit", label: "Edit", screen: "task-edit", rowClick: true },
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
r.screen({
|
|
145
|
+
id: "task-edit",
|
|
146
|
+
type: "entityEdit",
|
|
147
|
+
entity: "task",
|
|
148
|
+
dormant: true,
|
|
149
|
+
listScreenId: "task-list",
|
|
150
|
+
layout: {
|
|
151
|
+
sections: [
|
|
152
|
+
{
|
|
153
|
+
columns: 2,
|
|
154
|
+
fields: ["title", "assignee", "status", "priority", "dueDate", "estimateHours"],
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
r.nav({ id: "workboard", label: "workboard:nav.workboard", icon: "list", order: 40 });
|
|
161
|
+
r.nav({
|
|
162
|
+
id: "task-list",
|
|
163
|
+
label: "workboard:nav.tasks",
|
|
164
|
+
parent: "workboard:nav:workboard",
|
|
165
|
+
screen: "workboard:screen:task-list",
|
|
166
|
+
icon: "table",
|
|
167
|
+
order: 10,
|
|
168
|
+
});
|
|
169
|
+
r.nav({
|
|
170
|
+
id: "task-board",
|
|
171
|
+
label: "workboard:nav.board",
|
|
172
|
+
parent: "workboard:nav:workboard",
|
|
173
|
+
screen: "workboard:screen:task-board",
|
|
174
|
+
icon: "table",
|
|
175
|
+
order: 20,
|
|
176
|
+
});
|
|
177
|
+
r.nav({
|
|
178
|
+
id: "task-estimates",
|
|
179
|
+
label: "workboard:nav.estimates",
|
|
180
|
+
parent: "workboard:nav:workboard",
|
|
181
|
+
screen: "workboard:screen:task-estimates",
|
|
182
|
+
icon: "table",
|
|
183
|
+
order: 30,
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { LocalizedString } from "../shared-i18n";
|
|
2
|
+
|
|
3
|
+
export const WORKBOARD_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
4
|
+
"workboard:nav.workboard": { de: "Aufgaben", en: "Workboard" },
|
|
5
|
+
"workboard:nav.tasks": { de: "Alle Aufgaben", en: "All tasks" },
|
|
6
|
+
"workboard:nav.board": { de: "Aufgaben-Board", en: "Task board" },
|
|
7
|
+
"workboard:nav.estimates": { de: "Aufwand je Status", en: "Estimates by status" },
|
|
8
|
+
"screen:task-board.title": { de: "Aufgaben-Board", en: "Task board" },
|
|
9
|
+
"screen:task-estimates.title": { de: "Aufwand je Status", en: "Estimates by status" },
|
|
10
|
+
"screen:task-list.title": { de: "Aufgaben", en: "Tasks" },
|
|
11
|
+
"screen:task-edit.title": { de: "Aufgabe bearbeiten", en: "Edit task" },
|
|
12
|
+
"screen:task-edit.create.title": { de: "Aufgabe anlegen", en: "Create task" },
|
|
13
|
+
"screen:task-edit.edit.title": { de: "Aufgabe bearbeiten", en: "Edit task" },
|
|
14
|
+
"workboard:entity:task:field:title": { de: "Titel", en: "Title" },
|
|
15
|
+
"workboard:entity:task:field:status": { de: "Status", en: "Status" },
|
|
16
|
+
"workboard:entity:task:field:status:option:open": { de: "Offen", en: "Open" },
|
|
17
|
+
"workboard:entity:task:field:status:option:inProgress": { de: "In Arbeit", en: "In progress" },
|
|
18
|
+
"workboard:entity:task:field:status:option:blocked": { de: "Blockiert", en: "Blocked" },
|
|
19
|
+
"workboard:entity:task:field:status:option:done": { de: "Erledigt", en: "Done" },
|
|
20
|
+
"workboard:entity:task:field:priority": { de: "Priorität", en: "Priority" },
|
|
21
|
+
"workboard:entity:task:field:priority:option:low": { de: "Niedrig", en: "Low" },
|
|
22
|
+
"workboard:entity:task:field:priority:option:medium": { de: "Mittel", en: "Medium" },
|
|
23
|
+
"workboard:entity:task:field:priority:option:high": { de: "Hoch", en: "High" },
|
|
24
|
+
"workboard:entity:task:field:dueDate": { de: "Fällig am", en: "Due date" },
|
|
25
|
+
"workboard:entity:task:field:assignee": { de: "Zuständig", en: "Assignee" },
|
|
26
|
+
"workboard:entity:task:field:estimateHours": { de: "Aufwand (Std.)", en: "Estimate (h)" },
|
|
27
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { SeedFn } from "@cosmicdrift/kumiko-dev-server";
|
|
2
|
+
import { TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
|
|
3
|
+
|
|
4
|
+
type SeedTask = {
|
|
5
|
+
readonly title: string;
|
|
6
|
+
readonly status: "open" | "inProgress" | "blocked" | "done";
|
|
7
|
+
readonly priority: "low" | "medium" | "high";
|
|
8
|
+
readonly dueDate: string;
|
|
9
|
+
readonly assignee: string;
|
|
10
|
+
readonly estimateHours: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const TASKS: readonly SeedTask[] = [
|
|
14
|
+
{
|
|
15
|
+
title: "Quarterly VAT filing",
|
|
16
|
+
status: "inProgress",
|
|
17
|
+
priority: "high",
|
|
18
|
+
dueDate: "2026-11-10",
|
|
19
|
+
assignee: "Ines Vogel",
|
|
20
|
+
estimateHours: 6,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
title: "Renew office lease",
|
|
24
|
+
status: "open",
|
|
25
|
+
priority: "medium",
|
|
26
|
+
dueDate: "2026-12-01",
|
|
27
|
+
assignee: "Tomas Berg",
|
|
28
|
+
estimateHours: 3,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: "Onboard new support hire",
|
|
32
|
+
status: "open",
|
|
33
|
+
priority: "high",
|
|
34
|
+
dueDate: "2026-11-03",
|
|
35
|
+
assignee: "Ines Vogel",
|
|
36
|
+
estimateHours: 12,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
title: "Migrate invoices to new numbering",
|
|
40
|
+
status: "blocked",
|
|
41
|
+
priority: "high",
|
|
42
|
+
dueDate: "2026-11-15",
|
|
43
|
+
assignee: "Mara Lindqvist",
|
|
44
|
+
estimateHours: 16,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
title: "Update privacy policy",
|
|
48
|
+
status: "done",
|
|
49
|
+
priority: "medium",
|
|
50
|
+
dueDate: "2026-10-20",
|
|
51
|
+
assignee: "Tomas Berg",
|
|
52
|
+
estimateHours: 4,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
title: "Order replacement laptops",
|
|
56
|
+
status: "inProgress",
|
|
57
|
+
priority: "low",
|
|
58
|
+
dueDate: "2026-11-25",
|
|
59
|
+
assignee: "Mara Lindqvist",
|
|
60
|
+
estimateHours: 2,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: "Prepare annual audit documents",
|
|
64
|
+
status: "open",
|
|
65
|
+
priority: "high",
|
|
66
|
+
dueDate: "2027-01-15",
|
|
67
|
+
assignee: "Ines Vogel",
|
|
68
|
+
estimateHours: 24,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
title: "Fix mobile checkout layout",
|
|
72
|
+
status: "inProgress",
|
|
73
|
+
priority: "medium",
|
|
74
|
+
dueDate: "2026-11-05",
|
|
75
|
+
assignee: "Jonas Keller",
|
|
76
|
+
estimateHours: 8,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
title: "Review supplier contracts",
|
|
80
|
+
status: "blocked",
|
|
81
|
+
priority: "medium",
|
|
82
|
+
dueDate: "2026-11-30",
|
|
83
|
+
assignee: "Tomas Berg",
|
|
84
|
+
estimateHours: 10,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
title: "Archive 2025 customer files",
|
|
88
|
+
status: "done",
|
|
89
|
+
priority: "low",
|
|
90
|
+
dueDate: "2026-10-31",
|
|
91
|
+
assignee: "Jonas Keller",
|
|
92
|
+
estimateHours: 5,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
title: "Plan team offsite",
|
|
96
|
+
status: "open",
|
|
97
|
+
priority: "low",
|
|
98
|
+
dueDate: "2027-02-20",
|
|
99
|
+
assignee: "Mara Lindqvist",
|
|
100
|
+
estimateHours: 7,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
title: "Set up weekly sales report",
|
|
104
|
+
status: "inProgress",
|
|
105
|
+
priority: "medium",
|
|
106
|
+
dueDate: "2026-11-12",
|
|
107
|
+
assignee: "Jonas Keller",
|
|
108
|
+
estimateHours: 9,
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
export const seedWorkboardTasks: SeedFn = async (stack) => {
|
|
113
|
+
for (const task of TASKS) {
|
|
114
|
+
const res = await stack.http.write("workboard:write:task:create", task, TestUsers.admin);
|
|
115
|
+
if (!res.ok) {
|
|
116
|
+
// biome-ignore lint/suspicious/noConsole: dev-seed surfaces failures in the runner log
|
|
117
|
+
console.error(`[seed] workboard task "${task.title}" failed:`, await res.text());
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ClientFeatureDefinition,
|
|
3
|
+
TranslationsByLocale,
|
|
4
|
+
} from "@cosmicdrift/kumiko-renderer/web";
|
|
5
|
+
import { toClientTranslations } from "../shared-i18n";
|
|
6
|
+
import { WORKBOARD_I18N } from "./i18n";
|
|
7
|
+
|
|
8
|
+
const translations: TranslationsByLocale = toClientTranslations(WORKBOARD_I18N);
|
|
9
|
+
|
|
10
|
+
export const workboardClient: ClientFeatureDefinition = {
|
|
11
|
+
name: "workboard",
|
|
12
|
+
translations,
|
|
13
|
+
};
|
|
@@ -50,6 +50,7 @@ export const rentalTranslations: TranslationsByLocale = {
|
|
|
50
50
|
"rental.party.role.main": "Hauptmieter",
|
|
51
51
|
"rental.party.role.co": "Mitmieterin",
|
|
52
52
|
"rental.action.addParty": "Partei hinzufügen",
|
|
53
|
+
"rental.action.showTerminated": "Beendete Verträge",
|
|
53
54
|
|
|
54
55
|
"rental.tab.parties": "Vertragsparteien",
|
|
55
56
|
"rental.tab.positions": "Positionen",
|
|
@@ -214,6 +215,7 @@ export const rentalTranslations: TranslationsByLocale = {
|
|
|
214
215
|
"rental.party.role.main": "Main tenant",
|
|
215
216
|
"rental.party.role.co": "Co-tenant",
|
|
216
217
|
"rental.action.addParty": "Add party",
|
|
218
|
+
"rental.action.showTerminated": "Terminated leases",
|
|
217
219
|
|
|
218
220
|
"rental.tab.parties": "Parties",
|
|
219
221
|
"rental.tab.positions": "Positions",
|
|
@@ -52,6 +52,15 @@ export const leaseListScreen: EntityListScreenDefinition = {
|
|
|
52
52
|
pageSize: 25,
|
|
53
53
|
createLabel: "rental.action.createLease",
|
|
54
54
|
searchPlaceholder: "rental.search.placeholder",
|
|
55
|
+
toolbarActions: [
|
|
56
|
+
{
|
|
57
|
+
kind: "navigate",
|
|
58
|
+
id: "show-terminated",
|
|
59
|
+
label: "rental.action.showTerminated",
|
|
60
|
+
style: "primary",
|
|
61
|
+
screen: "lease-list-short",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
55
64
|
rowActions: [openLeaseDetail],
|
|
56
65
|
access: OPEN_ACCESS,
|
|
57
66
|
};
|
|
@@ -8,6 +8,7 @@ import { InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
|
8
8
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
9
9
|
import { composeEnvSchema } from "@cosmicdrift/kumiko-framework/env";
|
|
10
10
|
import { runProdApp } from "@cosmicdrift/kumiko-server-runtime";
|
|
11
|
+
import { BUNDLED_LISTS } from "../src/bundled-lists";
|
|
11
12
|
import {
|
|
12
13
|
APP_FEATURES,
|
|
13
14
|
AUTH_COMPOSE_OPTIONS,
|
|
@@ -27,6 +28,7 @@ await runProdApp({
|
|
|
27
28
|
featureToggles: true,
|
|
28
29
|
envSchema,
|
|
29
30
|
validateBootOptions: VALIDATE_BOOT_OPTIONS,
|
|
31
|
+
bundledLists: BUNDLED_LISTS,
|
|
30
32
|
// Smoke-only: exercises the hard PII boot gate + blind-index gate with
|
|
31
33
|
// every bundled feature mounted. Real apps wire createPgKmsAdapter(...).
|
|
32
34
|
kms: new InMemoryKmsAdapter(),
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// runs in a Playwright worker and must not pull the server import chain).
|
|
5
5
|
|
|
6
6
|
export const GALLERY_PROVIDER = "gallery-mock-provider";
|
|
7
|
+
export const GALLERY_MOR_PROVIDER = "gallery-mock-mor";
|
|
7
8
|
export const GALLERY_TERMS_SLUG = "gallery-terms";
|
|
8
9
|
export const GALLERY_OPERATOR_EMAIL = "billing@gallery.example";
|
|
9
10
|
export const GALLERY_PLAN_PATH = "/tenant-admin/billing-plans";
|
|
@@ -14,3 +15,7 @@ export const GALLERY_TENANT_HOST = "localhost";
|
|
|
14
15
|
export const GALLERY_APEX_HOST = "127.0.0.1";
|
|
15
16
|
|
|
16
17
|
export const GALLERY_PRICE_IDS = { starter: "price_gallery_starter", pro: "price_gallery_pro" };
|
|
18
|
+
export const GALLERY_MOR_PRICE_IDS = {
|
|
19
|
+
starter: "price_gallery_mor_starter",
|
|
20
|
+
pro: "price_gallery_mor_pro",
|
|
21
|
+
};
|
|
@@ -16,9 +16,12 @@ import {
|
|
|
16
16
|
createChannelEmailFeature,
|
|
17
17
|
createInMemoryTransport,
|
|
18
18
|
} from "@cosmicdrift/kumiko-bundled-features/channel-email";
|
|
19
|
+
import type { ExtraRouteDefinition } from "@cosmicdrift/kumiko-framework/api";
|
|
19
20
|
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
20
21
|
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
21
22
|
import {
|
|
23
|
+
GALLERY_MOR_PRICE_IDS,
|
|
24
|
+
GALLERY_MOR_PROVIDER,
|
|
22
25
|
GALLERY_OPERATOR_EMAIL,
|
|
23
26
|
GALLERY_PLAN_PATH,
|
|
24
27
|
GALLERY_PRICE_IDS,
|
|
@@ -28,25 +31,44 @@ import {
|
|
|
28
31
|
|
|
29
32
|
export const galleryMailTransport = createInMemoryTransport();
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
// The merchant-of-record mock is only offered to tenants the spec opts in, so
|
|
35
|
+
// the single-provider flows keep their original screens.
|
|
36
|
+
const morEnabledTenants = new Set<string>();
|
|
37
|
+
|
|
38
|
+
export const GALLERY_MOR_ENABLE_PATH = "/gallery/mor/:tenantId";
|
|
39
|
+
|
|
40
|
+
export function createGalleryMorEnableRoute(): ExtraRouteDefinition {
|
|
41
|
+
return {
|
|
42
|
+
method: "POST",
|
|
43
|
+
path: GALLERY_MOR_ENABLE_PATH,
|
|
44
|
+
entry: "anonymous",
|
|
45
|
+
handler: (c) => {
|
|
46
|
+
morEnabledTenants.add(c.req.param("tenantId") ?? "");
|
|
47
|
+
return c.body(null, 204);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function monthlyPrice(priceId: string, unitAmount: number): ProviderPrice {
|
|
53
|
+
return {
|
|
54
|
+
priceId,
|
|
55
|
+
unitAmount,
|
|
44
56
|
currency: "eur",
|
|
45
57
|
interval: "month",
|
|
46
58
|
intervalCount: 1,
|
|
47
59
|
active: true,
|
|
48
60
|
metadata: {},
|
|
49
|
-
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const PRICES: readonly ProviderPrice[] = [
|
|
65
|
+
monthlyPrice(GALLERY_PRICE_IDS.starter, 900),
|
|
66
|
+
monthlyPrice(GALLERY_PRICE_IDS.pro, 1900),
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const MOR_PRICES: readonly ProviderPrice[] = [
|
|
70
|
+
monthlyPrice(GALLERY_MOR_PRICE_IDS.starter, 990),
|
|
71
|
+
monthlyPrice(GALLERY_MOR_PRICE_IDS.pro, 1990),
|
|
50
72
|
];
|
|
51
73
|
|
|
52
74
|
async function resolveCurrentTier(db: TenantDb, tenantId: string): Promise<string> {
|
|
@@ -74,7 +96,6 @@ export function createGalleryBillingFeature(baseUrl: string): FeatureDefinition
|
|
|
74
96
|
viewRoles: ["TenantAdmin", "SystemAdmin"],
|
|
75
97
|
successPath: GALLERY_PLAN_PATH,
|
|
76
98
|
cancelPath: GALLERY_PLAN_PATH,
|
|
77
|
-
providerName: GALLERY_PROVIDER,
|
|
78
99
|
contact: {
|
|
79
100
|
labelKey: "gallery-screens:plan.enterprise.label",
|
|
80
101
|
benefits: [{ labelKey: "gallery-screens:plan.enterprise.benefit" }],
|
|
@@ -85,6 +106,24 @@ export function createGalleryBillingFeature(baseUrl: string): FeatureDefinition
|
|
|
85
106
|
consumerProtection: {
|
|
86
107
|
termsTextBlock: GALLERY_TERMS_SLUG,
|
|
87
108
|
vatNote: { de: "Preise inkl. USt.", en: "Prices include VAT." },
|
|
109
|
+
providerTexts: {
|
|
110
|
+
[GALLERY_PROVIDER]: {
|
|
111
|
+
checkoutNote: {
|
|
112
|
+
de: "Rechnung von Gallery Beispiel GmbH",
|
|
113
|
+
en: "Invoiced by Gallery Beispiel GmbH",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
[GALLERY_MOR_PROVIDER]: {
|
|
117
|
+
checkoutNote: {
|
|
118
|
+
de: "Verkäufer ist Dodo Payments · Rechnung und USt von Dodo Payments",
|
|
119
|
+
en: "Sold by Dodo Payments · invoice and VAT from Dodo Payments",
|
|
120
|
+
},
|
|
121
|
+
vatNote: {
|
|
122
|
+
de: "Rechnung und Umsatzsteuer stellt Dodo Payments als Merchant of Record.",
|
|
123
|
+
en: "Dodo Payments, as merchant of record, issues the invoice and charges the VAT.",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
88
127
|
operatorEmail: GALLERY_OPERATOR_EMAIL,
|
|
89
128
|
terminationScope: "platform",
|
|
90
129
|
legalLinks: {
|
|
@@ -105,6 +144,7 @@ export function createGalleryEmailFeature(): FeatureDefinition {
|
|
|
105
144
|
// The page URL carries the consent id so the spec can echo it on the webhook,
|
|
106
145
|
// exactly as a real provider echoes the checkout metadata.
|
|
107
146
|
const galleryProviderPlugin: SubscriptionProviderPlugin = {
|
|
147
|
+
displayName: "Stripe",
|
|
108
148
|
verifyAndParseWebhook: async (rawBody) => JSON.parse(rawBody),
|
|
109
149
|
priceToTier: {
|
|
110
150
|
[GALLERY_PRICE_IDS.starter]: "starter",
|
|
@@ -119,12 +159,47 @@ const galleryProviderPlugin: SubscriptionProviderPlugin = {
|
|
|
119
159
|
cancelSubscription: async () => {},
|
|
120
160
|
};
|
|
121
161
|
|
|
162
|
+
// Merchant-of-record stand-in: switches plans in the app with fixed figures
|
|
163
|
+
// (Starter to Pro: 6.67 EUR due now, 1.06 EUR of it VAT). Applying without a
|
|
164
|
+
// consent id fails, so a successful switch in the spec proves the consent went
|
|
165
|
+
// through.
|
|
166
|
+
const galleryMorProviderPlugin: SubscriptionProviderPlugin = {
|
|
167
|
+
displayName: "Dodo Payments",
|
|
168
|
+
isBillingEnabled: async (ctx) => morEnabledTenants.has(ctx.user.tenantId),
|
|
169
|
+
verifyAndParseWebhook: async (rawBody) => JSON.parse(rawBody),
|
|
170
|
+
priceToTier: {
|
|
171
|
+
[GALLERY_MOR_PRICE_IDS.starter]: "starter",
|
|
172
|
+
[GALLERY_MOR_PRICE_IDS.pro]: "pro",
|
|
173
|
+
},
|
|
174
|
+
retrievePrices: async (_ctx, priceIds) => MOR_PRICES.filter((p) => priceIds.includes(p.priceId)),
|
|
175
|
+
createCheckoutSession: async (_ctx, options) => {
|
|
176
|
+
const url = new URL(options.successUrl);
|
|
177
|
+
url.searchParams.set("provider", "mor");
|
|
178
|
+
if (options.consentId !== undefined) url.searchParams.set("consentId", options.consentId);
|
|
179
|
+
return { url: url.toString() };
|
|
180
|
+
},
|
|
181
|
+
cancelSubscription: async () => {},
|
|
182
|
+
previewPlanSwitch: async () => ({
|
|
183
|
+
amountDueNow: 667,
|
|
184
|
+
tax: 106,
|
|
185
|
+
currency: "eur",
|
|
186
|
+
credit: 333,
|
|
187
|
+
effectiveAt: "2027-01-15T00:00:00Z",
|
|
188
|
+
}),
|
|
189
|
+
applyPlanSwitch: async (_ctx, options) => {
|
|
190
|
+
if (options.consentId === undefined) {
|
|
191
|
+
throw new Error("gallery MoR mock: plan switch applied without a consent id");
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
122
196
|
export const galleryProviderFeature: FeatureDefinition = defineFeature(
|
|
123
197
|
"gallery-mock-provider",
|
|
124
198
|
(r) => {
|
|
125
199
|
r.describe("E2E-only mock subscription provider for the consent/termination gallery.");
|
|
126
200
|
r.requires({ features: ["billing-foundation"] });
|
|
127
201
|
r.useExtension("subscriptionProvider", GALLERY_PROVIDER, galleryProviderPlugin);
|
|
202
|
+
r.useExtension("subscriptionProvider", GALLERY_MOR_PROVIDER, galleryMorProviderPlugin);
|
|
128
203
|
},
|
|
129
204
|
);
|
|
130
205
|
|