From 7db38fa2a39733091bc1bac101f9839d7e683866 Mon Sep 17 00:00:00 2001 From: Shivanath Devinarayanan Date: Fri, 23 Oct 2026 01:50:14 +0400 Subject: [PATCH 1/2] fix(tracking): accept single inner spaces in tracking number lookups Tracking numbers are generated with the first three characters of the company name as their prefix, so a company named "Al Noor" issues numbers like "AL 7796486490AE". The lookup pattern added in #352 has no space in its character class, so the public tracking page and fleet-ops/lookup now answer "not found" for those numbers. Allow a space only when it sits between two allowed characters. Leading, trailing and repeated spaces, tabs, newlines, quotes and other symbols are still refused, and the 100 character limit is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Internal/v1/OrderController.php | 2 +- .../Support/TrackingPage/TrackingResolver.php | 2 +- .../Internal/OrderControllerContractsTest.php | 8 ++++++++ .../TrackingPage/TrackingResolverTest.php | 20 +++++++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/server/src/Http/Controllers/Internal/v1/OrderController.php b/server/src/Http/Controllers/Internal/v1/OrderController.php index 14cd0a4f5..2bbe6420d 100644 --- a/server/src/Http/Controllers/Internal/v1/OrderController.php +++ b/server/src/Http/Controllers/Internal/v1/OrderController.php @@ -1984,7 +1984,7 @@ protected function normalizeTrackingLookup($input): ?string $input = trim($input); - return preg_match('/^[A-Za-z0-9][A-Za-z0-9._\-]{0,99}$/', $input) === 1 ? $input : null; + return preg_match('/^[A-Za-z0-9](?:[A-Za-z0-9._\-]| (?=[A-Za-z0-9._\-])){0,99}$/', $input) === 1 ? $input : null; } /** diff --git a/server/src/Support/TrackingPage/TrackingResolver.php b/server/src/Support/TrackingPage/TrackingResolver.php index 09b3091fd..927116e0f 100644 --- a/server/src/Support/TrackingPage/TrackingResolver.php +++ b/server/src/Support/TrackingPage/TrackingResolver.php @@ -22,7 +22,7 @@ */ class TrackingResolver { - public const NUMBER_PATTERN = '/^[A-Za-z0-9][A-Za-z0-9._\-]{0,99}$/'; + public const NUMBER_PATTERN = '/^[A-Za-z0-9](?:[A-Za-z0-9._\-]| (?=[A-Za-z0-9._\-])){0,99}$/'; /** * The tracking number as typed, trimmed, or null when it can't be one. diff --git a/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php b/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php index 9b5e62f19..ff183cbe5 100644 --- a/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +++ b/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php @@ -1651,6 +1651,14 @@ function fleetopsSuppressStrNullDeprecations(): Closure expect($controller->lookup(new Request(['tracking' => 'TN-404']))->getData(true))->toBe($notFound) ->and($controller->trackingLookups)->toBe(['TN-404']); + // Numbers generated for companies with a space in the first three letters of + // their name ("AL 7796486490AE") reach the query; other spacing does not. + foreach (['AL 1', "AL\t1", "AL\n1", 'AL&1'] as $tracking) { + expect($controller->lookup(new Request(['tracking' => $tracking]))->getData(true))->toBe($notFound); + } + expect($controller->lookup(new Request(['tracking' => 'AL 7796486490AE']))->getData(true))->toBe($notFound) + ->and($controller->trackingLookups)->toBe(['TN-404', 'AL 7796486490AE']); + $trackedOrder = fleetopsInternalOrderLifecycleOrder('tracked-order'); $trackedOrder->trackerForTest = new FleetOpsInternalOrderLifecycleTrackerFake($trackedOrder); $controller->trackingOrder = $trackedOrder; diff --git a/server/tests/Unit/Support/TrackingPage/TrackingResolverTest.php b/server/tests/Unit/Support/TrackingPage/TrackingResolverTest.php index 40f2ee855..73ecd83e1 100644 --- a/server/tests/Unit/Support/TrackingPage/TrackingResolverTest.php +++ b/server/tests/Unit/Support/TrackingPage/TrackingResolverTest.php @@ -80,6 +80,26 @@ function trackingResolverSetting($db, string $key, mixed $value): void ->and((new TrackingResolver())->resolve('../etc'))->toBeNull(); }); +test('tracking numbers may hold single inner spaces, as generated prefixes can', function () { + // generateTrackingNumber() takes the first three letters of the company name, + // so a company called "Al Noor" issues numbers like "AL 7796486490AE". + $matches = fn (string $input) => preg_match(TrackingResolver::NUMBER_PATTERN, $input) === 1; + + expect(TrackingResolver::normalize('AL 7796486490AE'))->toBe('AL 7796486490AE') + ->and(TrackingResolver::normalize('ABC1234567890AE'))->toBe('ABC1234567890AE') + ->and(TrackingResolver::normalize(' AL 7796486490AE '))->toBe('AL 7796486490AE') + ->and($matches(' AL1'))->toBeFalse() + ->and($matches('AL1 '))->toBeFalse() + ->and(TrackingResolver::normalize('AL 1'))->toBeNull() + ->and(TrackingResolver::normalize("AL'1"))->toBeNull() + ->and(TrackingResolver::normalize('AL&1'))->toBeNull() + ->and(TrackingResolver::normalize("AL\t1"))->toBeNull() + ->and(TrackingResolver::normalize("AL\n1"))->toBeNull() + ->and(TrackingResolver::normalize('AL 1 2'))->toBe('AL 1 2') + ->and(TrackingResolver::normalize(str_repeat('A B', 33) . 'A'))->toBe(str_repeat('A B', 33) . 'A') + ->and(TrackingResolver::normalize(str_repeat('A B', 33) . 'AB'))->toBeNull(); +}); + test('each kind of tracking number opens its order for the right customer', function () { trackingResolverFixture(); $resolver = new TrackingResolver(); From 4ee3a825961bfe7f2af3e7d6167573009da5819d Mon Sep 17 00:00:00 2001 From: Shivanath Devinarayanan Date: Fri, 23 Oct 2026 01:50:50 +0400 Subject: [PATCH 2/2] fix(tracking): keep only letters and digits in the generated number prefix generateTrackingNumber() takes the first three characters of the company name as they are, so names such as "Al Noor" or "A&B Logistics" produce numbers containing a space or "&", and names in non-Latin scripts produce partial multibyte characters. Keep only A-Z and 0-9 from those three characters and fall back to FLB when nothing is left. Co-Authored-By: Claude Opus 5.5 (1M context) --- server/src/Models/TrackingNumber.php | 16 +++++++++++++--- server/tests/Unit/Models/TrackingNumberTest.php | 10 ++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/server/src/Models/TrackingNumber.php b/server/src/Models/TrackingNumber.php index a5c9f0c77..eadb881ea 100644 --- a/server/src/Models/TrackingNumber.php +++ b/server/src/Models/TrackingNumber.php @@ -184,9 +184,8 @@ public function getTypeAttribute() */ public static function generateTrackingNumber($region = 'SG', $length = 10): string { - $company = \Fleetbase\Models\Company::where('uuid', session('company'))->withoutGlobalScopes()->first(); - $companyName = $company ? strtoupper(substr($company->name, 0, 3)) : null; - $number = $companyName ?? 'FLB'; + $company = \Fleetbase\Models\Company::where('uuid', session('company'))->withoutGlobalScopes()->first(); + $number = static::trackingNumberPrefix($company ? $company->name : null); for ($i = 0; $i < $length; $i++) { $number .= mt_rand(0, 9); @@ -195,6 +194,17 @@ public static function generateTrackingNumber($region = 'SG', $length = 10): str return $number . strtoupper($region); } + /** + * The tracking number prefix for a company: the letters and digits among the + * first three characters of its name, upper-cased, or FLB when none are left. + */ + public static function trackingNumberPrefix(?string $companyName): string + { + $prefix = (string) preg_replace('/[^A-Z0-9]/', '', strtoupper(substr((string) $companyName, 0, 3))); + + return $prefix !== '' ? $prefix : 'FLB'; + } + /** * Generates a unique fleetbase tracking number. * diff --git a/server/tests/Unit/Models/TrackingNumberTest.php b/server/tests/Unit/Models/TrackingNumberTest.php index c0084249d..6722729c7 100644 --- a/server/tests/Unit/Models/TrackingNumberTest.php +++ b/server/tests/Unit/Models/TrackingNumberTest.php @@ -258,3 +258,13 @@ public function getBarcodePNG(...$arguments) } } }); + +test('tracking number prefixes keep only letters and digits from the company name', function () { + expect(TrackingNumber::trackingNumberPrefix('Nordic Freight'))->toBe('NOR') + ->and(TrackingNumber::trackingNumberPrefix('Al Noor Transport'))->toBe('AL') + ->and(TrackingNumber::trackingNumberPrefix('A&B Logistics'))->toBe('AB') + ->and(TrackingNumber::trackingNumberPrefix('3PL Hub'))->toBe('3PL') + ->and(TrackingNumber::trackingNumberPrefix('النور'))->toBe('FLB') + ->and(TrackingNumber::trackingNumberPrefix(' '))->toBe('FLB') + ->and(TrackingNumber::trackingNumberPrefix(null))->toBe('FLB'); +});