From 6a5e26315ebf5642e648c33620db1e228bc0c085 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sun, 7 Jun 2026 07:49:53 +0000 Subject: [PATCH] Import PDF documents when migrating QR codes from the platform. Links qr_documents to imported codes and copies PDF files when --storage-source is provided. Co-authored-by: Cursor --- DEPLOY.md | 6 +- app/Console/Commands/ImportQrPlusCommand.php | 89 +++++++++++++++++++- tests/Feature/ImportQrPlusCommandTest.php | 84 ++++++++++++++++++ 3 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 tests/Feature/ImportQrPlusCommandTest.php diff --git a/DEPLOY.md b/DEPLOY.md index 1f6672e..7a56b8c 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -73,8 +73,10 @@ php artisan config:cache route:cache view:cache # platform php artisan qr-plus:export --out=/tmp/qr-plus-export.json -# qr app -php artisan qr-plus:import /tmp/qr-plus-export.json --commit +# qr app (include --storage-source so PDF files copy across) +php artisan qr-plus:import /tmp/qr-plus-export.json \ + --storage-source=/var/www/ladill/current/storage/app/private/qr \ + --commit ``` ## 8. Smoke test diff --git a/app/Console/Commands/ImportQrPlusCommand.php b/app/Console/Commands/ImportQrPlusCommand.php index 152658f..d95c405 100644 --- a/app/Console/Commands/ImportQrPlusCommand.php +++ b/app/Console/Commands/ImportQrPlusCommand.php @@ -3,24 +3,30 @@ namespace App\Console\Commands; use App\Models\QrCode; +use App\Models\QrDocument; use App\Models\QrScanEvent; use App\Models\QrTransaction; use App\Models\QrWallet; use App\Models\User; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Storage; class ImportQrPlusCommand extends Command { protected $signature = 'qr-plus:import {file : Path to qr-plus-export.json from the platform} - {--commit : Actually write changes}'; + {--commit : Actually write changes} + {--storage-source= : Platform qr disk root (storage/app/private/qr) to copy PDF files}'; protected $description = 'Import QR Plus codes from the platform export'; /** @var array platform qr_code id → local id */ private array $codeMap = []; + /** @var array platform qr_document id → local id */ + private array $documentMap = []; + public function handle(): int { $commit = (bool) $this->option('commit'); @@ -55,6 +61,9 @@ class ImportQrPlusCommand extends Command foreach ($payload['qr_wallets'] ?? [] as $row) { $this->importWallet($row, $commit); } + foreach ($payload['qr_documents'] ?? [] as $row) { + $this->importDocument($row, $commit); + } foreach ($payload['qr_codes'] ?? [] as $row) { $this->importCode($row, $commit); } @@ -66,7 +75,15 @@ class ImportQrPlusCommand extends Command } }); + $documentCount = count($payload['qr_documents'] ?? []); + if ($documentCount > 0 && ! $this->option('storage-source')) { + $this->warn('Export includes PDF documents — pass --storage-source=/path/to/platform/storage/app/private/qr to copy files.'); + } + $this->info(($commit ? 'Imported' : 'Would import').' '.count($this->codeMap).' QR code(s).'); + if ($documentCount > 0) { + $this->line(' - qr_documents: '.count($this->documentMap)); + } return self::SUCCESS; } @@ -99,6 +116,31 @@ class ImportQrPlusCommand extends Command } } + /** @param array $row */ + private function importDocument(array $row, bool $commit): void + { + $user = $this->resolveUser($row); + if (! $user) { + return; + } + + $platformId = (int) ($row['platform_id'] ?? 0); + $path = (string) ($row['path'] ?? ''); + $attrs = collect($row)->except(['platform_id', 'owner_public_id', 'owner_email', 'id', 'user_id'])->all(); + $attrs['user_id'] = $user->id; + + if ($commit) { + $document = QrDocument::updateOrCreate( + ['user_id' => $user->id, 'path' => $path], + $attrs, + ); + $this->documentMap[$platformId] = $document->id; + $this->copyDocumentFile($path); + } else { + $this->documentMap[$platformId] = $platformId; + } + } + /** @param array $row */ private function importCode(array $row, bool $commit): void { @@ -109,9 +151,22 @@ class ImportQrPlusCommand extends Command $platformId = (int) ($row['platform_id'] ?? 0); $shortCode = (string) ($row['short_code'] ?? ''); - $attrs = collect($row)->except(['platform_id', 'owner_public_id', 'owner_email', 'id', 'user_id', 'qr_document_id'])->all(); + $platformDocumentId = (int) ($row['platform_qr_document_id'] ?? 0); + $attrs = collect($row)->except([ + 'platform_id', + 'platform_qr_document_id', + 'owner_public_id', + 'owner_email', + 'id', + 'user_id', + 'qr_document_id', + ])->all(); $attrs['user_id'] = $user->id; + if ($platformDocumentId && isset($this->documentMap[$platformDocumentId])) { + $attrs['qr_document_id'] = $this->documentMap[$platformDocumentId]; + } + if ($commit) { $code = QrCode::updateOrCreate(['short_code' => $shortCode], $attrs); $this->codeMap[$platformId] = $code->id; @@ -158,4 +213,34 @@ class ImportQrPlusCommand extends Command QrTransaction::updateOrCreate(['reference' => $reference], $attrs); } } + + private function copyDocumentFile(string $relativePath): void + { + if ($relativePath === '') { + return; + } + + $sourceRoot = $this->option('storage-source'); + if (! is_string($sourceRoot) || $sourceRoot === '' || ! is_dir($sourceRoot)) { + return; + } + + $sourceFile = rtrim($sourceRoot, '/').'/'.$relativePath; + if (! is_file($sourceFile)) { + $this->warn("Missing PDF on platform storage: {$sourceFile}"); + + return; + } + + if (Storage::disk('qr')->exists($relativePath)) { + return; + } + + $directory = dirname($relativePath); + if ($directory !== '.' && $directory !== '') { + Storage::disk('qr')->makeDirectory($directory); + } + + Storage::disk('qr')->put($relativePath, (string) file_get_contents($sourceFile)); + } } diff --git a/tests/Feature/ImportQrPlusCommandTest.php b/tests/Feature/ImportQrPlusCommandTest.php new file mode 100644 index 0000000..f3f8b8b --- /dev/null +++ b/tests/Feature/ImportQrPlusCommandTest.php @@ -0,0 +1,84 @@ +create(['public_id' => 'usr_test_pdf']); + $sourceRoot = storage_path('framework/testing/platform-qr'); + $relativePath = $user->id.'/documents/test-menu.pdf'; + $sourceFile = $sourceRoot.'/'.$relativePath; + + if (! is_dir(dirname($sourceFile))) { + mkdir(dirname($sourceFile), 0777, true); + } + file_put_contents($sourceFile, '%PDF-1.4 test'); + + $payload = [ + 'qr_wallets' => [], + 'qr_documents' => [[ + 'platform_id' => 42, + 'owner_public_id' => $user->public_id, + 'owner_email' => $user->email, + 'title' => 'Menu PDF', + 'disk' => 'qr', + 'path' => $relativePath, + 'mime_type' => 'application/pdf', + 'size_bytes' => 12, + 'page_count' => null, + 'created_at' => now()->toDateTimeString(), + 'updated_at' => now()->toDateTimeString(), + ]], + 'qr_codes' => [[ + 'platform_id' => 99, + 'platform_qr_document_id' => 42, + 'owner_public_id' => $user->public_id, + 'owner_email' => $user->email, + 'short_code' => 'menu-pdf', + 'type' => QrCode::TYPE_DOCUMENT, + 'label' => 'Menu PDF', + 'destination_url' => null, + 'payload' => json_encode(['content' => ['allow_download' => true], 'style' => []]), + 'is_active' => true, + 'scans_total' => 0, + 'created_at' => now()->toDateTimeString(), + 'updated_at' => now()->toDateTimeString(), + 'destination_updated_at' => now()->toDateTimeString(), + ]], + 'qr_scan_events' => [], + 'qr_transactions' => [], + ]; + + $exportPath = storage_path('framework/testing/qr-plus-import.json'); + file_put_contents($exportPath, json_encode($payload)); + + Artisan::call('qr-plus:import', [ + 'file' => $exportPath, + '--commit' => true, + '--storage-source' => $sourceRoot, + ]); + + $code = QrCode::where('short_code', 'menu-pdf')->first(); + $this->assertNotNull($code); + $this->assertNotNull($code->qr_document_id); + + $document = QrDocument::find($code->qr_document_id); + $this->assertNotNull($document); + $this->assertSame($relativePath, $document->path); + Storage::disk('qr')->assertExists($relativePath); + } +}