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
4 changes: 2 additions & 2 deletions server/resources/views/labels/default.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<div style="width: 420px; border: 2px #414141 solid; margin: auto;">
<div class="group" style="border-bottom: 1px #414141 solid; overflow: hidden;">
<div style="float: left; width: 130px; height: 120px; border-right: 1px #414141 solid; text-align: center; padding: 10px; box-sizing: border-box;">
<img src="data:image/png;base64,<?php echo Utils::notEmpty($trackingNumber) ? $trackingNumber->qr_code : $order->trackingNumber->qr_code; ?>" style="width: 100%; height: 100%; object-fit: cover;">
<img src="data:image/png;base64,<?php echo Utils::notEmpty($trackingNumber) ? $trackingNumber->qr_code : $order->trackingNumber->qr_code; ?>" style="width: 100px; height: 100px;">
</div>
<div style="margin-top: 10px; padding-left: 10px;">
<div style="font-size: 18px; font-weight: bold;"><?php echo e($company->name); ?></div>
Expand Down Expand Up @@ -185,7 +185,7 @@
<?php } ?>
<div class="group">
<div style="text-align: center; padding: 15px 0px; border-top: 1px #414141 solid;">
<img src="data:image/png;base64,<?php echo isset($trackingNumber) ? $trackingNumber->barcode : $order->trackingNumber->barcode; ?>" style="height: 90px; margin-top: 10px;">
<img src="data:image/png;base64,<?php echo isset($trackingNumber) ? $trackingNumber->barcode : $order->trackingNumber->barcode; ?>" style="max-width: 360px; max-height: 90px; margin-top: 10px;">
<div><?php echo e(strtoupper(isset($trackingNumber) ? $trackingNumber->tracking_number : $order->trackingNumber->tracking_number)); ?></div>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions server/resources/views/labels/entity-label.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
<div style="width: 420px; border: 2px #414141 solid; margin: auto;">
<div class="group" style="border-bottom: 1px #414141 solid; overflow: hidden;">
<div style="float: left; width: 130px; height: 120px; border-right: 1px #414141 solid; text-align: center; padding: 10px; box-sizing: border-box;">
<img src="data:image/png;base64,<?= Utils::notEmpty($trackingNumber) ? $trackingNumber->qr_code : $entity->qr_code ?>" style="width: 100%; height: 100%; object-fit: cover;">
<img src="data:image/png;base64,<?= Utils::notEmpty($trackingNumber) ? $trackingNumber->qr_code : $entity->qr_code ?>" style="width: 100px; height: 100px;">
</div>
<div style="margin-top: 10px;">
<span style="padding-left: 10px; font-size: 18px; font-weight: bold;"><?= strtoupper($company->name) ?></span>
Expand Down Expand Up @@ -114,7 +114,7 @@
<?php } ?>
<div class="group">
<div style="text-align: center; padding: 15px 0px; border-top: 1px #414141 solid;">
<img src="data:image/png;base64,<?= isset($trackingNumber) ? $trackingNumber->barcode : $entity->barcode ?>" style="height: 90px; margin-top: 10px;">
<img src="data:image/png;base64,<?= isset($trackingNumber) ? $trackingNumber->barcode : $entity->barcode ?>" style="max-width: 360px; max-height: 90px; margin-top: 10px;">
<div><?= strtoupper(isset($trackingNumber) ? $trackingNumber->tracking_number : $entity->public_id) ?></div>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions server/resources/views/labels/waypoint-label.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
<div style="width: 420px; border: 2px #414141 solid; margin: auto;">
<div class="group" style="border-bottom: 1px #414141 solid; overflow: hidden;">
<div style="float: left; width: 130px; height: 120px; border-right: 1px #414141 solid; text-align: center; padding: 10px; box-sizing: border-box;">
<img src="data:image/png;base64,<?php echo Utils::notEmpty($trackingNumber) ? $trackingNumber->qr_code : $order->trackingNumber->qr_code; ?>" style="width: 100%; height: 100%; object-fit: cover;">
<img src="data:image/png;base64,<?php echo Utils::notEmpty($trackingNumber) ? $trackingNumber->qr_code : $order->trackingNumber->qr_code; ?>" style="width: 100px; height: 100px;">
</div>
<div style="margin-top: 10px;">
<span style="padding-left: 10px; font-size: 18px; font-weight: bold;"><?php echo e($company->name); ?></span>
Expand Down Expand Up @@ -123,7 +123,7 @@
<?php } ?>
<div class="group">
<div style="text-align: center; padding: 15px 0px; border-top: 1px #414141 solid;">
<img src="data:image/png;base64,<?php echo isset($trackingNumber) ? $trackingNumber->barcode : $order->trackingNumber->barcode; ?>" style="height: 90px; margin-top: 10px;">
<img src="data:image/png;base64,<?php echo isset($trackingNumber) ? $trackingNumber->barcode : $order->trackingNumber->barcode; ?>" style="max-width: 360px; max-height: 90px; margin-top: 10px;">
<div><?php echo e(strtoupper(isset($trackingNumber) ? $trackingNumber->tracking_number : $order->trackingNumber->tracking_number)); ?></div>
</div>
</div>
Expand Down
5 changes: 3 additions & 2 deletions server/src/Http/Controllers/Api/v1/OrderController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use Fleetbase\FleetOps\Models\ServiceQuote;
use Fleetbase\FleetOps\Models\Waypoint;
use Fleetbase\FleetOps\Support\ResolvesOrderServiceStops;
use Fleetbase\FleetOps\Support\TrackingCode;
use Fleetbase\FleetOps\Support\Utils;
use Fleetbase\Http\Controllers\Controller;
use Fleetbase\Http\Resources\Comment as CommentResource;
Expand Down Expand Up @@ -1444,8 +1445,8 @@ public function captureQrScan(Request $request, string $id, ?string $subjectId =
return $this->apiError('Unable to capture QR code data.');
}

// validate
if ($subject && $code === $subject->uuid) {
// validate: the scan must name this subject, in any code format still in circulation
if (TrackingCode::matches($code, $subject)) {
// create verification proof
$proof = $this->createProof([
'company_uuid' => session('company'),
Expand Down
38 changes: 6 additions & 32 deletions server/src/Http/Controllers/Api/v1/TrackingNumberController.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,10 @@ public function fromQR(DecodeTrackingNumberQR $request)
// validate request inputs
$code = $request->input('code');

// get the model of from the code
$model = $this->findQrModel(['entities', 'orders'], ['uuid' => $code]);
// the order, waypoint, entity or place the code points at, in any format still in
// circulation: a tracking url, a bare tracking number or public id, or the bare
// owner uuid printed on older labels
$model = $this->findQrModel($code);

// if no model response with error
if (!$model) {
Expand Down Expand Up @@ -179,37 +181,9 @@ protected function deletedTrackingNumberResource(TrackingNumber $trackingNumber)
return new DeletedResource($trackingNumber);
}

protected function findQrModel(array $tables, array $where)
protected function findQrModel(string $code)
{
// Hydrate through the eloquent models so the resolved record can be
// wrapped in its typed resource — raw rows have no model class.
$modelMap = [
'entities' => \Fleetbase\FleetOps\Models\Entity::class,
'orders' => \Fleetbase\FleetOps\Models\Order::class,
];

foreach ($tables as $table) {
$modelClass = $modelMap[$table] ?? null;
if (!$modelClass) {
continue;
}

$model = $modelClass::where($where)->first();
if ($model) {
return $model;
}
}

// No raw-row fallback. Utils::findModel() was called with the ARRAY of table names
// and, when nothing matched, passed that array to DB::table() — which stringified
// it to the literal table name "Array" and threw SQLSTATE[42S02]. Every
// unresolvable code became a 500 instead of the 400 this method's caller already
// handles.
//
// The fallback could not have helped anyway: qrModelResource() serializes through
// a typed resource keyed on the model class, and a raw stdClass row has none. The
// mapped models above are the endpoint's whole contract.
return null;
return TrackingNumber::findOwnerByCode($code, session('company'));
}

protected function qrModelResource($model)
Expand Down
42 changes: 8 additions & 34 deletions server/src/Http/Resources/v1/TrackingNumber.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Fleetbase\FleetOps\Http\Resources\v1;

use Fleetbase\FleetOps\Support\TrackingCode;
use Fleetbase\FleetOps\Support\Utils;
use Fleetbase\Http\Resources\FleetbaseResource;
use Fleetbase\Support\Http;
Expand Down Expand Up @@ -30,47 +31,20 @@ public function toArray($request)
'status' => $this->last_status,
'status_code' => $this->last_status_code,
'qr_code' => $this->qr_code,
// The raw text encoded inside qr_code, exposed ONLY in debug mode.
//
// qr_code is a base64 PNG generated from owner_uuid (TrackingNumber::newBarcode),
// and the endpoints that consume a scanned code match on that uuid. A client
// scans the image to obtain it; an automated contract run cannot, so debug
// builds publish the value beside the image rather than the API growing a
// decode endpoint or handing out uuids generally.
//
// `when()` omits the key entirely when false — it is absent, not null — so a
// production response is byte-identical to before.
'qr_code_content' => $this->when(static::exposesQrCodeContent(), fn () => $this->owner_uuid),
// A value that scans as this tracking number: the QR content current labels
// print (see TrackingCode). It names the tracking number and the subject's
// public id, both already in this resource, so it is safe to publish, and lets
// clients that print their own labels, or cannot decode a PNG, use the code.
// Labels printed before it encode the owner uuid; resolvers accept both.
'qr_code_content' => $this->when($this->tracking_number, fn () => TrackingCode::qrContent($this->tracking_number, Utils::get($this->owner, 'public_id'))),
'barcode' => $this->barcode,
'url' => Utils::consoleUrl('track-order', ['order' => $this->tracking_number]),
'url' => Utils::consoleUrl(TrackingCode::PATH, [TrackingCode::TRACKING_PARAM => $this->tracking_number]),
'type' => Utils::getTypeFromClassName($this->owner_type),
'updated_at' => $this->updated_at,
'created_at' => $this->created_at,
];
}

/**
* Whether the QR code's decoded content may be published.
*
* Debug mode only, and fails closed: any failure to determine the debug state — no
* container, an application without the accessor — answers false rather than
* defaulting to exposure.
*/
protected static function exposesQrCodeContent(): bool
{
try {
$app = app();

if (!is_object($app) || !method_exists($app, 'hasDebugModeEnabled')) {
return false;
}

return (bool) $app->hasDebugModeEnabled();
} catch (\Throwable $e) {
return false;
}
}

/**
* Transform the resource into an webhook payload.
*
Expand Down
9 changes: 5 additions & 4 deletions server/src/Models/Entity.php
Original file line number Diff line number Diff line change
Expand Up @@ -471,10 +471,11 @@ public static function insertGetUuid($values = [], ?Payload $payload = null)
if ($result && $payload) {
// create tracking number for entity
$trackingNumberId = TrackingNumber::insertGetUuid([
'owner_uuid' => $uuid,
'owner_type' => Utils::getModelClassName('entity'),
'region' => $payload->getPickupRegion(),
'location' => Utils::parsePointToWkt($payload->getPickupLocation()),
'owner_uuid' => $uuid,
'owner_type' => Utils::getModelClassName('entity'),
'owner_public_id' => $values['public_id'],
'region' => $payload->getPickupRegion(),
'location' => Utils::parsePointToWkt($payload->getPickupLocation()),
]);

// set tracking number
Expand Down
98 changes: 96 additions & 2 deletions server/src/Models/TrackingNumber.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Fleetbase\Casts\PolymorphicType;
use Fleetbase\FleetOps\Casts\Point;
use Fleetbase\FleetOps\Support\TrackingCode;
use Fleetbase\FleetOps\Support\Utils;
use Fleetbase\LaravelMysqlSpatial\Eloquent\SpatialTrait;
use Fleetbase\Models\Model;
Expand All @@ -16,6 +17,7 @@
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Milon\Barcode\Facades\DNS1DFacade as DNS1D;
use Milon\Barcode\Facades\DNS2DFacade as DNS2D;

class TrackingNumber extends Model
Expand Down Expand Up @@ -236,6 +238,74 @@ public static function findTrackingOrFail($id)
throw (new \Illuminate\Database\Eloquent\ModelNotFoundException())->setModel(static::class, $id);
}

/**
* The record a scanned code points at, within one company: the order, waypoint,
* entity or place that owns the tracking number.
*
* Accepts every format TrackingCode::parse() reads, including the bare owner uuid
* printed on labels before codes carried a url, and a bare tracking number or public
* id from a barcode or typed by hand. Without a company nothing resolves.
*/
public static function findOwnerByCode(?string $code, ?string $companyUuid): ?\Illuminate\Database\Eloquent\Model
{
if (empty($companyUuid)) {
return null;
}

$parsed = TrackingCode::parse($code);

if ($parsed['uuid']) {
return static::ownerOf(static::firstForCode('owner_uuid', $parsed['uuid'], $companyUuid));
}

$number = $parsed['tracking_number'] ?? $parsed['reference'];
if ($number) {
$owner = static::ownerOf(static::firstForCode('tracking_number', $number, $companyUuid));

// A url names its tracking number deliberately, so an unknown one is a miss,
// not a cue to fall back to the owner it also names, which must agree.
if ($owner || $parsed['tracking_number']) {
return $owner && (!$parsed['public_id'] || data_get($owner, 'public_id') === $parsed['public_id']) ? $owner : null;
}
}

$publicId = $parsed['public_id'] ?? $parsed['reference'];

return $publicId ? static::ownerOf(static::firstForOwnerPublicId($publicId, $companyUuid)) : null;
}

protected static function firstForCode(string $column, string $value, string $companyUuid): ?self
{
return static::where('company_uuid', $companyUuid)->where($column, $value)->first();
}

protected static function firstForOwnerPublicId(string $publicId, string $companyUuid): ?self
{
// The public id's prefix names the owner's model, so only that table is searched.
$prefix = Str::contains($publicId, '_') ? Str::before($publicId, '_') : null;
$ownerClass = [
'order' => Order::class,
'waypoint' => Waypoint::class,
'entity' => Entity::class,
'place' => Place::class,
][$prefix] ?? null;

if (!$ownerClass) {
return null;
}

return static::where('company_uuid', $companyUuid)
// Rows inserted raw (waypoints, entities) store the class with a leading slash.
->whereIn('owner_type', [$ownerClass, '\\' . $ownerClass])
->whereIn('owner_uuid', $ownerClass::withoutGlobalScopes()->select('uuid')->where('public_id', $publicId))
->first();
}

protected static function ownerOf(?self $trackingNumber): ?\Illuminate\Database\Eloquent\Model
{
return $trackingNumber?->owner;
}

public function updateOwnerStatus(?TrackingStatus $trackingStatus = null)
{
$trackingStatus = $trackingStatus ?? $this->load(['status'])->getRelationValue('status');
Expand All @@ -254,6 +324,10 @@ public function updateOwnerStatus(?TrackingStatus $trackingStatus = null)

public static function insertGetUuid($values = [], ?Model $owner = null)
{
// Read before the fillable filter below drops it. Callers that insert the owner
// with a raw query, and so have no model to pass, hand its public_id over here.
$ownerPublicId = $owner ? data_get($owner, 'public_id') : ($values['owner_public_id'] ?? null);

$instance = new static();
$fillable = $instance->getFillable();
$insertKeys = array_keys($values);
Expand All @@ -276,8 +350,7 @@ public static function insertGetUuid($values = [], ?Model $owner = null)
}

$values['tracking_number'] = static::newTrackingNumber($values['region'] ?? 'SG');
$values['qr_code'] = static::newBarcode((string) ($values['owner_uuid'] ?? ''), 'QRCODE');
$values['barcode'] = static::newBarcode((string) ($values['owner_uuid'] ?? ''), 'PDF417');
$values = array_merge($values, static::codeImages($values['tracking_number'], $ownerPublicId));

if (isset($values['meta']) && (is_object($values['meta']) || is_array($values['meta']))) {
$values['meta'] = json_encode($values['meta']);
Expand Down Expand Up @@ -339,8 +412,29 @@ protected static function newTrackingNumber(string $region): string
return static::generateNumber($region);
}

/**
* The base64 PNGs printed on a label: a QR code carrying the tracking url (see
* TrackingCode) and a Code 128 barcode carrying the bare tracking number.
*
* @return array{qr_code: string, barcode: string}
*/
public static function codeImages(string $trackingNumber, ?string $ownerPublicId = null): array
{
return [
// Medium error correction: labels get scuffed, and the url fits comfortably.
'qr_code' => static::newBarcode(TrackingCode::qrContent($trackingNumber, $ownerPublicId), 'QRCODE,M'),
'barcode' => static::newBarcode(TrackingCode::barcodeContent($trackingNumber), 'C128'),
];
}

protected static function newBarcode(string $value, string $type): string
{
if ($type === 'C128') {
// 2px modules, 60px tall: a typical tracking number renders ~330px wide, inside
// the label's 360px cap, so it prints at its own resolution.
return DNS1D::getBarcodePNG($value, $type, 2, 60);
}

return DNS2D::getBarcodePNG($value, $type);
}

Expand Down
9 changes: 5 additions & 4 deletions server/src/Models/Waypoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,11 @@ public static function insertGetUuid($values = [], ?Payload $payload = null)
if ($result && $payload) {
// create tracking number for entity
$trackingNumberId = static::createTrackingNumber([
'owner_uuid' => $uuid,
'owner_type' => Utils::getModelClassName('waypoint'),
'region' => $payload->getPickupRegion(),
'location' => static::pickupLocationWkt($payload),
'owner_uuid' => $uuid,
'owner_type' => Utils::getModelClassName('waypoint'),
'owner_public_id' => $values['public_id'],
'region' => $payload->getPickupRegion(),
'location' => static::pickupLocationWkt($payload),
]);

// set tracking number
Expand Down
Loading
Loading