'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 transfer(): HasOne { return $this->hasOne(Transfer::class); } public function publicUrl(): string { return self::publicBaseUrl() . '/q/' . $this->short_code; } /** Direct link for emails and share UI — always on the Transfer app host. */ public function shareUrl(): string { $base = rtrim((string) config('app.url'), '/'); return $base . '/q/' . $this->short_code; } /** * Base URL for public QR links. Always the short platform domain * (ladill.com) — never the signed-in account/product host — so printed * codes and shared links stay short and host-independent of where the QR * was created. */ public static function publicBaseUrl(): string { $appUrl = (string) config('app.url'); $scheme = parse_url($appUrl, PHP_URL_SCHEME) ?: 'https'; $host = (string) config('app.platform_domain') ?: (parse_url($appUrl, PHP_URL_HOST) ?: 'ladill.com'); return $scheme . '://' . $host; } /** * 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 { if ($this->isPaymentType()) { return QrStyleDefaults::defaults(); } 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 isPaymentType(): bool { return $this->type === self::TYPE_PAYMENT; } public function isTransferType(): bool { return $this->type === self::TYPE_TRANSFER; } public function usesLandingPage(): bool { return in_array($this->type, [ self::TYPE_PAYMENT, self::TYPE_TRANSFER, ], 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; } }