docs: drop code references to local plan files
This commit is contained in:
1 parent
2a554bb7bf
commit
96d49fc488
15 files changed
+33
-40
No files matched your search
@@ -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: ~
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user