Event Registration

1. Overview

The Event Registration workflow enables users to register participants for events such as races, tournaments, and competitions. Unlike membership registration, event registration is simpler with a single-page person selection and immediate registration submission.

Key Features:

  • Multi-participant Registration - Register multiple people in a single transaction

  • LinkedPerson Integration - Select from existing linked persons

  • Order-based System - Creates Order entity for payment tracking

  • Immediate Registration - No multi-step process, instant registration

  • Payment Gateway Integration - Redirect to payment portal after registration

2. Event Concepts

2.1. Event Entities

event-entities

Event

Defines the overall competition or activity:

  • Name, description, dates

  • Location and venue

  • Organizer (Organisation)

  • Registration open/close dates

  • Status (upcoming, active, completed, cancelled)

Race

Specific category or distance within an event:

  • Name (e.g., "10km Run", "21km Half Marathon")

  • Distance, difficulty level

  • Age restrictions

  • Maximum participants

  • Price per participant

EventParticipant

Individual participant registration:

  • Links Person to Race

  • Stores participant-specific details

  • Tracks registration status

  • References Order for payment

  • Can include additional participant information (bib number, start time, etc.)

Order

Payment transaction container:

  • Groups multiple EventParticipants

  • Tracks payment status

  • Links to buyer (Person via userKey)

  • Total amount

  • Payment gateway reference

3. Registration Workflow

3.1. High-Level Flow

event-registration-flow

3.2. URL Parameters

Route Pattern:

/register

Query Parameters:

Parameter Description Required Example

eventId

Event ID to register for

Yes

42

orgId

Organisation ID (organizer)

Yes

8

userKey

User key (security context)

Optional

abc123xyz

Example URLs:

https://app.example.com/register?eventId=42&orgId=8&userKey=abc123xyz
https://app.example.com/register?eventId=42&orgId=8

Session vs URL userKey:

  • If userKey provided in URL → Use URL parameter (external link scenario)

  • If no userKey in URL → Use session userKey (logged-in user scenario)

4. Component: RegistrationComponent

4.1. Component Structure

File: src/main/webapp/app/entities/registration/list/registration.component.ts

Key Properties:

export class RegistrationComponent implements OnInit, OnDestroy {
  persons: IPersonSelect[] = [];
  selectForm!: FormGroup;
  selectCount = 0;
  isLoading = false;
  isSaving = false;

  _eventId: number | undefined;
  event: IEvent | undefined;
  _organisationId: number | undefined;
  organisation: IOrganisation | undefined;
  _userKey: string | undefined;
  principal: IPerson | undefined;
  sessionUserId: boolean = true;
}

4.2. Initialization

ngOnInit(): void {
  this.selectForm = this.fb.group({
    selection: this.fb.array([]),
  });

  // Subscribe to checkbox changes for count
  this.selectionChangeSubscription = this.selectForm.valueChanges
    .subscribe(data => {
      const arr: boolean[] = data.selection;
      this.selectCount = arr.reduce((n, checkbox) =>
        (checkbox ? n + 1 : n), 0);
    });

  this.handleNavigation();
}

4.3. Navigation Handler

protected handleNavigation(): void {
  combineLatest([this.activatedRoute.data, this.activatedRoute.queryParamMap])
    .subscribe(([data, params]) => {
      const eventId = params.get('eventId');
      if (eventId) {
        this.eventId = +eventId;
      }

      const organisationId = params.get('orgId');
      if (organisationId) {
        this.organisationId = +organisationId;
      }

      const userKey = params.get('userKey');
      if (userKey) {
        this.userKey = userKey;
        this.sessionUserId = false;
      } else {
        this.userKey = this.sessionService.getUserKey();
        this.sessionUserId = true;
      }

      this.loadPage(pageNumber, true);
    });
}

Property Setters with Side Effects:

set eventId(id) {
  if (this._eventId !== id) {
    this._eventId = id;
    this.filterChanged = true;
    if (id) {
      this.eventService.find(id).subscribe((res: HttpResponse<IEvent>) => {
        this.event = res.body ?? undefined;
      });
    }
  }
}

set organisationId(id) {
  if (this._organisationId !== id) {
    this._organisationId = id;
    this.filterChanged = true;
    if (id) {
      this.organisationService.find(id).subscribe(
        (res: HttpResponse<IOrganisation>) => {
          this.organisation = res.body ?? undefined;
        });
    }
  }
}

set userKey(userKey) {
  if (this._userKey !== userKey) {
    this._userKey = userKey;
    this.filterChanged = true;
    if (userKey) {
      this.personService.findByUserKeyOrCreate(userKey).subscribe(
        (res: HttpResponse<IPerson>) => {
          this.principal = res.body ?? undefined;
        });
    }
  }
}

API Endpoints Called:

  • GET /api/events/{id} - Load event details

  • GET /api/organisations/{id} - Load organisation details

  • GET /api/people/userkey/{userKey} - Load or create principal person

5. Loading LinkedPersons

loadPage(page?: number, dontNavigate?: boolean): void {
  const pageToLoad: number = page ?? this.page ?? 1;

  if (this.organisationId && this.userKey) {
    this.isLoading = true;
    this.personService.getAllLinkedOrgUsersByPrincipal(
      this.userKey,
      this.organisationId
    ).subscribe(
      (res: EmbeddedLinkedPersonDTO[]) => {
        this.isLoading = false;
        this.persons = res;
        this.selectForm.setControl('selection', this.buildSelection());
      },
      () => {
        this.isLoading = false;
        this.onError();
      }
    );
  }
}

API Endpoint: GET /api/people/linked?userKey={key}&organisationId={orgId}

Response:

[
  {
    "id": 1,
    "firstName": "John",
    "lastName": "Smith",
    "dateOfBirth": "1985-03-15",
    "gender": "MALE",
    "email": "[email protected]",
    "contactNumber": "0821234567"
  },
  {
    "id": 3,
    "firstName": "Billy",
    "lastName": "Smith",
    "dateOfBirth": "2010-11-03",
    "gender": "MALE",
    "email": null,
    "contactNumber": null
  }
]

6. Person Selection UI

6.1. Selection Form

person-selection-ui

6.2. Form Array for Selection

buildSelection(clear?: boolean): FormArray<FormControl<boolean | null>> {
  const arr = this.persons.map(person => {
    return this.fb.control(!clear ? !!person.selected : false);
  });
  return this.fb.array(arr);
}

get selectionControl(): FormArray<FormControl<boolean | null | undefined>> {
  return this.selectForm.get('selection') as FormArray;
}

Selection Count Tracking:

this.selectionChangeSubscription = this.selectForm.valueChanges
  .subscribe(data => {
    const arr: boolean[] = data.selection;
    this.selectCount = arr.reduce((n, checkbox) =>
      (checkbox ? n + 1 : n), 0);
  });

Display Selection Count:

<div>
  Selected: {{ selectCount }} participant{{ selectCount !== 1 ? 's' : '' }}
</div>

6.3. Validation

get canRegister(): boolean {
  return !!this.event &&
         this.selectForm.valid &&
         this.selectCount > 0 &&
         !this.isLoading &&
         !this.isSaving;
}

Validation Conditions:

  • Event must be loaded (!!this.event)

  • Form must be valid (this.selectForm.valid)

  • At least one person selected (this.selectCount > 0)

  • Not currently loading (!this.isLoading)

  • Not currently saving (!this.isSaving)

7. Registration Submission

7.1. Building Registration Payload

register(value: boolean[]) {
  if (this.canRegister) {
    const participants: IParticipantCandidate[] = [];

    this.selectionControl.value.forEach((checkbox, i) => {
      if (!!checkbox) {
        const person: IPerson = this.persons[i];
        const participant: IParticipantCandidate = {
          id: null,
          person
        };
        participants.push(participant);
      }
    });

    const order: NewOrder = {
      id: null,
      organisation: this.organisation ?? null,
      buyer: { id: null, userKey: this.userKey ?? null },
    };

    const registration: IEventParticipantRegistration = {
      event: this.event!,
      participants,
      order,
    };

    this.isSaving = true;
    this.registrationService.register(registration).subscribe(
      (res: HttpResponse<IEventParticipantRegistration>) => {
        this.log.debug('Registration', res.body);
        this.clear();
        this.isSaving = false;
        if (res.body?.infoURL) {
          location.href = res.body.infoURL;
        }
      },
      () => {
        this.clear();
        this.isSaving = false;
      }
    );
  }
}

7.2. Registration Model

File: src/main/webapp/app/entities/registration/model/event-participant-registration.model.ts

export interface IParticipantCandidate extends NewEventParticipant {
  questions?: IQuestion[];
  answer?: string;
  valid?: boolean;
  message?: string;
}

export interface IEventParticipantRegistration {
  event: IEvent;
  participants: IParticipantCandidate[];
  order?: IOrder | NewOrder;
  phase?: string;
  title?: string;
  message?: string;
  infoURL?: string;
  questionType?: string;
}

7.3. Request Payload Example

{
  "event": {
    "id": 42,
    "name": "Marathon 2024",
    "startDate": "2024-06-15T08:00:00Z"
  },
  "participants": [
    {
      "id": null,
      "person": {
        "id": 3,
        "firstName": "Billy",
        "lastName": "Smith",
        "dateOfBirth": "2010-11-03",
        "gender": "MALE"
      }
    }
  ],
  "order": {
    "id": null,
    "organisation": {
      "id": 8,
      "name": "Running Club"
    },
    "buyer": {
      "id": null,
      "userKey": "abc123xyz"
    }
  }
}

7.4. API Endpoint

Endpoint: POST /api/event-participants/register

Service:

@Injectable({ providedIn: 'root' })
export class EventParticipantRegistrationService {
  protected resourceUrl = this.applicationConfigService
    .getEndpointFor('api/event-participants/register', 'admin-service');

  register(registration: IEventParticipantRegistration):
    Observable<HttpResponse<IEventParticipantRegistration>> {
    return this.http.post<IEventParticipantRegistration>(
      this.resourceUrl,
      registration,
      { observe: 'response' }
    );
  }
}

8. Backend Processing

8.1. Registration Response

{
  "event": {
    "id": 42,
    "name": "Marathon 2024"
  },
  "participants": [
    {
      "id": 1523,
      "person": {
        "id": 3,
        "firstName": "Billy",
        "lastName": "Smith"
      },
      "bibNumber": "B1523",
      "status": "PENDING_PAYMENT"
    }
  ],
  "order": {
    "id": 892,
    "reference": "ORD-892-2024",
    "totalAmount": 350.00,
    "status": "PENDING"
  },
  "infoURL": "https://payment.gateway.com/pay?reference=ORD-892-2024&amount=350.00&return=https://app.example.com/registration/payment-return"
}

Response Properties:

  • participants - Created EventParticipant records with IDs

  • order - Created Order with reference and total

  • infoURL - Payment gateway URL (if payment required)

  • phase - Current registration phase (optional)

  • message - Status or error message (optional)

8.2. Backend Operations

Backend Flow:

  1. Validate Registration

    • Check event registration is open

    • Verify person eligibility

    • Check age restrictions for race

    • Verify maximum participants not exceeded

  2. Create EventParticipant Records

    • One EventParticipant per selected person

    • Link to Race (derived from Event)

    • Assign bib numbers (if applicable)

    • Set status to PENDING_PAYMENT

  3. Create Order

    • Calculate total amount (participants × race price)

    • Generate order reference

    • Link to Organisation

    • Store buyer userKey

  4. Payment Integration

    • If amount > 0, generate payment URL

    • Include order reference and return URL

    • Return infoURL in response

  5. Free Registration

    • If amount = 0, mark participants as REGISTERED

    • Send confirmation email

    • No payment redirect needed

9. Payment Flow

9.1. Payment Redirect

if (res.body?.infoURL) {
  location.href = res.body.infoURL;
}

Payment Gateway URL Example:

https://payment.gateway.com/pay?
  reference=ORD-892-2024&
  amount=350.00&
  return=https://app.example.com/registration/payment-return&
  cancel=https://app.example.com/registration/payment-cancel

9.2. Payment Return

Return URL: /registration/payment-return

Query Parameters:

Parameter Description Example

reference

Order reference

ORD-892-2024

status

Payment status

success, failed, cancelled

transactionId

Gateway transaction ID

TXN-12345678

Backend Processing on Return:

  1. Verify payment status with gateway

  2. Update Order status

  3. Update EventParticipant status

  4. Send confirmation email

  5. Display success/failure message

9.3. Payment States

payment-states

10. Adding Participants

10.1. Add Person Button

The UI includes an "Add Person" button to navigate to LinkedPerson workflow:

<a routerLink="/linked-person/search">
  <button type="button" class="btn btn-primary">
    <fa-icon icon="plus"></fa-icon>
    Add Person
  </button>
</a>

Flow:

  1. User clicks "Add Person"

  2. Navigate to /linked-person/search

  3. User searches/creates person

  4. Person is linked to principal

  5. Return to /register with updated person list

See LinkedPerson Management for details.

11. Differences from Membership Registration

Aspect Membership Registration Event Registration

Workflow

Multi-step process with questions

Single-page selection

Process Engine

Uses ProcessDefinition and ProcessInstance

Direct registration (no process)

Questions

Dynamic question workflow per person

No questions (or minimal via backend)

Payment Timing

After all questions answered

Immediately after selection

Resume Capability

Can resume from any step

No resume (single atomic operation)

Back Navigation

Back button through questions

No back navigation needed

State Management

Complex (FormDTO, ProcessState)

Simple (selection checkboxes)

Entity Created

Membership

EventParticipant

Typical Use Case

Annual membership with family

Race/tournament registration

12. Error Handling

12.1. Validation Errors

Client-Side:

  • No event loaded → Disable register button

  • No persons selected → Disable register button

  • Form invalid → Display validation messages

Server-Side:

{
  "phase": "VALIDATION_ERROR",
  "title": "Registration Error",
  "message": "One or more participants are not eligible for this event",
  "participants": [
    {
      "id": null,
      "person": { "id": 3, "firstName": "Billy", "lastName": "Smith" },
      "valid": false,
      "message": "Participant is under minimum age requirement (18 years)"
    }
  ]
}

Error Display:

<div *ngIf="registration.phase === 'VALIDATION_ERROR'" class="alert alert-danger">
  <h4>{{ registration.title }}</h4>
  <p>{{ registration.message }}</p>
  <ul>
    <li *ngFor="let p of registration.participants">
      <span *ngIf="!p.valid">
        {{ p.person.firstName }} {{ p.person.lastName }}: {{ p.message }}
      </span>
    </li>
  </ul>
</div>

12.2. Registration Failures

Common Failure Scenarios:

Scenario Cause Resolution

Event Full

Maximum participants reached

Show waitlist option

Registration Closed

Outside registration window

Display event information only

Duplicate Registration

Person already registered

Show existing registration

Payment Gateway Down

External service unavailable

Allow registration, mark as pending payment

Network Error

Connection timeout

Show retry button, preserve selection

12.3. Loading States

<div *ngIf="isLoading" class="text-center">
  <div class="spinner-border" role="status">
    <span class="sr-only">Loading persons...</span>
  </div>
</div>

<div *ngIf="isSaving" class="text-center">
  <div class="spinner-border" role="status">
    <span class="sr-only">Processing registration...</span>
  </div>
</div>

13. Integration Points

13.1. LinkedPerson Workflow

Event registration integrates with LinkedPerson management:

  1. User accesses registration URL

  2. System loads linked persons for user

  3. User can add more persons via LinkedPerson workflow

  4. User selects participants from linked persons

  5. Registration creates EventParticipant records

13.2. Order System

Event registration creates Order entities:

  • Order - Container for transaction

  • OrderLine - Individual line items (one per participant)

  • Payment - Payment record after gateway processing

See Financial Entities for details.

13.3. Email Notifications

Registration Confirmation:

  • Sent after successful registration (free events)

  • Sent after successful payment (paid events)

  • Includes participant details, event information

  • Includes payment receipt (if applicable)

Payment Receipt:

  • Sent after successful payment

  • Includes order reference, amount paid

  • Itemized list of participants

14. Best Practices

14.1. User Experience

Clear Event Information:

  • Display event name, date, location prominently

  • Show race details (distance, difficulty, restrictions)

  • Display price per participant

  • Calculate and show total as users select

Participant Management:

  • Allow easy addition of participants via LinkedPerson workflow

  • Display participant information clearly in table

  • Show eligibility warnings before registration

Loading Indicators:

  • Show spinner while loading persons

  • Show spinner during registration submission

  • Disable buttons during operations

14.2. Performance

Lazy Loading:

  • Load event and organisation details on demand

  • Use caching for frequently accessed data

Batch Operations:

  • Register all participants in single API call

  • Create all EventParticipant records in transaction

Payment Optimization:

  • Generate payment URL on server side

  • Use direct redirect (no intermediate pages)

14.3. Security

URL Parameter Validation:

  • Validate eventId exists and is accessible

  • Validate organisationId matches event

  • Verify userKey authorization

Order Security:

  • Verify buyer matches userKey

  • Validate order total matches calculated amount

  • Check for duplicate registrations

Payment Security:

  • Use HTTPS for all payment redirects

  • Verify payment gateway signatures on return

  • Validate order reference matches transaction