qrCodes() ->whereIn('type', \App\Support\Qr\QrTypeCatalog::storefrontTypes()) ->latest() ->get(); $previewDataUris = $qrCodes->mapWithKeys(function (QrCode $qr) { return [$qr->id => $this->imageGenerator->previewDataUri($qr)]; }); $wallet = $this->manager->walletFor($account); return view('merchant.storefronts.index', [ 'qrCodes' => $qrCodes, 'previewDataUris' => $previewDataUris, 'wallet' => $wallet, 'activeCount' => $qrCodes->where('is_active', true)->count(), 'pricePerQr' => QrWallet::pricePerQr(), 'topupUrl' => 'https://'.config('app.account_domain').'/wallet', ]); } public function create(Request $request): View { $requestedType = (string) $request->query('type', QrCode::TYPE_SHOP); if (! QrTypeCatalog::isValid($requestedType)) { $requestedType = QrCode::TYPE_SHOP; } $prefill = CrmPrefillCodec::decode($request->query('prefill')); if (($prefill['kind'] ?? null) === 'merchant_storefront' && ! empty($prefill['type'])) { $requestedType = (string) $prefill['type']; } return view('merchant.storefronts.create', [ 'requestedType' => $requestedType, 'prefill' => ($prefill['kind'] ?? null) === 'merchant_storefront' ? $prefill : null, 'catalog' => $this->catalogProducts(), ]); } /** * Active products from the merchant's Ladill CRM catalog, shaped for the * storefront editor's "add from catalog" picker. Resilient: returns [] if * CRM is unreachable so the form still works. * * @return array */ private function catalogProducts(): array { try { $response = CrmClient::for((string) ladill_account()->public_id) ->products(['type' => 'product', 'active' => 1, 'per_page' => 200]); } catch (\Throwable) { return []; } return collect($response['data'] ?? []) ->map(fn ($p) => [ 'name' => (string) ($p['name'] ?? ''), 'price' => number_format(((int) ($p['unit_price_minor'] ?? 0)) / 100, 2, '.', ''), 'currency' => strtoupper((string) ($p['currency'] ?? 'GHS')), 'description' => (string) ($p['description'] ?? ''), ]) ->filter(fn ($p) => $p['name'] !== '') ->values() ->all(); } public function store(Request $request): RedirectResponse { $account = ladill_account(); $request->validate([ 'label' => ['required', 'string', 'max:120'], 'type' => ['required', 'in:'.implode(',', QrTypeCatalog::storefrontTypes())], 'menu_logo' => ['nullable', 'image', 'mimes:jpeg,jpg,png,gif,webp', 'max:4096'], 'menu_cover' => ['nullable', 'image', 'mimes:jpeg,jpg,png,gif,webp', 'max:8192'], 'item_images' => ['nullable', 'array'], 'item_images.*' => ['array'], 'item_images.*.*' => ['mimes:jpeg,jpg,png,gif,webp', 'max:4096'], ]); $data = array_merge($request->all(), [ 'menu_logo' => $request->file('menu_logo'), 'menu_cover' => $request->file('menu_cover'), ]); try { $qrCode = $this->manager->create($account, $data); } catch (RuntimeException $e) { return back()->withInput()->with('error', $e->getMessage()); } return redirect()->route('merchant.storefronts.show', $qrCode) ->with('success', QrTypeCatalog::label($qrCode->type).' storefront created.'); } public function show(QrCode $storefront): View { $this->authorizeStorefront($storefront); return view('merchant.storefronts.show', [ 'qrCode' => $storefront->fresh(), 'previewDataUri' => $this->imageGenerator->previewDataUri($storefront), 'catalog' => $this->catalogProducts(), 'customDomains' => \App\Models\CustomDomain::where('qr_code_id', $storefront->id)->get(), 'customDomainsEnabled' => app(\App\Services\CustomDomain\CustomDomainService::class)->enabled(), 'customDomainServerIp' => config('customdomain.server_ip'), ]); } public function update(Request $request, QrCode $storefront): RedirectResponse { $this->authorizeStorefront($storefront); $request->validate([ 'label' => ['sometimes', 'string', 'max:120'], 'is_active' => ['sometimes', 'boolean'], 'menu_logo' => ['nullable', 'image', 'mimes:jpeg,jpg,png,gif,webp', 'max:4096'], 'menu_cover' => ['nullable', 'image', 'mimes:jpeg,jpg,png,gif,webp', 'max:8192'], 'item_images' => ['nullable', 'array'], 'item_images.*' => ['array'], 'item_images.*.*' => ['mimes:jpeg,jpg,png,gif,webp', 'max:4096'], ]); $data = array_merge($request->all(), [ 'is_active' => $request->boolean('is_active', $storefront->is_active), 'menu_logo' => $request->file('menu_logo'), 'menu_cover' => $request->file('menu_cover'), ]); try { $this->manager->update($storefront, $data); } catch (RuntimeException $e) { return back()->withInput()->with('error', $e->getMessage()); } return back()->with('success', 'Storefront updated.'); } public function destroy(QrCode $storefront): RedirectResponse { $this->authorizeStorefront($storefront); $this->manager->delete($storefront); return redirect() ->route('merchant.storefronts.index') ->with('success', 'Storefront deleted.'); } public function preview(QrCode $storefront): Response { $this->authorizeStorefront($storefront); $qrCode = $this->imageGenerator->ensureValidImages($storefront); $bytes = $this->imageGenerator->normalizeStoredPng($qrCode->png_path); if ($bytes === null) { $bytes = $this->imageGenerator->renderPng($qrCode->encodedPayload(), $qrCode->style()); $this->imageGenerator->generateAndStore($qrCode); } return response($bytes, 200, [ 'Content-Type' => 'image/png', 'Cache-Control' => 'private, max-age=3600', ]); } public function download(QrCode $storefront, string $format): StreamedResponse { $this->authorizeStorefront($storefront); $qrCode = $this->imageGenerator->ensureValidImages($storefront); $path = $format === 'svg' ? $qrCode->svg_path : $qrCode->png_path; $filename = Str::slug($qrCode->label).'-qr.'.($format === 'svg' ? 'svg' : 'png'); if ($format === 'png') { $bytes = $this->imageGenerator->normalizeStoredPng($path); if ($bytes === null) { $bytes = $this->imageGenerator->renderPng($qrCode->encodedPayload(), $qrCode->style()); $this->imageGenerator->generateAndStore($qrCode); } return response()->streamDownload(fn () => print($bytes), $filename, [ 'Content-Type' => 'image/png', ]); } if ($format === 'pdf') { $bytes = $this->imageGenerator->normalizeStoredPng($qrCode->png_path); if ($bytes === null) { $bytes = $this->imageGenerator->renderPng($qrCode->encodedPayload(), $qrCode->style()); $this->imageGenerator->generateAndStore($qrCode); } $pdf = $this->pdfExporter->fromPng($bytes, $qrCode->label); return response()->streamDownload(fn () => print($pdf), Str::slug($qrCode->label).'-qr.pdf', [ 'Content-Type' => 'application/pdf', ]); } abort_unless($path && Storage::disk('qr')->exists($path), 404); return Storage::disk('qr')->download($path, $filename); } private function authorizeStorefront(QrCode $storefront): void { abort_unless(in_array($storefront->type, \App\Support\Qr\QrTypeCatalog::storefrontTypes(), true), 404); abort_unless($storefront->user_id === ladill_account()->id, 403); } }