Import: Event Participant

1. Overview

The Event Participant import service processes spreadsheet files to bulk-register participants into an event. This document describes the current synchronous implementation and proposes improvements for the async import system.

2. Source Code

File Purpose

EventParticipantImportXLS.java

Main import service, column definitions, row extraction

EventParticipantServiceEx.java

Registration logic invoked per row

EventParticipantAddRequestDTO.java

Request payload for registration

EventParticipantAddResponseDTO.java

Response with outcome per row

Location: admin-service/src/main/java/za/co/idealogic/event/admin/service/

3. Column Definitions

The import supports 32 columns, with 2 required fields:

3.1. Required Fields

Column Type Aliases

FIRST_NAME

STRING

FirstName, Name, FN

LAST_NAME

STRING

LastName, Surname, SN, LN

3.2. Person Fields

Column Type Aliases

TITLE

STRING

Title

ID_NUMBER

STRING

ID, IDNumber, IdentityNumber, Passport, IDPassport

ID_TYPE

STRING

IDType (default: NATIONAL)

ID_COUNTRY

STRING (FK)

Country, IDCountry, Nationality (default: ZA)

GENDER

STRING

Gender, Gen, Sex

DOB

DATE

DOB, DateOfBirth, Birthday

CONTACT_NUMBER

STRING

ContactNumber, Cellphone, CellphoneNumber, CellNumber, Telephone, TelephoneNumber, Mobile

EMAIL

STRING

Email, EmailAddress

USER_KEY

LONG

UserKey

MAIN_MEMBER_ID

STRING

MainMember, MainMemberID

PERSON_EXTERNAL_ID

LONG

UID, PID

3.3. Address Fields

Column Type Aliases

ADDRESS

STRING

Address, Address1

TOWN

STRING

Town, City, TownCity, CityTown

POSTAL_CODE

STRING

PostalCode, Code, ZIP

3.4. Event Participation Fields

Column Type Aliases

EVENT_PARTICIPANT_ID

LONG

EPID, EventParticipantID

EVENT_PARTICIPANT_EXTERNAL_ID

STRING

EPREF

EVENT_CATEGORY_NAME

STRING (FK)

Category, CategoryName, EventCategory, EventCategoryName

OTHER_NUMBER

STRING

OtherNumber

3.5. Race Number Fields

Column Type Aliases

NUMBER

STRING

Number, Bib, BibNumber, RaceNumber, No

NUMBER_ID

INTEGER

NumberId

TAG_NUMBER

STRING

Tag, TagRef, TagReference, Chip

TAG_ID

INTEGER

TagId

3.6. Order Fields

Column Type Aliases

ORDER_ID

INTEGER

OrderID

ORDER_NUMBER

STRING

OrderNumber, Order#, ReceiptNumber

ORDER_STATUS

STRING

OrderStatus (default: P for Pending)

ORDER_PRODUCT_ID

INTEGER

Product, ProductID, OrderProduct, OrderProductID

ORDER_PRODUCT_AMOUNT

DECIMAL

ProductAmount, OrderProductAmount, Amount

3.7. Custom Fields

Column Type Aliases

CUSTOM_1

STRING

(none)

CUSTOM_2

STRING

(none)

CUSTOM_3

STRING

(none)

CUSTOM_LIST_1

STRING (FK)

Schoolyouareattending (TODO: make generic)

CUSTOM_LIST_2

STRING (FK)

(none)

CUSTOM_LIST_3

STRING (FK)

(none)

4. Foreign Key Resolution

Fields marked (FK) require resolution to entity IDs:

Field Target Entity Resolution Strategy

ID_COUNTRY

Country

Lookup by code (ZA, US) or name

EVENT_CATEGORY_NAME

EventCategory

Lookup by name within event

CUSTOM_LIST_1/2/3

CustomListValue

Lookup by value within list

5. Current Implementation

5.1. Processing Flow

1. Receive InputStream + orgId + eventId + sheetIndex
2. Load workbook into memory (XSSFWorkbook)
3. Lookup Organisation (tenantService.getOrganisation)
4. Lookup Event (eventService.findOneEntity)
5. For each row:
   a. Skip row 0 (header row, detect columns)
   b. Check if blank (first 5 cells empty)
   c. Create EventParticipantAddRequestDTO
   d. Call eventParticipantService.register()
   e. Collect response
6. Return list of responses

5.2. Row Extraction Logic

The createRequestDTO method maps spreadsheet values to DTO fields:

EventParticipantAddRequestDTO request = new EventParticipantAddRequestDTO();
request.setId(parseAsLong(row, map, Column.EVENT_PARTICIPANT_ID));
request.setExternalId(parseAsString(row, map, Column.EVENT_PARTICIPANT_EXTERNAL_ID));

PersonDTO person = new PersonDTO();
person.setFirstName(parseAsString(row, map, Column.FIRST_NAME));
person.setLastName(parseAsString(row, map, Column.LAST_NAME));
// ... more fields

5.3. Default Values

defaultValues.put(Column.ID_TYPE, "NATIONAL");
defaultValues.put(Column.ID_COUNTRY, Country.DEFAULT_COUNTRY_CODE);
defaultValues.put(Column.ORDER_STATUS, "P");

6. TODO Items

6.1. TODO 1: Refactor Identity Type Parsing (Line 168-174)

Current Code:

//TODO Refactor to PersonService. Also in MembershipImportXLS
final String identityTypeStr = parseAsString(row, map, Column.ID_TYPE);
IdentityType identityType;
if(identityTypeStr != null) {
    identityType = IdentityType.of(identityTypeStr);
} else {
    identityType = IdentityType.OTHER;
}

Issue: Duplicate code exists in MembershipImportXLS.

Recommendation: Extract to PersonService.parseIdentityType(String value) method.

6.2. TODO 2: Refactor Gender Parsing (Line 177-183)

Current Code:

//TODO Refactor to PersonService. Also in MembershipImportXLS
final String genderStr = parseAsString(row, map, Column.GENDER);
final Gender gender;
if(genderStr != null)
    gender = Gender.fromChar(genderStr);
else
    gender = Gender.UNKNOWN;

Issue: Duplicate code exists in MembershipImportXLS.

Recommendation: Extract to PersonService.parseGender(String value) method.

6.3. TODO 3: Make Custom List Header Generic (Line 154-155)

Current Code:

//TODO This needs to be more generic
addHeaderDictionary("Schoolyouareattending", Column.CUSTOM_LIST_1);

Issue: Hardcoded header for a specific client use case.

Recommendation: Remove client-specific alias or make configurable per organization.

6.4. TODO 4: Conditional Order Creation (Line 221)

Current Code:

//TODO Make this conditional. Consider all the fields that make up an order.
OrderDTO order = new OrderDTO();

Issue: Order is always created even when not needed.

Recommendation: Only create order if ORDER_NUMBER or ORDER_ID is present in spreadsheet.

6.5. TODO 5: Order Status Parsing (Line 227-234)

Current Code:

//TODO Ensure order status can be parsed
final String orderStatusStr = parseAsString(row, map, Column.ORDER_STATUS);
OrderStatus orderStatus = null;
if(orderStatusStr != null)
    orderStatus = OrderStatus.fromChar(orderStatusStr.substring(0, 1).toUpperCase());
else
    orderStatus = OrderStatus.PENDING;

Issue: Only uses first character, may fail on unexpected values.

Recommendation: Add proper validation and error handling.

6.6. TODO 6: Reduce Organisation Lookups (Line 278-280)

Current Code:

//TODO Too many org lookups in this call stack. Review and optimise
Organisation org = tenantService.getOrganisation(orgId);

Issue: Organisation is looked up per import, but may also be looked up again in nested service calls.

Recommendation: Cache organisation at import start and pass to all nested calls.

6.7. TODO 7: Fix Access Control Filtering (Line 284-285)

Current Code:

Event event = eventService.findOneEntity(eventId)
//TODO Fix			.filter(e -> org.equals(e.getOrganiser()))
    .orElseThrow(() -> new NotFoundException(...));

Issue: Access control check is commented out.

Recommendation: Uncomment and verify event belongs to organisation.

6.8. TODO 8: Improve Exception Handling (Line 323-332)

Current Code:

} catch (FileNotFoundException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (NotFoundException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}
return null;

Issue: Exceptions are swallowed with stack trace only.

Recommendation: Throw custom ImportException with meaningful message, or return error response.

7. Design Suggestions for Async Migration

7.1. 1. Separate Column Mapping from Row Processing

Create a two-phase approach:

// Phase 1: Analyze columns
ColumnMappingResult analyzeColumns(InputStream file, int sheetIndex);

// Phase 2: Process rows (after mapping confirmed)
void processRows(ImportJob job, Map<Column, Integer> confirmedMap);

7.2. 2. Extract FK Resolution to Separate Service

@Service
public class EventParticipantCellResolver implements CellValueResolver {
    public CellResolutionResult resolveEventCategory(String value, Event event);
    public CellResolutionResult resolveCountry(String value);
    public CellResolutionResult resolveCustomList(String value, CustomList list);
}

7.3. 3. Implement Batch Processing

Instead of calling register() per row:

// Collect batch of validated DTOs
List<EventParticipantAddRequestDTO> batch = new ArrayList<>();

// Process in batches of 100
if (batch.size() >= 100) {
    eventParticipantService.registerBatch(org, batch, event);
    batch.clear();
}

7.4. 4. Cache Reference Data

Pre-load at import start:

ImportContext context = new ImportContext();
context.setOrganisation(tenantService.getOrganisation(orgId));
context.setEvent(eventService.findOne(eventId));
context.setEventCategories(categoryService.findByEvent(eventId));
context.setCountries(countryService.findAll());