@smartkomm/plexus-cli 0.0.0-stage → 0.1.1

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 (82) hide show
  1. package/README.md +863 -2
  2. package/dist/cli.mjs +5432 -0
  3. package/dist/skill/.managed-by-plexus-cli +1 -0
  4. package/dist/skill/SKILL.md +224 -0
  5. package/dist/skill/references/accounts/email-changes.md +46 -0
  6. package/dist/skill/references/agents/use-shared-tools.md +52 -0
  7. package/dist/skill/references/applications/inspect-process-store.md +61 -0
  8. package/dist/skill/references/applications/start-a-test-application.md +66 -0
  9. package/dist/skill/references/audit/answer-access-request.md +115 -0
  10. package/dist/skill/references/config/attach-a-template.md +117 -0
  11. package/dist/skill/references/config/init-tenant.md +121 -0
  12. package/dist/skill/references/config/onboard-tenant.md +71 -0
  13. package/dist/skill/references/config/rename-program-stage.md +99 -0
  14. package/dist/skill/references/config/set-tenant-branding.md +150 -0
  15. package/dist/skill/references/config/set-tenant-terms.md +50 -0
  16. package/dist/skill/references/config/set-up-billing-catalog.md +73 -0
  17. package/dist/skill/references/config/update-tenant-config.md +174 -0
  18. package/dist/skill/references/config/wipe-tenant-config.md +107 -0
  19. package/dist/skill/references/export/create-named-export.md +47 -0
  20. package/dist/skill/references/feedback/create-and-triage-feedback.md +69 -0
  21. package/dist/skill/references/feedback/manage-feedback-tags.md +57 -0
  22. package/dist/skill/references/finance/change-contract-grant.md +36 -0
  23. package/dist/skill/references/finance/record-submission-date.md +33 -0
  24. package/dist/skill/references/interessenten/list-interessenten.md +54 -0
  25. package/dist/skill/references/marketing/demo-video-links.md +101 -0
  26. package/dist/skill/references/notifications/debug-mail-delivery.md +77 -0
  27. package/dist/skill/references/notifications/mail-level.md +90 -0
  28. package/dist/skill/references/notifications/reword-a-letter.md +155 -0
  29. package/dist/skill/references/notifications/trace-a-login.md +50 -0
  30. package/dist/skill/references/offer/add-workflow-node.md +268 -0
  31. package/dist/skill/references/offer/addresses.md +129 -0
  32. package/dist/skill/references/offer/appointment-task.md +79 -0
  33. package/dist/skill/references/offer/assignment-rule.md +195 -0
  34. package/dist/skill/references/offer/attach-form-to-workflow.md +79 -0
  35. package/dist/skill/references/offer/author-form-scenarios.md +97 -0
  36. package/dist/skill/references/offer/author-manifest-meta.md +94 -0
  37. package/dist/skill/references/offer/author-smartform.md +102 -0
  38. package/dist/skill/references/offer/bind-node-input.md +76 -0
  39. package/dist/skill/references/offer/completeness-check.md +137 -0
  40. package/dist/skill/references/offer/countersign-node.md +91 -0
  41. package/dist/skill/references/offer/create-pedagogy-unit.md +55 -0
  42. package/dist/skill/references/offer/declare-node-reads.md +99 -0
  43. package/dist/skill/references/offer/edit-live-tenant-offer.md +144 -0
  44. package/dist/skill/references/offer/edit-smartform.md +121 -0
  45. package/dist/skill/references/offer/edit-workflow-node.md +126 -0
  46. package/dist/skill/references/offer/engage-push-node.md +147 -0
  47. package/dist/skill/references/offer/insert-and-rewire-node.md +67 -0
  48. package/dist/skill/references/offer/node-tasks.md +162 -0
  49. package/dist/skill/references/offer/optional-assessment.md +65 -0
  50. package/dist/skill/references/offer/publish-offer-bundle.md +117 -0
  51. package/dist/skill/references/offer/set-offer-capacities.md +78 -0
  52. package/dist/skill/references/offer/staff-input-if-node.md +112 -0
  53. package/dist/skill/references/offer/task-appointment-result.md +69 -0
  54. package/dist/skill/references/offer/task-family-request.md +69 -0
  55. package/dist/skill/references/offer/task-result-files.md +52 -0
  56. package/dist/skill/references/offer/template-document-node.md +113 -0
  57. package/dist/skill/references/offer/workflow-default-channels.md +69 -0
  58. package/dist/skill/references/resolver/manage-contract-resolver.md +161 -0
  59. package/dist/skill/references/retention/erase-due-applications.md +99 -0
  60. package/dist/skill/references/retention/erase-due-parties.md +107 -0
  61. package/dist/skill/references/roles/migrate-parent-assignments.md +31 -0
  62. package/dist/skill/references/signing/README.md +113 -0
  63. package/dist/skill/references/signing/connect-a-school.md +90 -0
  64. package/dist/skill/references/signing/rotate-the-key.md +41 -0
  65. package/dist/skill/references/stammdaten/purge-deleted-person.md +68 -0
  66. package/dist/skill/references/stripe/connect-a-school.md +107 -0
  67. package/dist/skill/references/stripe/request-and-verify-a-payment.md +78 -0
  68. package/dist/skill/references/test-actions/drive-a-test-application.md +69 -0
  69. package/dist/skill/references/users/add-platform-admin.md +59 -0
  70. package/dist/skill/references/users/add-platform-user.md +59 -0
  71. package/dist/skill/references/users/add-tenant-admin.md +75 -0
  72. package/dist/skill/references/users/change-login-email.md +55 -0
  73. package/dist/skill/references/users/cli-access-for-tenant-admin.md +117 -0
  74. package/dist/skill/references/users/delete-permission-group.md +76 -0
  75. package/dist/skill/references/users/grant-cross-tenant-access.md +57 -0
  76. package/dist/skill/references/users/grant-feedback-assignable.md +43 -0
  77. package/dist/skill/references/users/set-password.md +57 -0
  78. package/dist/skill/references/users/use-test-account.md +79 -0
  79. package/dist/skill/references/worker-ops/debug-worker-jobs.md +90 -0
  80. package/dist/skill/references/worker-ops/verify-migrations.md +51 -0
  81. package/dist/spec.snapshot.json +156821 -0
  82. package/package.json +53 -4
@@ -0,0 +1,69 @@
1
+ # Author a workflow's default chat channels
2
+
3
+ **When to use:** every application of an offer should start with a chat channel
4
+ for a team combination (e.g. „Bewerbungsteam + Schulleitung"), in addition to the
5
+ automatic channel each participating team already gets.
6
+
7
+ Two kinds of channel exist in every application of a published version:
8
+
9
+ | kind | where it comes from | editable? |
10
+ |---|---|---|
11
+ | automatic | one per team the process gives work to (`assign`, composed tasks, …) | no — follows the graph |
12
+ | default | `graph.channels` in `workflow.json` | yes — this playbook |
13
+
14
+ Both are read from the application's **pinned** workflow version at chat time. No
15
+ row is written per application, so a new default channel reaches applications
16
+ started on the next published version; running applications keep the channels
17
+ of their version.
18
+
19
+ ## Prerequisites
20
+ - An offer bundle on disk with a `workflow.json` (`offer export …` or `offer init …`).
21
+ - Authoring is offline; `offer apply --publish` needs a token with the tenant's offer-configuration rights (a tenant admin has them).
22
+ - Members are **permission-group keys** of the tenant (`plexus-cli --env <env> --tenant <t> users groups`
23
+ → `key`); the applicant group is not a team.
24
+
25
+ ## Steps
26
+ ```bash
27
+ # What exists: automatic (read-only) + authored default channels.
28
+ plexus-cli offer workflow channels list --dir <bundle>
29
+
30
+ # Add one — the key is minted from the German label ONCE (here:
31
+ # bewerbungsteam-schulleitung). The label is German AND English.
32
+ plexus-cli offer workflow channels add --dir <bundle> \
33
+ --label "Bewerbungsteam + Schulleitung" --label-en "Admissions team + school leadership" \
34
+ --team bewerbungsteam --team schulleitung
35
+
36
+ # Rename / re-staff in place — pass the key; a rename never duplicates. A German
37
+ # rename always needs --label-en (the English is never copied from the German,
38
+ # and a channel name without English is refused).
39
+ plexus-cli offer workflow channels add --dir <bundle> --key bewerbungsteam-schulleitung \
40
+ --label "Aufnahme" --label-en "Admissions" --team bewerbungsteam --team schulleitung
41
+
42
+ # Remove an authored one by key (verb: `channels rm`).
43
+
44
+ # Release it.
45
+ plexus-cli offer validate --dir <bundle>
46
+ plexus-cli --env <env> offer apply --publish --dir <bundle>
47
+ ```
48
+
49
+ The same section is editable in the workflow IDE: „Team-Verantwortliche" → tab
50
+ „Kanäle" → „Kanal hinzufügen" (the same team picker as the cockpit chat's „+").
51
+
52
+ ## Verify
53
+ - `offer workflow channels list` shows the entry with source `Standardkanal`.
54
+ - After publish, start an application (e.g. convert an Interessent) and open its
55
+ cockpit: the chat rail carries a tab with the authored label.
56
+
57
+ ## Gotchas
58
+ - **A single team the process already assigns is refused** (`[channels/duplicatesDerived]`)
59
+ — it has its automatic channel.
60
+ - **Unknown team** (`[channels/unknownTeam]`) is only caught at `offer apply --publish`
61
+ (the offline validator has no tenant); the applicant group counts as unknown.
62
+ - A channel's chat identity is its **team set**: a manual „+" channel on the same
63
+ teams is the same thread, and the authored label wins.
64
+ - A permission group named by a default channel cannot be deleted while any
65
+ stored workflow version names it (`referenced-by-workflow`).
66
+
67
+ ## Related
68
+ - [Edit a workflow node](./edit-workflow-node.md)
69
+ - `plexus-cli offer workflow channels add --help` · `channels list` · `channels rm --key <key>`
@@ -0,0 +1,161 @@
1
+ # Manage a contract generator
2
+
3
+ **When to use:** Create or revise a tenant's TypeScript contract generator — or a combined generator over several of them — review scenarios, publish it, and pin an offer to that revision.
4
+
5
+ ## Prerequisites
6
+
7
+ - A configured environment and tenant, with `finanzen_vertraege.contracts.manage`.
8
+ - A contract template with an attached DOCX. Use `generator template upload` if needed.
9
+ - A real tenant's source and configuration live in that tenant's data; work on them in a private scratch directory.
10
+
11
+ ## Steps
12
+
13
+ Create a blank draft, or copy a concrete published revision:
14
+
15
+ ```bash
16
+ plexus-cli --env local --tenant test generator template list
17
+ plexus-cli --env local --tenant test generator create --key school-contract --name "Schulvertrag" --name-en "School contract" --contract-template-id <id>
18
+ plexus-cli --env local --tenant test generator clone school-contract@2
19
+ plexus-cli --env local --tenant test generator scaffold school-contract --out /tmp/generator.ts
20
+ ```
21
+
22
+ For an existing source, export both the TypeScript and its manifest. Edit the source and the explicit article/grant ID lists in the manifest. Saving checks the exported revision number and refuses concurrent changes.
23
+
24
+ ```bash
25
+ plexus-cli --env local --tenant test generator source export school-contract@3 --out /tmp/generator.ts
26
+ plexus-cli --env local --tenant test generator source save school-contract@3 --file /tmp/generator.ts --manifest /tmp/generator.ts.json
27
+ plexus-cli --env local --tenant test generator scenarios test school-contract@3 --input /tmp/input.json --json
28
+ ```
29
+
30
+ A blank draft can use a manifest containing `id`, `expectedSourceRevision: 0`, `billingItemDefinitionIds` and `grantTemplateIds`. Read its ID with `generator show`. Source must be self-contained and export one default object with `inputSchema`, `parties`, `resolve(config, input)`, `previewDocument(config, input)` and `signatureSpots(config, input)`. Imports and host capabilities are unavailable. The metadata describes conditional fields; the functions may use arbitrary synchronous TypeScript logic.
31
+
32
+ Review the result, including its signature declarations. Write `/tmp/reviewed.json` with the returned `sourceRevision` as `expectedSourceRevision`, `artifactHash`, and `scenarios: [{ key, input, expected }]`. `expected` is the entire returned `output`, including `contract` and `signatureSpots`. Keep explicit expected results: a test command never approves its observations automatically.
33
+
34
+ ```bash
35
+ plexus-cli --env local --tenant test generator scenarios approve school-contract@3 --file /tmp/reviewed.json
36
+ plexus-cli --env local --tenant test generator seal school-contract@3
37
+ plexus-cli --env local --tenant test generator assign school-contract@3 --offer-version-id <wip-id> --node contract
38
+ ```
39
+
40
+ Publish the WIP offer through the normal offer workflow. Existing applications retain their pinned generator revision. Archiving a revision hides it from new selections while existing pins remain executable.
41
+
42
+ For one-file export/import on the same environment:
43
+
44
+ ```bash
45
+ plexus-cli --env local --tenant test generator export school-contract@3 --out /tmp/generator.json
46
+ plexus-cli --env local --tenant test generator apply /tmp/generator.json
47
+ ```
48
+
49
+ The export includes source, manifests and reviewed scenarios. Apply saves a draft and checks imported expected results; it does not publish. IDs in this export belong to its environment.
50
+
51
+ ### Offer setup by stable catalog keys
52
+
53
+ The existing `contract/resolver-setup.json` filename and `resolver` property remain the bundle format. A source-backed setup adds these fields under `resolver`:
54
+
55
+ - `source`: a TypeScript file relative to the setup file.
56
+ - `billingItemKeys`: every article the generator may emit, including conditional branches.
57
+ - `grantTemplateKeys`: every grant template it may use. These must already exist in the catalog.
58
+ - `scenarios`: reviewed `{ key, input, expected }` cases. A new revision cannot publish without them.
59
+
60
+ Config and scenario values can use `{ "$billingItem": "article-key" }` and `{ "$grantTemplate": "grant-key" }` to reference declared entries. Prices retain the existing `{ "$config": "preise.key" }` reference. Template names and generator names are `{ de, en }`; `documentPlaceholderKeys` must match the DOCX.
61
+
62
+ ```bash
63
+ plexus-cli --env local --tenant test generator setup plan --file /tmp/offer/contract/resolver-setup.json
64
+ plexus-cli --env local --tenant test generator setup apply --file /tmp/offer/contract/resolver-setup.json
65
+ plexus-cli --env local --tenant test offer plan --dir /tmp/offer
66
+ ```
67
+
68
+ A setup that still names `implementation` can only copy a source already stored in that tenant's database. Ambiguous keys require cloning a concrete revision. New generators need no platform release.
69
+
70
+ **Known gap — the first revision of a new setup file.** `generator setup apply` refuses a new revision without reviewed `scenarios`, yet their `expected` output only exists once the source is saved on a draft and evaluated there — and the setup file needs it with `{ "$billingItem": … }` / `{ "$grantTemplate": … }` references, not the IDs `generator scenarios test` prints. There is no intended path through the setup file. Today's workaround: give the file one scenario with a placeholder `expected`; `setup apply` creates the draft and saves the source, then the scenario approval re-executes the scenario and refuses the mismatch — the draft and its source stay. Evaluate each scenario input on that draft (`generator scenarios test <key>@<draft revision> --input <input.json> --json`), review the `output`, write it into the file as `expected` with every article/grant ID replaced by its reference, and run `setup apply` again — it overwrites that draft and seals. Alternatively take the documented manual route above (create → `source save` → `scenarios test` → `scenarios approve` → `seal`) and write the setup file from the sealed revision afterwards. Proposed fix: `generator setup apply --draft` (stop after the source save, no scenario requirement) plus `generator setup scenarios --write` (evaluate every scenario input on that draft and write `expected` back into the file with references, for review).
71
+
72
+ ## Combined generator
73
+
74
+ A combined generator is a declaration, not code: one input mask and ordered parts, each pinning a **sealed** document revision by `key@revision`, mapping every part field to a mask field, an expression or a fixed value, optionally running only `when` an expression holds. It has no source, template, manifest or scenarios.
75
+
76
+ Seal the part revisions first (the steps above). Then write the declaration file — easiest by exporting an existing combined revision (the test tenant has `school-with-care`) and editing it. The shape (abridged: a real part maps every field its generator requires — the server names each one missing):
77
+
78
+ ```bash
79
+ plexus-cli --env local --tenant test generator combined export school-with-care@1 --out /tmp/combined.json
80
+ ```
81
+
82
+ ```json
83
+ {
84
+ "formatVersion": 1,
85
+ "kind": "combined",
86
+ "key": "school-with-lunch",
87
+ "name": { "de": "Schulvertrag mit Mittagessen", "en": "School contract with lunch" },
88
+ "description": { "de": "", "en": "" },
89
+ "inputSchema": { "fields": [
90
+ { "key": "startDate", "label": { "de": "Beginn", "en": "Start" }, "type": "date", "required": true },
91
+ { "key": "ganztag", "label": { "de": "Mit Hort", "en": "With after-school care" }, "type": "boolean", "defaultValue": false },
92
+ { "key": "lateCare", "label": { "de": "Spätbetreuung", "en": "Late care" }, "type": "boolean", "defaultValue": false, "visibleIf": "ganztag == true" }
93
+ ] },
94
+ "parts": [
95
+ { "partKey": "school", "generator": "school-contract@1",
96
+ "inputs": [ { "field": "startDate", "source": { "from": "field", "field": "startDate" } },
97
+ { "field": "admissionFeeMode", "source": { "from": "fixed", "value": "standard" } } ] },
98
+ { "partKey": "care", "generator": "after-school-care@1", "when": "ganztag == true",
99
+ "inputs": [ { "field": "pickupTime", "source": { "from": "expression", "expression": "if (lateCare == true) '18:00' else '16:00'" } } ] }
100
+ ]
101
+ }
102
+ ```
103
+
104
+ A file **without `id`** creates the draft of a new key and saves the declaration in one step; a refused declaration removes that empty draft again, so fix the file and save once more. A file **with `id`** (every export) targets exactly that revision, guarded by its `expectedSourceRevision`:
105
+
106
+ ```bash
107
+ plexus-cli --env local --tenant test generator combined save /tmp/combined.json
108
+ plexus-cli --env local --tenant test generator combined plan school-with-lunch@1 --input /tmp/mask-input.json --strict
109
+ plexus-cli --env local --tenant test generator seal school-with-lunch@1
110
+ ```
111
+
112
+ To revise a sealed combined revision, clone it, export the draft, edit, save, seal:
113
+
114
+ ```bash
115
+ plexus-cli --env local --tenant test generator clone school-with-care@1
116
+ plexus-cli --env local --tenant test generator combined export school-with-care@2 --out /tmp/combined.json
117
+ plexus-cli --env local --tenant test generator combined save /tmp/combined.json
118
+ plexus-cli --env local --tenant test generator seal school-with-care@2
119
+ ```
120
+
121
+ `generator combined create --key … --name … --name-en …` makes an empty draft (no mask, no parts) when you prefer to start from the workspace's editor. `plan` is the sandbox's „Positionen“ tab: which parts run for a mask input, the fields each still misses, and its roster issues; a refused input (a `when` that is no boolean) exits with the refusal. Like `generator test` it is a sandbox and exits 0 with fields missing; `--strict` exits 1 when the input would not generate (a missing field, an undecided part, a roster issue, a part's refusal), each named on stderr — for scripts and checks. The PDFs per part come from the workspace.
122
+
123
+ Pin it on a contract node like a document generator: the node's `resolver: { key, revision }` in the bundle (or `generator assign`), the reserved Träger as the node literal `inputs.traeger`, then `offer plan` and `offer apply --publish`. Write only `key` and `revision`: `offer plan|apply` stamp the pin from the environment the way publish does (mask, capacities, `parts`), so the node's `$node.<n>.contracts` resolves offline — no hand-made stamp. `offer validate` alone cannot know the kind and warns `[resolver/pinNotStamped]`. One „Erzeugen“ makes one contract per running part (a generation group), issued and signed together in one Vorgang.
124
+
125
+ - **No change, no write.** `save` of an unedited export prints `outcome: unchanged` and writes nothing; only the names changed → `renamed` (names are renamable at any status); otherwise `saved` (a new `sourceRevision`).
126
+ - **The server judges the declaration.** `save` and `seal` refuse with every gap named — `[generator/combinedDeclaration] …` (a required part field without a source, a type or option that does not fit, a `requiredIf` the mask cannot settle, an impossible roster, a priced mask field no part reads), `[generator/kindChange]` (a key that is already a document generator), the concurrent-save conflict. The CLI prints them verbatim; it does not pre-check.
127
+ - **Parts are pinned, never "newest".** `"generator": "school-contract"` without `@revision` is refused by the file reader. A part must be a sealed document revision when the combined one seals; an archived part blocks the seal, and a part used by a combined revision cannot be archived (`[generator/partInUse]`).
128
+ - **IDs belong to their environment.** An exported `id` only saves on the environment it came from; to bring a declaration to another environment, drop `id`/`expectedSourceRevision`/`revision`/`status` and save it there as a new key (or clone + export there and copy the `inputSchema` and `parts`).
129
+ - `generator export` / `generator source export` on a combined revision refuse and point to `generator combined export`; `show`, `list`, `usages`, `runs`, `clone`, `seal`, `archive`, `restore` and `delete-draft` take either kind.
130
+
131
+ ## Verify
132
+
133
+ ```bash
134
+ plexus-cli --env local --tenant test generator list
135
+ plexus-cli --env local --tenant test generator show school-contract@3 --json
136
+ plexus-cli --env local --tenant test generator usages school-contract@3
137
+ plexus-cli --env local --tenant test generator runs school-contract@3
138
+ ```
139
+
140
+ Also inspect the marked PDF and positions in the contract-template workspace. Scenario evaluation does not require the PDF converter; PDF preview does.
141
+
142
+ ## Idempotency / gotchas
143
+
144
+ - Source is compiled in a Node action and executed in QuickJS-WASM inside the Convex transaction. No database, network, filesystem, environment, timers, randomness or ambient clock reaches the guest. Input and output are bounded JSON; CPU, memory and stack are bounded.
145
+ - Published source, compiler artifact, interpreter identity, bootstrap hash and execution limits are frozen. A compiler/runtime change requires a reviewed new revision or an explicit audited migration. Display names remain renamable.
146
+ - Saving source/configuration or removing a scenario invalidates approval. A changed DOCX requires renewed review. Publishing rechecks every expected result against current resolved inputs.
147
+ - Optional `articleDecisions` output entries explain inclusion/omission. When supplied, they must cover the declared article manifest exactly and agree with actual lines. The editor shows when explanations are absent.
148
+ - Article reverse links come from the finite manifest, so conditional possibilities remain visible even when an example omits them.
149
+ - Signature captions must exist in the PDF. Publication checks declarations; rendering verifies their locations.
150
+ - Prices still come from the pricing kernel. Override lines need a price version for currency. Grants, parties and payer allocations pass the normal tenant and commercial validation before materialization.
151
+ - Historical signed snapshots are never rewritten during the table/source migration. An unavailable historical source remains explicitly unavailable; it is never re-pinned to different code.
152
+ - Production writes need an explicit environment and `--allow-prod`; this playbook does not authorize them.
153
+ - To build on an existing generator's code (a second school's variant of a contract), start from `generator clone` or `generator source export`. If you author it as several modules, bundle them into the one self-contained source first (e.g. esbuild with `bundle: true, format: "esm", target: "es2020"`) — `generator source save` refuses imports. The guest has no `Intl` and throws on it: replace every `toLocaleString` with hand-written German formatting. Run `generator scenarios test` on the draft before approving.
154
+ - Fee groups are part of a source manifest (`feeGroups`, with bilingual names and article IDs). In a setup file use `resolver.feeGroups: [{ key, name: { de, en }, billingItemKeys }]`; every member must also be in `resolver.billingItemKeys`. Source export/apply round-trips groups. The catalog has no `plans` section; declare article availability in `billingItems[].programs`. Workflow prices use `pricedGroup` and `derive.group`, resolved through pinned generator revisions.
155
+
156
+ ## Related
157
+
158
+ - `plexus-cli generator <verb> --help` for every verb above
159
+ - [Set up a billing catalog and Word templates](../config/set-up-billing-catalog.md)
160
+ - [Edit a live tenant offer](../offer/edit-live-tenant-offer.md)
161
+ - [Release an offer bundle](../offer/publish-offer-bundle.md)
@@ -0,0 +1,99 @@
1
+ # Erase applications past their retention period
2
+
3
+ **When to use:** on the monthly erasure date (below), or as soon as the weekly
4
+ GlitchTip warning „[retention] applications past their retention period await
5
+ erasure" names a tenant. Plexus erases nothing on its own: an
6
+ application whose retention period has ended stays, with all its personal data,
7
+ until someone runs this recipe.
8
+
9
+ > **The DSGVO duty behind it.** Storage limitation (Art. 5(1)(e) DSGVO) requires
10
+ > deleting personal data once its purpose and retention period have ended. Since
11
+ > erasure is no longer automatic, it happens only if a person does it, deliberately
12
+ > and regularly:
13
+ >
14
+ > - **Who:** the Plexus platform operator (the maintainer, or whoever holds a
15
+ > `_platform.tenants.accessAll` token for prod), on behalf of the schools.
16
+ > - **How often:** **monthly**, on the first working day, for **every** tenant on
17
+ > prod; additionally within a week whenever the weekly warning appears. The
18
+ > shortest retention period is 180 days, so a monthly run keeps any application at
19
+ > most about a month past its date.
20
+ > - **Record:** keep the `--json` output of each `apply` run (date, tenant, ids) in
21
+ > your operations log, one directory per run (e.g. `retention-<date>/`). Plexus
22
+ > stores no "who erased" row yet: the application only carries `redactedAt`.
23
+
24
+ ## What is due
25
+
26
+ The rule lives in one place on the server (`retentionDueAt`):
27
+ an application is due when it has ENDED (`endedAt` set) and `RETENTION_DAYS` of its
28
+ classification have passed since. The classification follows its outcome:
29
+
30
+ | outcome | classification | kept for |
31
+ |---|---|---|
32
+ | approved | admitted | 3650 days (10 years) |
33
+ | rejected | rejected | 180 days |
34
+ | withdrawn | withdrawn | 365 days |
35
+ | expired | expired | 180 days |
36
+ | waitlist | waitlist | 730 days |
37
+ | none | unknown | 365 days |
38
+
39
+ Test applications and soft-deleted ones („gelöscht") are listed too — both still hold
40
+ personal data. A still-running application is never due, even if soft-deleted.
41
+
42
+ ## Prerequisites
43
+
44
+ - `plexus-cli` with a platform-operator token for the target env (check the
45
+ connection with `plexus-cli --env prod config show` and `plexus-cli --env prod whoami`);
46
+ both functions need `_platform.tenants.accessAll`.
47
+ - The tenant slug (`plexus-cli --env prod platform tenants`).
48
+
49
+ ## Steps
50
+
51
+ ```bash
52
+ # 1. Read-only: what is due in this tenant. Note the number of rows.
53
+ plexus-cli --env prod --tenant <slug> retention plan
54
+
55
+ # 2. Dry run of the erasure — prints the same list, erases nothing.
56
+ plexus-cli --env prod --tenant <slug> retention apply
57
+
58
+ # 3. Erase. --expect is the count from step 1: if the server now finds a different
59
+ # number of due applications, nothing is erased and you read the plan again.
60
+ mkdir -p retention-$(date +%F)
61
+ plexus-cli --env prod --allow-prod --tenant <slug> retention apply --confirm --expect <n> --json \
62
+ > retention-$(date +%F)/<slug>.json
63
+ ```
64
+
65
+ Repeat for every tenant `platform tenants` lists.
66
+
67
+ ## Verify
68
+
69
+ ```bash
70
+ plexus-cli --env prod --tenant <slug> retention plan # → no rows
71
+ ```
72
+
73
+ An erased application keeps its row and its decision log (`application_events`);
74
+ its answers, documents, chat texts, access log and stored mail bodies are gone, and
75
+ `redactedAt` is set (server function `_redactApplicationCore` holds the full list).
76
+
77
+ ## Gotchas
78
+
79
+ - **Erasure cannot be undone.** Read the plan first; there is no restore.
80
+ - **The server decides, not the list.** `retention:eraseDue` refuses an application
81
+ that is not due, belongs to another tenant, or is already erased. A refusal stops the
82
+ run; its message (stderr, not the `--json` file) names the ids erased before it —
83
+ copy them into the record, rerun `plan` and continue.
84
+ - **A different count erases nothing.** Someone ended an application between `plan` and
85
+ `apply`, or another operator erased some — read `plan` again and confirm the new count.
86
+ - **Writes need `--env` named; prod needs `--allow-prod`** (`retention apply` is a
87
+ `confirm` command, its dry run counts as a read).
88
+ - **One request for one person** (Art. 17, a family asks to be erased now) is not this
89
+ recipe: that is „Bewerbung löschen" on the single application
90
+ (`admission.redactApplication`), independent of any period.
91
+
92
+ ## Related
93
+
94
+ - Weekly report: `retentionDueReport` (server cron „retention due report",
95
+ Mondays 03:00 UTC) — a GlitchTip `warning`, `area: admission.retention`, tags
96
+ `tenantSlug`, `dueCount`, `testDueCount`; a tenant whose read failed reports as an
97
+ `error` with `errorClass: scanFailed`.
98
+ - What an erasure removes from the access log is defined in the platform's logging
99
+ concept (Protokollierungskonzept, § 6); ask the platform operator for it.
@@ -0,0 +1,107 @@
1
+ # Delete persons past their school's deletion rule
2
+
3
+ **When to use:** together with the monthly erasure of applications
4
+ ([erase-due-applications.md](./erase-due-applications.md)), or as soon as the
5
+ weekly GlitchTip warning „[retention] persons past their tenant's deletion rule
6
+ await erasure" names a tenant. Plexus deletes nobody on its own: a
7
+ school switches a deletion rule on („Rollen" → „Löschregel"), and a person the
8
+ rule reaches stays, with all its data, until someone runs this recipe.
9
+
10
+ > **The duty behind it.** A school that switched the rule on has decided that
11
+ > master data without an active role is kept only for its waiting period
12
+ > (storage limitation, Art. 5(1)(e) DSGVO). Since deletion is not automatic,
13
+ > it happens only if a person does it:
14
+ >
15
+ > - **Who:** the Plexus platform operator (whoever holds a
16
+ > `_platform.config.manage` token for prod — the right of the other person
17
+ > hard delete, `stammdaten persons purge`).
18
+ > - **How often:** **monthly**, on the first working day, for every tenant with a
19
+ > rule; additionally within a week whenever the weekly warning appears.
20
+ > - **Record:** keep the `--json` output of each `apply` run in
21
+ > your operations log, one directory per run (e.g. `retention-<date>/`). A deleted person
22
+ > leaves a `party_role_events` row per role it held (actor: the operator) —
23
+ > and nothing else; the run's output is the record of who it was.
24
+
25
+ ## What is due
26
+
27
+ The rule (`tenants.partyRetention`: waiting period in days, optionally a former
28
+ leaf role) and the deadline live in one place on the server (`retentionDeadlineFor`): a person with no active or future
29
+ role, not a draft, is due the waiting period after the latest of its creation,
30
+ its last role change, its last role end and the rule's save time.
31
+
32
+ Due is not deletable. The hard delete (rule `retention`) refuses a person that
33
+
34
+ - any application names or binds — finished and erased ones included,
35
+ - is linked to a user account,
36
+ - a receipt holds (contract, invoice, signature, …),
37
+ - has more than 1000 history rows or 200 role periods.
38
+
39
+ `plan` asks the server this for every due person and shows the reason as
40
+ `blocked: …`; `apply` skips those. Blocked persons stay due — the weekly warning
41
+ counts them apart (`blockedCount`) and raises nothing for them alone.
42
+
43
+ ## Prerequisites
44
+
45
+ - `plexus-cli` with a platform-operator token holding `_platform.config.manage`
46
+ (check with `plexus-cli --env prod whoami`).
47
+ - The tenant slug (`plexus-cli --env prod platform tenants`). A tenant without a
48
+ switched-on rule answers `plan` with „hat keine eingeschaltete Löschregel".
49
+
50
+ ## Steps
51
+
52
+ ```bash
53
+ # 1. Read-only: who is due, and who of them may be deleted. Count the
54
+ # `would-erase` rows (blocked ones do not count).
55
+ plexus-cli --env prod --tenant <slug> retention parties plan
56
+
57
+ # 2. Dry run — the same list, deletes nothing.
58
+ plexus-cli --env prod --tenant <slug> retention parties apply
59
+
60
+ # 3. Delete. --expect is the would-erase count from step 1: if the server now
61
+ # finds a different number of deletable persons, nothing is deleted.
62
+ mkdir -p retention-$(date +%F)
63
+ plexus-cli --env prod --allow-prod --tenant <slug> retention parties apply --confirm --expect <n> --json \
64
+ > retention-$(date +%F)/<slug>-parties.json
65
+ ```
66
+
67
+ ## Verify
68
+
69
+ ```bash
70
+ plexus-cli --env prod --tenant <slug> retention parties plan # → only blocked rows, or none
71
+ ```
72
+
73
+ A deleted person is gone with its values, relationships, unit placements, value
74
+ and role history and the stored files only it held (the party-file rule; a file another person or a form still
75
+ names stays).
76
+
77
+ ## Gotchas
78
+
79
+ - **Deletion cannot be undone.** Read the plan first; there is no restore.
80
+ - **The server decides, not the list.** `partyRetention:eraseDue` refuses a person
81
+ of another tenant, one not (yet) due, a switched-off rule, and every block above.
82
+ A refusal stops the run; its message (stderr) names the persons deleted before it
83
+ — copy them into the record, rerun `plan` and continue.
84
+ - **A different count deletes nothing — but the count is not the list.** A role ended,
85
+ the rule changed or another operator ran it between `plan` and `apply` — read `plan`
86
+ again. If one person dropped out and another became due in between, the count still
87
+ matches and `apply` deletes the newcomer: the server still checks each one is due.
88
+ - **One person too large for one transaction stops the whole plan.** A deletion plan
89
+ above the relation engine's budget (`MAX_PLAN_ROWS` rows or the index-read cap) is
90
+ a bug-class error, not a block — `plan` aborts with `[relations] deletion plan
91
+ exceeds …` naming the person, and the weekly report marks the tenant `scanFailed`.
92
+ Review that person by hand (`stammdaten persons purge` after a soft delete, or
93
+ split its data) before the rest can run.
94
+ - **Soft-deleted persons („gelöscht")** are due like any other and marked so.
95
+ - **One person now** (staff deleted someone and the family asks for erasure) is
96
+ not this recipe: [`stammdaten persons purge`](../stammdaten/purge-deleted-person.md).
97
+ - **Writes need `--env` named; prod needs `--allow-prod`.**
98
+
99
+ ## Related
100
+
101
+ - Weekly report: `partyRetentionDueReport` (server cron „party retention
102
+ due report", Mondays 03:20 UTC) — a GlitchTip `warning`, `area: stammdaten.retention`,
103
+ tags `tenantSlug`, `dueCount` (deletable), `blockedCount`, `testDueCount` (persons only
104
+ test applications name or bind; today always blocked); a tenant whose read failed
105
+ reports as an `error` with `errorClass: scanFailed`. It shares its shape with the
106
+ applications' report.
107
+ - The rule and its UI: „Rollen" → „Löschregel" in the school's settings.
@@ -0,0 +1,31 @@
1
+ # Migrate direct assignments from a parent role
2
+
3
+ Use this when a tenant already has direct assignments to a role that has children.
4
+ The party detail marks these assignments invalid. Choose a descendant leaf whose
5
+ meaning fits the existing records; create a neutral leaf through the tenant
6
+ configuration first if the existing leaves imply facts you cannot verify.
7
+
8
+ 1. On staging, inspect `admin:roleConfiguration` and obtain the source and
9
+ destination role IDs. Check the source count with
10
+ `admin:roleAssignmentMigrationImpact` (counts above 2,000 are capped).
11
+ 2. Dry-run and apply the tenant configuration for any new destination role using
12
+ [Update a tenant's config](../config/update-tenant-config.md). Add a search
13
+ filter option if staff need to find the migrated parties.
14
+ 3. Once the app revision containing `admin:migrateRoleAssignmentsBatch` is live,
15
+ call it repeatedly until it returns `{"migrated":0}`:
16
+
17
+ ```bash
18
+ plexus-cli call admin:migrateRoleAssignmentsBatch --kind mutation --env staging \
19
+ client=<tenant> sourceRoleId=<parent-role-id> targetRoleId=<leaf-role-id> --json
20
+ ```
21
+
22
+ Each call moves at most 50 assignments, preserves dates and unit scope, and
23
+ writes role-change events. It rejects a target outside the source subtree,
24
+ a target with children, or overlapping assignments already on the target.
25
+ If a call fails, inspect the conflict rather than retrying it unchanged.
26
+ 4. Verify source count is zero, spot-check affected party details and search,
27
+ then repeat the configuration and migration against production. Export
28
+ production's own configuration first; never apply a staging snapshot blindly.
29
+
30
+ The command requires `administration.roles.manage` for the tenant. Run it under
31
+ an authenticated operator so each event records who made the change.
@@ -0,0 +1,113 @@
1
+ # Signing (e-signature) — `plexus-cli signing …`
2
+
3
+ The CLI surface of the **Signaturvorgang**: one frozen contract revision
4
+ (`contract_snapshots`) ↔ one process at a provider, driven by a fixed backend
5
+ state machine. Two remote providers, `docusign` and `insign`, plus the paper
6
+ path. Setting up a school's provider account:
7
+ [connect-a-school.md](./connect-a-school.md); rotating the platform key:
8
+ [rotate-the-key.md](./rotate-the-key.md).
9
+
10
+ Every command needs `--tenant <slug>` and a caller holding
11
+ `finanzen_vertraege.contracts.manage` on that tenant (the platform operator
12
+ tokens do; `plexus-cli whoami` shows who you are).
13
+
14
+ ## The school's connection
15
+
16
+ A provider account belongs to the SCHOOL, not to the deployment: each
17
+ tenant has at most one connection per provider and at most ONE active one — the
18
+ provider its contracts go out through. Setting one up is a platform act; the
19
+ recipe is [connect-a-school.md](./connect-a-school.md). The `test` tenant gets an
20
+ inSign sandbox connection out of the box, active, redirected to
21
+ `admin@plexus.swop.schule`.
22
+
23
+ ```bash
24
+ plexus-cli signing connection show --env local --tenant test # no network
25
+ plexus-cli signing connection test --provider insign --env local --tenant test # reaches the account
26
+ ```
27
+
28
+ `show` prints `E-Signatur: <Anbieter>|keine`, then one row per provider: `state`
29
+ (`platform_unconfigured ▸ incomplete ▸ unverified ▸ failing ▸ ready`), whether it
30
+ is active, the environment, the non-secret settings, the redirect, which secrets
31
+ are held (name · fingerprint · when — never the value), the last test and what
32
+ keeps it from `ready`. `test` reaches the account (DocuSign: JWT grant → token →
33
+ userinfo, and on `consent_required` the URL the school's sender must open once;
34
+ inSign: server version + owner), records the result on the connection and exits
35
+ 1 when it fails. A contract goes out only through a connection a test has
36
+ reached since its last change (`ready`, or `failing` after an earlier success).
37
+
38
+ ## Send a revision for signature
39
+
40
+ ```bash
41
+ plexus-cli signing list --env local --tenant test # existing requests
42
+ plexus-cli signing start --env local --tenant test --snapshot <contract_snapshots id>
43
+ plexus-cli signing show <signingRequestId> --env local --tenant test
44
+ ```
45
+
46
+ `start` validates (an active connection that has been tested, PDF, latest
47
+ revision, every declared signer has an e-mail — and for inSign a DISTINCT one
48
+ each, no open request on the revision, not already signed), inserts the request
49
+ as `pending` with the connection stamped on it, and schedules the provider call.
50
+ A few seconds later `show` reads `sent` + the `providerRef`; a refusal reads
51
+ `failed` with the provider's own message (DocuSign: `ANCHOR_TAB_STRING_NOT_FOUND`
52
+ when the PDF lacks the `/unterschrift:<role>:<n>/` markers; inSign: "refused the
53
+ signer invitation").
54
+
55
+ There is no `--provider`: the school's active connection decides, and a Vorgang
56
+ keeps talking to the account it started on even after the school switches.
57
+
58
+ On a demo/sandbox account every recipient is redirected to the connection's
59
+ redirect address (`show` prints the original address per signer). For inSign the
60
+ redirect is sub-addressed per recipient (`…+s1@`, `…+s2@`), because it
61
+ identifies an external user BY the address.
62
+
63
+ ## Bring the answer back
64
+
65
+ ```bash
66
+ plexus-cli signing reconcile <signingRequestId> --env local --tenant test
67
+ plexus-cli signing show <signingRequestId> --env local --tenant test
68
+ ```
69
+
70
+ `reconcile` re-reads the process from its provider and applies it — the return
71
+ path on local dev (no public webhook). On staging/prod the callback + the
72
+ 15-minute poll cron do this without anyone; `reconcile` is for "now". A
73
+ `completed` process stores the signed PDF + the completion certificate
74
+ (DocuSign's Certificate of Completion, inSign's Signaturprotokoll — URLs in
75
+ `show`) and writes one `signatures` row per signer, dated by the PROVIDER.
76
+
77
+ ## Correct course
78
+
79
+ ```bash
80
+ plexus-cli signing void <signingRequestId> --reason "Neuer Vertrag folgt" --env local --tenant test
81
+ plexus-cli signing resend <signingRequestId> --party <partyId> --env local --tenant test
82
+ ```
83
+
84
+ `void` ends the request (`voided`) and the row keeps YOUR reason, whether or not
85
+ the provider records one of its own. The contract may then be regenerated /
86
+ re-frozen and sent again. While a request is open, freezing a new revision is
87
+ refused (regeneration freeze).
88
+
89
+ `resend` reminds one recipient on DocuSign. inSign has no per-recipient resend —
90
+ it reminds every open recipient of the session at once, which is what the caller
91
+ means anyway.
92
+
93
+ ## Verify
94
+
95
+ - `signing show` → `status`, per-signer `status`/`signedAt`, artefact URLs, the
96
+ provider webhook `receipt …` rows recorded for the request (evidence, not business
97
+ events — the contract's history is `contract_events`).
98
+ - `plexus-cli call contractSnapshots:listRevisions '{"client":"test","contractId":"…"}'`
99
+ → `signerCount` / `signedAt` reflect the written `signatures`.
100
+
101
+ ## Gotchas
102
+
103
+ - `--snapshot` takes the **contract_snapshots** id (the revision), not the
104
+ contract id. `signing list --contract <id>` shows the requests of a contract.
105
+ - A request on an older revision (not the contract's latest) is refused — freeze first, then start.
106
+ - The DocuSign demo account watermarks documents ("Demonstration Document Only")
107
+ and may sit in a US region: never put real family data through it. inSign's
108
+ sandbox is worse in one respect — its credentials are PUBLISHED by the vendor,
109
+ which is why a sandbox connection cannot be saved without a redirect address.
110
+ - `signing start` answers "hat keinen Anbieter … aktiviert" or "nicht erfolgreich
111
+ getestet": run `signing connection show`, then `signing connection test`.
112
+ - An inSign process that has expired still reads `sent`: inSign cannot report
113
+ expiry, and guessing would end a Vorgang that is still live. Withdraw it.