From 57c7aa5f9f87ff1c74d6b64778113595b16e7116 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 2 Aug 2026 21:33:29 +0000 Subject: [PATCH] feat(web): use compact cut list identifiers --- .../20260802212500_RemoveJobNumberPadding.cs | 31 +++++++++++++++++ CutList.Web/Services/JobService.cs | 33 +++++-------------- tests/test_job_number_format.py | 28 ++++++++++++++++ 3 files changed, 68 insertions(+), 24 deletions(-) create mode 100644 CutList.Web/Migrations/20260802212500_RemoveJobNumberPadding.cs create mode 100644 tests/test_job_number_format.py diff --git a/CutList.Web/Migrations/20260802212500_RemoveJobNumberPadding.cs b/CutList.Web/Migrations/20260802212500_RemoveJobNumberPadding.cs new file mode 100644 index 0000000..d86a30d --- /dev/null +++ b/CutList.Web/Migrations/20260802212500_RemoveJobNumberPadding.cs @@ -0,0 +1,31 @@ +using CutList.Web.Data; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CutList.Web.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260802212500_RemoveJobNumberPadding")] + public partial class RemoveJobNumberPadding : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" + UPDATE Jobs + SET JobNumber = 'JOB-' + CAST(Id AS varchar(11)) + WHERE JobNumber LIKE 'JOB-%'; + "); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" + UPDATE Jobs + SET JobNumber = 'JOB-' + RIGHT('00000' + CAST(Id AS varchar(5)), 5) + WHERE JobNumber LIKE 'JOB-%'; + "); + } + } +} diff --git a/CutList.Web/Services/JobService.cs b/CutList.Web/Services/JobService.cs index 7a08235..b704f06 100644 --- a/CutList.Web/Services/JobService.cs +++ b/CutList.Web/Services/JobService.cs @@ -42,35 +42,17 @@ public class JobService { await using var context = _factory.CreateDbContext(); job ??= new Job(); - job.JobNumber = await GenerateJobNumberAsync(context); + job.JobNumber = CreateTemporaryJobNumber(); job.CreatedAt = DateTime.UtcNow; context.Jobs.Add(job); await context.SaveChangesAsync(); + + job.JobNumber = $"JOB-{job.Id}"; + await context.SaveChangesAsync(); return job; } - public async Task GenerateJobNumberAsync() - { - await using var context = _factory.CreateDbContext(); - return await GenerateJobNumberAsync(context); - } - - private static async Task GenerateJobNumberAsync(ApplicationDbContext context) - { - var maxNumber = await context.Jobs - .Where(j => j.JobNumber.StartsWith("JOB-")) - .Select(j => j.JobNumber) - .MaxAsync() as string; - - if (maxNumber == null) - return "JOB-00001"; - - var numPart = maxNumber.Substring(4); - if (int.TryParse(numPart, out var num)) - return $"JOB-{num + 1:D5}"; - - return $"JOB-{DateTime.UtcNow:yyyyMMddHHmmss}"; - } + private static string CreateTemporaryJobNumber() => $"TMP-{Guid.NewGuid():N}"[..20]; public async Task QuickCreateAsync(string? customer = null) { @@ -142,7 +124,7 @@ public class JobService var duplicate = new Job { - JobNumber = await GenerateJobNumberAsync(context), + JobNumber = CreateTemporaryJobNumber(), Name = string.IsNullOrWhiteSpace(original.Name) ? null : $"{original.Name} (Copy)", Customer = original.Customer, CuttingToolId = original.CuttingToolId, @@ -153,6 +135,9 @@ public class JobService context.Jobs.Add(duplicate); await context.SaveChangesAsync(); + duplicate.JobNumber = $"JOB-{duplicate.Id}"; + await context.SaveChangesAsync(); + // Copy parts foreach (var part in original.Parts) { diff --git a/tests/test_job_number_format.py b/tests/test_job_number_format.py new file mode 100644 index 0000000..1064cb5 --- /dev/null +++ b/tests/test_job_number_format.py @@ -0,0 +1,28 @@ +"""Regression checks for compact CutList identifiers.""" + +from __future__ import annotations + +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +JOB_SERVICE = REPO_ROOT / "CutList.Web/Services/JobService.cs" +UNPAD_MIGRATION = REPO_ROOT / "CutList.Web/Migrations/20260802212500_RemoveJobNumberPadding.cs" + + +class JobNumberFormatTests(unittest.TestCase): + def test_new_jobs_use_the_database_id_without_zero_padding(self) -> None: + service = JOB_SERVICE.read_text() + + self.assertIn('job.JobNumber = $"JOB-{job.Id}";', service) + self.assertNotIn("D5", service) + self.assertNotIn("MaxAsync() as string", service) + + def test_existing_job_numbers_are_converted_to_their_database_ids(self) -> None: + migration = UNPAD_MIGRATION.read_text() + + self.assertIn("SET JobNumber = 'JOB-' + CAST(Id AS varchar(11))", migration) + + +if __name__ == "__main__": + unittest.main()