Background
Contacts collected at trade shows, conferences or via business-card-scan apps arrive in batches as
CSV files. Today the only ingestion path is manual entry through the existing contacts UI — slow,
error-prone, and a non-starter for batches of 50+ contacts.
The shape of the CSVs is not fixed. Different sources (a card-scan app, a LinkedIn export, a
hand-rolled spreadsheet) produce different column layouts. A v1 importer therefore needs to be
column-mapping driven rather than tied to a single hard-coded schema.
Goal
Allow APP-ADMIN and IT-ADMIN users to bulk-import contacts via a CSV file through a multi-step
dialog that dynamically maps source columns onto a fixed set of ContactEntity attributes, with
sensible handling of validation errors and partial success.
Scope (v1)
- Multi-step dialog (Upload → Mapping → Preview → Result) opened from the contacts list page.
- Allowed mapping targets:
title, firstName (required), lastName (required), email,
position, phoneNumber, linkedIn-URL, website-URL. Eight targets total.
- File caps: 20 MB and 5 000 data rows.
- Auto-detect delimiter between
, and ;. Encoding chosen by the user (UTF-8 or Windows-1252).
- Optional header row (default on; off → generic
Spalte 1, Spalte 2, …).
- Preview backed by a backend round-trip that runs the same
transformAndValidate(rawRow, mapping)
code path the commit uses, so a row that previews valid cannot fail commit for a
transformation- or validation-related reason.
- Per-row partial success on commit: each row is committed in its own transaction. Failed rows are
returned to the frontend with row number, offending field, and reason; successful rows are not
rolled back.
- Result step shows
\"N created, M failed\", a table of the failed rows, and a downloadable
failure CSV (failed rows in original form plus two extra columns _error_field /
_error_reason) for re-upload after correction.
Out of scope (this issue)
- Company resolution / auto-create.
- Tag mapping.
- Deduplication of any kind (no email matching, no unique constraint, no skip status).
- Mapping templates (save & reuse).
- File persistence (CSV is parsed in memory and discarded).
- Aggregated import-operation audit-log row (per-contact entries are sufficient).
- Asynchronous job handling / status polling.
- Other socials beyond LinkedIn and Website.
- Other file formats (XLSX, vCard, JSON).
Acceptance criteria
- A user with
APP-ADMIN or IT-ADMIN sees an "Import" button next to the existing
"Export" button on /contacts and can complete an upload → mapping → preview → commit flow
for a valid CSV.
- A user without those roles does not see the button; backend endpoints reject the call with 403.
- The mapping step blocks proceeding until
firstName and lastName are mapped exactly once and
no target is mapped twice.
- The preview step renders the first three CSV rows as
ContactPreviewDto previews built by the
backend (no client-side transformation), with per-cell validation errors highlighted inline.
- Commit produces a structured response
{ createdCount, failedCount, failures: [{row, field, reason}] }. Successful rows are persisted; failed rows are not.
- 20 MB or > 5 000 rows is rejected with HTTP 413.
- Unsupported encoding is rejected with HTTP 415.
- The uploaded CSV is not persisted to disk, object storage, or the database.
- Each imported contact produces the same audit-log entry that a manual create produces.
Reference
Spec: specs/109-csv-import-contacts/ (design.md + behaviors.md, ~55 given-when-then scenarios).
Background
Contacts collected at trade shows, conferences or via business-card-scan apps arrive in batches as
CSV files. Today the only ingestion path is manual entry through the existing contacts UI — slow,
error-prone, and a non-starter for batches of 50+ contacts.
The shape of the CSVs is not fixed. Different sources (a card-scan app, a LinkedIn export, a
hand-rolled spreadsheet) produce different column layouts. A v1 importer therefore needs to be
column-mapping driven rather than tied to a single hard-coded schema.
Goal
Allow
APP-ADMINandIT-ADMINusers to bulk-import contacts via a CSV file through a multi-stepdialog that dynamically maps source columns onto a fixed set of
ContactEntityattributes, withsensible handling of validation errors and partial success.
Scope (v1)
title,firstName(required),lastName(required),email,position,phoneNumber,linkedIn-URL,website-URL. Eight targets total.,and;. Encoding chosen by the user (UTF-8 or Windows-1252).Spalte 1, Spalte 2, …).transformAndValidate(rawRow, mapping)code path the commit uses, so a row that previews valid cannot fail commit for a
transformation- or validation-related reason.
returned to the frontend with row number, offending field, and reason; successful rows are not
rolled back.
\"N created, M failed\", a table of the failed rows, and a downloadablefailure CSV (failed rows in original form plus two extra columns
_error_field/_error_reason) for re-upload after correction.Out of scope (this issue)
Acceptance criteria
APP-ADMINorIT-ADMINsees an "Import" button next to the existing"Export" button on
/contactsand can complete an upload → mapping → preview → commit flowfor a valid CSV.
firstNameandlastNameare mapped exactly once andno target is mapped twice.
ContactPreviewDtopreviews built by thebackend (no client-side transformation), with per-cell validation errors highlighted inline.
{ createdCount, failedCount, failures: [{row, field, reason}] }. Successful rows are persisted; failed rows are not.Reference
Spec:
specs/109-csv-import-contacts/(design.md + behaviors.md, ~55 given-when-then scenarios).