feat: provision a teamer from a received application
This commit is contained in:
1 parent
d5f4b12f0a
commit
b530679658
23 files changed
+2433
-148
No files matched your search
@@ -15,9 +15,22 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
* side without a release here, so nothing may switch the serializer's ALLOW_EXTRA_ATTRIBUTES
|
||||
* off: it would turn every later mapping change into a 400, silent because the finisher only
|
||||
* logs it.
|
||||
* - **Everything but the four basics is optional.** A form is configured without a field as
|
||||
* often as with it, so an absent key means "not part of this form", never "empty" - hence the
|
||||
* defaults, which also let the serializer construct the object from a payload of four keys.
|
||||
* - **Nine fields are required of every payload**: lastName, firstName, birthDate, email, gender,
|
||||
* eu, street, zipCode and city. They are what it takes to provision a teamer from a submission,
|
||||
* and BusPro's own mandatory fields for creating an address are among them (errors 885-891: Name,
|
||||
* 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).
|
||||
*
|
||||
* 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
|
||||
* this form", never "empty".
|
||||
*
|
||||
* A 400 costs a whole application, and the visitor sees their form submit normally either way.
|
||||
* That is accepted deliberately: the same people control the TYPO3 forms and this endpoint, the
|
||||
* response body names the offending field and the finisher logs it.
|
||||
*
|
||||
* The lengths are the storage limits of ApplicationFormSubmission, not an opinion about what a
|
||||
* visitor may type: they exist so an oversized value is a 400 the TYPO3 side logs, and not a
|
||||
@@ -25,6 +38,11 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
*/
|
||||
class ApplicationFormSubmissionDto
|
||||
{
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
public const GENDERS = ['F', 'M', 'D'];
|
||||
|
||||
public const MAX_DETAIL_LABEL_LENGTH = 255;
|
||||
|
||||
public const MAX_DETAIL_VALUE_LENGTH = 65535;
|
||||
@@ -47,10 +65,13 @@ class ApplicationFormSubmissionDto
|
||||
public readonly int|string|null $pageUid = null,
|
||||
#[Assert\Length(max: 2048, maxMessage: 'pageUrl must not exceed {{ limit }} characters.')]
|
||||
public readonly ?string $pageUrl = null,
|
||||
#[Assert\NotBlank(message: 'firstName must not be empty.')]
|
||||
#[Assert\Length(max: 255)]
|
||||
public readonly ?string $firstName = null,
|
||||
#[Assert\NotBlank(message: 'lastName must not be empty.')]
|
||||
#[Assert\Length(max: 255)]
|
||||
public readonly ?string $lastName = null,
|
||||
#[Assert\NotBlank(message: 'email must not be empty.')]
|
||||
#[Assert\Email(message: 'email is not a valid email address.')]
|
||||
#[Assert\Length(max: 255)]
|
||||
public readonly ?string $email = null,
|
||||
@@ -58,19 +79,37 @@ class ApplicationFormSubmissionDto
|
||||
public readonly ?string $phone = null,
|
||||
#[Assert\Length(max: 64)]
|
||||
public readonly ?string $mobile = null,
|
||||
#[Assert\Length(max: 64)]
|
||||
// The one field whose format is nailed down rather than stored as typed: provisioning
|
||||
// parses it as a date, and a free-text birthday would only fail later, in front of a
|
||||
// reviewer who cannot fix it.
|
||||
#[Assert\NotBlank(message: 'birthDate must not be empty.')]
|
||||
#[Assert\Date(message: 'birthDate must be a date in the format YYYY-MM-DD.')]
|
||||
public readonly ?string $birthDate = null,
|
||||
// The application's own vocabulary, not BusPro's: ApplicationFormSubmission::toPayload()
|
||||
// maps F to W on the way out, exactly as Teamer::toPayload() does.
|
||||
#[Assert\NotBlank(message: 'gender must not be empty.')]
|
||||
#[Assert\Choice(choices: self::GENDERS, message: 'gender must be one of F, M or D.')]
|
||||
public readonly ?string $gender = null,
|
||||
// The whole postal address, required together. Street and postcode are BusPro's own
|
||||
// Pflichtfelder for a new address (errors 891 and 890), so a submission without them is one
|
||||
// no teamer can be provisioned from; the town is not on that list, but an address carrying a
|
||||
// street and a postcode without one is not an address worth writing to the CRM either.
|
||||
#[Assert\NotBlank(message: 'street must not be empty.')]
|
||||
#[Assert\Length(max: 255)]
|
||||
public readonly ?string $street = null,
|
||||
#[Assert\NotBlank(message: 'zipCode must not be empty.')]
|
||||
#[Assert\Length(max: 32)]
|
||||
public readonly ?string $zipCode = null,
|
||||
#[Assert\NotBlank(message: 'city must not be empty.')]
|
||||
#[Assert\Length(max: 255)]
|
||||
public readonly ?string $city = null,
|
||||
// Whether the applicant holds an EU (incl. Switzerland) nationality - the one payload
|
||||
// field a rule depends on, hence mapped rather than left in $details. bool|string|int
|
||||
// for the same reason as pageUid: a form posts "1" or "" as readily as a JSON boolean,
|
||||
// and losing a whole application over the spelling would be absurd. Read it back
|
||||
// through isEu().
|
||||
// through isEu(). Required, because the receipt mail depends on it - but required as a
|
||||
// value, not as a truth: NotNull rather than NotBlank, or a lawful "no" would be a 400.
|
||||
#[Assert\NotNull(message: 'eu must not be omitted.')]
|
||||
public readonly bool|string|int|null $eu = null,
|
||||
public readonly array $details = [],
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use App\Entity\User;
|
||||
|
||||
/**
|
||||
* What provisioning actually did, so the controller can phrase the flash and the log entry
|
||||
* without re-deriving any of it (App\Service\Teamer\TeamerProvisioner).
|
||||
*
|
||||
* Every flag is a fact the reviewer wants to know: whether BusPro created the address or
|
||||
* recognised the applicant, whether an account already existed, and whether the password mail
|
||||
* went out - the last is not fatal, but without it the teamer cannot log in until somebody sends
|
||||
* it again.
|
||||
*/
|
||||
class TeamerProvisioningResult
|
||||
{
|
||||
public function __construct(
|
||||
private readonly User $user,
|
||||
private readonly bool $addressCreated,
|
||||
private readonly bool $userCreated,
|
||||
private readonly bool $alreadyTeamer,
|
||||
private readonly bool $passwordMailSent,
|
||||
private readonly bool $alreadyProvisioned = false,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* False means BusPro matched an address it already held and answered with its ids - not a
|
||||
* failure, and not a duplicate either.
|
||||
*/
|
||||
public function isAddressCreated(): bool
|
||||
{
|
||||
return $this->addressCreated;
|
||||
}
|
||||
|
||||
public function isUserCreated(): bool
|
||||
{
|
||||
return $this->userCreated;
|
||||
}
|
||||
|
||||
public function isAlreadyTeamer(): bool
|
||||
{
|
||||
return $this->alreadyTeamer;
|
||||
}
|
||||
|
||||
public function isPasswordMailSent(): bool
|
||||
{
|
||||
return $this->passwordMailSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* The submission had been provisioned before and nothing was sent to BusPro at all.
|
||||
*/
|
||||
public function isAlreadyProvisioned(): bool
|
||||
{
|
||||
return $this->alreadyProvisioned;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user