Story 1.16.1: Meaningful IDs for Public-Facing URLs

Last updated: November 23, 2025 Reading time: 28 minutes File size: 49.93 KB

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

  • 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.lastname identifiers
  • 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:

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:

sql
-- 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):

sql
-- 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

UI Components

New Components to Create:

  1. CompanyNameField Component

    typescript
    interface 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
  2. UsernameField Component

    typescript
    interface 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, .3 suffix 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)

  1. AC1: When creating a new event, the system automatically generates an event code in format BATbern{eventNumber} where eventNumber is the sequential event number

    • Event code is unique across all events
    • Event code is stored in event_code column
    • Event code is returned in API responses as the primary identifier
  2. 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 code
    • DELETE /api/v1/events/{eventCode} deletes event by code
    • Backend resolves event code to UUID internally
  3. 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”)

Companies (AC 4-7)

  1. 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
  2. AC5: Company entity has both name (ID) and displayName (UI label)

    • name used in URLs and as identifier (e.g., β€œGoogleZH”)
    • displayName shown in UI (e.g., β€œGoogle ZΓΌrich AG”)
    • Both fields required when creating company
    • Backend validates name format server-side
  3. 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”)
  4. 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)

  1. 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”
  2. 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
  3. 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
  4. 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)

  1. 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_creatingEvent
  • should_generateUniqueEventCode_when_multipleEventsCreated
  • should_formatEventCodeCorrectly_when_eventNumberProvided (e.g., BATbern56)
  • should_storeEventCode_when_eventPersisted

AC2 Tests - Event API Endpoints:

  • should_returnEvent_when_getByEventCode
  • should_return404_when_eventCodeNotFound
  • should_updateEvent_when_putWithEventCode
  • should_deleteEvent_when_deleteWithEventCode
  • should_resolveEventCodeToUUID_when_lookupPerformed

AC3 Tests - Frontend Event Routing:

  • should_navigateToEventDetail_when_eventCodeClicked
  • should_displayEventCodeInBreadcrumbs_when_onEventPage
  • should_loadEventData_when_routeParamIsEventCode

AC4 Tests - Company Name Validation:

  • should_acceptValidCompanyName_when_alphanumericOnly
  • should_rejectCompanyName_when_containsSpaces
  • should_rejectCompanyName_when_containsSpecialChars
  • should_rejectCompanyName_when_exceeds12Characters
  • should_autoSanitize_when_invalidCharactersEntered

AC5 Tests - Company Name vs Display Name:

  • should_storeNameAndDisplayName_when_companyCreated
  • should_returnDisplayName_when_companyRetrieved
  • should_validateNameFormat_when_serverSideValidation

AC6 Tests - Company API Endpoints:

  • should_returnCompany_when_getByCompanyName
  • should_returnCompany_when_getByCompanyName_caseInsensitive
  • should_updateCompany_when_putWithCompanyName

AC7 Tests - Company Name UX:

  • should_displayCharacterCount_when_typing
  • should_showHelpText_when_fieldFocused
  • should_displayErrorMessage_when_validationFails

AC8 Tests - Username Auto-Generation:

  • should_generateUsername_when_userCreated
  • should_convertGermanChars_when_generateUsername (Γ€β†’ae)
  • should_removeNonLetters_when_generateUsername
  • should_lowercaseUsername_when_generated

AC9 Tests - Username Editing:

  • should_allowUsernameEdit_when_onCreate
  • should_allowUsernameEdit_when_onUpdate
  • should_validateFormat_when_usernameChanged
  • should_regenerateUsername_when_refreshClicked

AC10 Tests - Username Collision Handling:

  • should_appendSuffix_when_usernameExists
  • should_incrementSuffix_when_multipleCollisions (.2, .3, .4)
  • should_throwError_when_usernameNotUnique

AC11 Tests - Role-Based User Routes:

  • should_loadUserProfile_when_accessViaUsersRoute
  • should_loadSpeakerView_when_accessViaSpeakersRoute
  • should_useSameUsername_when_differentRoleRoutes

AC12 Tests - Session Slug Generation:

  • should_generateSlug_when_sessionCreated
  • should_handleGermanChars_when_slugifyTitle
  • should_appendSuffix_when_slugExistsInEvent
  • should_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:

java
// 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):

java
@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:
  • 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.java
      • shared-kernel/src/test/java/ch/batbern/shared/types/CompanyIdTest.java
      • shared-kernel/src/test/java/ch/batbern/shared/types/UserIdTest.java
      • shared-kernel/src/test/java/ch/batbern/shared/types/SessionIdTest.java
  • Task 1.4: Implement ID Value Objects (GREEN Phase)

    • Update EventId to support eventCode field and fromEventCode()
    • Update CompanyId
    • Update UserId to support username field and fromUsername()
    • Create SessionId with slug
      • shared-kernel/src/main/java/ch/batbern/shared/types/EventId.java
      • shared-kernel/src/main/java/ch/batbern/shared/types/CompanyId.java
      • shared-kernel/src/main/java/ch/batbern/shared/types/UserId.java
      • shared-kernel/src/main/java/ch/batbern/shared/types/SessionId.java (create)

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__*.sql
      • services/company-user-management-service/src/main/resources/db/migration/V21__*.sql
      • services/company-user-management-service/src/main/resources/db/migration/V22__*.sql
      • services/event-management-service/src/main/resources/db/migration/V23__*.sql
  • 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:
  • 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.java
      • services/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:

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.java
      • services/company-user-management-service/src/main/java/ch/batbern/companyuser/domain/Company.java
  • 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.java
      • services/company-user-management-service/src/main/java/ch/batbern/companyuser/dto/CreateCompanyRequest.java
      • services/company-user-management-service/src/main/java/ch/batbern/companyuser/dto/CompanyResponse.java

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:
  • Task 5.4: User Service Implementation (GREEN Phase)

    • Inject SlugGenerationService into
    • 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.java
      • services/company-user-management-service/src/main/java/ch/batbern/companyuser/domain/User.java
  • 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 CreateUserRequest DTO: add optional username
    • Create UpdateUsernameRequest
    • Create @PatchMapping("/{username}/username")
    • Add username to UserResponse
      • services/company-user-management-service/src/main/java/ch/batbern/companyuser/controller/UserController.java
      • services/company-user-management-service/src/main/java/ch/batbern/companyuser/dto/CreateUserRequest.java
      • services/company-user-management-service/src/main/java/ch/batbern/companyuser/dto/UpdateUsernameRequest.java
      • services/company-user-management-service/src/main/java/ch/batbern/companyuser/dto/UserResponse.java

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.java
      • services/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.java
      • services/event-management-service/src/main/java/ch/batbern/events/service/SessionService.java
      • services/event-management-service/src/main/java/ch/batbern/events/domain/Session.java

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.ts
      • web-frontend/src/services/api/companiesApi.test.ts
      • web-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.ts
      • web-frontend/src/services/api/companiesApi.ts
      • web-frontend/src/services/api/usersApi.ts
      • web-frontend/src/types/api.ts

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:
  • 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
  • Task 10.3: Update Architecture Documentation

    • Update 03-data-architecture.md
    • Update 04-api-design.md

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:

java
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:

java
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:

java
// 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:

yaml
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:

yaml
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:

yaml
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:

java
// 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:

java
@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:

  1. Deploy database migrations (V20, V21, V22, V23)
  2. Deploy backend services (all services simultaneously - greenfield)
  3. Deploy frontend with updated routes
  4. Verify all E2E tests pass in staging
  5. Deploy to production

Rollback Procedure: Since this is greenfield (no existing data), rollback is straightforward:

  1. Revert frontend deployment
  2. Revert backend deployment
  3. Rollback database migrations (if needed)

QA Results

To be filled by QA Agent after implementation