PHASE 1: Core Backend v1.0

Trigger: "Execute Phase 1 as per masterplan."

Goal: The core backend is functional: entities, authentication (with ~16 roles and permission table), members, contracts, and products can be managed via REST API. Database schema is created via Flyway. OpenAPI documentation is generated.

Prerequisite: Phase 0 is completed.

Step 1.0 — Technical Conception

Create doc/developer/conception/phase-1-conception.md.

(reference: Chapter 06 — Module Architecture, Chapter 11 — Data Model, Chapter 13 — Security)

Content: - Scope: 5 modules (entity, auth, member, contract, product) + OpenAPI setup - Component Design: Mermaid diagram showing module dependencies and data flow - Interface Contracts: REST endpoint signatures for all 5 modules (from Chapter 10) - Data Flow: Request -> Controller -> Service -> Repository -> DB; JWT filter chain - Entity Design: All fields from Chapter 11 for Organization, User, Member, MembershipTemplate, Contract, Product, AuditLog - Error Handling: GlobalExceptionHandler patterns, error codes, i18n error messages - Edge Cases: Concurrent contract purchase, member number generation race condition, bcrypt timing attacks - Security: JWT RS256 flow, refresh token rotation, brute-force protection (Redis counter) - Roles & Permissions: Permission evaluation flow (JWT claims -> Spring Security -> @PreAuthorize) - Acceptance Criteria: Checklist matching Phase 1 Quality Gate - Design Chapter References: Ch06, Ch07, Ch10, Ch11, Ch13

Result: Technical conception document for Phase 1.


Step 1.1 — Entity/Organization Module

Create the membership-entity module.

(reference: Chapter 11 — Data Model, Chapter 10 — API Design)

Approach:

  1. Create JPA entity: Organization - @Version for optimistic locking - @EntityListeners(AuditingEntityListener.class) for createdAt/updatedAt - JSONB columns with @JdbcTypeCode(SqlTypes.JSON) for settings and custom attributes - Fields (from Chapter 11):
Field Type Constraints Description
id Long PK, generated Primary key
name String(200) NOT NULL Organization display name
slug String(100) UNIQUE, NOT NULL URL-safe identifier (auto-generated from name)
parentId Long FK nullable Parent organization (franchise hierarchy)
type String(30) NOT NULL CLUB / FRANCHISE / MANUFACTURER
street String(200) Address line
city String(100) City
postalCode String(20) Postal/ZIP code
country String(2) NOT NULL, default 'DE' ISO 3166-1 alpha-2
state String(100) State/province
contactEmail String(200) NOT NULL Primary contact email
contactPhone String(50) Phone (E.164 format)
website String(500) Website URL
logoUrl String(500) Logo image URL (stored in S3)
timezone String(50) default 'Europe/Berlin' IANA timezone
locale String(10) default 'de_DE' Default locale
currency String(3) default 'EUR' ISO 4217 currency code
tier String(20) default 'STARTER' STARTER / TEAM / PROFESSIONAL / ENTERPRISE
status String(20) default 'TRIAL' TRIAL / ACTIVE / SUSPENDED / DEACTIVATED
trialExpiresAt Instant nullable Trial expiration date
billingDay Integer default 1 Day of month for billing cycle (1-28)
settings JSONB default '{}' Extensible settings (password policy, feature toggles, branding)
customAttributeDefinitions JSONB default '[]' Custom member attribute schema
createdAt Instant auto Creation timestamp
updatedAt Instant auto Last modification
version Long @Version Optimistic lock
  • Settings JSONB schema: json { "passwordPolicy": { "minLength": 8, "requireUppercase": true, "requireNumber": true }, "featureToggles": { "shopEnabled": false, "crmEnabled": false }, "branding": { "primaryColor": "#2CC5CE", "accentColor": "#FF5722" }, "homepageConfig": { "heroTitle": "", "showPricing": true, "showSchedule": true } }
  1. Create DTOs: OrganizationDto, CreateOrganizationRequest, UpdateOrganizationRequest, OrganizationSettingsDto
  2. Create repository: OrganizationRepository extends JpaRepository - Custom queries: findBySlug, findByParentId, searchByNameContaining
  3. Create service: OrganizationService - CRUD operations - Parent-child hierarchy traversal (getChildren, getAncestors) - Settings management (JSONB merge, not overwrite) - Slug generation (transliterate + lowercase + deduplicate) - Tier enforcement (feature access based on tier)
  4. Create controller: OrganizationController - POST /api/entity — Create organization (SUPER_ADMIN only) - GET /api/entity/{id} — Get entity details - GET /api/entity/search?name={query}&type={type}&page={n}&size={n} — Search entities (paginated) - PUT /api/entity/{id} — Update entity - GET /api/entity/{id}/children — List child entities (franchise) - GET /api/entity/{id}/settings — Get entity settings - PUT /api/entity/{id}/settings — Update entity settings (JSONB merge)
  5. Create Flyway migration: V100__create_organization.sql - Table, sequence, indexes on slug (unique), parentId, status, type
  6. Create test: OrganizationServiceTest (JUnit 5, Mockito) - Tests: CRUD, slug generation, hierarchy traversal, settings merge, tier enforcement

Validation:

cd backend && ./mvnw test -pl membership-entity

Result: Organization entity with CRUD API, Flyway migration, and tests.


Step 1.2 — Authentication Module

Create the membership-auth module.

(reference: Chapter 13 — Security and Compliance, Chapter 07 — User Roles, ADR-AUTH-001)

Approach:

  1. Create JPA entities: User, UserRole - User fields (from Chapter 11):
Field Type Constraints Description
id Long PK, generated Primary key
entityId Long FK NOT NULL Organization reference
email String(200) NOT NULL Login email (unique per entity)
passwordHash String(200) NOT NULL bcrypt hash (cost factor 12)
firstName String(100) NOT NULL First name
lastName String(100) NOT NULL Last name
phone String(50) Phone (E.164)
locale String(10) default 'en' Preferred language for UI and emails
status String(30) default 'PENDING_VERIFICATION' PENDING_VERIFICATION / ACTIVE / LOCKED / DEACTIVATED
emailVerifiedAt Instant nullable When email was verified
lastLoginAt Instant nullable Last successful login
failedLoginCount Integer default 0 Failed login attempts (reset on success)
lockedUntil Instant nullable Lock expiration (brute-force protection)
refreshTokenHash String(200) nullable SHA-256 hash of current refresh token
refreshTokenExpiresAt Instant nullable Refresh token expiration
totpSecret String(200) nullable TOTP secret for 2FA (encrypted at rest)
totpEnabled boolean default false Whether 2FA is active
createdAt Instant auto
updatedAt Instant auto
version Long @Version Optimistic lock
  • UserRole: join table (userId, roleCode) with composite PK
  • Unique constraint: (entityId, email) — same email allowed in different organizations
  1. Create DTOs: - LoginRequest (email, password, totpCode?) - LoginResponse (accessToken, refreshToken, expiresIn, user: {id, email, firstName, lastName, roles[], permissions[], locale, entityId}) - RegisterRequest (email, password, firstName, lastName, entityId, locale?) - ChangePasswordRequest (currentPassword, newPassword) - ForgotPasswordRequest (email) - ResetPasswordRequest (token, newPassword) - UserDto, UpdateUserRequest

  2. Create service: AuthService - Register: validate email uniqueness, hash password, create user with PENDING_VERIFICATION, send verification email (via Communication queue) - Login: validate credentials, check status/lock, increment failedLoginCount on failure, generate tokens - Verify email: validate token (UUID, 24h expiry), set emailVerifiedAt, activate user - Resend verification: rate limited (1 per minute) - Forgot password: generate reset token (UUID, 1h expiry), send via email - Reset password: validate token, hash new password, invalidate all sessions - Refresh token: validate refresh token hash, rotate (issue new pair, invalidate old) - Logout: clear refresh token hash from DB - Brute-force protection (Redis):

    • Key: auth:failed:{entityId}:{email}, TTL: 15 min
    • Lock after 5 failures, unlock after 15 min
    • Return same error for wrong email and wrong password (prevent enumeration)
  3. Create JwtProvider (RS256 asymmetric signing): - Key pair from configuration files: JWT_PRIVATE_KEY_PATH, JWT_PUBLIC_KEY_PATH (NEVER hardcoded) - Access token: 15 min TTL - Refresh token: 30 days TTL, stored as SHA-256 hash in DB - JWT claims structure: json { "sub": "user-123", "iss": "membership-one", "iat": 1708000000, "exp": 1708000900, "entityId": 456, "roles": ["CLUB_ADMIN", "ACCOUNTANT"], "permissions": ["MEMBER_READ", "MEMBER_WRITE", "BILLING_READ", "BILLING_EXECUTE", "ACCOUNTING_READ"], "locale": "de", "tier": "PROFESSIONAL" } - Refresh token: opaque UUID, only hash stored server-side

  4. Create JwtAuthenticationFilter and PermissionEvaluator: - Filter: extract Bearer token, validate signature + expiration, set SecurityContext - @PreAuthorize("hasPermission('MEMBER_WRITE')") on controller methods - Permissions loaded from JWT claims (no DB lookup per request) - Tenant isolation: all queries filtered by entityId from JWT claims

  5. Create controller: AuthController (11 endpoints) - POST /api/auth/register — Register new user - POST /api/auth/login — Login (returns tokens) - POST /api/auth/verify-email?token={token} — Verify email address - POST /api/auth/resend-verification — Resend verification email - POST /api/auth/forgot-password — Request password reset - POST /api/auth/reset-password — Reset password with token - POST /api/auth/refresh — Refresh access token - POST /api/auth/logout — Invalidate refresh token - POST /api/auth/change-password — Change password (authenticated) - GET /api/auth/me — Get current user profile - PUT /api/auth/me — Update current user profile (name, locale, phone)

  6. Create Flyway migrations: V101__create_user.sql, V102__create_user_role.sql - Indexes: (entityId, email) unique, status, lastLoginAt - user_role: composite PK (user_id, role_code), FK to role seed table

  7. Create tests: AuthServiceTest, JwtProviderTest, PermissionEvaluatorTest - AuthServiceTest: register, login success/failure, brute-force lock, verify email, refresh rotation - JwtProviderTest: generate, validate, expired, tampered, claims extraction - PermissionEvaluatorTest: has permission, missing permission, role hierarchy

Result: Complete auth system with JWT RS256, ~16 roles, permission matrix, brute-force protection.


Step 1.3 — Member Management Module

Create the membership-member module.

(reference: Chapter 11 — Data Model, Chapter 10 — API Design)

Approach:

  1. Create JPA entities: Member, AuditLog - Member fields (from Chapter 11):
Field Type Constraints Description
id Long PK, generated
entityId Long FK NOT NULL Organization
memberNumber String(30) UNIQUE per entity Auto-generated (entity prefix + sequence)
firstName String(100) NOT NULL
lastName String(100) NOT NULL
email String(200) Contact email (not login email)
phone String(50) E.164 format
dateOfBirth LocalDate Required for age-restricted memberships
gender String(20) MALE / FEMALE / DIVERSE / UNSPECIFIED
street String(200)
city String(100)
postalCode String(20)
country String(2) default 'DE' ISO 3166-1
photoUrl String(500) Profile photo (S3 URL)
emergencyContactName String(200) Required for minors
emergencyContactPhone String(50)
responsiblePersonId Long FK nullable Legal guardian for minors
customAttributes JSONB default '{}' Entity-defined custom fields
notes String(2000) Internal admin notes
status String(20) default 'ACTIVE' ACTIVE / INACTIVE / SUSPENDED
joinDate LocalDate NOT NULL Date member joined
exitDate LocalDate nullable Date member left
userId Long FK nullable Linked user account (for self-service)
createdAt Instant auto
updatedAt Instant auto
version Long @Version
  • AuditLog: id, entityId, entityType (MEMBER/CONTRACT/ORGANIZATION), entityIdRef, action (CREATE/UPDATE/DELETE), changedFields (JSONB: {field: {old, new}}), performedBy, performedAt
  1. Create DTOs: MemberDto, CreateMemberRequest, UpdateMemberRequest, MemberSearchCriteria, MemberListItemDto (lightweight for table views)
  2. Create repository: MemberRepository with custom queries - searchByEntityIdAndCriteria(Long entityId, MemberSearchCriteria criteria, Pageable pageable) - Criteria: name (full-text), email, status, memberNumber, joinDate range, membership template
  3. Create service: MemberService - CRUD (create, read, update, soft-delete), search with pagination - Emergency contacts, responsible person (minors), custom attributes (JSONB) - Member number generation: SELECT nextval('member_number_seq_{entityId}') FOR UPDATE
    • Format: configurable per entity (default: M-{5-digit-sequence}, e.g., M-00001)
    • Audit logging on all mutations (AuditLog entry with JSON diff)
    • Custom attribute validation against entity's customAttributeDefinitions
    • Age calculation for age-restricted plans
    • Link member to user account (for self-service portal)
  4. Create controller: MemberController - POST /api/members — Create member - GET /api/members/{id} — Get member details - PUT /api/members/{id} — Update member - DELETE /api/members/{id} — Soft-delete (set status INACTIVE, set exitDate) - GET /api/members/search?name={q}&status={s}&page={n}&size={n}&sort={field,dir} — Search (paginated) - GET /api/members/{id}/audit — Get audit history - POST /api/members/{id}/link-user — Link member to user account
  5. Create Flyway migrations: V103__create_member.sql, V104__create_audit_log.sql - Indexes: (entityId, memberNumber) unique, (entityId, lastName, firstName), (entityId, email), (entityId, status) - Sequence per entity for member numbers (created dynamically by service)
  6. Create tests: MemberServiceTest - Tests: CRUD, search with pagination, member number generation (concurrent), custom attributes, audit log, age validation

Result: Member management with search, pagination, custom attributes, and audit trail.


Step 1.4 — Contract/Membership Module

Create the membership-contract module.

(reference: Chapter 11 — Data Model, Chapter 10 — API Design)

Approach:

  1. Create JPA entities: MembershipTemplate, Contract - MembershipTemplate fields:
Field Type Constraints Description
id Long PK
entityId Long FK NOT NULL Organization
name String(200) NOT NULL e.g., "Premium Membership"
description String(2000) Public description
price BigDecimal(19,4) NOT NULL Monthly price
currency String(3) default 'EUR' ISO 4217
billingCycle String(20) NOT NULL MONTHLY / QUARTERLY / SEMI_ANNUAL / ANNUAL
minimumTermMonths Integer default 1 Minimum contract duration
cancellationNoticeDays Integer default 30 Days notice before term end
autoRenew boolean default true Auto-renew at term end
ageMin Integer nullable Minimum age (null = no restriction)
ageMax Integer nullable Maximum age
maxMembers Integer nullable Capacity limit (null = unlimited)
visible boolean default true Show in public catalog
sortOrder Integer default 0 Display order
features JSONB default '[]' Included features list
status String(20) default 'ACTIVE' ACTIVE / ARCHIVED
createdAt Instant auto
updatedAt Instant auto
version Long @Version
  • Contract fields:
Field Type Constraints Description
id Long PK
entityId Long FK NOT NULL
memberId Long FK NOT NULL
templateId Long FK NOT NULL MembershipTemplate
status String(20) NOT NULL See state machine below
startDate LocalDate NOT NULL Contract start
endDate LocalDate nullable Term end (null = open-ended after minimum)
cancelledAt Instant nullable When cancellation was requested
cancelledBy Long nullable User who cancelled
cancellationReason String(500) nullable
monthlyPrice BigDecimal(19,4) NOT NULL Locked-in price at purchase time
currency String(3) default 'EUR'
signedAt Instant nullable Digital signature timestamp
signatureIp String(50) nullable IP at signing (audit)
signatureUserAgent String(500) nullable User agent at signing (audit)
createdAt Instant auto
updatedAt Instant auto
version Long @Version
  • Contract status state machine: PENDING_SIGNATURE → ACTIVE → CANCELLED → EXPIRED (auto, at endDate) → SUSPENDED (admin action, e.g., payment failure) SUSPENDED → ACTIVE (resumed) → CANCELLED (if not resolved)
  1. Create DTOs, repositories, services: - MembershipTemplateService — CRUD, visibility management, capacity check, pricing history - ContractService:
    • purchase(memberId, templateId) — Validate age, capacity, create PENDING_SIGNATURE contract
    • sign(contractId, signatureData) — Record signature, activate contract, schedule first billing
    • cancel(contractId, reason) — Validate cancellation notice period, set cancelledAt
    • suspend(contractId) — Admin suspend (e.g., payment failure)
    • resume(contractId) — Resume from SUSPENDED
    • expire() — Scheduled: find contracts past endDate, set EXPIRED
    • renew() — Scheduled: auto-renew contracts approaching endDate (if autoRenew = true)
  2. Create controllers: - MembershipTemplateController:
    • POST /api/membership-templates — Create template (CLUB_ADMIN)
    • GET /api/membership-templates — List templates for entity (admin)
    • GET /api/membership-templates/catalog/{entityId} — Public catalog (visible only)
    • PUT /api/membership-templates/{id} — Update template
    • DELETE /api/membership-templates/{id} — Archive template
    • ContractController:
    • POST /api/contracts — Purchase membership (creates contract)
    • POST /api/contracts/{id}/sign — Sign contract
    • GET /api/contracts/{id} — Get contract details
    • POST /api/contracts/{id}/cancel — Cancel contract
    • GET /api/contracts/member/{memberId} — List contracts for member
  3. Business rules: - Cancellation notice: contract cannot be cancelled if remaining days < cancellationNoticeDays - Auto-renewal: create new contract with same template at term end - Age restrictions: member's age must be between template.ageMin and template.ageMax - Capacity: count active contracts per template, reject if >= maxMembers - Price lock: contract stores price at purchase time (template price changes don't affect existing)
  4. Create Flyway migrations: V105__create_membership_template.sql, V106__create_contract.sql - Indexes: (entityId, status) on both, (memberId) on contract, (templateId) on contract
  5. Create tests: MembershipTemplateServiceTest, ContractServiceTest - ContractServiceTest: purchase, sign, cancel within notice, cancel past notice (rejected), expire, renew, suspend/resume, age restriction, capacity limit

Result: Full membership lifecycle with state machine, cancellation rules, auto-renewal, and price locking.


Step 1.5 — Product/Service Module

Create the membership-product module.

(reference: Chapter 11 — Data Model, Chapter 10 — API Design)

Approach:

  1. Create JPA entity: Product - Product fields:
Field Type Constraints Description
id Long PK
entityId Long FK NOT NULL Organization
name String(255) NOT NULL e.g., "Personal Training Session"
description String(2000) Public description
categoryCd String(50) NOT NULL COURSE, PERSONAL_TRAINING, MERCHANDISE, RENTAL, ADD_ON, DAY_PASS
typeCd String(50) NOT NULL ONE_TIME, RECURRING, PER_SESSION
statusCd String(50) default 'ACTIVE' ACTIVE / ARCHIVED / DRAFT
price BigDecimal(19,4) NOT NULL Base price
currency String(3) default 'EUR' ISO 4217
vatRate BigDecimal(5,2) NOT NULL e.g., 19.00 or 7.00
vatIncluded boolean default true Price includes VAT
maxCapacity Integer nullable Capacity limit (courses, events)
durationMinutes Integer nullable Session/class duration
isShopItem boolean default false Available in member shop
stockQuantity Integer nullable Inventory count (null = unlimited)
sortOrder Integer default 0 Display order in catalog
imageUrl String(500) nullable Product/course image
customAttributes JSONB default '{}' Extensible fields (SKU, difficulty, etc.)
visible boolean default true Show in public catalog
createdAt Instant auto
updatedAt Instant auto
version Long @Version
  1. Create DTOs, repository, service: - ProductService:
    • CRUD with tenant isolation (entityId from JWT)
    • Catalog query: visible products by category, sorted by sortOrder
    • Stock management: decrement on purchase, increment on cancel/return
    • Price calculation: getGrossPrice(), getNetPrice(), getVatAmount() based on vatIncluded flag
    • Category statistics: count products per category, revenue per category
  2. Create controller: - ProductController:
    • POST /api/products — Create product (CLUB_ADMIN)
    • GET /api/products — List products for entity (admin, paginated, filterable by category/status)
    • GET /api/products/catalog/{entityId} — Public catalog (visible + ACTIVE only, no auth required)
    • GET /api/products/{id} — Get product details
    • PUT /api/products/{id} — Update product
    • DELETE /api/products/{id} — Archive product (soft delete: status -> ARCHIVED)
  3. Create Flyway migration: V107__create_product.sql - Indexes: (entityId, statusCd), (entityId, categoryCd), (entityId, visible, sortOrder)
  4. Create tests: ProductServiceTest - Test cases: CRUD, catalog filtering, stock decrement/increment, VAT calculation, capacity check

Result: Product/service management with catalog, stock tracking, and VAT calculation.


Step 1.6 — OpenAPI Documentation

  1. Add springdoc-openapi-starter-webmvc-ui
  2. Configure info, security scheme (Bearer JWT), @Operation annotations
  3. Group by tag (Auth, Member, Contract, Product, Entity)

Result: Swagger UI at /api/swagger-ui.html.


Step 1.7 — Database Schema Validation

  1. Fresh database, run all Flyway migrations (V000, V001, V100-V107)
  2. Verify tables, sequences, FKs, indexes
  3. Insert test data via API, verify integrity

Result: Schema matches Chapter 11.


Step 1.8 — Phase 1 Documentation

  1. Create doc/developer/backend-guide.md (project structure, conventions, build/run)
  2. Create doc/developer/database-guide.md (schema, Flyway, entity patterns)
  3. Create doc/developer/api-reference.md (auth flow, endpoints, examples)

Step 1.9 — Update Intranet

Run python doc/intranet/build.py.


Phase 1 — Quality Gate

# Check Target
1 Conception document doc/developer/conception/phase-1-conception.md exists
2 Compilation ./mvnw clean compile — 0 errors
3 Tests ./mvnw test — 0 failures
4 Coverage >= 60% line coverage (JaCoCo)
5 App starts /api/health returns 200
6 Full flow Register -> verify -> login -> create member -> purchase membership
7 Permissions Unauthenticated -> 401; wrong role -> 403
8 OpenAPI /api/swagger-ui.html shows all endpoints
9 Security No hardcoded secrets; JWT RS256; bcrypt; @Version on all entities
10 Documentation backend-guide, database-guide, api-reference exist
11 Intranet regenerated
12 CLAUDE.md updated

Report: "Phase 1 completed." with entity count, endpoint count, test count.

---