diff --git a/config/packages/serializer.yaml b/config/packages/serializer.yaml index 258cd93..cf05a89 100644 --- a/config/packages/serializer.yaml +++ b/config/packages/serializer.yaml @@ -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: ~ diff --git a/migrations/Version20260925120000.php b/migrations/Version20260925120000.php index 5796869..f10b40b 100644 --- a/migrations/Version20260925120000.php +++ b/migrations/Version20260925120000.php @@ -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 diff --git a/src/BusProNet/ApiClient.php b/src/BusProNet/ApiClient.php index 23882a8..bfb6f1f 100644 --- a/src/BusProNet/ApiClient.php +++ b/src/BusProNet/ApiClient.php @@ -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. diff --git a/src/BusProNet/ResponseParser.php b/src/BusProNet/ResponseParser.php index ddbd576..c259c02 100644 --- a/src/BusProNet/ResponseParser.php +++ b/src/BusProNet/ResponseParser.php @@ -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); diff --git a/src/Controller/Api/ApplicationFormController.php b/src/Controller/Api/ApplicationFormController.php index 2d0b6f4..d29825d 100644 --- a/src/Controller/Api/ApplicationFormController.php +++ b/src/Controller/Api/ApplicationFormController.php @@ -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, ]); diff --git a/src/Email/ApplicationFormSubmissionMailer.php b/src/Email/ApplicationFormSubmissionMailer.php index 567eeb7..b33b908 100644 --- a/src/Email/ApplicationFormSubmissionMailer.php +++ b/src/Email/ApplicationFormSubmissionMailer.php @@ -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(), ]); diff --git a/src/Entity/ApplicationFormSubmission.php b/src/Entity/ApplicationFormSubmission.php index 63968c7..ae4c242 100644 --- a/src/Entity/ApplicationFormSubmission.php +++ b/src/Entity/ApplicationFormSubmission.php @@ -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 @@ -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 $payload diff --git a/src/Model/ApplicationFormSubmissionDto.php b/src/Model/ApplicationFormSubmissionDto.php index 459dd67..5d7db59 100644 --- a/src/Model/ApplicationFormSubmissionDto.php +++ b/src/Model/ApplicationFormSubmissionDto.php @@ -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> diff --git a/src/Service/Assignment/AssignmentPlanner.php b/src/Service/Assignment/AssignmentPlanner.php index 3fd09a7..5394ad9 100644 --- a/src/Service/Assignment/AssignmentPlanner.php +++ b/src/Service/Assignment/AssignmentPlanner.php @@ -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 diff --git a/src/Service/Teamer/TeamerProvisioner.php b/src/Service/Teamer/TeamerProvisioner.php index 5394e1c..dc7c1c4 100644 --- a/src/Service/Teamer/TeamerProvisioner.php +++ b/src/Service/Teamer/TeamerProvisioner.php @@ -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 diff --git a/tests/BusProNet/ResponseParserTest.php b/tests/BusProNet/ResponseParserTest.php index 38b56aa..bed3704 100644 --- a/tests/BusProNet/ResponseParserTest.php +++ b/tests/BusProNet/ResponseParserTest.php @@ -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 { diff --git a/tests/Controller/Admin/ApplicationFormSubmission/CreateTeamerControllerTest.php b/tests/Controller/Admin/ApplicationFormSubmission/CreateTeamerControllerTest.php index 4701f21..64d425b 100644 --- a/tests/Controller/Admin/ApplicationFormSubmission/CreateTeamerControllerTest.php +++ b/tests/Controller/Admin/ApplicationFormSubmission/CreateTeamerControllerTest.php @@ -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. diff --git a/tests/Controller/Api/ApplicationFormControllerTest.php b/tests/Controller/Api/ApplicationFormControllerTest.php index e947699..60e6b82 100644 --- a/tests/Controller/Api/ApplicationFormControllerTest.php +++ b/tests/Controller/Api/ApplicationFormControllerTest.php @@ -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 diff --git a/tests/Service/Assignment/AssignmentPlannerTest.php b/tests/Service/Assignment/AssignmentPlannerTest.php index 0100c56..cceb804 100644 --- a/tests/Service/Assignment/AssignmentPlannerTest.php +++ b/tests/Service/Assignment/AssignmentPlannerTest.php @@ -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 { diff --git a/tests/Service/Teamer/TeamerProvisionerTest.php b/tests/Service/Teamer/TeamerProvisionerTest.php index 62eca1c..12db58d 100644 --- a/tests/Service/Teamer/TeamerProvisionerTest.php +++ b/tests/Service/Teamer/TeamerProvisionerTest.php @@ -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