From 9583612fdc2224d28feb78cc2afbe1d45c596328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Sat, 7 Nov 2026 09:26:27 +0100 Subject: [PATCH] refactor: use english naming for the n8n assignments api --- .env | 2 +- config/packages/security.yaml | 2 +- config/services.yaml | 14 ++++---- .../Api/Assignment/ListController.php | 33 ++++++++--------- .../{PlacementStatus.php => PlaceStatus.php} | 4 +-- src/Model/AssignmentExportQueryDto.php | 35 ++++++++++++++++++ src/Model/PlacementExportQueryDto.php | 33 ----------------- src/Repository/AssignmentRepository.php | 4 +-- .../AccessToken/ApiKeyTokenHandler.php | 2 +- .../Api/Assignment/ListControllerTest.php | 36 +++++++++---------- 10 files changed, 84 insertions(+), 81 deletions(-) rename src/Enum/{PlacementStatus.php => PlaceStatus.php} (84%) create mode 100644 src/Model/AssignmentExportQueryDto.php delete mode 100644 src/Model/PlacementExportQueryDto.php diff --git a/.env b/.env index 18b77bc..2cc7c76 100644 --- a/.env +++ b/.env @@ -94,7 +94,7 @@ MYEP_OAUTH2_URL_RESOURCE_OWNER_DETAILS=https://my.ep-reisen.de/api/userinfo MYEP_OAUTH2_SCOPES=email,id,roles,profile # Shared key the sibling websites send in the X-Api-Key header. Empty here: set it per stage. APPLICATION_FORM_API_KEY= -# Shared key n8n reads the placements feed with, in the X-Api-Key header. Empty here: set it per stage. +# Shared key n8n reads the assignments feed with, in the X-Api-Key header. Empty here: set it per stage. N8N_API_KEY= # Booking link for the Team-Infocall, substituted for {bookingUrl} in the application form # receipt mail. Empty here: set it per stage. While it is empty that mail is not sent at all - diff --git a/config/packages/security.yaml b/config/packages/security.yaml index ec8b79f..be4d135 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -13,7 +13,7 @@ security: memory: users: api: { roles: [ ROLE_API ] } - n8n: { roles: [ ROLE_API_PLACEMENTS ] } + n8n: { roles: [ ROLE_API_ASSIGNMENTS ] } role_hierarchy: ROLE_TEAM_ADMIN: [ ROLE_ADMINISTRATIVE ] diff --git a/config/services.yaml b/config/services.yaml index 67769ec..b495f27 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -40,14 +40,14 @@ parameters: # rejects every request instead of letting them through. application_form_api_key: '%env(string:APPLICATION_FORM_API_KEY)%' - # Shared key n8n reads the placements feed with (GET /api/einsaetze). Same rules as above. + # Shared key n8n reads the assignments feed with (GET /api/assignments). Same rules as above. n8n_api_key: '%env(string:N8N_API_KEY)%' - # What the n8n placements feed covers: assignments in these houses (matched against the first + # What the n8n assignments feed covers: assignments in these houses (matched against the first # three characters of destination.hotelCode) for these job profiles - guide, course and # service-team work. Job profile ids are database ids, so check them against production. - placement_export.house_codes: [ DPW, DGS, DWW, DKI, DKS ] - placement_export.job_profile_ids: + assignment_export.house_codes: [ DPW, DGS, DWW, DKI, DKS ] + assignment_export.job_profile_ids: - 5 # Reiseleitung - 9 # Reiseleitung Ski - 10 # Reiseleitung Snowboard @@ -481,8 +481,8 @@ services: App\Controller\Api\Assignment\ListController: arguments: - $houseCodes: '%placement_export.house_codes%' - $jobProfileIds: '%placement_export.job_profile_ids%' + $houseCodes: '%assignment_export.house_codes%' + $jobProfileIds: '%assignment_export.job_profile_ids%' $logger: '@monolog.logger.api' tags: [ 'controller.service_arguments' ] @@ -514,7 +514,7 @@ when@dev: when@test: parameters: # a job profile id ListControllerTest inserts itself - db_test has no job profiles - placement_export.job_profile_ids: [ 900001 ] + assignment_export.job_profile_ids: [ 900001 ] mailing_bus_transport: 'sync' # the questionnaire tests need it offered; any day before the placements they create questionnaire_go_live: '2026-01-01' diff --git a/src/Controller/Api/Assignment/ListController.php b/src/Controller/Api/Assignment/ListController.php index e51ed59..4466c7a 100644 --- a/src/Controller/Api/Assignment/ListController.php +++ b/src/Controller/Api/Assignment/ListController.php @@ -5,8 +5,8 @@ namespace App\Controller\Api\Assignment; use App\Entity\Assignment; use App\Entity\Disposition; use App\Entity\Upload; -use App\Enum\PlacementStatus; -use App\Model\PlacementExportQueryDto; +use App\Enum\PlaceStatus; +use App\Model\AssignmentExportQueryDto; use App\Repository\AssignmentRepository; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -17,13 +17,14 @@ use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; /** - * The placements feed n8n reads: one row per place of the houses' guide, course and service-team + * The assignments feed n8n reads: one row per place of the houses' guide, course and service-team * assignments overlapping a date range. A row is a place, not a person - re-staffing a place * changes the person on the same platz_id, and an open place has person_id null. A called-off * assignment is left out entirely, as is a place the office struck. * - * The path, the query parameters and the field names are the consumer's (German); errors are - * English, like every response of the api firewall. + * The field names are the consumer's (German), the n8n workflow is built on them; the path and the + * query parameters follow the app's English URL vocabulary, and errors are English, like every + * response of the api firewall. */ class ListController extends AbstractController { @@ -39,19 +40,19 @@ class ListController extends AbstractController ) { } - #[Route('/api/einsaetze', name: 'app_api_assignment_list', methods: ['GET'])] - #[IsGranted('ROLE_API_PLACEMENTS')] + #[Route('/api/assignments', name: 'app_api_assignment_list', methods: ['GET'])] + #[IsGranted('ROLE_API_ASSIGNMENTS')] public function index( #[MapQueryString(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)] // Deliberately without a default: the resolver would hand that over unvalidated for a - // request without any query string, instead of the 400 naming von and bis. - PlacementExportQueryDto $query, + // request without any query string, instead of the 400 naming from and to. + AssignmentExportQueryDto $query, ): JsonResponse { $rows = []; - $assignments = $this->assignmentRepository->findForPlacementExport( - $query->getFrom(), - $query->getTo(), + $assignments = $this->assignmentRepository->findForExport( + $query->getFromDate(), + $query->getToDate(), $this->houseCodes, $this->jobProfileIds, ); @@ -63,9 +64,9 @@ class ListController extends AbstractController } // the range and the count, never the rows - they name people - $this->logger->info('Placements exported', [ - 'from' => $query->von, - 'to' => $query->bis, + $this->logger->info('Assignments exported', [ + 'from' => $query->from, + 'to' => $query->to, 'assignments' => count($assignments), 'places' => count($rows), ]); @@ -94,7 +95,7 @@ class ListController extends AbstractController 'bis' => $destination->getDateTo()->format('Y-m-d'), 'jobprofil_id' => $jobProfile->getId(), 'jobprofil' => $jobProfile->getName(), - 'status' => PlacementStatus::fromDisposition($disposition)->value, + 'status' => PlaceStatus::fromDisposition($disposition)->value, 'unter_26' => $this->isUnder26($disposition, $assignment), 'vertrag_liegt_vor' => $this->hasCheckedContract($disposition), // Older codes carry a slash ("DPWBSP/030229"), current ones do not. diff --git a/src/Enum/PlacementStatus.php b/src/Enum/PlaceStatus.php similarity index 84% rename from src/Enum/PlacementStatus.php rename to src/Enum/PlaceStatus.php index 4f0f57f..245996a 100644 --- a/src/Enum/PlacementStatus.php +++ b/src/Enum/PlaceStatus.php @@ -5,9 +5,9 @@ namespace App\Enum; use App\Entity\Disposition; /** - * The status of a place in the n8n placements feed. The values are the feed's contract. + * The status of a place in the n8n assignments feed. The values are the feed's contract. */ -enum PlacementStatus: string +enum PlaceStatus: string { case OPEN = 'offen'; case REQUESTED = 'angefragt'; diff --git a/src/Model/AssignmentExportQueryDto.php b/src/Model/AssignmentExportQueryDto.php new file mode 100644 index 0000000..e300389 --- /dev/null +++ b/src/Model/AssignmentExportQueryDto.php @@ -0,0 +1,35 @@ +from); + } + + public function getToDate(): \DateTimeImmutable + { + return new \DateTimeImmutable($this->to); + } +} diff --git a/src/Model/PlacementExportQueryDto.php b/src/Model/PlacementExportQueryDto.php deleted file mode 100644 index 3d63082..0000000 --- a/src/Model/PlacementExportQueryDto.php +++ /dev/null @@ -1,33 +0,0 @@ -von); - } - - public function getTo(): \DateTimeImmutable - { - return new \DateTimeImmutable($this->bis); - } -} diff --git a/src/Repository/AssignmentRepository.php b/src/Repository/AssignmentRepository.php index 73ac4af..2717b7f 100644 --- a/src/Repository/AssignmentRepository.php +++ b/src/Repository/AssignmentRepository.php @@ -433,7 +433,7 @@ class AssignmentRepository extends ServiceEntityRepository } /** - * The assignments the n8n placements feed reports: published, in one of the houses, for one of + * The assignments the n8n assignments feed reports: published, in one of the houses, for one of * the job profiles, and overlapping the range. Placements, their teamers and documents come * fetch-joined - every place row needs them. All placements are loaded, called-off ones included: * a fetch join on a filtered collection would leave the assignment with a partial one, and @@ -444,7 +444,7 @@ class AssignmentRepository extends ServiceEntityRepository * * @return list */ - public function findForPlacementExport(\DateTimeImmutable $from, \DateTimeImmutable $to, array $houseCodes, array $jobProfileIds): array + public function findForExport(\DateTimeImmutable $from, \DateTimeImmutable $to, array $houseCodes, array $jobProfileIds): array { if ([] === $houseCodes || [] === $jobProfileIds) { return []; diff --git a/src/Security/AccessToken/ApiKeyTokenHandler.php b/src/Security/AccessToken/ApiKeyTokenHandler.php index f0e1292..cabba85 100644 --- a/src/Security/AccessToken/ApiKeyTokenHandler.php +++ b/src/Security/AccessToken/ApiKeyTokenHandler.php @@ -14,7 +14,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; * to this application's UI. * * One key per caller, each resolving to its own user and roles: the websites posting application - * forms must not read placements, and n8n reading placements must not post forms. + * forms must not read assignments, and n8n reading assignments must not post forms. */ class ApiKeyTokenHandler implements AccessTokenHandlerInterface { diff --git a/tests/Controller/Api/Assignment/ListControllerTest.php b/tests/Controller/Api/Assignment/ListControllerTest.php index cec33b1..27a2708 100644 --- a/tests/Controller/Api/Assignment/ListControllerTest.php +++ b/tests/Controller/Api/Assignment/ListControllerTest.php @@ -18,7 +18,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\Response; /** - * The n8n placements feed against real rows in db_test: which assignments it selects, and what a + * The n8n assignments feed against real rows in db_test: which assignments it selects, and what a * place row says. How places are numbered is pinned in AssignmentTest. * * Every test runs inside a transaction that is rolled back, so db_test - shared between branches - @@ -65,24 +65,24 @@ class ListControllerTest extends WebTestCase public function testAKeylessRequestIsRejected(): void { - $this->client->request('GET', '/api/einsaetze?von=2026-12-01&bis=2026-12-31'); + $this->client->request('GET', '/api/assignments?from=2026-12-01&to=2026-12-31'); $this->assertSame(Response::HTTP_UNAUTHORIZED, $this->client->getResponse()->getStatusCode()); } public function testAWrongKeyIsRejected(): void { - $this->get('von=2026-12-01&bis=2026-12-31', 'wrong-key'); + $this->get('from=2026-12-01&to=2026-12-31', 'wrong-key'); $this->assertSame(Response::HTTP_UNAUTHORIZED, $this->client->getResponse()->getStatusCode()); } /** - * The websites' key must not read placements - it names people. + * The websites' key must not read the assignments feed - it names people. */ public function testTheApplicationFormKeyIsForbidden(): void { - $this->get('von=2026-12-01&bis=2026-12-31', self::APPLICATION_FORM_KEY); + $this->get('from=2026-12-01&to=2026-12-31', self::APPLICATION_FORM_KEY); $this->assertSame(Response::HTTP_FORBIDDEN, $this->client->getResponse()->getStatusCode()); } @@ -92,23 +92,23 @@ class ListControllerTest extends WebTestCase $this->get(''); $this->assertSame(Response::HTTP_BAD_REQUEST, $this->client->getResponse()->getStatusCode()); - $this->assertEqualsCanonicalizing(['von', 'bis'], array_column($this->decode()['errors'], 'field')); + $this->assertEqualsCanonicalizing(['from', 'to'], array_column($this->decode()['errors'], 'field')); } public function testAnUnusableDateIsRejected(): void { - $this->get('von=01.12.2026&bis=2026-12-31'); + $this->get('from=01.12.2026&to=2026-12-31'); $this->assertSame(Response::HTTP_BAD_REQUEST, $this->client->getResponse()->getStatusCode()); - $this->assertContains('von must be a date as YYYY-MM-DD.', array_column($this->decode()['errors'], 'message')); + $this->assertContains('from must be a date as YYYY-MM-DD.', array_column($this->decode()['errors'], 'message')); } public function testAnInvertedRangeIsRejected(): void { - $this->get('von=2026-12-31&bis=2026-12-01'); + $this->get('from=2026-12-31&to=2026-12-01'); $this->assertSame(Response::HTTP_BAD_REQUEST, $this->client->getResponse()->getStatusCode()); - $this->assertSame(['bis must not be before von.'], array_column($this->decode()['errors'], 'message')); + $this->assertSame(['to must not be before from.'], array_column($this->decode()['errors'], 'message')); } public function testAFilledPlaceNamesThePerson(): void @@ -119,7 +119,7 @@ class ListControllerTest extends WebTestCase $this->contract($disposition, Upload::STATUS_CHECKED); $this->entityManager->flush(); - $this->get('von=2026-12-01&bis=2026-12-31'); + $this->get('from=2026-12-01&to=2026-12-31'); $this->assertSame(Response::HTTP_OK, $this->client->getResponse()->getStatusCode()); $this->assertSame([[ @@ -151,7 +151,7 @@ class ListControllerTest extends WebTestCase $calledOff->setStatus(Disposition::STATUS_CALLED_OFF); $this->entityManager->flush(); - $this->get('von=2026-12-01&bis=2026-12-31'); + $this->get('from=2026-12-01&to=2026-12-31'); $rows = $this->decode(); $this->assertSame([$assignment->getId().'-1', $assignment->getId().'-2'], array_column($rows, 'platz_id')); @@ -170,7 +170,7 @@ class ListControllerTest extends WebTestCase $this->contract($disposition, Upload::STATUS_PENDING); $this->entityManager->flush(); - $this->get('von=2026-12-01&bis=2026-12-31'); + $this->get('from=2026-12-01&to=2026-12-31'); $this->assertSame('vertrag', $this->decode()[0]['status']); $this->assertFalse($this->decode()[0]['vertrag_liegt_vor']); @@ -181,7 +181,7 @@ class ListControllerTest extends WebTestCase $this->dispose($this->assignment()->setSkipFormalities(true), $this->teamer()); $this->entityManager->flush(); - $this->get('von=2026-12-01&bis=2026-12-31'); + $this->get('from=2026-12-01&to=2026-12-31'); $this->assertNull($this->decode()[0]['vertrag_liegt_vor']); } @@ -195,7 +195,7 @@ class ListControllerTest extends WebTestCase $this->dispose($this->assignment(), $this->teamer(dateOfBirth: '2000-12-06')); $this->entityManager->flush(); - $this->get('von=2026-12-01&bis=2026-12-31'); + $this->get('from=2026-12-01&to=2026-12-31'); $this->assertSame([false, true], array_column($this->decode(), 'unter_26')); } @@ -208,7 +208,7 @@ class ListControllerTest extends WebTestCase $this->assignment(from: '2027-01-01', to: '2027-01-08'); $this->entityManager->flush(); - $this->get('von=2026-12-01&bis=2026-12-31'); + $this->get('from=2026-12-01&to=2026-12-31'); $this->assertSame([$startsBefore->getId(), $endsAfter->getId()], array_column($this->decode(), 'einsatz_id')); } @@ -225,7 +225,7 @@ class ListControllerTest extends WebTestCase $this->assignment()->setJobProfile($otherProfile); $this->entityManager->flush(); - $this->get('von=2026-12-01&bis=2026-12-31'); + $this->get('from=2026-12-01&to=2026-12-31'); $this->assertSame([$included->getId()], array_column($this->decode(), 'einsatz_id')); } @@ -299,7 +299,7 @@ class ListControllerTest extends WebTestCase { $this->entityManager->clear(); - $this->client->request('GET', '/api/einsaetze?'.$query, server: [ + $this->client->request('GET', '/api/assignments?'.$query, server: [ 'HTTP_'.strtoupper(str_replace('-', '_', ApiKeyExtractor::HEADER)) => $key, ]); }