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 |
|---|---|
|
Main import service, column definitions, row extraction |
|
Registration logic invoked per row |
|
Request payload for registration |
|
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 |
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 |
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
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());