Import: Membership
|
Related reading
No dedicated requirements page exists for Membership import yet — the import inherits from the general Membership domain requirements. |
1. Overview
The Membership import bulk-adds members to a MembershipPeriod from an XLSX file. Each row is processed in its own transaction (REQUIRES_NEW) so one failure does not roll back the whole import. Rows orchestrate several entities: a RaceNumber (found or created), a User/Person (looked up or created via identity matching), an optional Order, and the Membership itself.
The HTTP surface is asynchronous from release 2.4: the upload returns HTTP 202 immediately and the caller polls a companion GET endpoint for the result DTO. Unlike the EP and Results imports (which delegate to a single bulk importer via the whole-file bridge), Membership runs row-by-row through the generic async framework. The aggregated result DTO is built on retrieval from the per-row ImportRowResult entries.
2. API
2.1. Endpoint Reference
| Endpoint | Purpose |
|---|---|
|
Upload an XLSX and start the async import. Returns |
|
Retrieve the |
2.2. Async Delivery Mode
The per-entity endpoint is a thin facade over the generic async framework. MembershipRowProcessor runs in row-by-row mode: the framework iterates the workbook, calls processRow(…) per row, and persists each outcome to ImportRowResult. The GET endpoint aggregates those rows into MembershipImportResultDTO at retrieval time.
PUT /api/memberships/import
?periodId={long} (required)
&orgId={long} (optional — defaults to caller's tenant)
&sheetIndex={int} (default: 0)
Content-Type: multipart/form-data
Body: file=<xlsx>
HTTP/1.1 202 Accepted
Location: /api/memberships/import/{jobId}
{ "identifier": "<jobId>", "status": "PROCESSING", "skipMappingPhases": true, ... }
# operator polls ...
GET /api/memberships/import/{jobId}
HTTP/1.1 200 OK # once COMPLETED or FAILED
{ ... MembershipImportResultDTO ... }
HTTP/1.1 409 Conflict # while still processing
See Import Operations Runbook for the polling procedure.
2.3. Request Body
Multipart form with a single file part containing the XLSX workbook. One sheet per upload; sheetIndex selects which sheet (0-based, default 0).
Query parameters:
-
periodId(required) — targetMembershipPeriod; the period’s organisation scopes the import -
orgId(optional) — override the caller’s tenant; must match the period’s organisation -
sheetIndex(default0) — 0-based sheet index within the workbook
2.4. Response: MembershipImportResultDTO
{
"totalRows": 120,
"created": 118,
"skipped": 1,
"errors": 1,
"issues": [
{ "lineNumber": 47, "outcome": "SKIPPED",
"message": "Membership number not supplied" },
{ "lineNumber": 93, "outcome": "ERROR",
"message": "Identity number does not match existing person" }
]
}
Invariant: totalRows == created + skipped + errors.
The issues list contains one entry per row that did not result in CREATED. Each issue carries the source line number, the outcome, and a human-readable message.
|
The DTO shape is deliberately minimal — just enough for an operator to tell whether the import succeeded, which rows failed, and why. It will be expanded in a follow-up if the operations team needs richer per-row fields (e.g. membership number, identity number). |
2.5. What to Look At
After polling returns the DTO, these are the patterns to confirm the import landed the way you expected:
-
Clean first run against a fresh period:
created ≈ totalRows,issues = [] -
Partial failure with recoverable rows: inspect
issues— each carries alineNumberpointing back into the source file so the operator can correct and re-run -
Systematic failure (e.g. all "identity number does not match"): stop, investigate the file’s identity columns, re-run; don’t blindly retry
See Import Operations Runbook for recovery procedures.
3. Column Definitions
The import supports 30 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 |
STRING |
Email, EmailAddress |
|
USER_KEY |
LONG |
UserKey |
MAIN_MEMBER_ID |
STRING |
MainMember, MainMemberID |
EXTERNAL_ID |
LONG |
UID, PID |
OTHER_NUMBER |
STRING |
OtherNumber |
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. Membership Fields
| Column | Type | Aliases |
|---|---|---|
MEMBERSHIP_ID |
INTEGER |
MID, MembershipID |
MEMBERSHIP_NUMBER |
STRING |
Member#, MemberNo, MemberNumber, Membership#, MembershipNo, MembershipNumber |
STATUS |
STRING |
Status, MembershipStatus (default: ACTIVE) |
MEMBERSHIP_TYPE_ID |
INTEGER (FK) |
MembershipType, MembershipTypeID |
MEMBERSHIP_PERIOD_ID |
INTEGER (FK) |
MembershipPeriod, MembershipPeriodID, Period, PeriodID |
MEMBERSHIP_CRITERIA_ID |
INTEGER (FK) |
MembershipCriteria, MembershipCriteriaID, Criteria, CriteriaID |
MEMBERSHIP_VALID_FROM |
DATE |
ValidFrom |
MEMBERSHIP_VALID_TO |
DATE |
ValidTo |
3.5. Order Fields
| Column | Type | Aliases |
|---|---|---|
ORDER_ID |
INTEGER |
OrderID |
ORDER_NUMBER |
STRING |
OrderNumber, 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
| Field | Target Entity | Resolution Strategy |
|---|---|---|
ID_COUNTRY |
Country |
Lookup by code (ZA, US) or name |
MEMBERSHIP_TYPE_ID |
MembershipType |
Lookup by id within organisation |
MEMBERSHIP_PERIOD_ID |
MembershipPeriod |
Lookup by id; must match |
MEMBERSHIP_CRITERIA_ID |
MembershipCriteria |
Lookup by id within the resolved period |
5. Processing Flow
5.1. Two-Layer Design
MembershipImportXLS (Spreadsheet Layer)
|
+--> createRequestDTO() [Parse cells to DTO]
|
+--> extractRow() [Process single row]
|
v
MembershipImportService (Business Logic Layer)
|
+--> addMember() [Orchestrate creation]
|
+--> findOrCreateNumber()
+--> lookupOrCreateUser()
+--> findOrCreateOrder()
+--> findOrCreateMembership()
5.2. Transaction Handling
Each row is processed in its own transaction via REQUIRES_NEW propagation:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public MembershipAddResponseDTO addMember(Organisation organisation,
MembershipAddRequestDTO request, MembershipPeriod period)
This isolates row failures — one bad row does not roll back already-imported rows.
5.3. Row-Level Flow
MembershipImportXLS:
1. Receive InputStream + orgId + periodId + sheetIndex
2. Load workbook into memory (XSSFWorkbook)
3. Lookup Organisation (tenantService.getOrganisation)
4. Lookup MembershipPeriod (verify belongs to org)
5. For each row:
a. Skip row 0 (header row, detect columns)
b. Check if blank (first 5 cells empty)
c. Create MembershipAddRequestDTO
d. Call membershipImportService.addMember()
e. Collect response → persist as ImportRowResult
MembershipImportService.addMember():
1. Validate request (period, person data)
2. Find or create RaceNumber (by id, else by number)
3. Find or create User (Person)
- By person id if provided
- Else by identity details (matchOrAdd)
- Else via existing number assignment
4. Infer gender from title if unknown
5. Set identity country / type based on country code
6. Update identification details
7. Assign personal details, save user
8. Find or create Order
9. Find or create Membership
10. Assign person to number
11. Return response
5.4. Person Resolution Strategy
Priority order:
-
By Person ID — if
person.idis provided on the row, look up directly -
By identity details — call
personService.matchOrAdd()with name / ID number, resolving across organisations -
By number assignment — if the row’s number already has a person assigned, use that person
matchOrAdd supports multi-organisation lookup: a person known to another org is added to the current organisation rather than duplicated.
Identity check after match:
// Verify identity number matches
if(candidate.getIdentityNumber() != null &&
candidate.getIdentityNumber().equals(personWrapper.getIdentityNumber()))
return persons.get(0);
else
throw new MembershipImportException("Identity number does not match");
5.5. Gender Inference
If gender is unknown or not supplied, infer from title:
if (person.getTitle() != null &&
(person.getGender() == null || person.getGender() == Gender.UNKNOWN)) {
String title = person.getTitle().replace(".", "").toUpperCase();
if (title.equals("MS") || title.equals("MRS") || title.equals("MISS"))
person.setGender(Gender.FEMALE);
else
person.setGender(Gender.MALE);
}
6. Outcome Classification
Each row produces one of three outcomes persisted on ImportRowResult:
| Outcome | Meaning |
|---|---|
|
New |
|
Row not processed — missing required field or pre-validation failure (e.g. membership number not supplied when period does not auto-generate). |
|
Exception occurred processing the row; the row’s |
No UPDATED outcome today: unlike EP, Membership does not upsert by an external key. Re-running the same file adds new memberships rather than updating existing ones. See Outstanding #5.
7. Source Code
| File | Purpose |
|---|---|
|
Spreadsheet parsing: column definitions, row extraction, per-row dispatch to the service layer. |
|
Row-by-row bridge — parses |
|
Business logic orchestration: person/number/order handling, transaction isolation. |
|
REST endpoints |
|
Request payload for member addition. |
|
Response with outcome per row. |
|
Aggregated response DTO returned by the GET endpoint. |
Location: admin-service/src/main/java/za/co/idealogic/event/admin/
8. Outstanding Items
Tracked informally — open an ADO ticket before acting on any of these.
| # | Topic | Status |
|---|---|---|
1 |
Reduce redundant Organisation lookups in the import call stack (mirrors EP Outstanding #6) |
Open |
2 |
Validate |
Open |
3 |
Replace |
Open |
4 |
Generate next membership number when not supplied — optional auto-generation when |
Open; today |
5 |
Add upsert-by- |
Open |
6 |
Review |
Open |
7 |
Locate the relevant |
Open |
8 |
Extract identity-type and gender parsing into |
Open |
9 |
Expand |
Open — deliberately minimal today |