@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,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.
|