Deploy Ladill Events / deploy (push) Successful in 46s
Public URLs, encoded payloads, slug preview, payment callbacks, and asset paths now use ladl.link instead of ladill.com/q/*. Co-authored-by: Cursor <cursoragent@cursor.com>
217 lines
5.5 KiB
PHP
217 lines
5.5 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\Support\LadillLink;
|
|
use App\Support\Qr\QrStyleDefaults;
|
|
use App\Support\Qr\QrTypeCatalog;
|
|
use App\Support\Qr\QrWifiPayload;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
|
|
class QrCode extends Model
|
|
{
|
|
public const TYPE_URL = 'url';
|
|
public const TYPE_DOCUMENT = 'document';
|
|
public const TYPE_LINK_LIST = 'link_list';
|
|
public const TYPE_VCARD = 'vcard';
|
|
public const TYPE_BUSINESS = 'business';
|
|
public const TYPE_IMAGE = 'image';
|
|
public const TYPE_CHURCH = 'church';
|
|
public const TYPE_MENU = 'menu';
|
|
public const TYPE_SHOP = 'shop';
|
|
public const TYPE_APP = 'app';
|
|
public const TYPE_BOOK = 'book';
|
|
public const TYPE_WIFI = 'wifi';
|
|
public const TYPE_COUPON = 'coupon';
|
|
public const TYPE_EVENT = 'event';
|
|
public const TYPE_ITINERARY = 'itinerary';
|
|
|
|
protected $fillable = [
|
|
'user_id',
|
|
'short_code',
|
|
'type',
|
|
'label',
|
|
'destination_url',
|
|
'qr_document_id',
|
|
'payload',
|
|
'is_active',
|
|
'png_path',
|
|
'svg_path',
|
|
'scans_total',
|
|
'unique_scans_total',
|
|
'last_scanned_at',
|
|
'destination_updated_at',
|
|
];
|
|
|
|
protected $casts = [
|
|
'payload' => 'array',
|
|
'is_active' => 'boolean',
|
|
'scans_total' => 'integer',
|
|
'unique_scans_total' => 'integer',
|
|
'last_scanned_at' => 'datetime',
|
|
'destination_updated_at' => 'datetime',
|
|
];
|
|
|
|
public function user(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class);
|
|
}
|
|
|
|
public function document(): BelongsTo
|
|
{
|
|
return $this->belongsTo(QrDocument::class, 'qr_document_id');
|
|
}
|
|
|
|
public function scanEvents(): HasMany
|
|
{
|
|
return $this->hasMany(QrScanEvent::class);
|
|
}
|
|
|
|
public function transactions(): HasMany
|
|
{
|
|
return $this->hasMany(QrTransaction::class);
|
|
}
|
|
|
|
public function saleOrders(): HasMany
|
|
{
|
|
return $this->hasMany(QrSaleOrder::class);
|
|
}
|
|
|
|
public function eventRegistrations(): HasMany
|
|
{
|
|
return $this->hasMany(QrEventRegistration::class);
|
|
}
|
|
|
|
public function publicUrl(): string
|
|
{
|
|
return LadillLink::url($this->short_code);
|
|
}
|
|
|
|
public function publicPath(string $suffix = ''): string
|
|
{
|
|
return $suffix === ''
|
|
? $this->publicUrl()
|
|
: LadillLink::path($this->short_code, $suffix);
|
|
}
|
|
|
|
public static function publicBaseUrl(): string
|
|
{
|
|
return LadillLink::baseUrl();
|
|
}
|
|
|
|
/**
|
|
* String encoded inside the QR image.
|
|
*
|
|
* Most types encode the stable short link so the printed code never changes
|
|
* when content is edited. WiFi is the exception: it bakes the network
|
|
* credentials directly into the image so devices auto-join on scan. That
|
|
* payload is frozen at creation (payload.wifi_encoded) — editing the network
|
|
* afterwards updates the saved info but never re-encodes the printed code.
|
|
*/
|
|
public function encodedPayload(): string
|
|
{
|
|
if ($this->type === self::TYPE_WIFI) {
|
|
$frozen = $this->payload['wifi_encoded'] ?? null;
|
|
|
|
return is_string($frozen) && $frozen !== ''
|
|
? $frozen
|
|
: QrWifiPayload::encode($this->content());
|
|
}
|
|
|
|
return $this->publicUrl();
|
|
}
|
|
|
|
/** WiFi codes encode their join payload directly (auto-join), not a redirect link. */
|
|
public function encodesDirectPayload(): bool
|
|
{
|
|
return $this->type === self::TYPE_WIFI;
|
|
}
|
|
|
|
public function typeLabel(): string
|
|
{
|
|
return QrTypeCatalog::label($this->type);
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
public function content(): array
|
|
{
|
|
return (array) ($this->payload['content'] ?? []);
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
public function style(): array
|
|
{
|
|
return QrStyleDefaults::merge($this->payload['style'] ?? null);
|
|
}
|
|
|
|
public function isUrlType(): bool
|
|
{
|
|
return $this->type === self::TYPE_URL;
|
|
}
|
|
|
|
public function isDocumentType(): bool
|
|
{
|
|
return $this->type === self::TYPE_DOCUMENT;
|
|
}
|
|
|
|
public function isImageType(): bool
|
|
{
|
|
return $this->type === self::TYPE_IMAGE;
|
|
}
|
|
|
|
public function isMenuType(): bool
|
|
{
|
|
return $this->type === self::TYPE_MENU;
|
|
}
|
|
|
|
public function isShopType(): bool
|
|
{
|
|
return $this->type === self::TYPE_SHOP;
|
|
}
|
|
|
|
public function isBookType(): bool
|
|
{
|
|
return $this->type === self::TYPE_BOOK;
|
|
}
|
|
|
|
public function acceptsOrders(): bool
|
|
{
|
|
return in_array($this->type, [self::TYPE_MENU, self::TYPE_SHOP, self::TYPE_CHURCH, self::TYPE_EVENT], true);
|
|
}
|
|
|
|
public function usesLandingPage(): bool
|
|
{
|
|
return in_array($this->type, [
|
|
self::TYPE_DOCUMENT,
|
|
self::TYPE_LINK_LIST,
|
|
self::TYPE_VCARD,
|
|
self::TYPE_BUSINESS,
|
|
self::TYPE_CHURCH,
|
|
self::TYPE_IMAGE,
|
|
self::TYPE_MENU,
|
|
self::TYPE_SHOP,
|
|
self::TYPE_APP,
|
|
self::TYPE_BOOK,
|
|
self::TYPE_WIFI,
|
|
self::TYPE_EVENT,
|
|
self::TYPE_ITINERARY,
|
|
], true);
|
|
}
|
|
|
|
public function resolvesToRedirect(): bool
|
|
{
|
|
return $this->type === self::TYPE_URL;
|
|
}
|
|
|
|
public function redirectUrl(): ?string
|
|
{
|
|
if ($this->destination_url) {
|
|
return $this->destination_url;
|
|
}
|
|
|
|
return $this->content()['url'] ?? null;
|
|
}
|
|
}
|