14 — QA Checklist (Bulk Module)
- 1. Functional core
- 2. File-level robustness (quoted commas, BOM, encoding, whitespace)
- 3. Validation matrix (per-row, exact strings in 08 §2)
- 4. Duplicates & idempotent re-upload
- 5. Row limits & large files (1000 rows)
- 6. Concurrency
- 7. Entity & error envelope
- 8. Export
- 9. Documentation drift (flagged)
- 10. Client-side (forward-looking)
Acceptance + regression checklist for the bulk import/export surface. Every item is traceable to source. Run against a seeded tenant (grades/sections/ academic years/classes exist) per 00-shared/10_QA_Baseline.md.
1. Functional core
-
POST /api/v1/bulk/import/studentswith a valid file returns 200 and a report whereimported + failed === totalRows(import-adapter.interface.ts:14-25). -
rowNumberof the first data row is 2 (header = row 1,bulk-import.service.ts:46) — matches the physical file line. -
Valid row creates: user (name/email), student (admissionNumber,
rollNumber), class enrollment, status
ACTIVE, admissionDate now (students-import.adapter.ts:62-85;student.service.ts:63-78). -
Valid row fans out
UserCreated→ in-appuser-created-notificationandStudentCreated→ in-appstudent-enrolled(event-queue-map.ts:10,28). -
One bad row does not abort the batch (
bulk-import.service.spec.ts:53-65).
2. File-level robustness (quoted commas, BOM, encoding, whitespace)
-
Quoted commas / quotes:
email,"last, Name Jr.",…—csv-parsehandles RFC-4180 quoting (bulk-import.service.ts:26-30); verify a field containing a comma inside quotes imports intact; unclosed quote → 400Malformed CSV: could not parse file.(:31-33). -
BOM: UTF-8 BOM (
EF BB BF) — client strips it in the isolate (15 §4); verify direct API call with BOM still parses (csv-parse tolerates it) and that preview == server result. -
Encoding: UTF-8 only (
bulk.controller.ts:47). Windows-1252 accents → client warning "re-encode as UTF-8"; never send mojibake. - CRLF vs LF: both accepted (parser handles); verify mixed line endings.
-
Header whitespace: header
" firstName"fails required check withMissing required column "firstName".(students-import.adapter.ts:43) — client must block pre-upload (06 §2.2); server behaviour is "row errors", not a crash. -
Empty lines: skipped (
skip_empty_lines:true,:28); header-only file → 400CSV must include a header row and data.(:34-35). -
Trailing empty cells / stray columns: unknown columns ignored;
blank optional
rollNumber→ omitted (:79).
3. Validation matrix (per-row, exact strings in 08 §2)
-
Missing each required column →
Missing required column "X". -
Bad email →
Invalid email format. -
Existing admissionNumber →
Admission number "X" already exists. -
Existing email →
Email "X" already registered. -
Unknown academicYear / grade / section →
… not found. -
Grade matched by code too (name OR code,
:118-121). -
Known grade+section but no class →
No class found for grade "X" section "Y". - No row with an unresolved ref is ever created.
4. Duplicates & idempotent re-upload
- Same file uploaded twice: second run reports every row as duplicate; zero new users/students; counts consistent.
-
Race fallback: concurrent create of same email → second row reuses the
existing user id and still creates the student (
students-import.adapter.ts:72-75). - Duplicate email + different admissionNumber: email row rejected, student not created.
- Duplicate admissionNumber + different email: same rejection semantics.
5. Row limits & large files (1000 rows)
-
1000-row file: all rows processed; report complete; no partial
response; response time is the long-pole (sequential awaits,
bulk-import.service.ts:45-63). - Client caps pick at 1000 rows / ~2 MB (10 §6) — verify warning copy and hard block.
- Mid-request network drop: client shows partial-upload guidance; a follow-up import reports completed rows as duplicates (06 §4.1a).
- Rate budget: a large import against the 100/min api tier (00-shared/07 §4) — verify 429 handling on rapid successive uploads.
6. Concurrency
-
Two simultaneous imports from different users: no cross-tenant bleed
(all repos tenant-scoped via
BaseRepository); duplicates handled per row by validation + race fallback. - Import while grades/classes are being edited: worst case = "not found" row errors, never corruption.
-
Import is not transactional (no rollback): partial success is the
designed behaviour — report reflects exactly what was created
(
bulk-import.service.spec.ts:53-65).
7. Entity & error envelope
-
POST /bulk/import/fees→ 404No import adapter for entity "fees".(bulk-import.service.ts:19); client never offers it (onlystudents,students-import.adapter.ts:15); others(planned)(IMPLEMENTATION_PLAN.md:172). -
Missing multipart file → 400 exact string (
bulk.controller.ts:43-46). - Unauthenticated → 401; no tenantId from body ever accepted (00-shared/07 §6).
8. Export
-
GET /bulk/export/students→Content-Type: text/csv,Content-Disposition: attachment; filename="students.csv"(bulk.controller.ts:50-60). -
Columns
admissionNumber, rollNumber, status, admissionDatein order; sorted by admissionNumber asc (students-import.adapter.ts:87-98); dates ISO; empty roster → header-only CSV (bulk-import.service.ts:67-71). - Export round-trip sanity: importing the exported roster fails as duplicates (export ≠ template — 04 §6) — expected, documented.
9. Documentation drift (flagged)
-
Shared ledger glossary claims imports are "processed asynchronously by
workers" (00-shared/01 §10) — code is synchronous; this doc set
is the corrected reference until async lands
(planned)(IMPLEMENTATION_PLAN.md:172). Add a red-line note when the async work ships.
10. Client-side (forward-looking)
- Preview and server agree: same parser + options in the isolate (15 §4) — test one file through both paths, results identical.
- Error strings displayed verbatim; row numbers match file lines.
- 429 countdown, offline banner, permission gate, a11y live regions (10 §8, 11 §6).
-
Analytics events fire
(proposed).