@smartkomm/plexus-cli 0.0.0-stage → 0.1.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 (80) hide show
  1. package/README.md +826 -2
  2. package/dist/cli.mjs +483 -0
  3. package/dist/skill/.managed-by-plexus-cli +1 -0
  4. package/dist/skill/SKILL.md +185 -0
  5. package/dist/skill/references/accounts/email-changes.md +46 -0
  6. package/dist/skill/references/applications/inspect-process-store.md +61 -0
  7. package/dist/skill/references/applications/start-a-test-application.md +66 -0
  8. package/dist/skill/references/audit/answer-access-request.md +115 -0
  9. package/dist/skill/references/config/attach-a-template.md +117 -0
  10. package/dist/skill/references/config/init-tenant.md +121 -0
  11. package/dist/skill/references/config/onboard-tenant.md +71 -0
  12. package/dist/skill/references/config/rename-program-stage.md +99 -0
  13. package/dist/skill/references/config/set-tenant-branding.md +150 -0
  14. package/dist/skill/references/config/set-tenant-terms.md +50 -0
  15. package/dist/skill/references/config/set-up-billing-catalog.md +73 -0
  16. package/dist/skill/references/config/update-tenant-config.md +170 -0
  17. package/dist/skill/references/config/wipe-tenant-config.md +107 -0
  18. package/dist/skill/references/export/create-named-export.md +47 -0
  19. package/dist/skill/references/feedback/create-and-triage-feedback.md +69 -0
  20. package/dist/skill/references/feedback/manage-feedback-tags.md +57 -0
  21. package/dist/skill/references/finance/change-contract-grant.md +36 -0
  22. package/dist/skill/references/finance/record-submission-date.md +33 -0
  23. package/dist/skill/references/interessenten/list-interessenten.md +54 -0
  24. package/dist/skill/references/notifications/debug-mail-delivery.md +77 -0
  25. package/dist/skill/references/notifications/mail-level.md +90 -0
  26. package/dist/skill/references/notifications/reword-a-letter.md +155 -0
  27. package/dist/skill/references/notifications/trace-a-login.md +50 -0
  28. package/dist/skill/references/offer/add-workflow-node.md +268 -0
  29. package/dist/skill/references/offer/addresses.md +129 -0
  30. package/dist/skill/references/offer/appointment-task.md +79 -0
  31. package/dist/skill/references/offer/assignment-rule.md +195 -0
  32. package/dist/skill/references/offer/attach-form-to-workflow.md +79 -0
  33. package/dist/skill/references/offer/author-form-scenarios.md +97 -0
  34. package/dist/skill/references/offer/author-manifest-meta.md +94 -0
  35. package/dist/skill/references/offer/author-smartform.md +102 -0
  36. package/dist/skill/references/offer/bind-node-input.md +76 -0
  37. package/dist/skill/references/offer/completeness-check.md +137 -0
  38. package/dist/skill/references/offer/countersign-node.md +91 -0
  39. package/dist/skill/references/offer/create-pedagogy-unit.md +55 -0
  40. package/dist/skill/references/offer/declare-node-reads.md +99 -0
  41. package/dist/skill/references/offer/edit-live-tenant-offer.md +144 -0
  42. package/dist/skill/references/offer/edit-smartform.md +121 -0
  43. package/dist/skill/references/offer/edit-workflow-node.md +126 -0
  44. package/dist/skill/references/offer/engage-push-node.md +147 -0
  45. package/dist/skill/references/offer/insert-and-rewire-node.md +67 -0
  46. package/dist/skill/references/offer/node-tasks.md +162 -0
  47. package/dist/skill/references/offer/optional-assessment.md +65 -0
  48. package/dist/skill/references/offer/publish-offer-bundle.md +117 -0
  49. package/dist/skill/references/offer/set-offer-capacities.md +78 -0
  50. package/dist/skill/references/offer/staff-input-if-node.md +112 -0
  51. package/dist/skill/references/offer/task-appointment-result.md +69 -0
  52. package/dist/skill/references/offer/task-family-request.md +69 -0
  53. package/dist/skill/references/offer/task-result-files.md +52 -0
  54. package/dist/skill/references/offer/template-document-node.md +113 -0
  55. package/dist/skill/references/offer/workflow-default-channels.md +69 -0
  56. package/dist/skill/references/resolver/manage-contract-resolver.md +159 -0
  57. package/dist/skill/references/retention/erase-due-applications.md +99 -0
  58. package/dist/skill/references/retention/erase-due-parties.md +107 -0
  59. package/dist/skill/references/roles/migrate-parent-assignments.md +31 -0
  60. package/dist/skill/references/signing/README.md +113 -0
  61. package/dist/skill/references/signing/connect-a-school.md +90 -0
  62. package/dist/skill/references/signing/rotate-the-key.md +41 -0
  63. package/dist/skill/references/stammdaten/purge-deleted-person.md +68 -0
  64. package/dist/skill/references/stripe/connect-a-school.md +107 -0
  65. package/dist/skill/references/stripe/request-and-verify-a-payment.md +78 -0
  66. package/dist/skill/references/test-actions/drive-a-test-application.md +59 -0
  67. package/dist/skill/references/users/add-platform-admin.md +59 -0
  68. package/dist/skill/references/users/add-platform-user.md +59 -0
  69. package/dist/skill/references/users/add-tenant-admin.md +75 -0
  70. package/dist/skill/references/users/change-login-email.md +55 -0
  71. package/dist/skill/references/users/cli-access-for-tenant-admin.md +117 -0
  72. package/dist/skill/references/users/delete-permission-group.md +76 -0
  73. package/dist/skill/references/users/grant-cross-tenant-access.md +57 -0
  74. package/dist/skill/references/users/grant-feedback-assignable.md +43 -0
  75. package/dist/skill/references/users/set-password.md +57 -0
  76. package/dist/skill/references/users/use-test-account.md +79 -0
  77. package/dist/skill/references/worker-ops/debug-worker-jobs.md +90 -0
  78. package/dist/skill/references/worker-ops/verify-migrations.md +51 -0
  79. package/dist/spec.snapshot.json +145815 -0
  80. package/package.json +53 -4
@@ -0,0 +1,121 @@
1
+ # Scaffold a brand-new tenant config from nothing
2
+
3
+ **When to use:** stand up a brand-new customer (tenant) from *nothing* — no snapshot,
4
+ no `.dev` export, no other environment to promote from. `config init` writes the
5
+ smallest valid config to disk; you then `config apply` it. If a config source already
6
+ exists (a `.dev` export / a tenant in another env), use
7
+ [Onboard / establish a tenant](./onboard-tenant.md) instead — that path skips the
8
+ scaffold.
9
+
10
+ ## Prerequisites
11
+ - **Env:** target with `--env <local|staging|prod>`. Tenant creation is implicit in
12
+ `config apply` (there is no separate "create tenant" verb). `config init` itself is
13
+ **offline** — no deployment, no auth.
14
+ - **Auth (for the apply, not the init):** a platform-admin token (`config apply` is
15
+ gated on `_platform.config.manage`). Confirm with `plexus-cli --env <env> whoami`.
16
+ The local admin-key fallback has no platform identity and is rejected — drive
17
+ `--env local` with a token too. A tenant admin's token cannot do this: a
18
+ tenant-scoped apply refuses a tenant that does not exist yet
19
+ ([CLI access for a tenant admin](../users/cli-access-for-tenant-admin.md)).
20
+ - **Tooling:** an installed, current `plexus-cli` (`plexus-cli --help`).
21
+ - **Prod:** every writing run against prod needs `--allow-prod` (`config apply`,
22
+ `users provision`); without it the CLI refuses.
23
+
24
+ ## Decide before you apply
25
+
26
+ Settle these with the school first — each one is a later playbook, but the answers
27
+ belong to onboarding:
28
+
29
+ | Decision | Default of the scaffold | Where it is set |
30
+ |---|---|---|
31
+ | Slug + display name | — (`--tenant`, `--name`) | step 1; the slug is the identity, never rename it |
32
+ | Modules the school bought | **all** grantable module permissions | trim `tenant.permissions` in the scaffold before step 4 |
33
+ | „Bewerbung" or „Anmeldung" | Bewerbung (no `terms` in the scaffold) | [Set a school's terms](./set-tenant-terms.md) |
34
+ | Which mail families get | `all` („Alle E-Mails“) | [Mail level](../notifications/mail-level.md) |
35
+ | Portal branding (colors, logo, fonts) | neutral | [Set tenant branding](./set-tenant-branding.md) |
36
+ | Payments / e-signature | none | [Stripe](../stripe/connect-a-school.md) · [Signing](../signing/connect-a-school.md) |
37
+
38
+ **Look at an existing tenant as a reference.** Export one you can reach into a private
39
+ scratch dir and compare:
40
+ `plexus-cli --env prod config export --tenant <existing slug> --out <scratch>/ref.json`.
41
+
42
+ ## Steps
43
+ ```bash
44
+ # 1. Scaffold a minimal, valid tenant config (offline — no deployment, no auth).
45
+ plexus-cli config init --tenant <slug> --name "<Display Name>" \
46
+ --out config/.dev/<slug>/config.json # --out defaults to this path
47
+
48
+ # 2. (Optional) edit config/.dev/<slug>/config.json — add custom fields, roles,
49
+ # extra units/legal entities, a `source` block if this tenant imports data, etc.
50
+ # The scaffold has NO `source` block: a tenant that never imports needs none.
51
+
52
+ # 3. Dry-run the apply (shows the create/update plan; writes nothing).
53
+ plexus-cli --env <env> config apply --tenant <slug> \
54
+ --from snapshot:config/.dev/<slug>/config.json
55
+
56
+ # 4. Commit it.
57
+ plexus-cli --env <env> config apply --tenant <slug> \
58
+ --from snapshot:config/.dev/<slug>/config.json --confirm
59
+
60
+ # 5. Provision the generic admin login (separate auth plane — see related playbook).
61
+ # Convention: admin@<short>.plexus.swop.schule, name "<Short> Administration",
62
+ # so a platform operator always has one login per school that belongs to no person.
63
+ plexus-cli --env <env> --tenant <slug> users provision admin@<short>.plexus.swop.schule \
64
+ --name "<Short> Administration" --group tenant_admin --output creds.csv --quiet
65
+ ```
66
+
67
+ Write `config/.dev/…` and `creds.csv` to a private scratch dir — never put a real
68
+ tenant's config or a password into a shared or public place. Hand the credentials over
69
+ in the three-line format of
70
+ [Add a tenant admin](../users/add-tenant-admin.md#hand-the-credentials-over--always-this-format).
71
+ Staff of the school get their own accounts the same way (their address, their name);
72
+ the generic admin is not shared with them.
73
+
74
+ **Created for you, not by you:** the apply also provisions the tenant's test applicant
75
+ `test@example.com` („Test Bewerber"), which then shows up in `users list` with the new
76
+ slug. That is intended — see [Use a tenant's test account](../users/use-test-account.md).
77
+
78
+ The scaffold emits the smallest config `apply` accepts: the tenant block (all
79
+ grantable module permissions + `defaultApplicantGroupKey`), one school operator
80
+ (legal entity), **one pedagogy unit** (the school offers link to), the `tenant_admin`
81
+ + `applicant` permission groups, and one display group (required — the system
82
+ identity fields attach to it). No users (provision them in step 5) and no `source`.
83
+ It is built by the same code that builds the platform's demo tenant, so the
84
+ scaffolded permission sets can never drift from it.
85
+
86
+ ## Verify
87
+ ```bash
88
+ # The tenant + its foundation now exist:
89
+ plexus-cli --env <env> --tenant <slug> config targets # foundation rows > 0
90
+ plexus-cli --env <env> --tenant <slug> config export # round-trips the unit, groups
91
+ plexus-cli --env <env> tenants # <slug> is listed
92
+ ```
93
+
94
+ Then, in this order: the decisions above (terms, mail level, branding, payments) →
95
+ offers on the pedagogy unit: `offer init --unit <unit key> --title … --title-en … --responsible <staff configKey>` →
96
+ `offer apply --publish` (see `plexus-cli offer --help` and
97
+ [Publish an offer bundle](../offer/publish-offer-bundle.md)).
98
+
99
+ ## Idempotency / gotchas
100
+ - **`config init` is offline and overwrites `--out`** — re-running regenerates the
101
+ same skeleton from `--tenant`/`--name`; it does not touch any deployment.
102
+ - **`config apply` is idempotent** — re-running reconciles to the config (no
103
+ duplicate tenant). Dry-run first; only `--confirm` writes.
104
+ - **`source` is optional.** Omit it for a non-importing tenant; add it only when
105
+ wiring a master-data import. Import code fails loud at import time if a source is
106
+ needed but absent — there is no fake-source fallback.
107
+ - **Teardown:** only `config wipe --target all --confirm --allow-destructive` deletes
108
+ the tenant **identity row** — after purging its applications and draining every
109
+ target, files included (only the logo stays: sent mail links it). `--target foundation` (even with `--cascade`) clears
110
+ child tables + dependent data but leaves the tenant row and sibling config targets —
111
+ by design (`tenants` is excluded from every wipe target). The CLI prints a note when
112
+ you wipe a non-`all` target so this is never a surprise.
113
+ - Staff accounts are a **separate plane** (Better Auth) — the config never creates
114
+ logins; provision them with `users provision` (step 5).
115
+
116
+ ## Related
117
+ - [Onboard / establish a tenant](./onboard-tenant.md) — establish from an existing
118
+ `.dev` export / env (the non-scaffold path).
119
+ - [Update a tenant's config](./update-tenant-config.md) · [Wipe a tenant's config](./wipe-tenant-config.md)
120
+ - [Add a tenant admin](../users/add-tenant-admin.md) — the credentialed login (step 5).
121
+ - Command reference: `plexus-cli config <verb> --help`.
@@ -0,0 +1,71 @@
1
+ # Onboard / establish a tenant
2
+
3
+ **When to use:** stand up a real tenant's config-as-code in an environment for the first time
4
+ (or refresh it). A real tenant's config lives in the database (DB-authoritative) and is
5
+ established from a snapshot or another environment — never from built-in seed data.
6
+
7
+ ## Prerequisites
8
+
9
+ - `plexus-cli` installed; platform-admin token for the target env (`_platform.config.manage`).
10
+ - A config **source** for the tenant, one of:
11
+ - a `config export` from another env (default path `config/.dev/<slug>/config.json`),
12
+ - an environment that already has the tenant (promote with `--from env:<srcEnv>`).
13
+ A real tenant's config is kept nowhere but its deployment — prod is its source of truth, so a
14
+ refresh of staging comes FROM prod, never the other way round.
15
+ - The `--tenant` value must be the **slug embedded in the snapshot** — apply rejects a mismatch.
16
+
17
+ ## Steps
18
+
19
+ ```bash
20
+ # Establish from a snapshot (dry-run first, then commit).
21
+ plexus-cli config apply --env <env> --tenant <slug> \
22
+ --from snapshot:config/.dev/<slug>/config.json
23
+ plexus-cli config apply --env <env> --tenant <slug> \
24
+ --from snapshot:config/.dev/<slug>/config.json --confirm
25
+
26
+ # OR copy a tenant that already exists on prod to staging (re-plans against the target's live state).
27
+ plexus-cli config apply --env staging --tenant <slug> --from env:prod --confirm
28
+ ```
29
+
30
+ **If the source carries `foundation.users`, the committing apply hands you passwords.**
31
+ Every declared user without a login account gets one minted right here, printed once
32
+ (`+ account <email> password: …`) — the server keeps only the hash. Redirect them instead
33
+ of running this in a shared terminal:
34
+
35
+ ```bash
36
+ plexus-cli config apply --env <env> --tenant <slug> \
37
+ --from snapshot:<path> --confirm --output creds.csv --quiet
38
+ ```
39
+
40
+ The dry-run above mints nothing (`--output` is inert and creates no file); it lists the
41
+ same users as `! no account <email>`, so you know beforehand whether the commit will emit
42
+ secrets.
43
+
44
+ After the config exists, iterate it live with [Update a tenant's config](./update-tenant-config.md),
45
+ run the real import, and publish offers via `plexus-cli offer apply`.
46
+
47
+ ## Verify
48
+
49
+ ```bash
50
+ plexus-cli config targets --env <env> --tenant <slug> # registry + live per-tenant row counts
51
+ plexus-cli config export --env <env> --tenant <slug> # full reconstructed config
52
+ ```
53
+
54
+ ## Idempotency / gotchas
55
+
56
+ - `apply` is **idempotent and additive on config tables** — it never wipes the data plane
57
+ (parties/values/applications). Re-running an establish is safe.
58
+ - **Deploys do not establish real tenants.** A staging/prod deploy applies only the
59
+ **template tenant** (`test`) — establishing a real tenant is this manual step.
60
+ - The **import mapping is DB-authoritative**: `custom_fields.sourceColumns` ships in the config
61
+ and is read by the import worker. The import **fails loud** if a tenant has fields but none
62
+ carry `sourceColumns` — i.e. the worker was deployed before the tenant was (re)applied:
63
+ re-run this apply.
64
+ - A structural schema change is a **migration** that a platform operator runs before the
65
+ apply — not part of onboarding, and not something the CLI does.
66
+
67
+ ## Related
68
+
69
+ - [Update a tenant's config](./update-tenant-config.md) · [Wipe a tenant's config](./wipe-tenant-config.md)
70
+ - [Scaffold a brand-new tenant config](./init-tenant.md) — when no source exists yet.
71
+ - Command semantics: `plexus-cli config apply --help`.
@@ -0,0 +1,99 @@
1
+ # Rename, add, reorder or retire a program stage (Klassenstufe)
2
+
3
+ **Use it to** change an offer's Klassenstufen — rename "Kindergarten Jahr 1" to
4
+ "Nursery 1", add a level, change the order, or retire one — without leaving a
5
+ second, stale level behind and without cutting applications loose.
6
+
7
+ ## The model in one paragraph
8
+
9
+ A stage is identified by the **key** in `bundle.yaml → program.stages[].key`, never
10
+ by its name. The stored key is `<program.key>-stufe-<key>`. Applications, capacity
11
+ cells and waitlists point at the stage row by id, so a rename that keeps the key
12
+ keeps all of them. `offer apply` reconciles by key: same key → update, new key →
13
+ insert, removed key → archive (refused while an active application sits on it),
14
+ re-listed archived key → restore. Array order = display order.
15
+
16
+ ## Declaratively (bundle — preferred for anything you want reproducible)
17
+
18
+ ```bash
19
+ # 1. Start from what is live — prod is the source of truth for a real tenant, so
20
+ # work on a fresh export in a scratch directory (export writes the bare keys back)
21
+ WORK=$(mktemp -d)
22
+ plexus-cli --env prod --tenant <slug> offer export --slug <offer-slug> --out $WORK/<offer-slug>
23
+ ```
24
+
25
+ ```yaml
26
+ # 2. Edit program.stages — KEEP the key of every level you rename. A name is
27
+ # German AND English; changing only the English is a rename too.
28
+ program:
29
+ stages:
30
+ - key: kindergarten-jahr-1 # existing level, new name
31
+ name: { de: Nursery 1, en: Nursery 1 }
32
+ - key: kindergarten-jahr-2
33
+ name: { de: Nursery 2, en: Nursery 2 }
34
+ - key: early-years-1 # new level
35
+ name: { de: Early Years 1, en: Early Years 1 }
36
+ ```
37
+
38
+ ```bash
39
+ # 3. Offline check, then look at the stage rows of the plan
40
+ plexus-cli offer validate --dir $WORK/<offer-slug>
41
+ plexus-cli --env prod offer plan --dir $WORK/<offer-slug>
42
+ # ~ stage:kindergarten-jahr-1 ~stage kindergarten-jahr-1 ("Kindergarten Jahr 1" → "Nursery 1", en "Kindergarten year 1" → "Nursery 1")
43
+ # + stage:early-years-1 +stage early-years-1 (Early Years 1)
44
+
45
+ # 4. Apply
46
+ plexus-cli --env prod offer apply --allow-prod --dir $WORK/<offer-slug>
47
+ ```
48
+
49
+ ## Imperatively (one-off, no bundle)
50
+
51
+ ```bash
52
+ # --level takes the level's KEY; a German rename always needs --name-en (the
53
+ # English is never copied from the German, and a name without English is refused).
54
+ # Against prod: --env prod … --allow-prod
55
+ plexus-cli --env <env> offer level rename --slug <offer-slug> --level kindergarten-jahr-1 --name "Nursery 1" --name-en "Nursery 1"
56
+ plexus-cli --env <env> offer level add --slug <offer-slug> --name "Early Years 1" --name-en "Early Years 1" --key early-years-1
57
+ plexus-cli --env <env> offer level remove --slug <offer-slug> --level early-years-1
58
+ plexus-cli --env <env> offer level restore --slug <offer-slug> --level early-years-1
59
+ ```
60
+
61
+ After imperative changes, **export the bundle again before the next `apply`**:
62
+ otherwise the old bundle still lists the old state, and applying it undoes your
63
+ changes (renames them back, archives the added levels).
64
+
65
+ ## Verify
66
+
67
+ ```bash
68
+ plexus-cli --env prod offer plan --dir $WORK/<offer-slug> # no stage rows = in sync
69
+ plexus-cli --env prod offer show --slug <offer-slug> # levels in order
70
+ ```
71
+
72
+ ## Gotchas
73
+
74
+ - **Changing a key is not a rename.** A new key inserts a new level and archives the old
75
+ one. If the old one has active applications, the apply is refused — which is what you
76
+ want: those applications would stay on a level nobody sees.
77
+ - **`[stage/hasActiveApplications]`** — the level you dropped (or `level remove`d) still
78
+ has active applications. `offer plan` shows it in advance: `-stage … — REFUSED: n
79
+ active application(s)`. Rename it instead, or close those applications first. No
80
+ stage was changed: the reconcile is one transaction. (The manifest draft and the
81
+ unit/program link apply wrote before it stay — unpublished; don't publish by hand.)
82
+ - **A level moves to another offer** (e.g. Grade 12 moves from Primary to Secondary). Keys are per
83
+ program, so the target offer gets a *new* row and the source offer's row is archived.
84
+ Nothing re-points an application across programs. While applications are active on
85
+ the source row, keep the level listed in the source bundle and close its cell
86
+ (`offer capacity set --slug … --run … --level … --close`), then drop it in a later apply.
87
+ - **`[stage/hasOpenLeads]`** is a warning, not a refusal: open Interessenten keep
88
+ pointing at the archived level and cannot be converted until they are moved to an
89
+ active level.
90
+ - **`[stage/duplicateKey]` from apply but not from validate** — the stored key is taken
91
+ by a stage of *another* program in the tenant (keys are unique per tenant). Choose a
92
+ different key.
93
+ - **A `-stage` row showing a key like `kindergarten:vorschule:lz3k9`** — a legacy stage
94
+ created in the master-data admin before stage keys existed, keyed outside the `<program>-stufe-`
95
+ scheme. No bundle key can address it, so `apply` archives it (through the guard).
96
+ `offer export` refuses such a row with `[stage/foreignKey]`. No CLI verb re-keys a
97
+ stage today — raise it with the platform operator instead of hand-editing around it.
98
+ - A bundle **without** `program.stages` leaves the levels alone; `stages: []` archives
99
+ all of them (guarded).
@@ -0,0 +1,150 @@
1
+ # Set a tenant's external-portal branding
2
+
3
+ **When to use:** give a tenant's public `/e/**` Bewerbungsportal its own white-label
4
+ look — brand colors, corner radius, logo size, header style, and font families, plus an
5
+ uploaded logo and uploaded font files — on staging and/or prod, without a reseed.
6
+
7
+ ## Prerequisites
8
+
9
+ - `plexus-cli` installed and current (`plexus-cli --help`).
10
+ - A token configured for each target env (`plexus-cli --env <env> config show` prints
11
+ the resolved connection): a platform-admin token holding `_platform.config.manage` (the
12
+ same right that gates `config apply`), or — for your OWN tenant only — a tenant
13
+ admin's token holding `administration.config.manage` (see
14
+ [CLI access for a tenant admin](../users/cli-access-for-tenant-admin.md)). Such a
15
+ token changes branding **only through `config apply`** (branding is one of the few
16
+ `tenant.*` settings it may change); every `plexus-cli branding …` command needs the
17
+ platform right and refuses it. A local admin key without a user identity does **not**
18
+ satisfy this gate.
19
+ - The `plexus-cli branding …` commands must be **live on the target env** first (they
20
+ call backend functions of the same release). A platform operator rolls a release out
21
+ to staging, then prod; only then run the CLI.
22
+ - The tenant **slug** (e.g. `test`). Always **stage on staging first**, verify in the
23
+ browser, then promote to prod.
24
+
25
+ ## The two halves of branding
26
+
27
+ | Part | Where it lives | How to set it |
28
+ | --- | --- | --- |
29
+ | **Theme tokens** — colors, radius, logoSize, bannerDensity, filledHeader, websiteUrl, font **families** | `tenant_branding` row columns; config-portable (exported in `tenant.branding`) | `plexus-cli branding set-theme` (surgical) **or** `config apply` (whole-config) |
30
+ | **Logo** | `_storage` id column (a single `primary` slot); carried by config **by hash** (`tenant.brandingAssets.logo`) | `plexus-cli branding set-logo` (one-off upload) **or** `config apply` (bytes from the snapshot / source env) |
31
+ | **Font files** (WOFF2, one per slot `heading` / `body`) | `_storage` id columns; carried by config **by hash** (`tenant.brandingAssets.fontHeading` / `fontBody`) | `plexus-cli branding set-font --slot heading\|body --file <woff2>` **or** `config apply` |
32
+
33
+ A `storageId` is deployment-local, so config never carries it. It carries each uploaded
34
+ file like a library file: `{ sha256, sizeBytes, mimeType }` under `tenant.brandingAssets`, and
35
+ `config export` downloads the bytes beside `config.json` as `branding/<slot>.<ext>`
36
+ (`path` in the entry). `config apply --from snapshot:…` or `--from env:<src>` uploads
37
+ only the files whose hash the target does not hold; the dry run prints
38
+ `~ brand branding/logo content differs — would upload`. A missing slot — or a snapshot
39
+ without the block — leaves the target's file untouched. A replaced **logo** file is never
40
+ deleted (sent mail links it); a replaced **font** file is.
41
+ Fonts are served from the app's own Convex origin — **never** point a browser at an
42
+ external font host (a private-network host is blocked by the browser; the Google-Fonts
43
+ CDN is a GDPR problem).
44
+
45
+ ## The theme shape
46
+
47
+ Every field is optional — omit a key to fall back to the platform default:
48
+
49
+ ```jsonc
50
+ {
51
+ "colors": {
52
+ "primary": "#1E4BCD", // brand primary → --color-primary (buttons, rings, filled header)
53
+ "accent": "#0AAA5F", // secondary accent → --color-accent (links, hovers, dots)
54
+ "background": "#FFFFFF", // portal page bg → --color-bg
55
+ "foreground": "#111111" // body text → --color-fg
56
+ },
57
+ "radius": "sharp", // "sharp" | "rounded" | "soft"
58
+ "logoSize": "max", // banner logo height: "sm" | "md" | "lg" | "max" (default lg)
59
+ "bannerDensity": "normal", // "compact" | "normal" | "spacious" (default normal)
60
+ "filledHeader": false, // true → top nav bar filled with the PRIMARY colour (on-primary text)
61
+ "websiteUrl": "https://www.example-schule.de",
62
+ "fonts": {
63
+ "heading": { "family": "Space Grotesk" }, // family only — the file is uploaded (set-font)
64
+ "body": { "family": "Lora" }
65
+ }
66
+ }
67
+ ```
68
+
69
+ - `colors.*` are `#RRGGBB` hex strings; `radius`/`logoSize`/`bannerDensity` are the enums above.
70
+ - A font entry is `{ family }`. With an uploaded file for that slot the portal loads it as
71
+ `@font-face` under that family; without one the family must already be available
72
+ (system/bundled). A font `url` is **refused** (`fonts.<slot>.url is no longer
73
+ supported`) — upload the file instead.
74
+ - Bad values (malformed hex, bad enum, a font `url`) fail loud server-side — the write
75
+ aborts, no partial state.
76
+
77
+ ## Steps — the surgical CLI path (recommended)
78
+
79
+ ```bash
80
+ # 1. Inspect current branding (branding + resolved logo URL).
81
+ plexus-cli --env staging --tenant <slug> branding show
82
+
83
+ # 2. Set the theme tokens from a JSON file (replaces the theme wholesale; leaves the logo).
84
+ plexus-cli --env staging --tenant <slug> branding set-theme --file theme.json
85
+
86
+ # 3. Upload the logo — from a local file …
87
+ plexus-cli --env staging --tenant <slug> branding set-logo --file logo.png
88
+ # … or straight from a URL (fetched + uploaded by the CLI):
89
+ plexus-cli --env staging --tenant <slug> branding set-logo \
90
+ --url https://cdn.example.com/logo-black.png
91
+
92
+ # 4. Upload the font files — the slot's family must be set first (step 2), the file is
93
+ # applied under it. WOFF2 only (signature-checked), ≤ 2 MB; a replaced file is deleted.
94
+ plexus-cli --env staging --tenant <slug> branding set-font --slot heading --file heading.woff2
95
+ plexus-cli --env staging --tenant <slug> branding set-font --slot body --file body.woff2
96
+ # (`branding clear-font --slot <slot>` removes a file; the family stays.)
97
+
98
+ # 5. Verify, then promote to prod (re-run 1–4 with --env prod — never assume prod == staging).
99
+ plexus-cli --env prod --tenant <slug> branding show
100
+ ```
101
+
102
+ - `set-theme` replaces the whole theme (a submitted-absent field clears the stored one),
103
+ mirroring the admin form — so the file must carry the **complete**
104
+ theme you want.
105
+ - The logo cap: images only, ≤ 3 MB. Pick the variant that reads on the header — a **dark
106
+ logo** for the default light bar, a **white/inverse logo** when `filledHeader: true`
107
+ (the bar is the dark primary colour). `branding clear-logo` removes it.
108
+
109
+ ## Alternatives
110
+
111
+ - **Whole-config rollout (`config apply`).** The theme round-trips through the config
112
+ snapshot's `tenant.branding` block and the files through `tenant.brandingAssets`, so
113
+ `config export → edit → apply` (or `apply --from env:staging` onto prod) carries the
114
+ complete brand, bytes included — but it re-plans the *entire* tenant config, so prefer
115
+ it when branding rides along with other config changes or moves between environments.
116
+ To swap a file in a snapshot, replace `branding/<slot>.<ext>` and keep its `path`: the
117
+ CLI re-hashes the file on disk. For a one-off change, `set-theme` / `set-logo` /
118
+ `set-font` are surgical.
119
+ - **Admin UI.** A tenant admin (holding `administration.branding.manage`) edits the same
120
+ row live under **Administration → Branding**. Use it for quick per-tenant tweaks; use the CLI
121
+ for reproducible, cross-env rollout by a platform admin.
122
+
123
+ ## Gotchas
124
+
125
+ - **Release gate:** a brand-new `plexus-cli branding` verb (or a new theme field) must be
126
+ live on the env before the CLI call resolves — otherwise the call fails with an unknown
127
+ function. Ask the platform operator whether the release is out.
128
+ - `config export` names the logo and the font files by hash and writes their bytes to
129
+ `branding/`; `branding show` resolves their URLs (`logoUploads`, `fontUploads`). A
130
+ committing apply refuses a changed hash it has no bytes for
131
+ (`[branding/contentMissing]`), a font file that is not WOFF2 (`[branding/fontRefused]`)
132
+ and a font slot without an own family (`[branding/fontFamilyMissing]`).
133
+ - `set-font` refuses (`INVALID`, German message) a slot without a saved
134
+ family, a family named like the back office's own fonts (`Inter Variable`, `Azeret Mono
135
+ Variable` — an `@font-face` is document-wide), anything over 2 MB and anything that is
136
+ not WOFF2 — convert TTF/OTF/WOFF first (e.g. `woff2_compress`).
137
+ - Removing a family (config / `set-theme`) leaves its file attached but unused; the admin
138
+ page flags it. Clear the file with `branding clear-font` when you drop the family.
139
+ - A deployment that once held external font URLs dropped them on upgrade (the platform
140
+ operator has the list of dropped URLs); re-upload those files with `set-font`.
141
+ - Verify in the browser: DevTools → Network → the `.woff2` request from the Convex origin
142
+ answers 200 with `Access-Control-Allow-Origin: <the portal origin>` (fonts load
143
+ cross-origin; Convex storage reflects the requesting origin).
144
+ - This is a live-DB operation; nothing else records it. A `config export` snapshot is
145
+ your only copy — keep it out of any shared or public place.
146
+
147
+ ## Related
148
+
149
+ - [Update a tenant's config](./update-tenant-config.md) · [Onboard / establish a tenant](./onboard-tenant.md)
150
+ - Command semantics: `plexus-cli branding <verb> --help`, `plexus-cli config apply --help`.
@@ -0,0 +1,50 @@
1
+ # Set a school's terms („Anmeldung" statt „Bewerbung")
2
+
3
+ **When to use:** a school calls the process by another name — it runs an
4
+ „Anmeldeverfahren", not a „Bewerbungsverfahren". One setting switches the whole product for that school: the UI,
5
+ task titles, feed and party notes, applicant-name fallbacks, mail blocks and buttons,
6
+ and every refusal the browser shows. URLs (`/e/units/<unit>/bewerben`) stay as they are.
7
+
8
+ ## What it is
9
+
10
+ - `tenant.terms` in the tenant config — today one term: `application`, a **closed
11
+ choice**: `"Bewerbung"` (platform default) or `"Anmeldung"`. The browser and the
12
+ backend read the same rules, so both always say the same word.
13
+ - Stored on `tenants.terms` **only when it deviates**. Absent = the platform's word.
14
+ - An unknown term or choice is refused by `config set` / `config apply`
15
+ (`[terms/unknownTerm]`, `[terms/unknownChoice]`) — nothing silently falls back.
16
+ - The person follows the choice: „Bewerber:in" → „Anmelder:in", „Bewerberportal" →
17
+ „Anmeldeportal".
18
+ - Not rewritten: a school's own texts (letter deviations, offer/board labels, team
19
+ names — change those via `offer`/`config` as usual).
20
+
21
+ ## Set it (one command, dry-run first)
22
+
23
+ ```bash
24
+ # dry run — prints the one-row plan
25
+ plexus-cli --env staging config set --tenant <slug> --target tenant \
26
+ --key <slug> --field terms --value '{"application":"Anmeldung"}'
27
+ # commit
28
+ plexus-cli --env staging config set --tenant <slug> --target tenant \
29
+ --key <slug> --field terms --value '{"application":"Anmeldung"}' --confirm
30
+ ```
31
+
32
+ Equivalent through a scratch export (a real tenant's config lives only in its
33
+ deployment — keep the export private): `config export --tenant <slug> --out <scratch>/config.json` → set
34
+ `"tenant": { …, "terms": { "application": "Anmeldung" } }` → `config apply --tenant
35
+ <slug> --from snapshot:<scratch>/config.json` (dry run) → `--confirm`.
36
+
37
+ Back to the platform word: `--value '{}'` (or remove the block and apply).
38
+
39
+ ## Verify
40
+
41
+ - `plexus-cli --env <env> config export --tenant <slug>` shows `tenant.terms`.
42
+ - Portal `https://<host>/<slug>/e` shows „Jetzt anmelden", nav „Anmelden", the
43
+ sign-in button „Einloggen"; the first server-rendered frame already says it (no flash).
44
+ - Staff board: „Anmeldungen", „Anmeldung ablehnen", …
45
+
46
+ ## Gotchas
47
+
48
+ - Texts the server **stored** before the switch (feed notes, party notes, task titles
49
+ already spawned) keep the word they were written with — history is not rewritten.
50
+ - The portal's English UI is unaffected (terms are German words).
@@ -0,0 +1,73 @@
1
+ # Set up a billing catalog
2
+
3
+ Use `catalog export`, `plan`, and `apply` for articles, their price versions,
4
+ grant templates, program offerings, and Word templates. Real tenant
5
+ files stay outside Git. Follow [edit a live tenant offer](../offer/edit-live-tenant-offer.md)
6
+ for production changes.
7
+
8
+ ```json
9
+ {
10
+ "revenueAccounts": [{ "code": "4000", "name": "Tuition" }],
11
+ "billingItems": [{
12
+ "key": "tuition",
13
+ "name": { "de": "Schulgeld", "en": "Tuition" },
14
+ "itemType": "recurring",
15
+ "revenueAccount": "4000",
16
+ "taxTreatment": "exempt_ustg_4_nr_21",
17
+ "config": {},
18
+ "price": { "amount": 720, "period": "month", "currency": "EUR", "effectiveFrom": "2026-08-01" },
19
+ "programs": ["school"]
20
+ }],
21
+ "grantTemplates": [{
22
+ "key": "income-support",
23
+ "name": { "de": "Einkommensermäßigung", "en": "Income reduction" },
24
+ "eligibleItems": ["tuition"],
25
+ "scope": "aggregate",
26
+ "reason": "needsBased",
27
+ "kind": "absolute",
28
+ "rule": { "amountMinor": 0 },
29
+ "floorTotal": 15000,
30
+ "pricePeriod": "month",
31
+ "eligibility": {},
32
+ "retroactiveRevocation": false
33
+ }]
34
+ }
35
+ ```
36
+
37
+ Fixed `price.amount` uses euros at this authoring boundary. `period` is required.
38
+ A tree price instead supplies `tree`, `currency`, and `effectiveFrom`. Its leaves
39
+ store explicit absolute cents and their own `period`; no yearly/monthly conversion
40
+ occurs. Referenced pricing attributes must already exist in the tenant.
41
+
42
+ Grant `rule.amountMinor` and `floorTotal` use cents. Templates select eligible
43
+ article definitions through `eligibleItems`. Applying a template freezes actual
44
+ contract lines and agreed terms. An aggregate floor applies once to their sum.
45
+ A line template uses `scope: "line"`. Grants target contract lines and never read pricing attributes.
46
+
47
+ Every article names its direct `programs`. `programs: []` explicitly retires its
48
+ offering. The catalog rejects the retired `plans` section. Fixed fee-group labels
49
+ and article membership belong to a pinned contract-generator revision; edit them
50
+ through [generator source manifests](../resolver/manage-contract-resolver.md).
51
+
52
+ ```bash
53
+ plexus-cli --env prod --tenant <tenant> catalog export --out <scratch>/catalog.json
54
+ plexus-cli --env staging --tenant <tenant> catalog plan --file <scratch>/catalog.json
55
+ plexus-cli --env staging --tenant <tenant> catalog apply --file <scratch>/catalog.json
56
+ plexus-cli --env staging --tenant <tenant> catalog plan --file <scratch>/catalog.json
57
+ ```
58
+
59
+ The final plan should report `same` everywhere. Export chooses each article's newest
60
+ version, including a future version. Changing a price publishes a new immutable
61
+ version; never rewrite history to correct a current price. Stable keys preserve
62
+ identity across renames. A keyed row referring to an unkeyed article must be fixed
63
+ before export. Unkeyed catalog rows are counted in the export report.
64
+
65
+ Apply plans every section first, then writes accounts/articles/prices, program
66
+ assignments, grant templates, and Word templates. Each section is transactional;
67
+ a later refusal does not undo earlier sections. Fix the refusal and rerun. A
68
+ published workflow's required article offering cannot be removed.
69
+
70
+ `documentTemplates` optionally contains bilingual `name`, `key`, `file` (DOCX path
71
+ relative to the catalog), and `role`, with optional `icon` and `fileNamePattern`.
72
+ Files upload only when their SHA-256 changes. Template setup also requires tenant
73
+ configuration permission. Existing template roles cannot change through this command.