Story 1.16.1: Meaningful IDs for Public-Facing URLs
Table of Contents
- Story 1.16.1: Meaningful IDs for Public-Facing URLs
- Status
- Story
- Domain Context
- Requirements Context
- Architecture Context
- Wireframe Context
- Acceptance Criteria
- Test Specifications (TDD)
- Tasks / Subtasks (TDD Workflow)
- Phase 1: Shared Kernel Infrastructure
- Phase 2: Database Migrations
- Phase 3: Backend - Event Service
- Phase 4: Backend - Company Service
- Phase 5: Backend - User Service
- Phase 6: Backend - Session Service
- Phase 7: Frontend - Components
- Phase 8: Frontend - API Services
- Phase 9: Frontend - Routing
- Phase 10: E2E Testing & Documentation
- Phase 11: Refactoring & Optimization (REFACTOR)
- Dev Notes - Implementation Guide
- Definition of Done Checklist
- Change Log
- Dev Agent Record
- QA Results
Story 1.16.1: Meaningful IDs for Public-Facing URLs
Status
Done
Story
As a BATbern platform user (attendee, organizer, or partner), I want meaningful, human-readable URLs for events, companies, and user profiles, so that I can easily share links, remember URLs, and navigate the platform intuitively without dealing with cryptic UUIDs.
Domain Context
Primary Domain
Shared/Infrastructure - Cross-cutting concern affecting all bounded contexts:
- Event Management Domain (event codes for public sharing)
- Company Management Domain (company name identifiers)
- User Management Domain (username-based profiles)
- Speaker Coordination Domain (uses user identities)
- Attendee Experience Domain (content discovery via friendly URLs)
Involved Services
- All Microservices: This is a shared-kernel infrastructure change
- Event Management Service (event codes)
- Company User Management Service (company names, usernames)
- Speaker Coordination Service (uses usernames for speakers)
- Partner Coordination Service (uses company names and usernames)
- Attendee Experience Service (session slugs, event codes)
- API Gateway (routing updates for friendly IDs)
Cross-Domain Dependencies
- Shared Kernel: ID value objects (EventId, CompanyId, UserId, SessionId)
- Database Layer: All service databases need schema migrations
- API Gateway: URL routing patterns updated for friendly IDs
- Frontend: React Router and API service layer updates
Requirements Context
Related Functional Requirements
- FR-Infrastructure-001: Platform must support user-friendly, shareable URLs
- FR-SEO-001: Public-facing pages must have SEO-friendly URL structures
- FR-UX-001: URLs should be intuitive and memorable for end users
- FR-API-001: API endpoints must support both UUID and friendly ID lookups (transition period)
Workflow Steps (if applicable)
Not directly tied to the 16-step event workflow, but affects:
- All Steps: Event management screens will use
BATbern{number}in URLs - Steps 2-4: Speaker coordination uses
firstname.lastnameidentifiers - Steps 1-16: Company references use short company names
Acceptance Criteria Source
This is a greenfield infrastructure story. Acceptance criteria defined based on architectural requirements and user experience goals.
Architecture Context
Architecture Patterns
Referenced Architecture Documents:
- API Design (04-api-design.md): RESTful patterns, path parameter handling, identifier resolution
- Frontend (05-frontend-architecture.md): React Router v6 patterns, API service layer design
- Backend (06-backend-architecture.md): DDD value objects, repository patterns, service layer
- Data Models (03-data-architecture.md): Entity schemas, unique constraints, indexing strategy
Key Patterns Applied:
- Value Object Pattern: ID types encapsulate both UUID and friendly identifier
- Repository Pattern: Add
findBy{FriendlyId}()methods alongside UUID lookups - Slug Generation Pattern: German character handling, collision resolution
- Dual Identifier Strategy: UUID as primary key, friendly ID as unique alternate key
Database Schema Reference
Architecture Schema (Source of Truth):
- Doc: docs/architecture/03-data-architecture.md
- Sections:
- Events (Section: Event Management Service Database Schema)
- Companies (Section: Company Management Service Database Schema)
- Users (Section: User Management Service Database Schema)
- Sessions (Section: Event Management Service Database Schema)
Migration Check (Dev Agent completes in Task 0):
- Existing Migrations: To be identified in Task 0
- Status: β New migrations required for all entity types
- Action: Create V*__add_meaningful_ids.sql migrations for each service
Schema Changes Required:
-- Events table enhancement
ALTER TABLE events
ADD COLUMN event_code VARCHAR(20) UNIQUE NOT NULL;
CREATE UNIQUE INDEX idx_events_event_code ON events(event_code);
-- Companies table enhancement
ALTER TABLE companies
ADD COLUMN display_name VARCHAR(255) NOT NULL;
ALTER TABLE companies
ADD CONSTRAINT chk_company_name_format CHECK (name ~ '^[a-zA-Z0-9]{1,12}$');
CREATE UNIQUE INDEX idx_companies_name_lower ON companies(LOWER(name));
-- Users table enhancement
ALTER TABLE users
ADD COLUMN username VARCHAR(100) UNIQUE;
CREATE UNIQUE INDEX idx_users_username ON users(username);
ALTER TABLE users
ADD CONSTRAINT chk_username_format CHECK (username ~ '^[a-z]+\\.[a-z]+(\\.[0-9]+)?$');
-- Sessions table enhancement
ALTER TABLE sessions
ADD COLUMN slug VARCHAR(150);
CREATE UNIQUE INDEX idx_sessions_slug_event ON sessions(event_id, slug);
Infrastructure Components
AWS Services:
- API Gateway: Route patterns updated to accept friendly IDs (
/events/{eventCode}vs/events/{id}) - RDS PostgreSQL: Schema migrations, unique indexes on friendly ID columns
- ElastiCache: Cache friendly ID β UUID mappings for performance
- Lambda: No specific Lambda changes (API Gateway + ECS services)
Database Indexes (Performance Critical):
-- Events
CREATE UNIQUE INDEX idx_events_event_code ON events(event_code);
-- Companies (case-insensitive)
CREATE UNIQUE INDEX idx_companies_name_lower ON companies(LOWER(name));
-- Users
CREATE UNIQUE INDEX idx_users_username ON users(username);
-- Sessions (composite, event-scoped)
CREATE UNIQUE INDEX idx_sessions_slug_event ON sessions(event_id, slug);
Wireframe Context
Wireframe References
- No specific wireframes - This is backend/infrastructure focused
- Impact on existing screens:
- Event detail URLs change from
/events/{uuid}to/events/BATbern56 - Company detail URLs change from
/companies/{uuid}to/companies/GoogleZH - User profile URLs change from
/users/{uuid}to/users/john.doe
- Event detail URLs change from
UI Components
New Components to Create:
-
CompanyNameField Component
typescriptinterface CompanyNameFieldProps { value: string; onChange: (value: string) => void; error?: string; helperText?: string; }- Real-time validation: 12 chars max, alphanumeric only
- Auto-sanitization: removes invalid characters
- Material-UI TextField with InputAdornment
-
UsernameField Component
typescriptinterface UsernameFieldProps { firstName: string; lastName: string; value: string; onChange: (value: string) => void; onGenerate: () => void; // Regenerate button error?: string; }- Auto-generation from first/last name
- Collision handling UI (shows
.2,.3suffix if needed) - Editable with validation
- Refresh icon button to regenerate
Modified Components:
- EventDetailScreen: Update routing, breadcrumbs to use event codes
- CompanyManagementScreen: Use company name field with validation
- UserProfileScreen: Display username prominently, allow editing
Acceptance Criteria
Events (AC 1-3)
-
AC1: When creating a new event, the system automatically generates an event code in format
BATbern{eventNumber}whereeventNumberis the sequential event number- Event code is unique across all events
- Event code is stored in
event_codecolumn - Event code is returned in API responses as the primary identifier
-
AC2: API endpoints accept event code as path parameter
GET /api/v1/events/{eventCode}returns event by code (e.g.,BATbern56)PUT /api/v1/events/{eventCode}updates event by codeDELETE /api/v1/events/{eventCode}deletes event by code- Backend resolves event code to UUID internally
-
AC3: Frontend uses event codes in all URLs and navigation
- Event detail page route:
/events/{eventCode} - Session detail route:
/events/{eventCode}/sessions/{sessionSlug} - Registration route:
/events/{eventCode}/register - Breadcrumbs display event code (e.g., βBATbern 56β)
- Event detail page route:
Companies (AC 4-7)
-
AC4: Company name field enforces ID-compliant format in frontend
- Maximum 12 characters
- Alphanumeric only (a-z, A-Z, 0-9)
- Real-time validation with clear error messages
- Auto-sanitization removes invalid characters
-
AC5: Company entity has both
name(ID) anddisplayName(UI label)nameused in URLs and as identifier (e.g., βGoogleZHβ)displayNameshown in UI (e.g., βGoogle ZΓΌrich AGβ)- Both fields required when creating company
- Backend validates
nameformat server-side
-
AC6: API endpoints accept company name as path parameter
GET /api/v1/companies/{companyName}returns company (case-insensitive)PUT /api/v1/companies/{companyName}updates company- Company name lookup is case-insensitive (βGoogleZHβ = βgooglezhβ)
-
AC7: Company name validation provides user-friendly feedback
- Frontend shows character count (e.g., β8/12 charactersβ)
- Error message: βOnly letters and numbers allowed (no spaces)β
- Suggestion: βExample: GoogleZH, UBS, Swisscomβ
Users (AC 8-11)
-
AC8: Username auto-generated when creating new user
- Format:
{firstname}.{lastname}(lowercase, slugified) - German characters converted (Γ€βae, ΓΆβoe, ΓΌβue)
- Non-letter characters removed
- Example: βPeter MΓΌllerβ β βpeter.muellerβ
- Format:
-
AC9: Username is editable in frontend during create and update
- Username field shown with auto-generated value pre-filled
- User can edit to custom username (must follow format)
- Refresh button regenerates from first/last name
- Real-time validation shows format requirements
-
AC10: Collision handling appends numeric suffix automatically
- If βjohn.doeβ exists, next is βjohn.doe.2β
- Then βjohn.doe.3β, βjohn.doe.4β, etc.
- Backend handles collision detection via unique constraint
- Frontend displays collision error, offers incremented version
-
AC11: Speaker/Organizer/Partner routes use same username (role-based views)
/users/john.doe- Generic user profile/speakers/john.doe- Speaker-specific view (same user)/organizers/john.doe- Organizer-specific view (same user)- No separate βSpeakerIdβ - roles are properties on User entity
Sessions (AC 12)
- AC12: Session slug auto-generated from title, unique within event
- Slugify session title (lowercase, hyphens, remove special chars)
- Handle German characters (Γ€βae, ΓΆβoe, ΓΌβue, Γβss)
- Collision handling within event: append
-2,-3, etc. - URL format:
/events/{eventCode}/sessions/{sessionSlug} - Example: βBlockchain Security 101β β βblockchain-security-101β
Test Specifications (TDD)
Test Scenarios by Acceptance Criteria
AC1 Tests - Event Code Generation:
should_generateEventCode_when_creatingEventshould_generateUniqueEventCode_when_multipleEventsCreatedshould_formatEventCodeCorrectly_when_eventNumberProvided(e.g., BATbern56)should_storeEventCode_when_eventPersisted
AC2 Tests - Event API Endpoints:
should_returnEvent_when_getByEventCodeshould_return404_when_eventCodeNotFoundshould_updateEvent_when_putWithEventCodeshould_deleteEvent_when_deleteWithEventCodeshould_resolveEventCodeToUUID_when_lookupPerformed
AC3 Tests - Frontend Event Routing:
should_navigateToEventDetail_when_eventCodeClickedshould_displayEventCodeInBreadcrumbs_when_onEventPageshould_loadEventData_when_routeParamIsEventCode
AC4 Tests - Company Name Validation:
should_acceptValidCompanyName_when_alphanumericOnlyshould_rejectCompanyName_when_containsSpacesshould_rejectCompanyName_when_containsSpecialCharsshould_rejectCompanyName_when_exceeds12Charactersshould_autoSanitize_when_invalidCharactersEntered
AC5 Tests - Company Name vs Display Name:
should_storeNameAndDisplayName_when_companyCreatedshould_returnDisplayName_when_companyRetrievedshould_validateNameFormat_when_serverSideValidation
AC6 Tests - Company API Endpoints:
should_returnCompany_when_getByCompanyNameshould_returnCompany_when_getByCompanyName_caseInsensitiveshould_updateCompany_when_putWithCompanyName
AC7 Tests - Company Name UX:
should_displayCharacterCount_when_typingshould_showHelpText_when_fieldFocusedshould_displayErrorMessage_when_validationFails
AC8 Tests - Username Auto-Generation:
should_generateUsername_when_userCreatedshould_convertGermanChars_when_generateUsername(Γ€βae)should_removeNonLetters_when_generateUsernameshould_lowercaseUsername_when_generated
AC9 Tests - Username Editing:
should_allowUsernameEdit_when_onCreateshould_allowUsernameEdit_when_onUpdateshould_validateFormat_when_usernameChangedshould_regenerateUsername_when_refreshClicked
AC10 Tests - Username Collision Handling:
should_appendSuffix_when_usernameExistsshould_incrementSuffix_when_multipleCollisions(.2, .3, .4)should_throwError_when_usernameNotUnique
AC11 Tests - Role-Based User Routes:
should_loadUserProfile_when_accessViaUsersRouteshould_loadSpeakerView_when_accessViaSpeakersRouteshould_useSameUsername_when_differentRoleRoutes
AC12 Tests - Session Slug Generation:
should_generateSlug_when_sessionCreatedshould_handleGermanChars_when_slugifyTitleshould_appendSuffix_when_slugExistsInEventshould_allowDuplicateSlugs_when_differentEvents
Test File Locations
Backend Tests:
Event Service Tests:
- Unit: services/event-management-service/src/test/java/ch/batbern/events/service/EventServiceTest.java
- Integration: services/event-management-service/src/test/java/ch/batbern/events/controller/EventControllerIntegrationTest.java
- Repository: services/event-management-service/src/test/java/ch/batbern/events/repository/EventRepositoryTest.java
Company Service Tests:
- Unit: services/company-user-management-service/src/test/java/ch/batbern/companyuser/service/CompanyServiceTest.java
- Integration: services/company-user-management-service/src/test/java/ch/batbern/companyuser/controller/CompanyControllerIntegrationTest.java
- Repository: services/company-user-management-service/src/test/java/ch/batbern/companyuser/repository/CompanyRepositoryTest.java
User Service Tests:
- Unit: services/company-user-management-service/src/test/java/ch/batbern/companyuser/service/UserServiceTest.java
- Integration: services/company-user-management-service/src/test/java/ch/batbern/companyuser/controller/UserControllerIntegrationTest.java
Shared Kernel Tests:
- Unit: shared-kernel/src/test/java/ch/batbern/shared/types/EventIdTest.java
- Unit: shared-kernel/src/test/java/ch/batbern/shared/types/CompanyIdTest.java
- Unit: shared-kernel/src/test/java/ch/batbern/shared/types/UserIdTest.java
- Unit: shared-kernel/src/test/java/ch/batbern/shared/service/SlugGenerationServiceTest.java
Frontend Tests:
Component Tests:
- web-frontend/src/components/Company/CompanyNameField.test.tsx
- web-frontend/src/components/User/UsernameField.test.tsx
- web-frontend/src/components/Events/EventDetailScreen.test.tsx
Service Tests:
- web-frontend/src/services/api/eventsApi.test.ts
- web-frontend/src/services/api/companiesApi.test.ts
- web-frontend/src/services/api/usersApi.test.ts
Routing Tests:
- web-frontend/src/App.test.tsx (routing scenarios)
E2E Tests:
E2E Workflows:
- bruno-tests/events-api/get-event-by-code.bru
- bruno-tests/companies-api/get-company-by-name.bru
- bruno-tests/users-api/get-user-by-username.bru
Test Data & Mocks
Test Data Builders:
// EventTestDataBuilder
public class EventTestDataBuilder {
private Integer eventNumber = 56;
private String eventCode = "BATbern56";
public EventTestDataBuilder withEventNumber(int number) {
this.eventNumber = number;
this.eventCode = "BATbern" + number;
return this;
}
public Event build() {
return Event.builder()
.id(UUID.randomUUID())
.eventNumber(eventNumber)
.eventCode(eventCode)
.title("Test Event " + eventNumber)
.build();
}
}
// UserTestDataBuilder
public class UserTestDataBuilder {
private String firstName = "John";
private String lastName = "Doe";
private String username = "john.doe";
public UserTestDataBuilder withName(String first, String last) {
this.firstName = first;
this.lastName = last;
this.username = slugify(first) + "." + slugify(last);
return this;
}
public User build() {
return User.builder()
.id(UUID.randomUUID())
.cognitoUserId("us-east-1_test123")
.firstName(firstName)
.lastName(lastName)
.username(username)
.email(username + "@test.com")
.build();
}
}
Test Containers (MANDATORY):
@Testcontainers
@SpringBootTest
public abstract class AbstractIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("batbern_test")
.withUsername("test")
.withPassword("test")
.withReuse(true);
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
registry.add("spring.jpa.hibernate.ddl-auto", () -> "validate");
registry.add("spring.flyway.enabled", () -> "true");
}
}
CRITICAL: All backend integration tests MUST use PostgreSQL via Testcontainers. NO H2 or other in-memory databases.
Tasks / Subtasks (TDD Workflow)
Phase 1: Shared Kernel Infrastructure
-
Task 1.1: Create SlugGenerationService (RED Phase)
- Write failing test:
- Write failing test:
- Write failing test:
- Write failing test:
- Write failing test:
- File:
-
Task 1.2: Implement SlugGenerationService (GREEN Phase)
- Implement
slugify(String text) - Implement
slugifyName(String name) - Implement
generateUsername(String first, String last) - Implement
ensureUniqueUsername(String baseUsername) - Implement
generateSessionSlug(String title, UUID eventId) - File:
- Implement
-
Task 1.3: Update ID Value Objects (RED Phase)
- Write failing test:
EventId.fromEventCode("BATbern56") - Write failing test:
CompanyId.from("GoogleZH") - Write failing test:
UserId.fromUsername("john.doe") - Write failing test:
SessionId.generate(title) -
shared-kernel/src/test/java/ch/batbern/shared/types/EventIdTest.javashared-kernel/src/test/java/ch/batbern/shared/types/CompanyIdTest.javashared-kernel/src/test/java/ch/batbern/shared/types/UserIdTest.javashared-kernel/src/test/java/ch/batbern/shared/types/SessionIdTest.java
- Write failing test:
-
Task 1.4: Implement ID Value Objects (GREEN Phase)
- Update
EventIdto supporteventCodefield andfromEventCode() - Update
CompanyId - Update
UserIdto supportusernamefield andfromUsername() - Create
SessionIdwithslug -
shared-kernel/src/main/java/ch/batbern/shared/types/EventId.javashared-kernel/src/main/java/ch/batbern/shared/types/CompanyId.javashared-kernel/src/main/java/ch/batbern/shared/types/UserId.javashared-kernel/src/main/java/ch/batbern/shared/types/SessionId.java(create)
- Update
Phase 2: Database Migrations
-
Task 2.1: Create Flyway Migrations
- Create migration: sql
ALTER TABLE events ADD COLUMN event_code VARCHAR(20) UNIQUE; CREATE UNIQUE INDEX idx_events_event_code ON events(event_code); ALTER TABLE events ADD CONSTRAINT chk_event_code_format CHECK (event_code ~ '^BATbern[0-9]+$'); - Create migration: sql
ALTER TABLE companies ADD COLUMN display_name VARCHAR(255) NOT NULL DEFAULT 'temp'; ALTER TABLE companies ADD CONSTRAINT chk_company_name_format CHECK (name ~ '^[a-zA-Z0-9]{1,12}$'); CREATE UNIQUE INDEX idx_companies_name_lower ON companies(LOWER(name)); - Create migration: sql
ALTER TABLE users ADD COLUMN username VARCHAR(100) UNIQUE; CREATE UNIQUE INDEX idx_users_username ON users(username); ALTER TABLE users ADD CONSTRAINT chk_username_format CHECK (username ~ '^[a-z]+\\.[a-z]+(\\.[0-9]+)?$'); - Create migration: sql
ALTER TABLE sessions ADD COLUMN slug VARCHAR(150); CREATE UNIQUE INDEX idx_sessions_slug_event ON sessions(event_id, slug); -
services/event-management-service/src/main/resources/db/migration/V20__*.sqlservices/company-user-management-service/src/main/resources/db/migration/V21__*.sqlservices/company-user-management-service/src/main/resources/db/migration/V22__*.sqlservices/event-management-service/src/main/resources/db/migration/V23__*.sql
- Create migration:
-
Task 2.2: Test Migrations with Testcontainers
Phase 3: Backend - Event Service
-
Task 3.1: Event Repository Methods (RED Phase)
- Write failing test:
- Write failing test:
- File:
-
Task 3.2: Event Repository Implementation (GREEN Phase)
- Add method:
- Add method:
- File:
-
Task 3.3: Event Service Logic (RED Phase)
- Write failing test:
EventService.create() - Write failing test:
EventService.findByEventCode() - File:
- Write failing test:
-
Task 3.4: Event Service Implementation (GREEN Phase)
- Implement event code generation on create:
- Implement
findByEventCode(String code) - Update entity to include
eventCode -
services/event-management-service/src/main/java/ch/batbern/events/service/EventService.javaservices/event-management-service/src/main/java/ch/batbern/events/domain/Event.java
-
Task 3.5: Event Controller Endpoints (RED Phase)
- Write failing integration test:
- Write failing integration test:
- Write failing integration test:
- File:
-
Task 3.6: Event Controller Implementation (GREEN Phase)
- Update
@GetMapping("/{eventCode}") - Update
@PutMapping("/{eventCode}") - Update
@DeleteMapping("/{eventCode}") - File:
- Update
Phase 4: Backend - Company Service
-
Task 4.1: Company Repository Methods (RED Phase)
- Write failing test:
- Write failing test:
- File:
-
Task 4.2: Company Repository Implementation (GREEN Phase)
- Add method:
- Add method:
- File:
-
Task 4.3: Company Service Logic (RED Phase)
- File:
-
Task 4.4: Company Service Implementation (GREEN Phase)
- Implement
create() - Implement
findByName(String name) - Update Company entity: add
displayName -
services/company-user-management-service/src/main/java/ch/batbern/companyuser/service/CompanyService.javaservices/company-user-management-service/src/main/java/ch/batbern/companyuser/domain/Company.java
- Implement
-
Task 4.5: Company Controller & DTOs (RED Phase)
- Write failing test:
- Write failing test:
POST /api/v1/companies - File:
-
Task 4.6: Company Controller & DTOs Implementation (GREEN Phase)
- Update
@GetMapping("/{companyName}") - Update
CreateCompanyRequest - Add
@Pattern - Add
displayName -
services/company-user-management-service/src/main/java/ch/batbern/companyuser/controller/CompanyController.javaservices/company-user-management-service/src/main/java/ch/batbern/companyuser/dto/CreateCompanyRequest.javaservices/company-user-management-service/src/main/java/ch/batbern/companyuser/dto/CompanyResponse.java
- Update
Phase 5: Backend - User Service
-
Task 5.1: User Repository Methods (RED Phase)
- Write failing test:
- Write failing test:
- File:
-
Task 5.2: User Repository Implementation (GREEN Phase)
- Add method:
- Add method:
- File:
-
Task 5.3: User Service Logic (RED Phase)
- Write failing test: collision handling appends
.2, - File:
- Write failing test: collision handling appends
-
Task 5.4: User Service Implementation (GREEN Phase)
- Inject
SlugGenerationServiceinto - Implement username generation on create:
- Implement collision handling using
- Implement
updateUsername() - Update User entity: add
username -
services/company-user-management-service/src/main/java/ch/batbern/companyuser/service/UserService.javaservices/company-user-management-service/src/main/java/ch/batbern/companyuser/domain/User.java
- Inject
-
Task 5.5: User Controller & DTOs (RED Phase)
- Write failing test:
- Write failing test:
POST /api/v1/users - Write failing test:
PATCH /api/v1/users/{username}/username - File:
-
Task 5.6: User Controller & DTOs Implementation (GREEN Phase)
- Update
@GetMapping("/{username}") - Update
CreateUserRequestDTO: add optionalusername - Create
UpdateUsernameRequest - Create
@PatchMapping("/{username}/username") - Add
usernametoUserResponse -
services/company-user-management-service/src/main/java/ch/batbern/companyuser/controller/UserController.javaservices/company-user-management-service/src/main/java/ch/batbern/companyuser/dto/CreateUserRequest.javaservices/company-user-management-service/src/main/java/ch/batbern/companyuser/dto/UpdateUsernameRequest.javaservices/company-user-management-service/src/main/java/ch/batbern/companyuser/dto/UserResponse.java
- Update
Phase 6: Backend - Session Service
-
Task 6.1: Session Repository & Service (RED Phase)
- Write failing test:
- Write failing test:
-
services/event-management-service/src/test/java/ch/batbern/events/repository/SessionRepositoryTest.javaservices/event-management-service/src/test/java/ch/batbern/events/service/SessionServiceTest.java
-
Task 6.2: Session Repository & Service Implementation (GREEN Phase)
- Update Session entity: add
slug -
services/event-management-service/src/main/java/ch/batbern/events/repository/SessionRepository.javaservices/event-management-service/src/main/java/ch/batbern/events/service/SessionService.javaservices/event-management-service/src/main/java/ch/batbern/events/domain/Session.java
- Update Session entity: add
Phase 7: Frontend - Components
-
Task 7.1: CompanyNameField Component (RED Phase)
- File:
-
Task 7.2: CompanyNameField Component Implementation (GREEN Phase)
- File:
-
Task 7.3: UsernameField Component (RED Phase)
- File:
-
Task 7.4: UsernameField Component Implementation (GREEN Phase)
- File:
Phase 8: Frontend - API Services
-
Task 8.1: Update API Services (RED Phase)
- Write failing test:
- Write failing test:
- Write failing test:
- Write failing test:
-
web-frontend/src/services/api/eventsApi.test.tsweb-frontend/src/services/api/companiesApi.test.tsweb-frontend/src/services/api/usersApi.test.ts
-
Task 8.2: API Services Implementation (GREEN Phase)
- Update
eventsApi.getEvent() - Update
companiesApi.getCompany() - Update
usersApi.getUser() - Add
usersApi.updateUsername() -
web-frontend/src/services/api/eventsApi.tsweb-frontend/src/services/api/companiesApi.tsweb-frontend/src/services/api/usersApi.tsweb-frontend/src/types/api.ts
- Update
Phase 9: Frontend - Routing
-
Task 9.1: Update React Router Routes (RED Phase)
- Write failing test: navigate to
/events/BATbern56 - Write failing test: navigate to
/companies/GoogleZH - Write failing test: navigate to
/users/john.doe - Write failing test: navigate to
/speakers/john.doe - File:
- Write failing test: navigate to
-
Task 9.2: React Router Implementation (GREEN Phase)
- Update route:
- Update route:
- Update route:
- Add route:
- Add route:
- File:
Phase 10: E2E Testing & Documentation
-
Task 10.1: Create Bruno E2E Tests
- Create:
- Create:
- Create:
- Create:
-
Task 10.2: Update OpenAPI Documentation
- Update Events API spec with
eventCode - Update Companies API spec with
companyName - Update Users API spec with
username
- Update Events API spec with
-
Task 10.3: Update Architecture Documentation
- Update
03-data-architecture.md - Update
04-api-design.md
- Update
Phase 11: Refactoring & Optimization (REFACTOR)
-
Task 11.1: Backend Refactoring
-
Task 11.2: Frontend Refactoring
-
Task 11.3: Performance Testing
-
Task 11.4: Final Verification
Dev Notes - Implementation Guide
Service Setup
Directory Structure:
services/
βββ event-management-service/
β βββ src/main/java/ch/batbern/events/
β β βββ domain/Event.java (add eventCode field)
β β βββ domain/Session.java (add slug field)
β β βββ repository/EventRepository.java (add findByEventCode)
β β βββ repository/SessionRepository.java (add findBySlugAndEventId)
β β βββ controller/EventController.java (update path params)
β βββ src/main/resources/db/migration/
β βββ V20__add_event_code_column.sql
β βββ V23__add_session_slug.sql
β
βββ company-user-management-service/
β βββ src/main/java/ch/batbern/companyuser/
β β βββ domain/Company.java (add displayName field)
β β βββ domain/User.java (add username field)
β β βββ repository/CompanyRepository.java (add findByNameIgnoreCase)
β β βββ repository/UserRepository.java (add findByUsername)
β β βββ controller/CompanyController.java (update path params)
β β βββ controller/UserController.java (update path params, add PATCH)
β βββ src/main/resources/db/migration/
β βββ V21__add_company_display_name.sql
β βββ V22__add_user_username.sql
β
βββ shared-kernel/
βββ src/main/java/ch/batbern/shared/
β βββ types/EventId.java (add eventCode support)
β βββ types/CompanyId.java (add validation)
β βββ types/UserId.java (add username support)
β βββ types/SessionId.java (create new)
β βββ service/SlugGenerationService.java (create new)
βββ src/test/java/ch/batbern/shared/
βββ types/EventIdTest.java
βββ types/CompanyIdTest.java
βββ types/UserIdTest.java
βββ service/SlugGenerationServiceTest.java
Frontend Structure:
web-frontend/src/
βββ components/
β βββ Company/
β β βββ CompanyNameField.tsx (create new)
β β βββ CompanyNameField.test.tsx (create new)
β βββ User/
β βββ UsernameField.tsx (create new)
β βββ UsernameField.test.tsx (create new)
β
βββ services/api/
β βββ eventsApi.ts (update to use event codes)
β βββ companiesApi.ts (update to use company names)
β βββ usersApi.ts (update to use usernames)
β
βββ types/
β βββ api.ts (update type definitions)
β
βββ App.tsx (update routes)
Technical Design Notes
Slug Generation Algorithm:
private String slugify(String text) {
return text.toLowerCase()
// German characters
.replaceAll("Γ€", "ae")
.replaceAll("ΓΆ", "oe")
.replaceAll("ΓΌ", "ue")
.replaceAll("Γ", "ss")
// French characters
.replaceAll("Γ©", "e")
.replaceAll("Γ¨", "e")
.replaceAll("Γ ", "a")
.replaceAll("Γͺ", "e")
.replaceAll("Γ§", "c")
// Remove diacritics
.normalize("NFD")
.replaceAll("\\p{M}", "")
// Convert to URL-safe
.replaceAll("[^a-z0-9]+", "-")
.replaceAll("^-|-$", "")
.substring(0, Math.min(text.length(), 100)); // Max 100 chars
}
Collision Handling Pattern:
private String ensureUniqueUsername(String baseUsername) {
if (!userRepository.existsByUsername(baseUsername)) {
return baseUsername;
}
// Find next available suffix
int suffix = 2;
while (userRepository.existsByUsername(baseUsername + "." + suffix)) {
suffix++;
}
return baseUsername + "." + suffix;
}
ID Resolution Strategy:
// In controllers - accept both UUID and friendly ID
@GetMapping("/events/{idOrCode}")
public ResponseEntity<EventResponse> getEvent(@PathVariable String idOrCode) {
Event event;
if (isUUID(idOrCode)) {
event = eventService.findById(UUID.fromString(idOrCode));
} else {
event = eventService.findByEventCode(idOrCode);
}
return ResponseEntity.ok(toResponse(event));
}
private boolean isUUID(String str) {
try {
UUID.fromString(str);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
API Contracts
Events API:
paths:
/api/v1/events/{eventCode}:
get:
summary: Get event by code
parameters:
- name: eventCode
in: path
required: true
schema:
type: string
pattern: '^BATbern[0-9]+$'
example: 'BATbern56'
responses:
'200':
description: Event found
content:
application/json:
schema:
$ref: '#/components/schemas/EventResponse'
'404':
description: Event not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
message: 'Event not found: BATbern56'
timestamp: '2025-01-15T10:30:00Z'
status: 404
Companies API:
paths:
/api/v1/companies/{companyName}:
get:
summary: Get company by name (case-insensitive)
parameters:
- name: companyName
in: path
required: true
schema:
type: string
pattern: '^[a-zA-Z0-9]{1,12}$'
example: 'GoogleZH'
responses:
'200':
description: Company found
content:
application/json:
schema:
$ref: '#/components/schemas/CompanyResponse'
Users API:
paths:
/api/v1/users/{username}:
get:
summary: Get user by username
parameters:
- name: username
in: path
required: true
schema:
type: string
pattern: '^[a-z]+\.[a-z]+(\.[0-9]+)?$'
example: 'john.doe'
responses:
'200':
description: User found
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
/api/v1/users/{username}/username:
patch:
summary: Update username
parameters:
- name: username
in: path
required: true
schema:
type: string
example: 'john.doe'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- newUsername
properties:
newUsername:
type: string
pattern: '^[a-z]+\.[a-z]+(\.[0-9]+)?$'
example: 'john.smith'
responses:
'200':
description: Username updated successfully
'409':
description: Username already taken
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
message: 'Username already taken: john.smith. Try: john.smith.2'
timestamp: '2025-01-15T10:30:00Z'
status: 409
Testing Requirements
Coverage Targets:
- Unit tests: >90% coverage
- Integration tests: >80% coverage
- E2E tests: All happy paths + critical error scenarios
Test Data Requirements:
// Event test data
EventTestDataBuilder.aEvent()
.withEventNumber(56)
.withEventCode("BATbern56")
.build();
// Company test data
CompanyTestDataBuilder.aCompany()
.withName("GoogleZH")
.withDisplayName("Google ZΓΌrich AG")
.build();
// User test data with collision
UserTestDataBuilder.aUser()
.withName("Peter", "MΓΌller")
.withUsername("peter.mueller")
.build();
UserTestDataBuilder.aUser()
.withName("Peter", "MΓΌller")
.withUsername("peter.mueller.2") // Collision handling
.build();
PostgreSQL Test Configuration:
@Testcontainers
@SpringBootTest
public abstract class AbstractIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("batbern_test")
.withUsername("test")
.withPassword("test")
.withReuse(true); // Reuse container across tests for speed
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
registry.add("spring.jpa.hibernate.ddl-auto", () -> "validate");
registry.add("spring.flyway.enabled", () -> "true");
}
}
Definition of Done Checklist
Development Complete
Schema Validation Complete
Migration Alignment:
Entity Validation:
Documentation:
Infrastructure Complete
Frontend Complete
Review Ready
Change Log
| Date | Version | Description | Author |
|---|---|---|---|
| 2025-01-15 | 1.0 | Initial story creation | Claude (Sonnet 4.5) |
Dev Agent Record
Agent Model Used
To be filled by dev agent during implementation
Implementation Approach
To be filled by dev agent during implementation
Debug Log References
To be filled by dev agent during implementation
Completion Notes
To be filled by dev agent during implementation
Files Changed
To be filled by dev agent during implementation:
Created:
- List of new files
Modified:
- List of changed files
Deleted:
- List of removed files (if any)
Deployment Notes
Migration Order:
- Deploy database migrations (V20, V21, V22, V23)
- Deploy backend services (all services simultaneously - greenfield)
- Deploy frontend with updated routes
- Verify all E2E tests pass in staging
- Deploy to production
Rollback Procedure: Since this is greenfield (no existing data), rollback is straightforward:
- Revert frontend deployment
- Revert backend deployment
- Rollback database migrations (if needed)
QA Results
To be filled by QA Agent after implementation