docs: drop code references to local plan files

This commit is contained in:
fromme committed 2026-10-16 09:14:44 +02:00
1 parent 2a554bb7bf
commit 96d49fc488
15 files changed
+33 -40

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
# Enabled for #[MapRequestPayload] on the api controllers - nothing else in this application # Enabled for #[MapRequestPayload] on the api controllers - nothing else in this application
# serializes. ALLOW_EXTRA_ATTRIBUTES stays at its default (true) on purpose: the TYPO3 form # serializes. ALLOW_EXTRA_ATTRIBUTES stays at its default (true) on purpose: the TYPO3 form
# payload grows keys without a release here, and rejecting an unknown one would turn every # payload grows keys without a release here, and rejecting an unknown one would turn every
# later mapping change into a 400 (see .ddev/plans/application-form-endpoint.md). # later mapping change into a 400.
framework: framework:
serializer: serializer:
name_converter: ~ name_converter: ~
+1 -2
View File
@@ -9,8 +9,7 @@ use Doctrine\Migrations\AbstractMigration;
/** /**
* What it takes to provision a teamer from a received application: the applicant's gender, and the * What it takes to provision a teamer from a received application: the applicant's gender, and the
* outcome of each of the three provisioning writes * outcome of each of the three provisioning writes.
* (.ddev/plans/application-form-teamer-provisioning.md).
* *
* Emptying the table first is deliberate. gender is NOT NULL because BusPro refuses an address * Emptying the table first is deliberate. gender is NOT NULL because BusPro refuses an address
* without one and Teamer::$gender is not nullable either, and the existing rows predate the * without one and Teamer::$gender is not nullable either, and the existing rows predate the
+3 -2
View File
@@ -146,8 +146,9 @@ class ApiClient
* "login" switches the password check off, which only works when the request identifies the * "login" switches the password check off, which only works when the request identifies the
* person by id: both ids are required, and the person id is "idadresseperson" on the way in * person by id: both ids are required, and the person id is "idadresseperson" on the way in
* although the response calls it "idperson". The element order and the single-selection * although the response calls it "idperson". The element order and the single-selection
* payload are what BusPro was verified to accept - the art is undocumented, see * payload are what BusPro accepted when verified against production (2026-09-25, selection
* .ddev/plans/buspro-crm-selection-write.md before changing the shape. * 1070 toggled off and back on) - the art is undocumented, so re-verify before changing the
* shape.
* *
* Idempotent: setting a selection that is already set answers änderung=True and changes * Idempotent: setting a selection that is already set answers änderung=True and changes
* nothing, so a caller may write unconditionally and take isUpdated() as the success check. * nothing, so a caller may write unconditionally and take isUpdated() as the success check.
+1 -1
View File
@@ -54,7 +54,7 @@ class ResponseParser
// to be routed to createCrmAttributesResponse(), which walks a // to be routed to createCrmAttributesResponse(), which walks a
// "selektionsmerkmale" node that is absent here and would have returned an // "selektionsmerkmale" node that is absent here and would have returned an
// empty response with every role flag false - dead code until something // empty response with every role flag false - dead code until something
// sent the art (.ddev/plans/buspro-crm-selection-write.md). // sent the art.
return $this->createProfileUpdateResponse($xml); return $this->createProfileUpdateResponse($xml);
case 'Adresse_Neu': case 'Adresse_Neu':
return $this->createRegistrationResponse($xml); return $this->createRegistrationResponse($xml);
@@ -21,8 +21,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
* *
* The visitor waits for this response inside their own form submission and TYPO3 gives up after * The visitor waits for this response inside their own form submission and TYPO3 gives up after
* a few seconds without retrying, so this stores the row and answers - notification mail and any * a few seconds without retrying, so this stores the row and answers - notification mail and any
* matching against known people belong in a worker, not here * matching against known people belong in a worker, not here.
* (.ddev/plans/MYEP-APPLICATION-ENDPOINT.md §6).
*/ */
class ApplicationFormController extends AbstractController class ApplicationFormController extends AbstractController
{ {
@@ -58,7 +57,7 @@ class ApplicationFormController extends AbstractController
'formLabel' => $submission->getFormLabel(), 'formLabel' => $submission->getFormLabel(),
'pageUid' => $submission->getPageUid(), 'pageUid' => $submission->getPageUid(),
'submittedAt' => $submission->getSubmittedAt()->format(\DateTimeInterface::ATOM), 'submittedAt' => $submission->getSubmittedAt()->format(\DateTimeInterface::ATOM),
// the labels and the answers are personal data (§7), their number is not // the labels and the answers are personal data, their number is not
'detailCount' => count($submission->getDetails()), 'detailCount' => count($submission->getDetails()),
'receiptSent' => $receiptSent, 'receiptSent' => $receiptSent,
]); ]);
@@ -61,7 +61,7 @@ class ApplicationFormSubmissionMailer
if (true !== $submission->isEu()) { if (true !== $submission->isEu()) {
$this->logger->info('Application form receipt mail skipped, not eligible', [ $this->logger->info('Application form receipt mail skipped, not eligible', [
'uuid' => $submission->getUuid(), 'uuid' => $submission->getUuid(),
// The value, not the applicant's nationality: a flag the form set (§7). // The value, not the applicant's nationality: a flag the form set.
'euFieldPresent' => null !== $submission->isEu(), 'euFieldPresent' => null !== $submission->isEu(),
]); ]);
+5 -6
View File
@@ -32,8 +32,7 @@ use Symfony\Component\Uid\Uuid;
* *
* The typed columns are the mapped core agreed with the TYPO3 side. "details" is everything * The typed columns are the mapped core agreed with the TYPO3 side. "details" is everything
* else the visitor filled in, keyed by the German field label in the order the form asked - * else the visitor filled in, keyed by the German field label in the order the form asked -
* display only, never a schema, never queried on (see * display only, never a schema, never queried on.
* .ddev/plans/MYEP-APPLICATION-ENDPOINT.md §3.3).
*/ */
#[ORM\Entity(repositoryClass: ApplicationFormSubmissionRepository::class)] #[ORM\Entity(repositoryClass: ApplicationFormSubmissionRepository::class)]
// What a list and a dedupe check ask for: the submissions of one form, newest first. // What a list and a dedupe check ask for: the submissions of one form, newest first.
@@ -129,7 +128,7 @@ class ApplicationFormSubmission implements TimestampableEntityInterface
* of the payload - fromDto() must not touch them. * of the payload - fromDto() must not touch them.
* *
* The mails carry a time because a send time is a machine fact and stays one once the * The mails carry a time because a send time is a machine fact and stays one once the
* notification mail of §6 is automated. A held call does not: the reviewer records it * notification mail is automated. A held call does not: the reviewer records it
* afterwards and knows the day, so storing an invented time would only look precise. * afterwards and knows the day, so storing an invented time would only look precise.
*/ */
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
@@ -175,7 +174,7 @@ class ApplicationFormSubmission implements TimestampableEntityInterface
/** /**
* Everything the visitor filled in beyond the mapped core, keyed by the German field label. * Everything the visitor filled in beyond the mapped core, keyed by the German field label.
* Display only: the labels are staff-written and change on a reword, so nothing may query * Display only: the labels are staff-written and change on a reword, so nothing may query
* this, build a column from it or migrate old rows to a new spelling (§3.3). * this, build a column from it or migrate old rows to a new spelling.
* *
* The key order is whatever the engine hands back and is not part of what this carries: on * The key order is whatever the engine hands back and is not part of what this carries: on
* MariaDB (dev and production) JSON is an alias for LONGTEXT and the document survives * MariaDB (dev and production) JSON is an alias for LONGTEXT and the document survives
@@ -189,7 +188,7 @@ class ApplicationFormSubmission implements TimestampableEntityInterface
/** /**
* The request body as received. Mapped keys this application does not know yet are ignored * The request body as received. Mapped keys this application does not know yet are ignored
* on the way in (§3.2) and the TYPO3 side widens a form's mapping without a release here, * on the way in and the TYPO3 side widens a form's mapping without a release here,
* so without this copy those answers would be lost for good instead of backfillable. * so without this copy those answers would be lost for good instead of backfillable.
* *
* @var array<string, mixed> * @var array<string, mixed>
@@ -214,7 +213,7 @@ class ApplicationFormSubmission implements TimestampableEntityInterface
* *
* The raw payload is passed in separately because the DTO no longer has it: the resolver * The raw payload is passed in separately because the DTO no longer has it: the resolver
* drops every key the DTO has no property for, and those are the mapped fields the TYPO3 * drops every key the DTO has no property for, and those are the mapped fields the TYPO3
* side has added and this application does not know yet (§3.2). Keeping the body is what * side has added and this application does not know yet. Keeping the body is what
* makes them backfillable instead of lost. * makes them backfillable instead of lost.
* *
* @param array<string, mixed> $payload * @param array<string, mixed> $payload
+4 -5
View File
@@ -8,8 +8,8 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
/** /**
* One submitted TYPO3 form, mapped from the request body by #[MapRequestPayload]. * One submitted TYPO3 form, mapped from the request body by #[MapRequestPayload].
* *
* Two rules from the contract (.ddev/plans/MYEP-APPLICATION-ENDPOINT.md §3.2, §3.3) shape this * Two rules from the contract with the TYPO3 side shape this class and must survive any
* class and must survive any rewrite: * rewrite:
* *
* - **Unknown keys are ignored, never rejected.** The mapped vocabulary grows on the TYPO3 * - **Unknown keys are ignored, never rejected.** The mapped vocabulary grows on the TYPO3
* side without a release here, so nothing may switch the serializer's ALLOW_EXTRA_ATTRIBUTES * side without a release here, so nothing may switch the serializer's ALLOW_EXTRA_ATTRIBUTES
@@ -21,8 +21,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
* Vorname, Geschlecht, EMail, PLZ, Straße) - a payload without those cannot become a teamer at * Vorname, Geschlecht, EMail, PLZ, Straße) - a payload without those cannot become a teamer at
* all, and asking a reviewer to retype them from the answers would be worse than refusing the * all, and asking a reviewer to retype them from the answers would be worse than refusing the
* payload. The endpoint only ever receives application forms, so this holds for every payload and * payload. The endpoint only ever receives application forms, so this holds for every payload and
* there is no form-type discriminator * there is no form-type discriminator.
* (.ddev/plans/application-form-teamer-provisioning.md).
* *
* Everything beyond the nine - phone, mobile, nationality, bank details - stays optional and is * Everything beyond the nine - phone, mobile, nationality, bank details - stays optional and is
* filled in by the teamer themselves afterwards, so an absent key there still means "not part of * filled in by the teamer themselves afterwards, so an absent key there still means "not part of
@@ -334,7 +333,7 @@ class ApplicationFormSubmissionDto
/** /**
* The blob as it is stored: numbers arrive as strings from the form editor but nothing stops * The blob as it is stored: numbers arrive as strings from the form editor but nothing stops
* a mapping from sending one unquoted, and an empty answer is dropped rather than kept - * a mapping from sending one unquoted, and an empty answer is dropped rather than kept -
* TYPO3 strips those before sending (§3.3), so a label present in the blob always has * TYPO3 strips those before sending, so a label present in the blob always has
* content. The received order is the form's order and is never touched. * content. The received order is the form's order and is never touched.
* *
* @return array<string, string|bool|list<string>> * @return array<string, string|bool|list<string>>
+1 -1
View File
@@ -12,7 +12,7 @@ use App\Service\Common\PickupOptions;
/** /**
* Turns a staffing matrix row into the draft assignments for one travel date. * Turns a staffing matrix row into the draft assignments for one travel date.
* *
* The rules, agreed 2026-10-15 (see .ddev/plans/teamer-auto-assignments-plan.md): * The rules, agreed with the stakeholders on 2026-10-15:
* - one assignment per role, the role's head count as its places; * - one assignment per role, the role's head count as its places;
* - without split, each ski level is a Kursleitung of its own, one place; * - without split, each ski level is a Kursleitung of its own, one place;
* - with split, a Reiseleitung takes over each level through the combined profile, one place * - with split, a Reiseleitung takes over each level through the combined profile, one place
+2 -3
View File
@@ -35,9 +35,8 @@ use Psr\Log\LoggerInterface;
* address is never created twice, and the selection write is idempotent by design. * address is never created twice, and the selection write is idempotent by design.
* *
* Nothing here reads the applicant's profile back from BusPro. Adressdaten needs the customer's * Nothing here reads the applicant's profile back from BusPro. Adressdaten needs the customer's
* password, which provisioning deliberately never has (see * password, which provisioning deliberately never has, so the local records are built from the data that
* .ddev/plans/application-form-teamer-provisioning.md), so the local records are built from the * was sent plus the ids that came back.
* data that was sent plus the ids that came back.
* *
* This is also the one place in the application that grants a teamer their CRM marker rather than * This is also the one place in the application that grants a teamer their CRM marker rather than
* reading it. Everything else stays read-only: roles and hotel codes are synced from BusPro on * reading it. Everything else stays read-only: roles and hotel codes are synced from BusPro on
+1 -2
View File
@@ -194,8 +194,7 @@ class ResponseParserTest extends TestCase
/** /**
* The write answers idadresse + idperson + änderung, not a selection tree - the shape * The write answers idadresse + idperson + änderung, not a selection tree - the shape
* createProfileUpdateResponse() reads. It used to be routed to the CRM attribute parser, which * createProfileUpdateResponse() reads. It used to be routed to the CRM attribute parser, which
* would have returned an empty response with every role flag false * would have returned an empty response with every role flag false.
* (.ddev/plans/buspro-crm-selection-write.md).
*/ */
public function testParseCrmSelectionWriteResponse(): void public function testParseCrmSelectionWriteResponse(): void
{ {
@@ -14,8 +14,8 @@ use Symfony\Component\HttpFoundation\Response;
/** /**
* Everything around the provisioning click that can be exercised without writing to BusPro. * Everything around the provisioning click that can be exercised without writing to BusPro.
* *
* The write itself cannot: the teamer CRM selection is only changeable in the production database * The write itself cannot: the teamer CRM selection is only changeable in the production database,
* (.ddev/plans/buspro-crm-selection-write.md), and a POST that gets as far as the api client would * and a POST that gets as far as the api client would
* open a socket to it from a test. TeamerProvisionerTest covers the flow against a mocked client; * open a socket to it from a test. TeamerProvisionerTest covers the flow against a mocked client;
* what is left here is the route, the confirmation and the one POST that is safe to make - a * what is left here is the route, the confirmation and the one POST that is safe to make - a
* submission TeamerProvisioner refuses before it sends anything. * submission TeamerProvisioner refuses before it sends anything.
@@ -13,8 +13,8 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
/** /**
* The cases here are the ones the TYPO3 side actually produces * The cases here are the ones the TYPO3 side actually produces:
* (.ddev/plans/MYEP-APPLICATION-ENDPOINT.md §8): labels with slashes and umlauts, a long textarea * labels with slashes and umlauts, a long textarea
* answer, a duplicate label, and a mapped key this application does not know - which must not be a * answer, a duplicate label, and a mapped key this application does not know - which must not be a
* 400, or a later mapping change on the website silently stops every submission of that form. * 400, or a later mapping change on the website silently stops every submission of that form.
* *
@@ -22,8 +22,7 @@ use Symfony\Component\HttpFoundation\Response;
* birthDate, email, gender, eu, street, zipCode and city, and every one of them is a 400 when it is * birthDate, email, gender, eu, street, zipCode and city, and every one of them is a 400 when it is
* missing. That is * missing. That is
* deliberately harsh - a 400 costs a whole application - and each rejection case is pinned here * deliberately harsh - a 400 costs a whole application - and each rejection case is pinned here
* one field at a time, because these are the fields a teamer cannot be provisioned without * one field at a time, because these are the fields a teamer cannot be provisioned without.
* (.ddev/plans/application-form-teamer-provisioning.md).
*/ */
class ApplicationFormControllerTest extends WebTestCase class ApplicationFormControllerTest extends WebTestCase
{ {
@@ -412,7 +411,7 @@ class ApplicationFormControllerTest extends WebTestCase
/** /**
* A duplicate label is disambiguated by the identifier on the TYPO3 side, so both answers * A duplicate label is disambiguated by the identifier on the TYPO3 side, so both answers
* survive as separate keys (§3.3). The order is MySQL's, not the form's - a json column * survive as separate keys. The order is MySQL's, not the form's - a json column
* sorts an object's keys by length - so only the set of labels is pinned here. * sorts an object's keys by length - so only the set of labels is pinned here.
*/ */
public function testEveryLabelSurvivesIncludingADisambiguatedDuplicate(): void public function testEveryLabelSurvivesIncludingADisambiguatedDuplicate(): void
@@ -528,7 +527,7 @@ class ApplicationFormControllerTest extends WebTestCase
} }
/** /**
* §5.1 is not implemented yet: a double-clicked submit button stores two rows for now, and * Dedupe is not implemented yet: a double-clicked submit button stores two rows for now, and
* this pins that it is a known state rather than an accident. * this pins that it is a known state rather than an accident.
*/ */
public function testTheSamePayloadTwiceIsStoredTwiceForNow(): void public function testTheSamePayloadTwiceIsStoredTwiceForNow(): void
@@ -13,7 +13,7 @@ use App\Service\Assignment\AssignmentPlanner;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
* One test per row of the planner table in .ddev/plans/teamer-auto-assignments-plan.md. * Pins the rules listed on AssignmentPlanner, one test per case.
*/ */
class AssignmentPlannerTest extends TestCase class AssignmentPlannerTest extends TestCase
{ {
@@ -24,10 +24,9 @@ use Psr\Log\NullLogger;
/** /**
* The three provisioning writes are not a transaction and only the first and the third can be * The three provisioning writes are not a transaction and only the first and the third can be
* rehearsed against BusPro's test database - the teamer selection is changeable in production only * rehearsed against BusPro's test database - the teamer selection is changeable in production only.
* (.ddev/plans/buspro-crm-selection-write.md). So the orchestration is pinned here against a mocked * So the orchestration is pinned here against a mocked client: what runs, in which order, what is
* client: what runs, in which order, what is persisted before the next step, and what a retry does * persisted before the next step, and what a retry does after a failure halfway.
* after a failure halfway.
*/ */
#[AllowMockObjectsWithoutExpectations] #[AllowMockObjectsWithoutExpectations]
class TeamerProvisionerTest extends TestCase class TeamerProvisionerTest extends TestCase