'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 campaign(): BelongsTo { return $this->belongsTo(Campaign::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 */ public function content(): array { return (array) ($this->payload['content'] ?? []); } /** @return array */ 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; } }