Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
40 changes: 36 additions & 4 deletions server/src/Models/TrackingNumber.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,47 @@ 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);
}

return $number . strtoupper($region);
return $number . static::trackingNumberSuffix($region);
}

/**
* The tracking number prefix for a company: the first three letters or
* digits of its name, upper-cased, or FLB when it has none. Spaces and
* punctuation are skipped, so "Al Noor Transport" gives ALN.
*/
public static function trackingNumberPrefix(?string $companyName): string
{
$prefix = substr(static::trackingNumberCharacters($companyName), 0, 3);

return $prefix !== '' ? $prefix : 'FLB';
}

/**
* The tracking number suffix for a region: its letters and digits,
* upper-cased, or SG when it has none. A region can be a province or
* district name when the pickup has no country ("New York" gives NEWYORK).
*/
public static function trackingNumberSuffix(?string $region): string
{
$suffix = static::trackingNumberCharacters($region);

return $suffix !== '' ? $suffix : 'SG';
}

/**
* The ASCII letters and digits of a value, upper-cased. A tracking number
* holds nothing else, so it can always be typed, scanned and looked up.
*/
protected static function trackingNumberCharacters(?string $value): string
{
return (string) preg_replace('/[^A-Z0-9]/', '', strtoupper((string) $value));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion server/src/Support/TrackingPage/TrackingResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
33 changes: 33 additions & 0 deletions server/tests/Unit/Models/TrackingNumberTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,36 @@ 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('ALN')
->and(TrackingNumber::trackingNumberPrefix('A&B Logistics'))->toBe('ABL')
->and(TrackingNumber::trackingNumberPrefix("O'Brien Couriers"))->toBe('OBR')
->and(TrackingNumber::trackingNumberPrefix('3PL Hub'))->toBe('3PL')
->and(TrackingNumber::trackingNumberPrefix('Ab'))->toBe('AB')
->and(TrackingNumber::trackingNumberPrefix('Café Express'))->toBe('CAF')
->and(TrackingNumber::trackingNumberPrefix('النور'))->toBe('FLB')
->and(TrackingNumber::trackingNumberPrefix(' '))->toBe('FLB')
->and(TrackingNumber::trackingNumberPrefix(null))->toBe('FLB');
});

test('tracking number suffixes keep only letters and digits from the region', function () {
expect(TrackingNumber::trackingNumberSuffix('sg'))->toBe('SG')
->and(TrackingNumber::trackingNumberSuffix('New York'))->toBe('NEWYORK')
->and(TrackingNumber::trackingNumberSuffix(' AE '))->toBe('AE')
->and(TrackingNumber::trackingNumberSuffix('Île-de-France'))->toBe('LEDEFRANCE')
->and(TrackingNumber::trackingNumberSuffix('القاهرة'))->toBe('SG')
->and(TrackingNumber::trackingNumberSuffix(''))->toBe('SG')
->and(TrackingNumber::trackingNumberSuffix(null))->toBe('SG');
});

test('generated tracking numbers hold only letters and digits whatever the company name and region', function () {
session(['company' => null]);

foreach (['New York', 'Abu Dhabi', 'ae', "Côte d'Ivoire", ''] as $region) {
expect(TrackingNumber::generateTrackingNumber($region, 10))->toMatch('/^[A-Z0-9]+$/');
}

expect(TrackingNumber::generateTrackingNumber('New York', 4))->toMatch('/^FLB\d{4}NEWYORK$/');
});
20 changes: 20 additions & 0 deletions server/tests/Unit/Support/TrackingPage/TrackingResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading