All features

Feature breakdown · built with CraftHQ

CustomersImport

Bulk import from a spreadsheet

Whole-team time at this build's pace

Estimated ~20 h

2 people incl. the product owner, requirements to merge

Estimate at this build's pace, not a measurement of this feature

Update hundreds of existing records from one sheet, matched on a stable code. A staff member can update only the records they own, and changing the owner or reviewer is refused on the server.

Size: ~25 FP · Proxy estimate; an in-house count, not independently checked. Range 18 to 30. · How we counted 25 FP

How long a feature this size usually takes

Whole-team time — this build vs a typical team

~25 FP · FP × 6.8 to 9.4 h (ISBSG 2023)

Whole-team time at this build's pace2 people incl. the product owner, requirements to merge
~20 h15 to 24 h

Estimate at this build's pace, not a measurement of this feature

A typical team, whole lifecycleRequirements to release · middle half of 512 comparable projects (Java, Agile), ISBSG 2023
~170 to 235 habout 4.5–6 person-weeksmedian ~198 h
grey band = a typical team, middle halfhatched = our whole-team estimate

Both figures are whole-team time over the whole lifecycle, so they compare like for like. Ours is this build's pace — about 0.8 hours per function point across the whole build (two people, including the product owner, with requirements and review time). The industry range is an ISBSG project-level figure applied to one feature, so it is indicative.

Workflow

How it moved through the workflow

Select a stage. The build shipped, so every stage is complete; a person approved the merge.

Request

Update hundreds of existing records from one sheet; assignment stays an admin decision, and a staff member updates only the records they own.

Questions

Four decisions went to the business before building: the match key, bad or self-assigned owners and reviewers, records in a locked stage, and whether a staff import may reassign.

Approved

The product owner signs off the breakdown by moving the story into the approved state, and the build runs against that reference.

Definition of ready

The questions are answered.
The acceptance criteria are testable.
The size is estimated.
The scope is checked for splitting.
The product owner has signed off.

Building

CraftHQ's build agents write the code and the tests together, run as a loop; a failing check halts the work until it passes.

Checks

Three checks run on every pull request: one on the product owner's machine as CraftHQ builds, one on the engineer's machine before review, and a code-quality scan in CI. Merging is gated by the CI check passing and a person's approval, enforced by branch protection.

Review

An engineer reviews the pull request and merges it.

Merged

The pull request is merged to main after a person approves it. Nothing merges without that approval.

Definition of done

Every acceptance criterion is checked; where a scenario has a dedicated test it is cited, and where it was verified by review that is stated.
The three checks are green.
A person approved the merge.

Breakdown

The feature breakdown

Business first, then the engineering view. Tab through it.

Overview

The gap it closes

Updating hundreds of records by hand is slow, and letting anyone change records in bulk is risky. Bulk import updates existing records only; it does not create them. A staff member can update only their own records, and changing the owner or reviewer stays an admin decision enforced on the server.

Who uses it

Staff update the records they own in bulk; an administrator can update across the tenant. Creating a record is done through onboarding, not through bulk import.

User story

As a staff member, I want to update my own records in bulk from one sheet, matched on a stable code, without being able to change anyone else's records or their owner and reviewer.

Examples

Example 1

A staff member uploads a sheet of updates to records they own, each matched on its stable code. The rows are staged; on an explicit Apply, every valid row is updated and a bad row is rejected one by one with a reason while the rest go through.

Example 2

A staff member uploads a sheet that smuggles in an assignment column. The server refuses the assignment change, so the records keep their existing owner and reviewer. The attempt is not just hidden in the screen.

Dependencies & risks

Depends on

The existing records and the staff list, a stable code to match each row, and a shared check for which workflow stages are locked.

Risk & mitigation

A bad or smuggled row could corrupt data. Reduced by staging first and changing nothing until an explicit Apply, committing each row on its own, rejecting bad rows with a reason, and enforcing the admin-only assignment rule on the server, not just in the screen. A rejected row is reported with its reason and can be fixed and re-applied.

How it works

Simplified diagrams of the parts involved, the steps of one request, and where it runs.

Architecture

People
Adminupdates across the tenant
Staff memberupdates owned records
Your app
Import pathper-row commit
Locked-stage guardshared by both imports
Customer recordsupdated by the import
Audit logrecords applied updates
Template downloadpre-filled with your records
  • 1update sheet
  • 2stage, then check each row
  • 3apply valid row updates on Apply
  • 4download the pre-filled template
Simplified.

Sequence

Browseradmin or staff
App serverimport and guard
Databasecustomer records
  • 1upload a sheet
  • 2check each row, including the locked-stage and assignment rules
  • 3commit valid rows one by one
  • 4a result of updated and rejected rows after Apply
Simplified.

Infrastructure

Client (untrusted)
Web appthe import screens
Your app
App serverimport path and guards; authenticates, pins the tenant
Databaseper-tenant isolation, import-run history
File storageuploaded sheets and templates
  • 1upload a sheet
  • 2read the sheet
  • 3update records, write run history
Simplified.

Acceptance criteria

Each scenario is written as a Gherkin spec. The Engineering tab shows the test that covers each one, or notes where it was verified by review.

@customers @import
Feature: Bulk import from a spreadsheet
As a staff member
I want to update many records from one sheet
So that I can change hundreds of records at once without re-keying
Bulk import updates existing records only; it does not create them. Rows are matched on a stable system-generated code, a staff member can update only the records they own, and changing the owner or reviewer is refused on the server. Uploads are staged first and nothing changes until an explicit Apply.
Background:
Given a spreadsheet is prepared with the required columns and the stable code column
Rule: Bulk import updates existing records and never creates them
Scenario: A sheet updates records matched on a stable code
Given a staff member uploads a sheet of updates matched on the stable code column
When the batch is applied
Then the matching records are updated and no records are created
Scenario: A row that matches no record is rejected
Given a sheet row carries a code that matches no record
When the batch is applied
Then the row is rejected and nothing is created
Scenario: Two different records with the same name are not merged silently
Given two records share a name
When the batch is staged
Then the collision is reported for confirmation, never merged on the name alone
Scenario: Re-running the same file does not duplicate work
Given a batch has already been applied for a file
When the same file is submitted again under the same idempotency key
Then the work is not duplicated
Rule: A staff import only touches the records that staff member owns
Scenario: A staff import updates owned records only
Given a staff member uploads a sheet matched on the stable code
When the batch is applied
Then only records that staff member owns are updated
Scenario: A staff import cannot change the owner or reviewer
Given a staff member's sheet includes an assignment column
When the batch is applied
Then the assignment change is refused on the server
And the records keep their existing owner and reviewer
Rule: Nothing changes until an explicit Apply, and a bad row is rejected on its own
Scenario: Staging changes nothing until Apply
Given a sheet has been uploaded and staged
When no Apply has been requested
Then no records have changed
Scenario: A blank required column rejects the row
Given a staged sheet has a row with a blank required column
When the batch is applied
Then that row is rejected and the other rows are applied
Scenario: A record in a locked workflow stage is rejected at apply time
Given a record is in a locked workflow stage
When an import tries to change it
Then that row is rejected
Rule: A file that is not well-formed is refused before any row is applied
Scenario Outline: File-level checks
Given an admin or staff member uploads a "<file>"
When the import is validated
Then the result is "<result>"
Examples:
| file | result |
| sheet with a wrong or missing header | refused with a clear message |
| file over the 1,000-row cap | refused |
| wrong file type by content bytes | refused |
| an archive | refused |
| non-Latin names and leading zeros | preserved, not mangled |
Scenario: A partial run reports what applied and what did not
Given a file has a mix of good and bad rows
When the batch is applied
Then the good rows are applied
And a report lists each rejected row with its reason so it can be fixed and re-applied
@edge-cases
Scenario Outline: Row outcomes during apply
Given an import row is "<condition>"
When the batch is applied
Then the row result is "<result>"
Examples:
| condition | result |
| matches a record the staff owns | updated |
| matches no record | rejected, nothing created |
| unknown owner or reviewer | rejected |
| owner equal to reviewer | rejected |
| duplicated within the same file | reported, not applied twice |

Questions

Questions raised in planning, each with the decision taken.

What matches a sheet row to a record?

An update must hit exactly the right existing record, so the match key has to be unambiguous. A name is not unique.

AA stable system-generated code. A name is never used to merge two records silently, and a same-name collision is reported for confirmation. · recommended
BMatch on the record's name.
Decided: A stable system-generated code; a same-name collision is reported, never merged silently (option A).

What happens when a row names an unknown owner or reviewer, or the same person for both?

Auto-creating a user from a sheet, or allowing a person to own and review their own work, would both break the rules the import is meant to keep.

AReject the row, and never auto-create a user. · recommended
BCreate the missing user and import the row.
Decided: Reject the row; never auto-create users (option A).

What happens to a record that is mid-workflow when an import tries to change it?

Changing a record that is in a locked stage could undo work that is already in review.

AReject the row during locked stages; allow the change during preparation. · recommended
BAllow the change at any stage.
Decided: Reject during locked stages, allow during preparation (option A).

Can a staff member change assignments through their import?

Assignment is an admin decision. A staff import that could reassign would be a way around that rule.

ANo. The assignment change is refused on the server, not just hidden in the screen. · recommended
BYes, if the sheet includes an assignment column.
Decided: No, server-enforced (option A).

How we sized it

A function point measures what the software does — its inputs, outputs, lookups and the data it stores — regardless of language or how many lines it took. Each counted item is weighted by complexity: a user input counts 3–6 points, an output or report 4–7, a lookup 3–6, data the feature owns 7–10, and data read from elsewhere 5–7.

  • Admin bulk import that creates and assigns records
    User input
    6
  • Staff bulk update of their own records
    User input
    6
  • Download the import template
    Output or report
    5
  • View the history of import runs
    Lookup
    4
  • Bulk field update with auto-finalise
    User input
    4
  • Total
    25
25 function points (range 18–30). IFPUG-style count: each user action, lookup, output and stored data set, weighted by complexity. Technical plumbing is excluded, and a data store is counted once in the feature that owns it.

Not counted here

  • The records themselves: counted once, in their owning feature

Components touched

Spreadsheet update import

Update-only: it matches each row to an existing record on a stable system-generated code and does not create records. A staff member can update only the records they own; an administrator can update across the tenant.

Layered validation

Whole-file rules (naming, a 1,000-row cap, file type by content bytes), then a pinned header and column schema with a strict date format, then per-row identity and ownership checks, then a workflow-lock check at apply time. Each updated record goes through the single patch writer, so every record invariant is re-checked and audited.

Staging and explicit Apply

An upload is staged first; nothing changes in the records until an explicit Apply. Each row is applied on its own, so a bad row is rejected with a reason and can be fixed and re-applied while the rest go through.

Shared locked-stage guard

One guard that rejects an update to a record in a locked workflow stage at apply time.

Document-upload apply path

A separate document-upload apply path matches an existing record and refuses an ambiguous match for confirmation; like the spreadsheet path it never creates a record.

Data touched

Customer recordswrite

Updated by the import for the records in scope; bulk import does not create records.

Staff listread

Read to resolve owner and reviewer; a row naming an unknown person is rejected.

Import-run historywrite

A staged batch and a record of each import run and its per-row outcomes.

Audit logappend

Each applied record update goes through the single patch writer and is audited.

Security decisions

  • Bulk import is update-only; it does not create records, and a row is matched on a stable system-generated code, never merged on a name.
  • The admin-only assignment rule is enforced on the server, so a smuggled assignment column is refused, not just hidden in the screen.
  • A staff import is limited to records that staff member owns.
  • A row naming an unknown owner or reviewer, or the same person for both, is rejected; a user is never auto-created.
  • An update to a record in a locked workflow stage is rejected at apply time.
  • Nothing changes until an explicit Apply; each applied update runs through the single patch writer and is audited.

Subtasks

  • The update-only spreadsheet import, matched on a stable code.
  • Layered validation: file structure, a pinned header and column schema, per-row identity and ownership, and a workflow-lock check at apply time.
  • Staging with an explicit Apply; per-row application with a reason on each rejected row.
  • The shared locked-stage guard.
  • The document-upload apply path matches an existing record, with ambiguity refusal.

How each scenario is verified

  • A staff import updates owned records by a stable code
    A bulk-import integration test updates records on the staff path, matched on a stable code; a row matching no record is rejected and creates nothing, and an unmapped match is rejected while the rest proceed.
  • A staff import cannot change the owner or reviewer
    A server-side integration test checks that an assignment column put into a staff sheet is refused by the server, not just hidden in the screen.
  • Nothing changes until an explicit Apply
    A staging test confirms a fully staged batch writes zero changes until Apply, and a no-match row is flagged while the rest of the batch proceeds.
  • File rules and per-row outcomes
    A rules and integration test cover the 1,000-row cap, file-naming and archive rejection, a same-name collision reported for confirmation rather than fuzzy-matched, and idempotency replay and concurrent submits.
  • A record in a locked stage is rejected at apply time
    Verified by review of the apply-time lock check.

Risk tier

Core A person approves every merge. This is a Core-tier change, so it also carries the fullest set of automated tests; a regression here affects the whole product, not one page.

Activity

Shipped across four pull requests.

  • Planned, then built as four pull requests
  • Each merged to main after a person approved it

Start here

Want this on your backlog?

Get a free baseline report on one repository, or scope a 3-month pilot measured against what your team’s work costs today.

Measured from your own git history
Every pull request tested before review
A person approves every merge