Story 4.1.5b: Backend Implementation for Anonymous Event Registration
Table of Contents
- Story 4.1.5b: Backend Implementation for Anonymous Event Registration
- Dependencies
- Status
- ⚡ ADR-006 Simplification
- Story
- Domain Context
- Acceptance Criteria
- Tasks / Subtasks (TDD Workflow)
- Phase 1: Database Migrations (RED Phase)
- Phase 2: Entity Layer Updates (GREEN Phase)
- Phase 3: DTO Layer (GREEN Phase)
- Phase 4: Service Layer (GREEN Phase)
- Phase 5: Controller Layer (GREEN Phase)
- Phase 6: Security Configuration (GREEN Phase)
- Phase 7: Component Tests (RED-GREEN-REFACTOR)
- Phase 8: Integration & Documentation
- Dev Notes - Implementation Guide
- Architecture Alignment
- Breaking Changes
- Testing Strategy
- Definition of Done Checklist
- Change Log
Story 4.1.5b: Backend Implementation for Anonymous Event Registration
Dependencies
Depends on: Story 4.1.5a (Registration Architecture Foundation) Blocks: Story 4.1.5 (Registration Wizard Frontend)
Status
Not Started
⚡ ADR-006 Simplification
Unified user profile approach reduces implementation effort by ~50% vs dual-table (ADR-005):
- ❌ NO anonymous_* fields in Registration entity (7 fields removed)
- ❌ NO account linking logic in registrations (claimed_by, claimed_at removed)
- ✅ Simple: attendee_id always references user_profiles (anonymous users have cognito_user_id = NULL)
- ✅ Cleaner: Account linking is 3 lines of code (UPDATE user_profiles.cognito_user_id)
- ✅ Better: Company as FK to companies table (not free text)
Estimated Effort: 2-3 hours (vs 4-6 hours for ADR-005)
Story
As a backend developer, I want to implement anonymous event registration support in the Event Management Service, so that the frontend can support both authenticated and anonymous registration flows per ADR-006 (unified user profile approach).
Domain Context
Primary Domain
Attendee Experience Domain (Backend support for registration)
Involved Services
- Event Management Service (Java/Spring Boot)
- PostgreSQL (registrations table)
- API Gateway (public endpoint security configuration)
Cross-Domain Dependencies
- Depends on: Story 4.1.5a (Migrations V9 + V11, ADR-006, OpenAPI spec)
- Integrates with: Company-User-Management Service (user_profiles for account creation and linking)
- Security Pattern: Public endpoints using confirmation code as access token
- Key ADR-006 Benefit: 85% code reduction vs dual-table (ADR-005 rejected)
Acceptance Criteria
- Migrations Applied: V9 (registrations) + V11 (user_profiles with nullable cognito_user_id)
- User Profile Creation: POST
/api/v1/events/{eventCode}/registrationscreates user_profile with cognito_user_id = NULL for anonymous users - Confirmation Code Format: Generated in
BAT-YYYY-NNNNNNformat using PostgreSQL sequence - Public Retrieval: GET
/api/v1/events/{eventCode}/registrations/{registrationCode}works without authentication, returns user data from user_profiles - Public Update: PATCH
/api/v1/events/{eventCode}/registrations/{registrationCode}updates preferences without authentication - Public Cancellation: DELETE
/api/v1/events/{eventCode}/registrations/{registrationCode}cancels registration without authentication - Company FK Integrity: Registration request references company_id (FK to companies table), not free text
- Unified User Reference: registrations.attendee_id always NOT NULL (references user_profiles)
- Security Configuration: Anonymous endpoints use
permitAll(), admin endpoints remainauthenticated() - Component Tests Pass: All registration flows tested with real PostgreSQL (Testcontainers)
- Account Linking Works: When anonymous user creates Cognito account, user_profile.cognito_user_id updated (3 lines of code)
Tasks / Subtasks (TDD Workflow)
Phase 1: Database Migrations (RED Phase)
- Task 1: Apply Migrations V9 + V11 to Dev Databases
- Event Management Service:
- Verify
registrations - Verify
attendee_id
- Verify
- Company-User-Management Service:
- Verify
cognito_user_id - Verify
claimed_at
- Verify
- Event Management Service:
Phase 2: Entity Layer Updates (GREEN Phase)
-
Task 2: Update Registration Entity (Simplified - ADR-006)
- Keep
@Table(name = "registrations") - Rename field:
registrationCode→ - Keep
attendeeIdas NOT NULL:@Column(nullable = false) - Remove old fields:
attendeeName, - NO anonymous_ fields
- NO account linking fields
-
-
newsletterSubscribed -
eventReminders -
specialRequests -
sessionPreferences -
attendanceConfirmed -
actualAttendance
-
- Add
@PrePersist - Simplification
- Keep
-
Task 3: Update RegistrationRepository (Simplified - ADR-006)
- Rename method:
findByEventCodeAndRegistrationCode()→ - Add method:
- NO anonymous email methods
- Simplification
- Rename method:
Phase 3: DTO Layer (GREEN Phase)
-
Task 4: Create New CreateRegistrationRequest DTO
- Required fields:
email,firstName,lastName, - Optional fields:
company,role,communicationPreferences, - Breaking Change: Remove
attendeeUsername,attendeeName
- Required fields:
-
Task 5: Create New PatchRegistrationRequest DTO
- Fields:
specialRequests,
- Fields:
-
Task 6: Update Registration Response DTO
- Add
registrationCodefield (rename fromregistrationCode - Conditional logic: Show
anonymousEmailXORattendeeId
- Add
Phase 4: Service Layer (GREEN Phase)
- Task 7: Create RegistrationService
- Method:
- Check for duplicate:
- Method:
- Method:
- Method:
- Method:
- Method:
Phase 5: Controller Layer (GREEN Phase)
-
Task 8: Update RegistrationController
- BREAKING CHANGE: Rename all
{registrationCode}path params → -
- Use new
CreateRegistrationRequest - Call
- Use new
- Add GET endpoint:
- Call
-
- Change param:
{registrationCode}→ - Use new
PatchRegistrationRequest - Call
- Change param:
-
- Change param:
{registrationCode}→ - Call
- Change param:
- BREAKING CHANGE: Rename all
-
Task 9: Add QR Code Endpoint (Story 4.1.6 dependency)
- Add GET endpoint:
- Support optional
?size=300
Phase 6: Security Configuration (GREEN Phase)
- Task 10: Update SecurityConfig
- File:
- Add to
permitAll()java.requestMatchers(HttpMethod.POST, "/api/v1/events/*/registrations").permitAll() // Anonymous registration .requestMatchers(HttpMethod.GET, "/api/v1/events/*/registrations/*").permitAll() // Public lookup by confirmation code .requestMatchers(HttpMethod.PATCH, "/api/v1/events/*/registrations/*").permitAll() // Public update .requestMatchers(HttpMethod.DELETE, "/api/v1/events/*/registrations/*").permitAll() // Public cancel .requestMatchers(HttpMethod.GET, "/api/v1/events/*/registrations/*/qr").permitAll() // QR code - Keep admin endpoints authenticated:
GET /api/v1/events/*/registrations
Phase 7: Component Tests (RED-GREEN-REFACTOR)
-
Task 11: Write Component Tests for Anonymous Registration
- Test 11.1:
- Test 11.2:
- Test 11.3:
- Test 11.1:
-
Task 12: Write Component Tests for Public Retrieval
- Test 12.1:
- Test 12.2:
- Test 12.1:
-
Task 13: Write Component Tests for Public Update
- Test 13.1:
- Test 13.2:
- Test 13.1:
-
Task 14: Write Component Tests for Public Cancellation
- Test 14.1:
- Test 14.2:
- Test 14.1:
-
Task 15: Write Component Tests for Mutual Exclusivity
- Test 15.1:
- Test 15.2:
- Test 15.1:
-
Task 16: Write Component Tests for Backward Compatibility
- Test 16.1:
- Test 16.1:
Phase 8: Integration & Documentation
-
Task 17: Run All Tests
- Execute:
-
Task 18: Update Story Documentation
Dev Notes - Implementation Guide
Confirmation Code Generation
Database Sequence (already in V9 migration):
CREATE SEQUENCE IF NOT EXISTS registration_code_seq START 1;
Entity @PrePersist Method:
@PrePersist
protected void onCreate() {
if (this.registrationCode == null) {
// Generate confirmation code: BAT-YYYY-NNNNNN
int year = LocalDate.now().getYear();
Long sequenceValue = // fetch NEXTVAL('registration_code_seq') via native query
this.registrationCode = String.format("BAT-%d-%06d", year, sequenceValue);
}
this.createdAt = Instant.now();
this.updatedAt = Instant.now();
}
Alternative (simpler): Let database DEFAULT generate confirmation code, then read it back after INSERT.
Mutual Exclusivity Validation
Database Level (V9 migration):
ADD CONSTRAINT check_registration_type
CHECK (
(attendee_id IS NOT NULL AND anonymous_email IS NULL) OR
(attendee_id IS NULL AND anonymous_email IS NOT NULL)
);
Application Level (optional additional validation):
@PrePersist
@PreUpdate
protected void validateRegistrationType() {
boolean hasAttendee = attendeeId != null;
boolean hasAnonymous = anonymousEmail != null;
if (hasAttendee && hasAnonymous) {
throw new IllegalStateException("Registration must be EITHER authenticated OR anonymous, not both");
}
if (!hasAttendee && !hasAnonymous) {
throw new IllegalStateException("Registration must have either attendeeId or anonymousEmail");
}
}
Security Configuration Example
SecurityConfig.java:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
// Public registration endpoints (anonymous access)
.requestMatchers(HttpMethod.POST, "/api/v1/events/*/registrations").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/events/*/registrations/*").permitAll()
.requestMatchers(HttpMethod.PATCH, "/api/v1/events/*/registrations/*").permitAll()
.requestMatchers(HttpMethod.DELETE, "/api/v1/events/*/registrations/*").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/events/*/registrations/*/qr").permitAll()
// Admin endpoints (authentication required)
.requestMatchers(HttpMethod.GET, "/api/v1/events/*/registrations").authenticated()
// All other requests require authentication
.anyRequest().authenticated()
);
return http.build();
}
DTO Mapping Examples
Anonymous Registration Request → Entity:
Registration registration = new Registration();
registration.setEventId(event.getId()); // Internal UUID
registration.setEventCode(eventCode); // Public identifier
registration.setAttendeeId(null); // Anonymous registration
registration.setAnonymousEmail(request.getEmail());
registration.setAnonymousFirstName(request.getFirstName());
registration.setAnonymousLastName(request.getLastName());
registration.setAnonymousCompany(request.getCompany());
registration.setAnonymousRole(request.getRole());
registration.setNewsletterSubscribed(request.getCommunicationPreferences().getNewsletterSubscribed());
registration.setEventReminders(request.getCommunicationPreferences().getEventReminders());
registration.setSpecialRequests(request.getSpecialRequests());
registration.setStatus("confirmed");
// registrationCode auto-generated by @PrePersist
Entity → Registration Response:
RegistrationResponse response = new RegistrationResponse();
response.setConfirmationCode(registration.getConfirmationCode());
response.setEventCode(registration.getEventCode());
// Conditional: show EITHER anonymous OR authenticated fields
if (registration.getAnonymousEmail() != null) {
response.setAnonymousEmail(registration.getAnonymousEmail());
response.setAnonymousFirstName(registration.getAnonymousFirstName());
response.setAnonymousLastName(registration.getAnonymousLastName());
response.setAnonymousCompany(registration.getAnonymousCompany());
response.setAnonymousRole(registration.getAnonymousRole());
} else {
response.setAttendeeId(registration.getAttendeeId());
}
response.setStatus(registration.getStatus());
response.setNewsletterSubscribed(registration.getNewsletterSubscribed());
response.setEventReminders(registration.getEventReminders());
response.setSpecialRequests(registration.getSpecialRequests());
response.setRegistrationDate(registration.getRegistrationDate());
QR Code Generation (Story 4.1.6 dependency)
Add ZXing dependency to build.gradle:
dependencies {
implementation 'com.google.zxing:core:3.5.1'
implementation 'com.google.zxing:javase:3.5.1'
}
QRCodeService.java:
@Service
public class QRCodeService {
public byte[] generateQRCode(String data, int width, int height) throws IOException {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(data, BarcodeFormat.QR_CODE, width, height);
ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
return pngOutputStream.toByteArray();
}
}
Controller Method:
@GetMapping("/registrations/{registrationCode}/qr")
public ResponseEntity<byte[]> getRegistrationQR(
@PathVariable String eventCode,
@PathVariable String registrationCode,
@RequestParam(defaultValue = "300") int size
) throws IOException {
Registration registration = registrationService.getRegistrationByConfirmationCode(eventCode, registrationCode);
// Generate QR code with registration data (JSON format)
String qrData = String.format(
"{\"registrationCode\":\"%s\",\"eventCode\":\"%s\"}",
registrationCode, eventCode
);
byte[] qrCodeImage = qrCodeService.generateQRCode(qrData, size, size);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
headers.setContentLength(qrCodeImage.length);
return ResponseEntity.ok()
.headers(headers)
.body(qrCodeImage);
}
Architecture Alignment
This story implements the backend portion of ADR-005 (Anonymous Event Registration):
ADR-005 Key Decisions Implemented:
- ✅ Anonymous registration without authentication (public POST endpoint)
- ✅ Confirmation code as access token (public GET/PATCH/DELETE by confirmation code)
- ✅ Email-based account linking foundation (anonymous_email field, future UserCreatedEvent handler)
- ✅ Mutually exclusive patterns (CHECK constraint enforced)
- ✅ Privacy-first design (minimal PII storage, no unnecessary authentication)
OpenAPI Spec Alignment:
- ✅ All endpoints match events-api.openapi.yml (Story 4.1.5a update)
- ✅ DTOs match schemas in lines 2111-2190
- ✅ Security patterns match
security: []annotations - ✅ Confirmation code format matches pattern
^BAT-[0-9]{4}-[0-9]{6}$
Breaking Changes
1. Table Rename
Before: @Table(name = "registrations")
After: @Table(name = "registrations")
Impact: All JPA queries, native SQL, and references must update
2. Field Rename
Before: registrationCode (String)
After: registrationCode (String)
Impact: All controller paths, DTOs, service methods must update
3. DTO Changes
Before:
CreateRegistrationRequest {
attendeeUsername (required)
attendeeName (required)
attendeeEmail (required)
}
After:
CreateRegistrationRequest {
email (required)
firstName (required)
lastName (required)
company (optional)
role (optional)
termsAccepted (required)
}
Impact: Frontend must update API client calls
4. Path Parameter Changes
Before: /api/v1/events/{eventCode}/registrations/{registrationCode}
After: /api/v1/events/{eventCode}/registrations/{registrationCode}
Impact: Frontend routing, API client methods must update
Testing Strategy
Component Tests (Testcontainers)
- PostgreSQL: Use real database with V9 migration applied
- Test Data Builders: RegistrationTestDataBuilder for both patterns
- Coverage Target: >90% for new registration service methods
Manual Testing Checklist
- Run migration V9:
./gradlew :services:event-management-service:flywayMigrate - Verify table structure:
\d registrationsin psql - Test anonymous registration via curl (no auth header)
- Test public retrieval with confirmation code
- Test update preferences without auth
- Test cancellation without auth
- Test backward compatibility with authenticated registrations
Definition of Done Checklist
Development Complete
Infrastructure Complete
Documentation Complete
Review Ready
Change Log
| Date | Version | Description | Author |
|---|---|---|---|
| 2025-11-08 | 1.0 | Initial story creation - Backend implementation for anonymous registration (Story 4.1.5a implementation) | James (Developer Agent) |
| 2025-11-08 | 2.0 | Simplified for ADR-006 - Unified user profile approach reduces effort by 50%, removes anonymous_* fields, simplifies account linking | Winston (Architect Agent) |