|
28 | 28 | failed_rows: 0, |
29 | 29 | skipped_rows: 1 |
30 | 30 | ) |
31 | | - expect(company.clients.kept.pluck(:name)).to contain_exactly("Acme LLC", "Very Long Client Name That Exc") |
| 31 | + expect(company.clients.kept.pluck(:name)).to contain_exactly("Acme LLC", "Very Long Client Name T-715152") |
32 | 32 | expect(company.projects.kept.count).to eq(3) |
33 | 33 | expect(ProjectMember.kept.joins(project: :client).where(clients: { company_id: company.id }).count).to eq(3) |
34 | 34 |
|
|
43 | 43 | internal = company.projects.kept.find_by!(name: "Internal") |
44 | 44 | expect(internal).not_to be_billable |
45 | 45 | expect(internal.timesheet_entries.kept).to all(be_non_billable) |
46 | | - expect(company.clients.kept.find_by!(name: "Very Long Client Name That Exc").currency).to eq("USD") |
| 46 | + expect(company.clients.kept.find_by!(name: "Very Long Client Name T-715152").currency).to eq("USD") |
47 | 47 | expect(website.project_members.kept.find_by!(user: paul).hourly_rate).to eq(150) |
48 | 48 | end |
49 | 49 |
|
|
153 | 153 | "zero_hour_skipped" => 1, |
154 | 154 | "duplicates_skipped" => 0 |
155 | 155 | ) |
156 | | - expect(data_import.summary.dig("clients", "to_create")).to contain_exactly("Acme LLC", "Very Long Client Name That Exc") |
| 156 | + expect(data_import.summary.dig("clients", "to_create")).to contain_exactly("Acme LLC", "Very Long Client Name T-715152") |
157 | 157 | expect(data_import.summary.dig("projects", "to_create")).to contain_exactly( |
158 | 158 | "Acme LLC / Internal", |
159 | 159 | "Acme LLC / Website", |
160 | | - "Very Long Client Name That Exc / Advisory" |
| 160 | + "Very Long Client Name T-715152 / Advisory" |
161 | 161 | ) |
162 | 162 | expect(data_import.summary.dig("date_range")).to eq("from" => "2026-01-02", "to" => "2026-01-11") |
163 | 163 | expect(data_import.summary).not_to have_key("row_errors") |
|
188 | 188 | expect(TimesheetEntry.kept.last.work_date).to eq(Date.new(2026, 1, 7)) |
189 | 189 | end |
190 | 190 |
|
| 191 | + it "keeps project names distinct when their 30-character prefixes collide" do |
| 192 | + attach_csv_contents(data_import, <<~CSV) |
| 193 | + Date,Client,Project,Project Code,Hours,First Name,Last Name |
| 194 | + 2026-01-01,Acme LLC,Acme 1H 2025 Committed Hours (PC),PC,1,Paul,Connors |
| 195 | + CSV |
| 196 | + |
| 197 | + described_class.new(data_import).process |
| 198 | + |
| 199 | + second_import = create(:data_import, company:, user: actor) |
| 200 | + attach_csv_contents(second_import, <<~CSV) |
| 201 | + Date,Client,Project,Hours,First Name,Last Name |
| 202 | + 2026-01-02,Acme LLC,Acme 1H 2025 Committed Hours (LW),1,Paul,Connors |
| 203 | + CSV |
| 204 | + described_class.new(second_import).process |
| 205 | + |
| 206 | + projects = company.projects.kept.order(:name) |
| 207 | + expect(projects.pluck(:name).uniq.size).to eq(2) |
| 208 | + expect(projects.pluck(:name).map(&:length)).to all(be <= 30) |
| 209 | + expect(projects.pluck(:description)).to contain_exactly( |
| 210 | + "Harvest project: Acme 1H 2025 Committed Hours (LW)", |
| 211 | + "Harvest project: Acme 1H 2025 Committed Hours (PC)\nHarvest project code: PC" |
| 212 | + ) |
| 213 | + expect(projects.flat_map { |project| project.timesheet_entries.kept.pluck(:work_date) }).to contain_exactly( |
| 214 | + Date.new(2026, 1, 1), |
| 215 | + Date.new(2026, 1, 2) |
| 216 | + ) |
| 217 | + expect(data_import.reload.summary["warnings"]).to include(match(/Project name truncated:/)) |
| 218 | + |
| 219 | + reimport = create(:data_import, company:, user: actor) |
| 220 | + attach_csv_contents(reimport, <<~CSV) |
| 221 | + Date,Client,Project,Hours,First Name,Last Name |
| 222 | + 2026-01-02,Acme LLC,Acme 1H 2025 Committed Hours (LW),1,Paul,Connors |
| 223 | + 2026-01-01,Acme LLC,Acme 1H 2025 Committed Hours (PC),1,Paul,Connors |
| 224 | + CSV |
| 225 | + |
| 226 | + expect do |
| 227 | + described_class.new(reimport).process |
| 228 | + end.not_to change { [company.projects.kept.count, TimesheetEntry.kept.count] } |
| 229 | + expect(reimport.reload).to have_attributes(imported_rows: 0, skipped_rows: 2) |
| 230 | + end |
| 231 | + |
| 232 | + it "keeps client names distinct when their 30-character prefixes collide" do |
| 233 | + attach_csv_contents(data_import, <<~CSV) |
| 234 | + Date,Client,Project,Hours,First Name,Last Name |
| 235 | + 2026-01-01,Acme Corporate Legal Services Alpha,Website,1,Paul,Connors |
| 236 | + CSV |
| 237 | + |
| 238 | + described_class.new(data_import).process |
| 239 | + |
| 240 | + second_import = create(:data_import, company:, user: actor) |
| 241 | + attach_csv_contents(second_import, <<~CSV) |
| 242 | + Date,Client,Project,Hours,First Name,Last Name |
| 243 | + 2026-01-02,Acme Corporate Legal Services Bravo,Website,1,Paul,Connors |
| 244 | + CSV |
| 245 | + described_class.new(second_import).process |
| 246 | + |
| 247 | + expect(company.clients.kept.pluck(:name).uniq.size).to eq(2) |
| 248 | + expect(company.projects.kept.count).to eq(2) |
| 249 | + expect(data_import.reload.summary["warnings"]).to include(match(/Client name truncated:/)) |
| 250 | + end |
| 251 | + |
| 252 | + it "reuses unambiguous client and project names created by the legacy importer" do |
| 253 | + client_name = "Acme Corporate Legal Services Alpha" |
| 254 | + project_name = "Acme 1H 2025 Committed Hours (PC)" |
| 255 | + create(:data_import, company:, user: actor, status: "completed", summary: { |
| 256 | + "warnings" => ["Client name truncated: #{client_name}", "Project name truncated: #{project_name}"] |
| 257 | + }) |
| 258 | + legacy_client = create(:client, company:, name: client_name.first(30)) |
| 259 | + legacy_project = create(:project, client: legacy_client, name: project_name.first(30)) |
| 260 | + create(:timesheet_entry, project: legacy_project, user: paul, work_date: Date.new(2026, 1, 1), duration: 60, note: "", source: "import") |
| 261 | + attach_csv_contents(data_import, <<~CSV) |
| 262 | + Date,Client,Project,Hours,First Name,Last Name |
| 263 | + 2026-01-01,#{client_name},#{project_name},1,Paul,Connors |
| 264 | + CSV |
| 265 | + |
| 266 | + expect do |
| 267 | + described_class.new(data_import).process |
| 268 | + end.not_to change { [company.clients.count, company.projects.count, TimesheetEntry.count] } |
| 269 | + |
| 270 | + expect(data_import.reload).to have_attributes(imported_rows: 0, skipped_rows: 1) |
| 271 | + end |
| 272 | + |
191 | 273 | it "records invalid hours, date, and blank client rows without creating entries" do |
192 | 274 | attach_csv_contents(data_import, <<~CSV) |
193 | 275 | Date,Client,Project,Hours,First Name,Last Name |
|
238 | 320 | ) |
239 | 321 | end |
240 | 322 |
|
241 | | - it "truncates project names and reuses a matching project" do |
| 323 | + it "shortens long project names deterministically" do |
242 | 324 | client = create(:client, company:, name: "Acme LLC") |
243 | | - project = create(:project, client:, name: "A Project Name Longer Than Thi", billable: true) |
244 | 325 | attach_csv_contents(data_import, <<~CSV) |
245 | 326 | Date,Client,Project,Hours,First Name,Last Name,Billable? |
246 | 327 | 2026-01-01,Acme LLC,A Project Name Longer Than Thirty Characters,1,Paul,Connors,Yes |
247 | 328 | CSV |
248 | 329 |
|
249 | 330 | described_class.new(data_import).process |
250 | 331 |
|
251 | | - expect(client.projects.count).to eq(1) |
| 332 | + project = client.projects.find_by!(name: "A Project Name Longer T-ae2f52") |
252 | 333 | expect(project.timesheet_entries.kept).to exist |
253 | 334 | expect(data_import.reload.summary["warnings"]).to include( |
254 | 335 | "Project name truncated: A Project Name Longer Than Thirty Characters" |
255 | 336 | ) |
| 337 | + |
| 338 | + reimport = create(:data_import, company:, user: actor) |
| 339 | + attach_csv_contents(reimport, <<~CSV) |
| 340 | + Date,Client,Project,Hours,First Name,Last Name,Billable? |
| 341 | + 2026-01-01,Acme LLC,A PROJECT NAME LONGER THAN THIRTY CHARACTERS,1,Paul,Connors,Yes |
| 342 | + CSV |
| 343 | + |
| 344 | + expect do |
| 345 | + described_class.new(reimport).process |
| 346 | + end.not_to change { [client.projects.count, TimesheetEntry.kept.count] } |
256 | 347 | end |
257 | 348 |
|
258 | 349 | it "restores an archived client before importing entries" do |
|
0 commit comments