Skip to content

Add CSV import for contacts #39

Description

@herbie-bot

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions