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
# 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
# later mapping change into a 400 (see .ddev/plans/application-form-endpoint.md).
# later mapping change into a 400.
framework:
serializer:
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
* outcome of each of the three provisioning writes
* (.ddev/plans/application-form-teamer-provisioning.md).
* outcome of each of the three provisioning writes.
*
* 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
+3 -2
View File
@@ -146,8 +146,9 @@ class ApiClient
* "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
* 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
* .ddev/plans/buspro-crm-selection-write.md before changing the shape.
* payload are what BusPro accepted when verified against production (2026-09-25, selection
* 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
* 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
// "selektionsmerkmale" node that is absent here and would have returned an
// 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);
case 'Adresse_Neu':
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
* 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
* (.ddev/plans/MYEP-APPLICATION-ENDPOINT.md §6).
* matching against known people belong in a worker, not here.
*/
class ApplicationFormController extends AbstractController
{
@@ -58,7 +57,7 @@ class ApplicationFormController extends AbstractController
'formLabel' => $submission->getFormLabel(),
'pageUid' => $submission->getPageUid(),
'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()),
'receiptSent' => $receiptSent,
]);
@@ -61,7 +61,7 @@ class ApplicationFormSubmissionMailer
if (true !== $submission->isEu()) {
$this->logger->info('Application form receipt mail skipped, not eligible', [
'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(),
]);
+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
* 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
* .ddev/plans/MYEP-APPLICATION-ENDPOINT.md §3.3).
* display only, never a schema, never queried on.
*/
#[ORM\Entity(repositoryClass: ApplicationFormSubmissionRepository::class)]
// 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.
*
* 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.
*/
#[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.
* 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
* 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
* 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.
*
* @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
* 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.
*
* @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].
*
* Two rules from the contract (.ddev/plans/MYEP-APPLICATION-ENDPOINT.md §3.2, §3.3) shape this
* class and must survive any rewrite:
* Two rules from the contract with the TYPO3 side shape this class and must survive any
* rewrite:
*
* - **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
@@ -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
* 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
* there is no form-type discriminator
* (.ddev/plans/application-form-teamer-provisioning.md).
* there is no form-type discriminator.
*
* 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
@@ -334,7 +333,7 @@ class ApplicationFormSubmissionDto
/**
* 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 -
* 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.
*
* @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.
*
* 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;
* - 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
+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.
*
* Nothing here reads the applicant's profile back from BusPro. Adressdaten needs the customer's
* password, which provisioning deliberately never has (see
* .ddev/plans/application-form-teamer-provisioning.md), so the local records are built from the
* data that was sent plus the ids that came back.
* password, which provisioning deliberately never has, so the local records are built from the 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
* 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
* createProfileUpdateResponse() reads. It used to be routed to the CRM attribute parser, which
* would have returned an empty response with every role flag false
* (.ddev/plans/buspro-crm-selection-write.md).
* would have returned an empty response with every role flag false.
*/
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.
*
* 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
* The write itself cannot: the teamer CRM selection is only changeable in the production database,
* 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;
* 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.
@@ -13,8 +13,8 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Response;
/**
* 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
* The cases here are the ones the TYPO3 side actually produces:
* 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
* 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
* missing. That is
* 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
* (.ddev/plans/application-form-teamer-provisioning.md).
* one field at a time, because these are the fields a teamer cannot be provisioned without.
*/
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
* 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.
*/
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.
*/
public function testTheSamePayloadTwiceIsStoredTwiceForNow(): void
@@ -13,7 +13,7 @@ use App\Service\Assignment\AssignmentPlanner;
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
{
@@ -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
* 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
* client: what runs, in which order, what is persisted before the next step, and what a retry does
* after a failure halfway.
* rehearsed against BusPro's test database - the teamer selection is changeable in production only.
* So the orchestration is pinned here against a mocked client: what runs, in which order, what is
* persisted before the next step, and what a retry does after a failure halfway.
*/
#[AllowMockObjectsWithoutExpectations]
class TeamerProvisionerTest extends TestCase