@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.
- package/README.md +826 -2
- package/dist/cli.mjs +483 -0
- package/dist/skill/.managed-by-plexus-cli +1 -0
- package/dist/skill/SKILL.md +185 -0
- package/dist/skill/references/accounts/email-changes.md +46 -0
- package/dist/skill/references/applications/inspect-process-store.md +61 -0
- package/dist/skill/references/applications/start-a-test-application.md +66 -0
- package/dist/skill/references/audit/answer-access-request.md +115 -0
- package/dist/skill/references/config/attach-a-template.md +117 -0
- package/dist/skill/references/config/init-tenant.md +121 -0
- package/dist/skill/references/config/onboard-tenant.md +71 -0
- package/dist/skill/references/config/rename-program-stage.md +99 -0
- package/dist/skill/references/config/set-tenant-branding.md +150 -0
- package/dist/skill/references/config/set-tenant-terms.md +50 -0
- package/dist/skill/references/config/set-up-billing-catalog.md +73 -0
- package/dist/skill/references/config/update-tenant-config.md +170 -0
- package/dist/skill/references/config/wipe-tenant-config.md +107 -0
- package/dist/skill/references/export/create-named-export.md +47 -0
- package/dist/skill/references/feedback/create-and-triage-feedback.md +69 -0
- package/dist/skill/references/feedback/manage-feedback-tags.md +57 -0
- package/dist/skill/references/finance/change-contract-grant.md +36 -0
- package/dist/skill/references/finance/record-submission-date.md +33 -0
- package/dist/skill/references/interessenten/list-interessenten.md +54 -0
- package/dist/skill/references/notifications/debug-mail-delivery.md +77 -0
- package/dist/skill/references/notifications/mail-level.md +90 -0
- package/dist/skill/references/notifications/reword-a-letter.md +155 -0
- package/dist/skill/references/notifications/trace-a-login.md +50 -0
- package/dist/skill/references/offer/add-workflow-node.md +268 -0
- package/dist/skill/references/offer/addresses.md +129 -0
- package/dist/skill/references/offer/appointment-task.md +79 -0
- package/dist/skill/references/offer/assignment-rule.md +195 -0
- package/dist/skill/references/offer/attach-form-to-workflow.md +79 -0
- package/dist/skill/references/offer/author-form-scenarios.md +97 -0
- package/dist/skill/references/offer/author-manifest-meta.md +94 -0
- package/dist/skill/references/offer/author-smartform.md +102 -0
- package/dist/skill/references/offer/bind-node-input.md +76 -0
- package/dist/skill/references/offer/completeness-check.md +137 -0
- package/dist/skill/references/offer/countersign-node.md +91 -0
- package/dist/skill/references/offer/create-pedagogy-unit.md +55 -0
- package/dist/skill/references/offer/declare-node-reads.md +99 -0
- package/dist/skill/references/offer/edit-live-tenant-offer.md +144 -0
- package/dist/skill/references/offer/edit-smartform.md +121 -0
- package/dist/skill/references/offer/edit-workflow-node.md +126 -0
- package/dist/skill/references/offer/engage-push-node.md +147 -0
- package/dist/skill/references/offer/insert-and-rewire-node.md +67 -0
- package/dist/skill/references/offer/node-tasks.md +162 -0
- package/dist/skill/references/offer/optional-assessment.md +65 -0
- package/dist/skill/references/offer/publish-offer-bundle.md +117 -0
- package/dist/skill/references/offer/set-offer-capacities.md +78 -0
- package/dist/skill/references/offer/staff-input-if-node.md +112 -0
- package/dist/skill/references/offer/task-appointment-result.md +69 -0
- package/dist/skill/references/offer/task-family-request.md +69 -0
- package/dist/skill/references/offer/task-result-files.md +52 -0
- package/dist/skill/references/offer/template-document-node.md +113 -0
- package/dist/skill/references/offer/workflow-default-channels.md +69 -0
- package/dist/skill/references/resolver/manage-contract-resolver.md +159 -0
- package/dist/skill/references/retention/erase-due-applications.md +99 -0
- package/dist/skill/references/retention/erase-due-parties.md +107 -0
- package/dist/skill/references/roles/migrate-parent-assignments.md +31 -0
- package/dist/skill/references/signing/README.md +113 -0
- package/dist/skill/references/signing/connect-a-school.md +90 -0
- package/dist/skill/references/signing/rotate-the-key.md +41 -0
- package/dist/skill/references/stammdaten/purge-deleted-person.md +68 -0
- package/dist/skill/references/stripe/connect-a-school.md +107 -0
- package/dist/skill/references/stripe/request-and-verify-a-payment.md +78 -0
- package/dist/skill/references/test-actions/drive-a-test-application.md +59 -0
- package/dist/skill/references/users/add-platform-admin.md +59 -0
- package/dist/skill/references/users/add-platform-user.md +59 -0
- package/dist/skill/references/users/add-tenant-admin.md +75 -0
- package/dist/skill/references/users/change-login-email.md +55 -0
- package/dist/skill/references/users/cli-access-for-tenant-admin.md +117 -0
- package/dist/skill/references/users/delete-permission-group.md +76 -0
- package/dist/skill/references/users/grant-cross-tenant-access.md +57 -0
- package/dist/skill/references/users/grant-feedback-assignable.md +43 -0
- package/dist/skill/references/users/set-password.md +57 -0
- package/dist/skill/references/users/use-test-account.md +79 -0
- package/dist/skill/references/worker-ops/debug-worker-jobs.md +90 -0
- package/dist/skill/references/worker-ops/verify-migrations.md +51 -0
- package/dist/spec.snapshot.json +145815 -0
- package/package.json +53 -4
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Make a task close with an appointment („Termin vereinbaren")
|
|
2
|
+
|
|
3
|
+
**Use it to** turn a workflow-step task such as „Termin für das Kennenlerngespräch
|
|
4
|
+
vereinbaren" into one that is completed by entering an appointment: date,
|
|
5
|
+
time, duration, place and host. Completing it books the appointment at the
|
|
6
|
+
application, mails the family (portal owner) date/time/place with a `termin.ics`
|
|
7
|
+
calendar file, frees the task it
|
|
8
|
+
`blocks`, and lists the appointment under „Anstehende Termine" on the staff dashboard.
|
|
9
|
+
|
|
10
|
+
## The declaration
|
|
11
|
+
|
|
12
|
+
On the task (a composed `kind: "task"` in the node's `tasks`):
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"key": "termin",
|
|
17
|
+
"kind": "task",
|
|
18
|
+
"title": { "de": "Termin für das Kennenlerngespräch vereinbaren", "en": "…" },
|
|
19
|
+
"assign": { "group": "schulverwaltung" },
|
|
20
|
+
"blocks": { "templateKey": "gespraech" },
|
|
21
|
+
"result": {
|
|
22
|
+
"kind": "appointment",
|
|
23
|
+
"title": { "de": "Kennenlerngespräch", "en": "Introductory talk" },
|
|
24
|
+
"durationMinutes": 45
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- `result.title` is the **neutral** title the family's letter and every calendar file
|
|
30
|
+
carry — never a family name.
|
|
31
|
+
- `durationMinutes` (5–480) prefills the dialog; staff may change it per appointment.
|
|
32
|
+
- Publish refuses: an unknown `kind` (`[taskResult/unknownKind]`), a result on an
|
|
33
|
+
approval (`[taskResult/onApproval]`), a title missing `de` or `en`
|
|
34
|
+
(`[taskResult/appointmentMissingTitle]`), a bad duration
|
|
35
|
+
(`[taskResult/appointmentInvalidDuration]`), a stray field
|
|
36
|
+
(`[taskResult/unknownField]`). `offer validate` catches all of them offline.
|
|
37
|
+
|
|
38
|
+
## With the CLI
|
|
39
|
+
|
|
40
|
+
One spelling, the same verb as every other task result — no `:`-separated
|
|
41
|
+
free text, so a title may contain a colon:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
plexus-cli offer workflow node task add --dir $WORK/$S --node kennenlernen --key termin \
|
|
45
|
+
--title 'Termin für das Kennenlerngespräch vereinbaren' --team schulverwaltung --due-in-days 3 \
|
|
46
|
+
--blocks gespraech \
|
|
47
|
+
--result appointment --duration 45 \
|
|
48
|
+
--result-title 'Kennenlerngespräch' --result-title-en 'Introductory talk'
|
|
49
|
+
# change later: node task set … --duration 60 / --result-title '…'
|
|
50
|
+
plexus-cli offer workflow node list --dir $WORK/$S
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Then the usual live-tenant path ([Edit a live tenant's offer](./edit-live-tenant-offer.md)):
|
|
54
|
+
`offer validate` → `--env prod offer plan` → `--env prod offer apply --allow-prod --publish` → plan
|
|
55
|
+
again until every row is `same`.
|
|
56
|
+
|
|
57
|
+
## Gotchas
|
|
58
|
+
|
|
59
|
+
- **Running applications keep their pinned graph.** An application already sitting on
|
|
60
|
+
the node with an open `termin` row keeps its old declaration until it is re-pinned;
|
|
61
|
+
new entries into the node get „Termin vereinbaren" in the action bar.
|
|
62
|
+
- **Staff books it from the action bar** („Termin vereinbaren"), like every task; the
|
|
63
|
+
dialog names every missing field, refuses a slot more than two years ahead, and
|
|
64
|
+
closes with a toast. In the Testmodus the shortcut „<Aufgabe> (Test): Termin morgen
|
|
65
|
+
10:00" books through the same verb.
|
|
66
|
+
- **No portal owner, no mail.** An application staff created has no portal account; the
|
|
67
|
+
appointment is saved and the toast warns „Keine Mail an die Familie …". The same
|
|
68
|
+
happens when the school's outgoing mail is off.
|
|
69
|
+
- The zone is the tenant's (`Europe/Berlin` for every DACH tenant today).
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Let a task ask the family and wait for its answer („Nachforderung")
|
|
2
|
+
|
|
3
|
+
**Use it to** turn „Nachforderung: auf Unterlagen der Familie warten" from a task the
|
|
4
|
+
team closes by hand into one that asks the family itself and completes itself the
|
|
5
|
+
moment the family sends. The family gets the field in its task „Offene Felder beantworten" in the portal
|
|
6
|
+
(the task letter `applicant.tasksOpen`, the task's deadline, a reminder before it); the
|
|
7
|
+
team's task waits and holds the decision until then. Nobody ticks it off.
|
|
8
|
+
|
|
9
|
+
## The declaration
|
|
10
|
+
|
|
11
|
+
On a task that a `spawnTasks` optional action references (and only there; at most
|
|
12
|
+
one such task per action):
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"key": "unterlagenAnfordern",
|
|
17
|
+
"kind": "task",
|
|
18
|
+
"title": { "de": "Nachforderung: auf Unterlagen der Familie warten", "en": "Follow-up: waiting for the family's documents" },
|
|
19
|
+
"assign": { "group": "bewerbungsteam" },
|
|
20
|
+
"params": { "dueInDays": 7 },
|
|
21
|
+
"familyRequest": {
|
|
22
|
+
"fields": [{ "scope": "node", "owner": "form", "key": "admission", "path": "kind.weitereUnterlagen" }]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- **What is asked is chosen at the click.** Pressing the action opens a dialog: the
|
|
28
|
+
presser ticks the fields and writes the text the family reads. The text becomes the
|
|
29
|
+
note at each asked field — in the portal and in the task letter.
|
|
30
|
+
- Exactly one of `fields` (the fields offered, each a full address
|
|
31
|
+
`$node.<formNode>.admission.<field>` — one form) or `from` (the form instance
|
|
32
|
+
`$node.<formNode>.admission`: every file field (`docRef`) of that form is offered). There
|
|
33
|
+
is no default form: an empty `familyRequest` is refused. A **free upload** is a `docRef` field
|
|
34
|
+
with `docRefConstraints: { multiple: true }` („Weitere Unterlagen") — the field must
|
|
35
|
+
exist in the application's form version.
|
|
36
|
+
- `params.dueInDays` is the family's deadline and the task's.
|
|
37
|
+
- Put the action into the decision's `requiresOneOf` if the decision should wait for it.
|
|
38
|
+
|
|
39
|
+
## CLI
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
plexus-cli offer workflow node task set --dir <dir> --node decision --key unterlagenAnfordern \
|
|
43
|
+
--due-in-days 7 \
|
|
44
|
+
--family-request-fields '$node.form.admission.kind.weitereUnterlagen'
|
|
45
|
+
# or --family-request-from '$node.form.admission': every file field of that form
|
|
46
|
+
plexus-cli offer validate --dir <dir>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Remove with `--clear-family-request`. The task must not carry `result`, `when`,
|
|
50
|
+
`locksEvidence` or `blocks` (and nothing may block it) — `offer validate` names each.
|
|
51
|
+
|
|
52
|
+
## Verify
|
|
53
|
+
|
|
54
|
+
1. Cockpit: press the action → the dialog offers the fields; tick one, write the text,
|
|
55
|
+
„Anfordern" → the task shows the clock (system-closed), the decision buttons read
|
|
56
|
+
„Wartet auf: …".
|
|
57
|
+
2. Portal `/e`: „Offene Felder beantworten" (field under „Nachzureichen"), the text at the field, „bis <Datum>".
|
|
58
|
+
3. Upload and send as the family → the task is `done`, the decision buttons are free.
|
|
59
|
+
|
|
60
|
+
## Gotchas
|
|
61
|
+
|
|
62
|
+
- A field already under an open request, hidden, or printed into a held document is not
|
|
63
|
+
offered; the trigger refuses it with the reason, and nothing is spawned.
|
|
64
|
+
- An empty or unchanged answer answers nothing (the same file again is no new document):
|
|
65
|
+
the field stays asked.
|
|
66
|
+
- Withdrawing the request („Zurückziehen") cancels the task; the decision stays held by
|
|
67
|
+
`requiresOneOf` until another action completes.
|
|
68
|
+
- Rows of applications on an OLDER offer version keep the old task; only applications
|
|
69
|
+
pinned to the published version get the family request.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Let a task's result carry files
|
|
2
|
+
|
|
3
|
+
**Use it to** let the person who completes a task attach files to their answer. Example: the
|
|
4
|
+
teacher's trial-day feedback with an English and a maths placement sheet, as PDF or photo.
|
|
5
|
+
|
|
6
|
+
## The model
|
|
7
|
+
|
|
8
|
+
- **One switch on the task:** `result.files: true`. It is allowed only on prose results:
|
|
9
|
+
`freetext`, `recommendation`, or a `choice` without `writes`. Publish refuses it anywhere
|
|
10
|
+
else (`[taskResult/filesOnKind]`).
|
|
11
|
+
- **Text and files are ONE message** in the chat the result names (`result.chat`). The row
|
|
12
|
+
keeps no text, and the chat keeps, shows and erases the files (GDPR erasure included).
|
|
13
|
+
- **Up to 20 files per result**.
|
|
14
|
+
A normal chat message takes 5. Each file may be PDF, JPEG, PNG or WebP of up to 25 MB;
|
|
15
|
+
an iPhone HEIC photo is converted to JPEG in the browser.
|
|
16
|
+
- **Where the files show:** in the result's message — cockpit → Verlauf/Aktivitäten (the
|
|
17
|
+
chat's tab); a click opens the file in the viewer. The task itself shows at most how
|
|
18
|
+
many files its result carries. Never in the „Dokumente" rail:
|
|
19
|
+
that lists the application's documents, not task attachments. Nothing is
|
|
20
|
+
copied, so deleting the message removes its files.
|
|
21
|
+
- **Who sees them:** whoever reads the feedback text — the SAME chat rule. In a
|
|
22
|
+
`team:<group>` chat that means the members of that group plus platform admins. A
|
|
23
|
+
`tenant_admin` who is in no such group sees neither the text nor the files. Before
|
|
24
|
+
choosing `team:principals`, check that the readers really hold that group (`plexus-cli
|
|
25
|
+
users show <email>`).
|
|
26
|
+
|
|
27
|
+
## CLI
|
|
28
|
+
|
|
29
|
+
Work in a scratch dir for a real tenant
|
|
30
|
+
([edit-live-tenant-offer.md](./edit-live-tenant-offer.md)).
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
plexus-cli --env prod --tenant <slug> offer export --slug <offer> --out <dir>
|
|
34
|
+
plexus-cli offer workflow node task set --dir <dir> --node decision --key tasterFeedback --files
|
|
35
|
+
plexus-cli offer validate --dir <dir>
|
|
36
|
+
plexus-cli --env prod --tenant <slug> offer plan --dir <dir> # workflow: change
|
|
37
|
+
# apply only with the school's go (the process owner):
|
|
38
|
+
plexus-cli --env prod --tenant <slug> offer apply --dir <dir> --publish --allow-prod
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A new task: `node task add … --result freetext --chat <chat> --files`.
|
|
42
|
+
|
|
43
|
+
## Verify
|
|
44
|
+
|
|
45
|
+
1. Open the task in the cockpit. The dialog shows „Dateien (optional)" with the paperclip,
|
|
46
|
+
which accepts several files at once.
|
|
47
|
+
2. Complete it with two files. Verlauf, tab of the result's chat: one message with the
|
|
48
|
+
text and both files.
|
|
49
|
+
3. „Dokumente": neither file is listed there.
|
|
50
|
+
4. As staff outside the chat's team: neither the text nor the files show in Verlauf.
|
|
51
|
+
|
|
52
|
+
Test tenant: Grundschule → „Probetag auswerten" (Schulleitung) takes files.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Generate a letter from a Word template (governedDocument `production: 'template'`)
|
|
2
|
+
|
|
3
|
+
**Use it to** let a workflow document node (Platzangebot, Bestätigung, Bescheinigung) fill a tenant Word
|
|
4
|
+
template and render the PDF automatically — no resolver, no deploy.
|
|
5
|
+
|
|
6
|
+
## Prerequisites
|
|
7
|
+
|
|
8
|
+
- The Word template is uploaded in the tenant's template module (Vorlagen). Its placeholders use
|
|
9
|
+
`<< input.<feld> >>` (sections `<<#input.<feld>>> … <</input.<feld>>>`).
|
|
10
|
+
- **Exactly one language per letter (required):** every template node declares a
|
|
11
|
+
required enum field (options `de`, `en`), names it in `template.localeField`, binds it to
|
|
12
|
+
`$env.applicant.language` (the applicant's language — the default) and wraps each language's
|
|
13
|
+
text in a section `<<#input.<feld>.de>> … <</input.<feld>.de>>` /
|
|
14
|
+
`<<#input.<feld>.en>> … <</input.<feld>.en>>`. Exactly one section renders, and `input.*` prints
|
|
15
|
+
the way that language writes it (numbers, dates, Ja/Yes, prices, names). A node without
|
|
16
|
+
`localeField` is refused at publish (`[governedDocument/localeFieldMissing]`); a missing section
|
|
17
|
+
blocks publish and file replacement. The English twin `en.input.*` of the old fixed bilingual
|
|
18
|
+
letters is gone — a file naming it is `[governedDocument/unknownPlaceholder]`. A German-only
|
|
19
|
+
school declares one option `de` and binds the field to the fixed value `de`.
|
|
20
|
+
- **Switching a LIVE template to language sections — upload it under a NEW key.** A file
|
|
21
|
+
replacement is checked against EVERY published node naming the key, archived versions included
|
|
22
|
+
(`publishedNodesFillingTemplate` — they still carry running applications), and a node published
|
|
23
|
+
without `localeField` accepts no file at all. So the old key stays with the old versions for
|
|
24
|
+
good; the sectioned file goes in as a new template:
|
|
25
|
+
1. upload the sectioned Word file as a new template and give it a new key (§ 1, e.g.
|
|
26
|
+
`platzangebot-v2`);
|
|
27
|
+
2. in the exported bundle point `template.key` at it, add the field `sprache` to
|
|
28
|
+
`template.fields`, set `template.localeField: 'sprache'` and bind it:
|
|
29
|
+
`plexus-cli offer workflow node set-input --dir <bundle> --node <id> --input sprache --address '$env.applicant.language'`
|
|
30
|
+
([Bind a node input](./bind-node-input.md));
|
|
31
|
+
3. `offer validate` → `plan` → `apply --publish`.
|
|
32
|
+
Applications pinned to the old version produce no letter any more (the mask names
|
|
33
|
+
`localeFieldMissing`); a re-pin onto the new version is refused, because the new binding changes
|
|
34
|
+
the process topology — restart such a (test) application on the new version.
|
|
35
|
+
- The deployment already runs a release that knows template nodes (validators first, then
|
|
36
|
+
publish); if publish refuses the node type, that needs a platform operator.
|
|
37
|
+
|
|
38
|
+
## 1. Give the template its stable key
|
|
39
|
+
|
|
40
|
+
There is no UI field yet — set it through the public mutation (the same `updateMetadata` the UI calls,
|
|
41
|
+
so name / role / file-name pattern must be passed as they are):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
plexus-cli call templates:list --env staging client=<slug> --json # id, name, roleId
|
|
45
|
+
plexus-cli call templates:updateMetadata --kind mutation --env staging client=<slug> \
|
|
46
|
+
templateId=<id> name='Platzangebot' roleId=<roleId> configKey=platzangebot --json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The key is unique per tenant and cannot move while a published process names it.
|
|
50
|
+
|
|
51
|
+
## 2. Author the node in the bundle (JSON)
|
|
52
|
+
|
|
53
|
+
`plexus-cli offer workflow node add` cannot create a governedDocument node; edit `workflow.json`
|
|
54
|
+
(export → edit → plan → apply, see [Edit a live tenant's offer](./edit-live-tenant-offer.md)):
|
|
55
|
+
|
|
56
|
+
```jsonc
|
|
57
|
+
{
|
|
58
|
+
"id": "platzangebot", "type": "governedDocument", "label": "Platzangebot",
|
|
59
|
+
"production": "template", "approval": "release", "responseStrategy": "confirm",
|
|
60
|
+
"responseDueInDays": 14,
|
|
61
|
+
"template": {
|
|
62
|
+
"key": "platzangebot",
|
|
63
|
+
"fields": [
|
|
64
|
+
{ "key": "kind_vorname", "label": "Vorname des Kindes", "type": "string", "required": true },
|
|
65
|
+
{ "key": "datum", "label": "Datum", "type": "date", "processSource": "generatedAt" },
|
|
66
|
+
{ "key": "antwortfrist", "label": "Antwortfrist", "type": "number", "processSource": "responseDueInDays" },
|
|
67
|
+
{ "key": "sprache", "label": { "de": "Sprache des Schreibens", "en": "Language of the letter" },
|
|
68
|
+
"type": "enum", "required": true,
|
|
69
|
+
"options": [
|
|
70
|
+
{ "value": "de", "label": { "de": "Deutsch", "en": "German" } },
|
|
71
|
+
{ "value": "en", "label": { "de": "Englisch", "en": "English" } }
|
|
72
|
+
] }
|
|
73
|
+
],
|
|
74
|
+
"cockpitFields": [],
|
|
75
|
+
"localeField": "sprache"
|
|
76
|
+
},
|
|
77
|
+
"inputs": {
|
|
78
|
+
"kind_vorname": {
|
|
79
|
+
"kind": "binding",
|
|
80
|
+
"address": { "scope": "node", "owner": "form", "key": "admission", "path": "kind.vorname" }
|
|
81
|
+
},
|
|
82
|
+
"sprache": { "kind": "binding", "address": { "scope": "env", "key": "applicant.language" } }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A bound form field must be exported by the form (`export: true`) and is named by its full address
|
|
88
|
+
(`$node.form.admission.kind.vorname` — bind it with
|
|
89
|
+
`plexus-cli offer workflow node set-input --node platzangebot --input kind_vorname --from form --field kind.vorname`);
|
|
90
|
+
a form that does not run before the node is `[address/notVisible]`, a field it does not export
|
|
91
|
+
`[address/pathNotInTarget]`. An old bundle (`wires`, a binding with `path`) is refused
|
|
92
|
+
— export it again. The deadline alone can be changed
|
|
93
|
+
with `plexus-cli offer workflow node set --id platzangebot --response-due-days 14` (`""` removes it).
|
|
94
|
+
|
|
95
|
+
## 3. Validate, plan, publish
|
|
96
|
+
|
|
97
|
+
`plexus-cli offer validate <dir>` checks the node shape offline (including the language field,
|
|
98
|
+
`[governedDocument/localeFieldMissing]`). **Publish** additionally refuses a
|
|
99
|
+
key no template carries (`[governedDocument/unknownTemplate]`) and any placeholder the fields cannot
|
|
100
|
+
fill (`[governedDocument/unknownPlaceholder]`).
|
|
101
|
+
|
|
102
|
+
## Verify
|
|
103
|
+
|
|
104
|
+
An application entering the node gets a run in `contract_generation_runs` (producer: the template)
|
|
105
|
+
and a PDF version on the node's slot within seconds; the cockpit's „Überarbeiten" opens the mask.
|
|
106
|
+
|
|
107
|
+
## Gotchas
|
|
108
|
+
|
|
109
|
+
- Generation is automatic only while the producer row is open and unblocked; after „Freigabe
|
|
110
|
+
anfordern" nothing regenerates. A changed template file is caught at submit („nicht mehr aktuell").
|
|
111
|
+
- Replacing the template file is refused while a published node could not fill its placeholders.
|
|
112
|
+
- **CLI gap:** the `template` pin (key, fields, cockpitFields) has no `offer workflow node` verb yet —
|
|
113
|
+
JSON only.
|
|
@@ -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,159 @@
|
|
|
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
|
+
## Combined generator
|
|
71
|
+
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
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):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
plexus-cli --env local --tenant test generator combined export school-with-care@1 --out /tmp/combined.json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"formatVersion": 1,
|
|
83
|
+
"kind": "combined",
|
|
84
|
+
"key": "school-with-lunch",
|
|
85
|
+
"name": { "de": "Schulvertrag mit Mittagessen", "en": "School contract with lunch" },
|
|
86
|
+
"description": { "de": "", "en": "" },
|
|
87
|
+
"inputSchema": { "fields": [
|
|
88
|
+
{ "key": "startDate", "label": { "de": "Beginn", "en": "Start" }, "type": "date", "required": true },
|
|
89
|
+
{ "key": "ganztag", "label": { "de": "Mit Hort", "en": "With after-school care" }, "type": "boolean", "defaultValue": false },
|
|
90
|
+
{ "key": "lateCare", "label": { "de": "Spätbetreuung", "en": "Late care" }, "type": "boolean", "defaultValue": false, "visibleIf": "ganztag == true" }
|
|
91
|
+
] },
|
|
92
|
+
"parts": [
|
|
93
|
+
{ "partKey": "school", "generator": "school-contract@1",
|
|
94
|
+
"inputs": [ { "field": "startDate", "source": { "from": "field", "field": "startDate" } },
|
|
95
|
+
{ "field": "admissionFeeMode", "source": { "from": "fixed", "value": "standard" } } ] },
|
|
96
|
+
{ "partKey": "care", "generator": "after-school-care@1", "when": "ganztag == true",
|
|
97
|
+
"inputs": [ { "field": "pickupTime", "source": { "from": "expression", "expression": "if (lateCare == true) '18:00' else '16:00'" } } ] }
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
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`:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
plexus-cli --env local --tenant test generator combined save /tmp/combined.json
|
|
106
|
+
plexus-cli --env local --tenant test generator combined plan school-with-lunch@1 --input /tmp/mask-input.json
|
|
107
|
+
plexus-cli --env local --tenant test generator seal school-with-lunch@1
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
To revise a sealed combined revision, clone it, export the draft, edit, save, seal:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
plexus-cli --env local --tenant test generator clone school-with-care@1
|
|
114
|
+
plexus-cli --env local --tenant test generator combined export school-with-care@2 --out /tmp/combined.json
|
|
115
|
+
plexus-cli --env local --tenant test generator combined save /tmp/combined.json
|
|
116
|
+
plexus-cli --env local --tenant test generator seal school-with-care@2
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`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. The PDFs per part come from the workspace.
|
|
120
|
+
|
|
121
|
+
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 apply --publish`. One „Erzeugen“ makes one contract per running part (a generation group), issued and signed together in one Vorgang.
|
|
122
|
+
|
|
123
|
+
- **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`).
|
|
124
|
+
- **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.
|
|
125
|
+
- **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]`).
|
|
126
|
+
- **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`).
|
|
127
|
+
- `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.
|
|
128
|
+
|
|
129
|
+
## Verify
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
plexus-cli --env local --tenant test generator list
|
|
133
|
+
plexus-cli --env local --tenant test generator show school-contract@3 --json
|
|
134
|
+
plexus-cli --env local --tenant test generator usages school-contract@3
|
|
135
|
+
plexus-cli --env local --tenant test generator runs school-contract@3
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Also inspect the marked PDF and positions in the contract-template workspace. Scenario evaluation does not require the PDF converter; PDF preview does.
|
|
139
|
+
|
|
140
|
+
## Idempotency / gotchas
|
|
141
|
+
|
|
142
|
+
- 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.
|
|
143
|
+
- 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.
|
|
144
|
+
- Saving source/configuration or removing a scenario invalidates approval. A changed DOCX requires renewed review. Publishing rechecks every expected result against current resolved inputs.
|
|
145
|
+
- 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.
|
|
146
|
+
- Article reverse links come from the finite manifest, so conditional possibilities remain visible even when an example omits them.
|
|
147
|
+
- Signature captions must exist in the PDF. Publication checks declarations; rendering verifies their locations.
|
|
148
|
+
- 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.
|
|
149
|
+
- 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.
|
|
150
|
+
- Production writes need an explicit environment and `--allow-prod`; this playbook does not authorize them.
|
|
151
|
+
- 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.
|
|
152
|
+
- 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.
|
|
153
|
+
|
|
154
|
+
## Related
|
|
155
|
+
|
|
156
|
+
- `plexus-cli generator <verb> --help` for every verb above
|
|
157
|
+
- [Set up a billing catalog and Word templates](../config/set-up-billing-catalog.md)
|
|
158
|
+
- [Edit a live tenant offer](../offer/edit-live-tenant-offer.md)
|
|
159
|
+
- [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.
|