@smartkomm/plexus-cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +826 -2
  2. package/dist/cli.mjs +483 -0
  3. package/dist/skill/.managed-by-plexus-cli +1 -0
  4. package/dist/skill/SKILL.md +185 -0
  5. package/dist/skill/references/accounts/email-changes.md +46 -0
  6. package/dist/skill/references/applications/inspect-process-store.md +61 -0
  7. package/dist/skill/references/applications/start-a-test-application.md +66 -0
  8. package/dist/skill/references/audit/answer-access-request.md +115 -0
  9. package/dist/skill/references/config/attach-a-template.md +117 -0
  10. package/dist/skill/references/config/init-tenant.md +121 -0
  11. package/dist/skill/references/config/onboard-tenant.md +71 -0
  12. package/dist/skill/references/config/rename-program-stage.md +99 -0
  13. package/dist/skill/references/config/set-tenant-branding.md +150 -0
  14. package/dist/skill/references/config/set-tenant-terms.md +50 -0
  15. package/dist/skill/references/config/set-up-billing-catalog.md +73 -0
  16. package/dist/skill/references/config/update-tenant-config.md +170 -0
  17. package/dist/skill/references/config/wipe-tenant-config.md +107 -0
  18. package/dist/skill/references/export/create-named-export.md +47 -0
  19. package/dist/skill/references/feedback/create-and-triage-feedback.md +69 -0
  20. package/dist/skill/references/feedback/manage-feedback-tags.md +57 -0
  21. package/dist/skill/references/finance/change-contract-grant.md +36 -0
  22. package/dist/skill/references/finance/record-submission-date.md +33 -0
  23. package/dist/skill/references/interessenten/list-interessenten.md +54 -0
  24. package/dist/skill/references/notifications/debug-mail-delivery.md +77 -0
  25. package/dist/skill/references/notifications/mail-level.md +90 -0
  26. package/dist/skill/references/notifications/reword-a-letter.md +155 -0
  27. package/dist/skill/references/notifications/trace-a-login.md +50 -0
  28. package/dist/skill/references/offer/add-workflow-node.md +268 -0
  29. package/dist/skill/references/offer/addresses.md +129 -0
  30. package/dist/skill/references/offer/appointment-task.md +79 -0
  31. package/dist/skill/references/offer/assignment-rule.md +195 -0
  32. package/dist/skill/references/offer/attach-form-to-workflow.md +79 -0
  33. package/dist/skill/references/offer/author-form-scenarios.md +97 -0
  34. package/dist/skill/references/offer/author-manifest-meta.md +94 -0
  35. package/dist/skill/references/offer/author-smartform.md +102 -0
  36. package/dist/skill/references/offer/bind-node-input.md +76 -0
  37. package/dist/skill/references/offer/completeness-check.md +137 -0
  38. package/dist/skill/references/offer/countersign-node.md +91 -0
  39. package/dist/skill/references/offer/create-pedagogy-unit.md +55 -0
  40. package/dist/skill/references/offer/declare-node-reads.md +99 -0
  41. package/dist/skill/references/offer/edit-live-tenant-offer.md +144 -0
  42. package/dist/skill/references/offer/edit-smartform.md +121 -0
  43. package/dist/skill/references/offer/edit-workflow-node.md +126 -0
  44. package/dist/skill/references/offer/engage-push-node.md +147 -0
  45. package/dist/skill/references/offer/insert-and-rewire-node.md +67 -0
  46. package/dist/skill/references/offer/node-tasks.md +162 -0
  47. package/dist/skill/references/offer/optional-assessment.md +65 -0
  48. package/dist/skill/references/offer/publish-offer-bundle.md +117 -0
  49. package/dist/skill/references/offer/set-offer-capacities.md +78 -0
  50. package/dist/skill/references/offer/staff-input-if-node.md +112 -0
  51. package/dist/skill/references/offer/task-appointment-result.md +69 -0
  52. package/dist/skill/references/offer/task-family-request.md +69 -0
  53. package/dist/skill/references/offer/task-result-files.md +52 -0
  54. package/dist/skill/references/offer/template-document-node.md +113 -0
  55. package/dist/skill/references/offer/workflow-default-channels.md +69 -0
  56. package/dist/skill/references/resolver/manage-contract-resolver.md +159 -0
  57. package/dist/skill/references/retention/erase-due-applications.md +99 -0
  58. package/dist/skill/references/retention/erase-due-parties.md +107 -0
  59. package/dist/skill/references/roles/migrate-parent-assignments.md +31 -0
  60. package/dist/skill/references/signing/README.md +113 -0
  61. package/dist/skill/references/signing/connect-a-school.md +90 -0
  62. package/dist/skill/references/signing/rotate-the-key.md +41 -0
  63. package/dist/skill/references/stammdaten/purge-deleted-person.md +68 -0
  64. package/dist/skill/references/stripe/connect-a-school.md +107 -0
  65. package/dist/skill/references/stripe/request-and-verify-a-payment.md +78 -0
  66. package/dist/skill/references/test-actions/drive-a-test-application.md +59 -0
  67. package/dist/skill/references/users/add-platform-admin.md +59 -0
  68. package/dist/skill/references/users/add-platform-user.md +59 -0
  69. package/dist/skill/references/users/add-tenant-admin.md +75 -0
  70. package/dist/skill/references/users/change-login-email.md +55 -0
  71. package/dist/skill/references/users/cli-access-for-tenant-admin.md +117 -0
  72. package/dist/skill/references/users/delete-permission-group.md +76 -0
  73. package/dist/skill/references/users/grant-cross-tenant-access.md +57 -0
  74. package/dist/skill/references/users/grant-feedback-assignable.md +43 -0
  75. package/dist/skill/references/users/set-password.md +57 -0
  76. package/dist/skill/references/users/use-test-account.md +79 -0
  77. package/dist/skill/references/worker-ops/debug-worker-jobs.md +90 -0
  78. package/dist/skill/references/worker-ops/verify-migrations.md +51 -0
  79. package/dist/spec.snapshot.json +145815 -0
  80. package/package.json +53 -4
@@ -0,0 +1,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.
@@ -0,0 +1,90 @@
1
+ # Connect a school to its signing provider
2
+
3
+ Give one tenant its own account at DocuSign or inSign, so its contracts are
4
+ signed through **the school's** account instead of a deployment-wide one, and
5
+ make that account the one its contracts go out through. The day-to-day signing
6
+ commands: [README.md](./README.md).
7
+
8
+ ## When to use it
9
+
10
+ - A school is going to send contracts for electronic signature for the first time.
11
+ - A school switches provider, rotates its password / HMAC key, or its
12
+ connection stopped working and you need to see **why**.
13
+
14
+ ## Prerequisites
15
+
16
+ - **`_platform.config.manage`** for `set`, `activate` and `remove`;
17
+ `finanzen_vertraege.contracts.manage` for `show` and `test`. Setting up a
18
+ connection opens a commercial relationship with a provider in the school's
19
+ name — a platform act, like Stripe Connect.
20
+ - The installation holds `SIGNING_CREDENTIALS_KEY` (otherwise every connection
21
+ reads `platform_unconfigured` and no secret can be stored). For DocuSign also
22
+ the platform app: `DOCUSIGN_OAUTH_HOST`, `DOCUSIGN_INTEGRATION_KEY`,
23
+ `DOCUSIGN_PRIVATE_KEY`. Setting these needs a platform operator; the CLI cannot.
24
+ - The school's account data: DocuSign — the sender's API user id (GUID) and,
25
+ for callbacks, the Connect HMAC key; inSign — base URL, API user, password,
26
+ owner id (`foruser`).
27
+
28
+ ## Steps
29
+
30
+ ```bash
31
+ # 1. Where does this school stand?
32
+ plexus-cli signing connection show --env staging --tenant <slug>
33
+
34
+ # 2a. inSign. The password comes from stdin — NEVER as an argument.
35
+ printf %s "$INSIGN_PASSWORD" | plexus-cli signing connection set --provider insign \
36
+ --base-url https://insign.<anbieter>.de --username <api-user> --owner-id <owner> \
37
+ --secret-stdin --env staging --tenant <slug>
38
+
39
+ # 2b. DocuSign. The HMAC key from a file (or stdin); without one, no callback is
40
+ # registered and the 15-minute poll is the only return path.
41
+ plexus-cli signing connection set --provider docusign --user-id <GUID> \
42
+ --redirect-to admin@plexus.swop.schule --secret-file ./hmac.txt \
43
+ --env staging --tenant <slug>
44
+
45
+ # 3. Reach the account. DocuSign: on consent_required open the printed
46
+ # consentUrl ONCE as the school's sender, then test again.
47
+ plexus-cli signing connection test --provider insign --env staging --tenant <slug>
48
+
49
+ # 4. Make it the provider the school's contracts go out through.
50
+ plexus-cli signing connection activate --provider insign --env staging --tenant <slug>
51
+
52
+ # 5. Prove it.
53
+ plexus-cli signing connection show --env staging --tenant <slug>
54
+ # E-Signatur: inSign · state=ready active=yes
55
+ ```
56
+
57
+ Change a field later with the same `set`; give only what changes plus the
58
+ required fields. A secret not given stays as it is. Remove one with
59
+ `--clear-secret hmacKey` (DocuSign) or `--clear-secret password` (inSign).
60
+
61
+ ## Verify
62
+
63
+ `state=ready` (or `failing` after an earlier success) and `active=yes` is what
64
+ `signing start` needs. Then send a test revision —
65
+ [README.md](./README.md) → "Send a revision for signature".
66
+
67
+ ## Gotchas
68
+
69
+ - **Secrets go in through stdin or a file only.** An argument would land in the
70
+ shell history and in `ps`. `show` prints the secret's name, a fingerprint and
71
+ when it was set; nothing ever prints the value.
72
+ - **A `production` account is refused outside prod.** inSign: any base URL but
73
+ the vendor's sandbox host counts as production; DocuSign: the deployment's
74
+ OAuth host decides.
75
+ - **Demo needs a redirect, production forbids it.** A sandbox account must carry
76
+ `--redirect-to` (nobody may address a real family from a sandbox whose
77
+ credentials are public); a production account must not (every contract would
78
+ land in one mailbox). `set` refuses either mistake.
79
+ - **Switching keeps open Vorgänge where they are.** `activate` on another
80
+ provider deactivates the old connection in the same step, but every Vorgang
81
+ already started keeps talking to the account it started on — poll, reconcile,
82
+ void and its callbacks included. `remove` is refused while one is open.
83
+ - **The account is part of an open Vorgang.** While one is open, changing
84
+ DocuSign's `--user-id` or inSign's `--base-url` / `--owner-id` is refused;
85
+ rotating the password or HMAC key is allowed.
86
+ - **`state=unverified` after every change.** Any `set` invalidates the last
87
+ test; run `test` again before the next contract goes out.
88
+ - **`state=platform_unconfigured`** is about the installation, not the school:
89
+ `SIGNING_CREDENTIALS_KEY` (or DocuSign's platform app) is missing there.
90
+ - **`remove` is a dry run** unless `--confirm`; the secrets go with the row.
@@ -0,0 +1,41 @@
1
+ # Rotate the signing credentials key
2
+
3
+ ## When to use it
4
+ `SIGNING_CREDENTIALS_KEY` (the platform key every school's signing secrets are
5
+ sealed under) must change — routine rotation or a suspected leak.
6
+
7
+ ## Prerequisites
8
+ - Platform right `_platform.config.manage` on the target environment.
9
+ - A platform operator who can change the deployment's environment
10
+ (`SIGNING_CREDENTIALS_KEY` and its previous slot) and deploy — the CLI cannot.
11
+
12
+ ## Steps
13
+ 1. Needs a platform operator: move the current key into the previous slot, set a
14
+ new current key (`openssl rand -base64 32`), deploy. Nothing breaks in between:
15
+ envelopes under the old key open through the previous slot.
16
+ 2. Re-seal every connection under the new key:
17
+ ```bash
18
+ plexus-cli signing connection reseal --env <env>
19
+ ```
20
+ 3. Check the slots:
21
+ ```bash
22
+ plexus-cli signing status --env <env>
23
+ ```
24
+
25
+ ## Verify
26
+ `signing status` shows `sealedPrevious: 0`, `sealedUnknown: 0` and
27
+ `previousSlot: nothing sealed under it — can be removed`. Then the platform
28
+ operator clears the previous slot from the deployment's environment and deploys.
29
+
30
+ ## Idempotency / gotchas
31
+ - `reseal` is idempotent; a second run reports `resealed: 0`. It exits 1 while
32
+ anything is left under the previous or an unknown key.
33
+ - Never clear the previous slot before `reseal`: every envelope still under it
34
+ becomes `incomplete` („mit Schlüssel … versiegelt, den … nicht trägt") and
35
+ DocuSign callbacks of those schools fail until the old key is back.
36
+ - Fingerprints change with the key (they are keyed by it); the secrets do not.
37
+ - A save during the run wins: its envelope is left alone (`changedMeanwhile`).
38
+
39
+ ## Related
40
+ - [Signing commands](./README.md)
41
+ - [Connect a school to its signing provider](./connect-a-school.md)
@@ -0,0 +1,68 @@
1
+ # Hard-delete a person staff deleted
2
+
3
+ **When to use:** a school (or a data-subject erasure request) needs a person
4
+ gone **for good** — the row, everything that belongs to it, its value history
5
+ and the stored files only it holds. In the app staff can only **soft**-delete
6
+ („Person löschen“ moves the person to „Gelöschte“, restorable); the hard delete
7
+ exists only here.
8
+
9
+ ## Prerequisites
10
+ - **Env + tenant:** `--env <local|staging|prod> --tenant <slug>`. Prod also needs
11
+ `--allow-prod` for the writing run.
12
+ - **Auth:** your own platform token with `_platform.config.manage` (the right that
13
+ commits a tenant wipe). Check with `plexus-cli --env <env> whoami`.
14
+ - **The person is soft-deleted first.** Staff deletes it in the app (Personen →
15
+ person → trash icon), or it already sits under „Gelöschte“. A person that is
16
+ not deleted is refused — the two steps are deliberate.
17
+ - **The party id.** List the deleted persons of the tenant:
18
+ ```bash
19
+ plexus-cli --env <env> --tenant <slug> stammdaten persons --deleted
20
+ ```
21
+
22
+ ## Steps
23
+ 1. **Plan** (no write — the default):
24
+ ```bash
25
+ plexus-cli --env <env> --tenant <slug> stammdaten persons purge <partyId>
26
+ ```
27
+ Prints the person, when staff deleted it, the rows per table (the party, its
28
+ persons row, roles, values and their links, placements, enrollments, search
29
+ entries, `value_history`, …) and every receipt that refuses the commit.
30
+ 2. **Commit** once the plan shows no `Refused` block:
31
+ ```bash
32
+ plexus-cli --env <env> --tenant <slug> stammdaten persons purge <partyId> --confirm [--allow-prod]
33
+ ```
34
+
35
+ ## Verify
36
+ - `plexus-cli --env <env> --tenant <slug> stammdaten persons --deleted` no longer
37
+ lists the person.
38
+ - Re-running the plan fails with „Der Datensatz wurde nicht gefunden.“
39
+
40
+ ## Idempotency / gotchas
41
+ - **Not reversible.** The soft delete is the reversible step; this one is not.
42
+ - **Refusals (both plan and commit):** a person an application in flight names as
43
+ Antragsteller:in or binds („gehört zu einer laufenden Bewerbung“) — finish or
44
+ discard the application first; a person linked to a user account — remove the
45
+ account's access first.
46
+ - **Receipts block the commit:** a contract, invoice, signature or other
47
+ `restrict` holder is listed as `table:id ← table:id (column)`. They are records
48
+ the school must keep; the person then stays soft-deleted.
49
+ - **One transaction.** A person with more rows than the deletion engine's plan
50
+ limit fails loud instead of half-deleting.
51
+ - **Files:** the plan lists every file the person's values and value history name,
52
+ and why it stays (`kept … held by`). A file goes only when the upload ledger
53
+ proves it a party file of this school and nothing else holds it. It stays as
54
+ `unproven` (no ledger row names this school — a file staff uploaded into a
55
+ person before upload stamping existed carries no school stamp, nor does one saved
56
+ more than two hours after its upload; such a file is never re-stamped without
57
+ proof), `upload:<surface>` (staged for a template, library,
58
+ brand file, import, chat …), `ledger` (staged for another school), `shared` (claimed again elsewhere after it
59
+ came in — a logo, a form, a feedback attachment), `form` /
60
+ `artifact` / `snapshot` / `signature` / `return` (a receipt that holds the file) or
61
+ `party` (another person's value names it, any school). Note `form`: while the
62
+ school has ANY application left, a form might hold the file (the fieldMap copy),
63
+ so it stays — expected, not a bug.
64
+ - `--json` prints the raw report (`rows`, `blockers`, `applied`).
65
+
66
+ ## Related
67
+ - Deleting every person past a school's deletion rule at once:
68
+ [Delete persons past their school's deletion rule](../retention/erase-due-parties.md).
@@ -0,0 +1,107 @@
1
+ # Connect a school to its own Stripe account
2
+
3
+ Give one tenant a Stripe account of its own, so its families' money lands with
4
+ **the school** rather than in a shared account.
5
+
6
+ ## When to use it
7
+
8
+ - A school is going to collect money through Plexus for the first time.
9
+ - A school's connection stopped working and you need to see **why** (there are
10
+ eight distinct reasons, and they need different people to act).
11
+
12
+ ## Prerequisites
13
+
14
+ - **The school sets Stripe up itself.** A member holding
15
+ `finanzen_vertraege.contracts.manage` **and** `finanzen_vertraege.payments.connect`
16
+ presses „Stripe einrichten" on Administration → Integrationen → Stripe, in
17
+ their own browser. Whoever finishes Stripe's form becomes the owner of the
18
+ school's Stripe login, so it should be the school's legal representative
19
+ (Geschäftsführung), and that person needs a Plexus account. **The CLI cannot
20
+ start it, and neither can a platform admin** (refused even with a
21
+ membership). Stripe forbids handing onboarding links on, which is why
22
+ `stripe connect` / `stripe onboarding-link` are gone.
23
+ - The deployment holds the **platform's** Stripe key with Connect enabled (set by
24
+ a platform operator; the CLI cannot).
25
+ Accounts are created through **Accounts v2** (`/v2/core/accounts`); a
26
+ *claimable sandbox* key (`rkcs_…`) cannot create accounts at all.
27
+ - A **real mailbox** for the school, entered in the dialog. Stripe writes to it
28
+ itself (onboarding, receipts, dispute notices), and that post bypasses
29
+ Plexus entirely, so `MAIL_DELIVERY_MODE` cannot intercept or redirect it.
30
+ - A school that **already uses Stripe** signs into the form with its existing
31
+ Stripe login and reuses its company details (networked onboarding). It still
32
+ gets a new connected account.
33
+
34
+ ## Steps
35
+
36
+ ```bash
37
+ # 1. Where does this school stand?
38
+ plexus-cli stripe status --env staging --tenant <slug>
39
+ # state=unconnected → nothing exists yet.
40
+
41
+ # 2. The school's representative signs in to Plexus and presses
42
+ # „Stripe einrichten" (Integrationen → Stripe): e-mail for Stripe's post and
43
+ # country, then „Weiter zu Stripe". Plexus creates the account and the
44
+ # browser goes straight to Stripe's form. Back in Plexus, the page asks
45
+ # Stripe at once. If the form is left half done, „Angaben bei Stripe
46
+ # vervollständigen" on the same page continues it.
47
+ # Testing URL for the representative (member persona, never the platform
48
+ # admin deep login, which is refused here):
49
+ # https://<host>/login?as=<member e-mail>&redirect=/<slug>/admin/integrations/stripe
50
+
51
+ # 3. Watch it land. The v2.core.account* events refresh the mirror by themselves:
52
+ plexus-cli stripe status --env staging --tenant <slug>
53
+ # connectedBy=<name> <when> → who started it.
54
+ # card=restricted: requirements_past_due + due=[…] → Stripe still waits.
55
+ # card=pending → Stripe is checking.
56
+ # card=active → Stripe has cleared it.
57
+ # sepa=… (!) → card works, SEPA-Lastschrift does not: the school enables
58
+ # it in its own Stripe dashboard (payment methods).
59
+
60
+ # 4. The school's own consent. Nothing collects without it, however happy
61
+ # Stripe is (page: „Einzug freigeben", or with the school's say-so):
62
+ plexus-cli stripe allow --env staging --tenant <slug>
63
+
64
+ # 5. Prove it:
65
+ plexus-cli stripe status --env staging --tenant <slug>
66
+ # state=ready
67
+ ```
68
+
69
+ ## Verify
70
+
71
+ `state=ready` is the only state a payment can start from. Then request one
72
+ through the real primitive — see
73
+ [Request a Stripe payment and follow it](./request-and-verify-a-payment.md).
74
+
75
+ ## Gotchas
76
+
77
+ - **`asked` empty means "never asked".** A seeded connection carries no Stripe
78
+ verdict on purpose; the first account event or `stripe refresh` fills it.
79
+ - **`error` set means the last read failed** (`stripe refresh` or an event's
80
+ re-read). It is shown, never interpreted — even "does not have access" does
81
+ not flip the state to revoked by itself.
82
+ - **A stale `no` is the expensive direction.** The mirror is refreshed by
83
+ webhook, and a missed delivery leaves a school unable to collect although it
84
+ may. `plexus-cli stripe refresh` is the way out — there is no equivalent
85
+ problem in the other direction, because Stripe refuses a charge on an account
86
+ it has since restricted.
87
+ - **`state=revoked` cannot be fixed here.** The school removed Plexus's access
88
+ in their own Stripe dashboard. Connecting again is not built;
89
+ `stripe allow` refuses rather than pretending.
90
+ - **One school, one account, forever.** Once a school has an account, „Stripe
91
+ einrichten" only continues Stripe's form for it and never creates a second
92
+ one, because replacing the reference would orphan every open payment pointing
93
+ at the first account. A double click is harmless: one idempotency key per
94
+ tenant gives back the same account.
95
+ - **The link died** (reopened, older than a few minutes, opened by a chat
96
+ preview): Stripe sends the person back with `?onboarding=expired`. The page
97
+ asks before sending them in again. It never redirects on its own.
98
+ - **`state=platform_unconfigured`** is about the installation, not the school —
99
+ clearing `stripe_mode` on a deployment removes every `STRIPE_*` variable while
100
+ the schools' rows stay behind.
101
+ - **`state=closed`** — Stripe closed the account. Onboarding cannot reopen it;
102
+ only a new account helps, which means replacing the connection (not built).
103
+ - **Two destinations, two secrets** (set by a platform operator): without
104
+ `STRIPE_WEBHOOK_SECRET` no account event (thin) verifies, without
105
+ `STRIPE_CONNECT_WEBHOOK_SECRET` no school payment (snapshot) — `/stripe/webhook`
106
+ answers **503** naming the variable (not 400 — "cannot check" is not "did not
107
+ check out"); payments then settle only through the 5-minute poll.
@@ -0,0 +1,78 @@
1
+ # Eine Stripe-Zahlung anfordern und nachverfolgen
2
+
3
+ Wofür: eine Einmalzahlung über Stripe auslösen und ihren Weg bis `succeeded`
4
+ verfolgen — lokal, auf Staging oder in Produktion. Voraussetzung ist eine
5
+ verbundene Schule: [Schule mit Stripe verbinden](./connect-a-school.md).
6
+
7
+ Für die Hand statt fürs Skript gibt es dasselbe unter **Administration →
8
+ Integrationen** (`/$tenant/admin/integrations`) — gleiche Funktionen, gleiche
9
+ Rechte, nur mit Knöpfen. Dieses Playbook ist der skriptbare Weg.
10
+
11
+ Für Zahlungen hat `plexus-cli stripe` (noch) keine eigenen Befehle — die Gruppe
12
+ verbindet nur die Schule; der Zahlungsadapter wird über `plexus-cli call`
13
+ bedient (`plexus-cli functions --grep stripe` listet die Funktionen).
14
+
15
+ ## 0. Verbindung prüfen
16
+
17
+ ```bash
18
+ plexus-cli --env local --json call stripe:status client=<tenant>
19
+ ```
20
+
21
+ `configured: false` nennt in `missing`/`invalid` genau die Variablen, an denen es
22
+ liegt. `mode` sagt, ob echtes Geld im Spiel ist.
23
+
24
+ ## 1. Zahlung anfordern
25
+
26
+ ```bash
27
+ plexus-cli --env local --json call stripe:create --kind mutation \
28
+ client=<tenant> \
29
+ amountMinor=18000 \
30
+ currency=eur \
31
+ description="Anmeldegebuehr Schuljahr 2026/27" \
32
+ customerEmail=eltern@beispiel.example \
33
+ requestKey=admissionFee:<applicationId> \
34
+ returnPath=/<tenant>/e/bewerbung/<applicationId>
35
+ ```
36
+
37
+ - **`amountMinor` sind CENT**, nicht Euro. 180 € sind `18000`.
38
+ - **`requestKey` leitet sich aus der Sache ab**, für die gezahlt wird — nie aus
39
+ einem Zeitstempel. Derselbe Schlüssel gibt dieselbe Zeile zurück
40
+ (`reused: true`), solange ein Versuch läuft oder die Forderung bezahlt ist.
41
+ - **`returnPath` ist ein app-relativer Pfad.** Eine absolute URL wird abgewiesen.
42
+
43
+ ## 2. Zustand lesen
44
+
45
+ ```bash
46
+ plexus-cli --env local --json call stripe:get client=<tenant> stripePaymentId=<id>
47
+ ```
48
+
49
+ `checkoutUrl` ist die Zahlungsseite (nur solange `status: open`). `events` zeigt
50
+ die von Stripe zugestellten, signaturgeprüften Rückrufe.
51
+
52
+ ## 3. Wenn nichts passiert
53
+
54
+ Zuerst: **das ist normal, bis jemand zahlt.** Danach der Reihe nach:
55
+
56
+ ```bash
57
+ # von Hand abgleichen (der Cron tut es alle 5 Minuten von selbst)
58
+ plexus-cli --env local --json call stripe:reconcileNow --kind mutation \
59
+ client=<tenant> stripePaymentId=<id>
60
+ ```
61
+
62
+ - `status: requires_session` länger als ein paar Sekunden ⇒ die Aktion ist
63
+ gescheitert. `error` auf der Zeile nennt Stripes Grund; nach zehn Minuten
64
+ markiert der Reaper sie als `failed`.
65
+ - `status: processing` ⇒ SEPA. Stripe selbst rät zu **mindestens sechs
66
+ Werktagen** Geduld, bevor man eine Lastschrift als erfolgreich betrachtet.
67
+ - Keine `events`, obwohl gezahlt wurde ⇒ der Webhook kommt nicht an (das richtet
68
+ der Plattformbetreiber ein); der Abgleich-Cron holt es trotzdem ein.
69
+
70
+ ## 4. Zurückziehen
71
+
72
+ ```bash
73
+ plexus-cli --env local --json call stripe:cancel --kind mutation \
74
+ client=<tenant> stripePaymentId=<id> reason="Platz anderweitig vergeben"
75
+ ```
76
+
77
+ Schließt erst die Seite bei Stripe, dann die Zeile. Eine bereits eingegangene
78
+ Zahlung wird dabei **nie** zurückgezogen — der Aufruf gleicht sie stattdessen ab.