chore: drop the teamer provisioning feature flag
This commit is contained in:
1 parent
b530679658
commit
5141673bcd
5 files changed
+135
-82
No files matched your search
@@ -114,7 +114,6 @@ FEATURE_SANITIZE_UPLOADS=false
|
|||||||
FEATURE_STAMP_INVOICES=false
|
FEATURE_STAMP_INVOICES=false
|
||||||
FEATURE_DATEV_EMAIL=false
|
FEATURE_DATEV_EMAIL=false
|
||||||
FEATURE_MYEP_LOGIN=false
|
FEATURE_MYEP_LOGIN=false
|
||||||
FEATURE_APPLICATION_FORM_TEAMER_PROVISIONING=false
|
|
||||||
|
|
||||||
UPLOAD_REPLACEMENT_FILE=assets/pdf/chicken.pdf
|
UPLOAD_REPLACEMENT_FILE=assets/pdf/chicken.pdf
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,3 @@ flagception:
|
|||||||
myep_login:
|
myep_login:
|
||||||
default: false
|
default: false
|
||||||
env: FEATURE_MYEP_LOGIN
|
env: FEATURE_MYEP_LOGIN
|
||||||
# Provisioning writes to the production CRM and cannot be rehearsed: the teamer selection
|
|
||||||
# is only changeable there. Ships off.
|
|
||||||
application_form_teamer_provisioning:
|
|
||||||
default: false
|
|
||||||
env: FEATURE_APPLICATION_FORM_TEAMER_PROVISIONING
|
|
||||||
@@ -7,7 +7,6 @@ use App\Htmx\HxRedirectResponse;
|
|||||||
use App\Model\TeamerProvisioningResult;
|
use App\Model\TeamerProvisioningResult;
|
||||||
use App\Service\Teamer\TeamerProvisioner;
|
use App\Service\Teamer\TeamerProvisioner;
|
||||||
use App\Service\Teamer\TeamerProvisioningException;
|
use App\Service\Teamer\TeamerProvisioningException;
|
||||||
use Flagception\Manager\FeatureManagerInterface;
|
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -19,14 +18,14 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|||||||
* Makes a teamer out of an applicant who passed the interview: one click, because the submission
|
* Makes a teamer out of an applicant who passed the interview: one click, because the submission
|
||||||
* carries everything the CRM needs and there is nothing left for the reviewer to type.
|
* carries everything the CRM needs and there is nothing left for the reviewer to type.
|
||||||
*
|
*
|
||||||
* Behind a feature flag, because provisioning writes to the production CRM and cannot be rehearsed
|
* The click writes to the CRM for real - three writes, none of them undoable from here - so what
|
||||||
* anywhere else - the teamer selection is only changeable there.
|
* goes to BusPro is shown in the confirmation before it goes, and TeamerProvisioner refuses a
|
||||||
|
* submission it cannot provision before sending anything.
|
||||||
*/
|
*/
|
||||||
class CreateTeamerController extends AbstractController
|
class CreateTeamerController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly TeamerProvisioner $provisioner,
|
private readonly TeamerProvisioner $provisioner,
|
||||||
private readonly FeatureManagerInterface $featureManager,
|
|
||||||
private readonly LoggerInterface $logger,
|
private readonly LoggerInterface $logger,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -35,10 +34,6 @@ class CreateTeamerController extends AbstractController
|
|||||||
#[IsGranted('ROLE_TEAM_ADMIN')]
|
#[IsGranted('ROLE_TEAM_ADMIN')]
|
||||||
public function index(ApplicationFormSubmission $submission, Request $request): Response
|
public function index(ApplicationFormSubmission $submission, Request $request): Response
|
||||||
{
|
{
|
||||||
if (false === $this->featureManager->isActive('application_form_teamer_provisioning')) {
|
|
||||||
throw $this->createNotFoundException('Teamer provisioning is not enabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (false === $request->isMethod(Request::METHOD_POST)) {
|
if (false === $request->isMethod(Request::METHOD_POST)) {
|
||||||
return $this->render('admin/application_form_submission/modal_create_teamer.html.twig', [
|
return $this->render('admin/application_form_submission/modal_create_teamer.html.twig', [
|
||||||
'submission' => $submission,
|
'submission' => $submission,
|
||||||
|
|||||||
@@ -106,41 +106,39 @@
|
|||||||
{{ form_rest(callsForm) }}
|
{{ form_rest(callsForm) }}
|
||||||
{{ form_end(callsForm) }}
|
{{ form_end(callsForm) }}
|
||||||
</div>
|
</div>
|
||||||
{% if feature('application_form_teamer_provisioning') or submission.teamerCreatedAt %}
|
<div>
|
||||||
<div>
|
<dl class="divide-y divide-gray-100">
|
||||||
<dl class="divide-y divide-gray-100">
|
{{ _self.row('übernommen am', submission.teamerCreatedAt ? submission.teamerCreatedAt|date('d.m.Y, H:i') : null) }}
|
||||||
{{ _self.row('übernommen am', submission.teamerCreatedAt ? submission.teamerCreatedAt|date('d.m.Y, H:i') : null) }}
|
</dl>
|
||||||
</dl>
|
<div class="pt-4">
|
||||||
<div class="pt-4">
|
{% if submission.user and submission.user.teamer %}
|
||||||
{% if submission.user and submission.user.teamer %}
|
<a href="{{ path('app_administrative_teamer_profile', { 'uuid': submission.user.teamer.uuid, 'r': forwarded_return_url() }) }}"
|
||||||
<a href="{{ path('app_administrative_teamer_profile', { 'uuid': submission.user.teamer.uuid, 'r': forwarded_return_url() }) }}"
|
class="btn btn--small btn--secondary">
|
||||||
class="btn btn--small btn--secondary">
|
Teamer:innenprofil öffnen
|
||||||
Teamer:innenprofil öffnen
|
</a>
|
||||||
</a>
|
{% elseif submission.email %}
|
||||||
{% elseif submission.email %}
|
<button type="button"
|
||||||
<button type="button"
|
class="btn btn--small"
|
||||||
class="btn btn--small"
|
hx-get="{{ path('app_admin_application_form_submission_create_teamer', { 'uuid': submission.uuid, 'r': forwarded_return_url() }) }}"
|
||||||
hx-get="{{ path('app_admin_application_form_submission_create_teamer', { 'uuid': submission.uuid, 'r': forwarded_return_url() }) }}"
|
hx-target="body"
|
||||||
hx-target="body"
|
hx-swap="beforeend">
|
||||||
hx-swap="beforeend">
|
Teamer:in anlegen
|
||||||
Teamer:in anlegen
|
</button>
|
||||||
</button>
|
{% if submission.busProAddressId %}
|
||||||
{% if submission.busProAddressId %}
|
{# Step 1 is done and the rest is not: naming it here is what makes a
|
||||||
{# Step 1 is done and the rest is not: naming it here is what makes a
|
retry a resume rather than a guess. #}
|
||||||
retry a resume rather than a guess. #}
|
<div class="pt-2 text-sm">
|
||||||
<div class="pt-2 text-sm">
|
Die BusPro-Adresse {{ submission.busProAddressId }} wurde schon angelegt,
|
||||||
Die BusPro-Adresse {{ submission.busProAddressId }} wurde schon angelegt,
|
die Übernahme wurde aber nicht abgeschlossen.
|
||||||
die Übernahme wurde aber nicht abgeschlossen.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<div class="text-sm">
|
|
||||||
Ohne E-Mail-Adresse kann kein Teamer:innenkonto angelegt werden.
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
{% else %}
|
||||||
|
<div class="text-sm">
|
||||||
|
Ohne E-Mail-Adresse kann kein Teamer:innenkonto angelegt werden.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="font-bold text-lg pb-4">
|
<h2 class="font-bold text-lg pb-4">
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
|||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The feature gate, which is the only part of provisioning a test can exercise: everything behind
|
* Everything around the provisioning click that can be exercised without writing to BusPro.
|
||||||
* it writes to the production CRM, where the teamer selection is changeable - the test database
|
|
||||||
* answers aenderbar="False" for it (.ddev/plans/buspro-crm-selection-write.md).
|
|
||||||
*
|
*
|
||||||
* With the flag off the route must not exist at all, not merely hide its button: the modal is
|
* The write itself cannot: the teamer CRM selection is only changeable in the production database
|
||||||
* reachable by url, and a reviewer who bookmarked it would otherwise write to BusPro from a
|
* (.ddev/plans/buspro-crm-selection-write.md), and a POST that gets as far as the api client would
|
||||||
* release where the feature is meant to be dormant.
|
* 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.
|
||||||
*/
|
*/
|
||||||
class CreateTeamerControllerTest extends WebTestCase
|
class CreateTeamerControllerTest extends WebTestCase
|
||||||
{
|
{
|
||||||
@@ -37,51 +37,115 @@ class CreateTeamerControllerTest extends WebTestCase
|
|||||||
$this->client->loginUser($this->reviewer());
|
$this->client->loginUser($this->reviewer());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testTheDisabledFeatureHidesTheRouteEntirely(): void
|
public function testTheDetailPageOffersTheProvisioningButton(): void
|
||||||
{
|
{
|
||||||
$this->client->request('GET', $this->url());
|
$submission = $this->submission();
|
||||||
|
|
||||||
$this->assertSame(Response::HTTP_NOT_FOUND, $this->client->getResponse()->getStatusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The POST is the one that writes, so it is pinned separately rather than trusted to share the
|
|
||||||
* guard with the GET.
|
|
||||||
*/
|
|
||||||
public function testTheDisabledFeatureRefusesThePostAsWell(): void
|
|
||||||
{
|
|
||||||
$this->client->request('POST', $this->url());
|
|
||||||
|
|
||||||
$this->assertSame(Response::HTTP_NOT_FOUND, $this->client->getResponse()->getStatusCode());
|
|
||||||
$this->assertNull($this->submission()->getTeamerCreatedAt());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testTheDetailPageOffersNoProvisioningWhileTheFeatureIsOff(): void
|
|
||||||
{
|
|
||||||
$crawler = $this->client->request(
|
$crawler = $this->client->request(
|
||||||
'GET',
|
'GET',
|
||||||
sprintf('/admin/application-form-submission/detail/%s', $this->submission()->getUuid())
|
sprintf('/admin/application-form-submission/detail/%s', $submission->getUuid()),
|
||||||
|
['r' => rawurlencode('/admin/application-form-submission')]
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
$this->assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||||
$this->assertCount(0, $crawler->filter('button[hx-get*="create-teamer"]'));
|
|
||||||
|
$button = $crawler->filter('button[hx-get*="create-teamer"]');
|
||||||
|
|
||||||
|
$this->assertCount(1, $button);
|
||||||
|
// Like every other action on that page: it hands on the return url it was given, so "zurück"
|
||||||
|
// keeps pointing at the list.
|
||||||
|
$this->assertStringContainsString('r='.rawurlencode('/admin/application-form-submission'), (string) $button->attr('hx-get'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function url(): string
|
/**
|
||||||
|
* The three writes cannot be undone from here, so the confirmation names what goes to BusPro
|
||||||
|
* rather than asking "are you sure".
|
||||||
|
*/
|
||||||
|
public function testTheConfirmationNamesWhatGoesToBusPro(): void
|
||||||
{
|
{
|
||||||
return sprintf('/admin/application-form-submission/create-teamer/%s', $this->submission()->getUuid());
|
$crawler = $this->client->request('GET', $this->url($this->submission()));
|
||||||
|
|
||||||
|
$this->assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode());
|
||||||
|
|
||||||
|
$modal = $crawler->filter('#htmx-confirmation-modal')->text();
|
||||||
|
|
||||||
|
$this->assertStringContainsString('Anna Muster', $modal);
|
||||||
|
$this->assertStringContainsString('01.01.1990', $modal);
|
||||||
|
$this->assertStringContainsString('Musterstraße 1, 10115 Berlin', $modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A row from before the tightened endpoint contract. The confirmation says so before the
|
||||||
|
* reviewer clicks, because BusPro would answer the same thing only after the attempt.
|
||||||
|
*/
|
||||||
|
public function testTheConfirmationFlagsAnIncompleteSubmission(): void
|
||||||
|
{
|
||||||
|
$crawler = $this->client->request('GET', $this->url($this->incompleteSubmission()));
|
||||||
|
|
||||||
|
$modal = $crawler->filter('#htmx-confirmation-modal')->text();
|
||||||
|
|
||||||
|
$this->assertStringContainsString('ohne Geburtsdatum', $modal);
|
||||||
|
$this->assertStringContainsString('ohne Straße, PLZ und Ort', $modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The only POST a test may make: TeamerProvisioner refuses an unprovisionable submission before
|
||||||
|
* it touches the api client, so nothing here reaches BusPro. It covers what the reviewer
|
||||||
|
* actually sees when provisioning fails - the refusal's own wording, not "es ist ein Fehler
|
||||||
|
* aufgetreten".
|
||||||
|
*/
|
||||||
|
public function testAnUnprovisionableSubmissionFailsWithoutWritingAnything(): void
|
||||||
|
{
|
||||||
|
$submission = $this->incompleteSubmission();
|
||||||
|
|
||||||
|
$this->client->request('POST', $this->url($submission));
|
||||||
|
|
||||||
|
$response = $this->client->getResponse();
|
||||||
|
|
||||||
|
$this->assertSame(Response::HTTP_OK, $response->getStatusCode());
|
||||||
|
$this->assertStringContainsString('detail', (string) $response->headers->get('HX-Redirect'));
|
||||||
|
|
||||||
|
$flashes = $this->client->getRequest()->getSession()->getFlashBag()->peekAll();
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('error', $flashes);
|
||||||
|
$this->assertStringContainsString('Geburtsdatum', $flashes['error'][0]);
|
||||||
|
|
||||||
|
$this->entityManager->clear();
|
||||||
|
$stored = $this->entityManager->getRepository(ApplicationFormSubmission::class)->find($submission->getId());
|
||||||
|
|
||||||
|
$this->assertNotNull($stored);
|
||||||
|
$this->assertFalse($stored->isProvisioned());
|
||||||
|
$this->assertNull($stored->getBusProAddressId());
|
||||||
|
$this->assertNull($stored->getUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function url(ApplicationFormSubmission $submission): string
|
||||||
|
{
|
||||||
|
return sprintf('/admin/application-form-submission/create-teamer/%s', $submission->getUuid());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function submission(): ApplicationFormSubmission
|
private function submission(): ApplicationFormSubmission
|
||||||
{
|
{
|
||||||
$repository = $this->entityManager->getRepository(ApplicationFormSubmission::class);
|
return $this->persist(
|
||||||
$submission = $repository->findOneBy([], ['id' => 'DESC']);
|
$this->newSubmission()
|
||||||
|
->setBirthDate('1990-01-01')
|
||||||
|
->setStreet('Musterstraße 1')
|
||||||
|
->setZipCode('10115')
|
||||||
|
->setCity('Berlin')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (null !== $submission) {
|
/**
|
||||||
return $submission;
|
* Everything the contract now requires, missing - which only a row stored before it can be.
|
||||||
}
|
*/
|
||||||
|
private function incompleteSubmission(): ApplicationFormSubmission
|
||||||
|
{
|
||||||
|
return $this->persist($this->newSubmission()->setBirthDate('01.01.1990'));
|
||||||
|
}
|
||||||
|
|
||||||
$submission = (new ApplicationFormSubmission(
|
private function newSubmission(): ApplicationFormSubmission
|
||||||
|
{
|
||||||
|
return (new ApplicationFormSubmission(
|
||||||
'applicationform',
|
'applicationform',
|
||||||
'Bewerbungsformular',
|
'Bewerbungsformular',
|
||||||
new \DateTimeImmutable('2026-09-22T12:30:00+02:00'),
|
new \DateTimeImmutable('2026-09-22T12:30:00+02:00'),
|
||||||
@@ -90,9 +154,11 @@ class CreateTeamerControllerTest extends WebTestCase
|
|||||||
->setFirstName('Anna')
|
->setFirstName('Anna')
|
||||||
->setLastName('Muster')
|
->setLastName('Muster')
|
||||||
->setEmail('[email protected]')
|
->setEmail('[email protected]')
|
||||||
->setBirthDate('1990-01-01')
|
|
||||||
;
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function persist(ApplicationFormSubmission $submission): ApplicationFormSubmission
|
||||||
|
{
|
||||||
$this->entityManager->persist($submission);
|
$this->entityManager->persist($submission);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user