onQueue('default'); } public function handle(): void { $export = DataExport::with(['user', 'tenant'])->find($this->exportId); if (! $export) { return; } if (! $export->user) { $export->update([ 'status' => DataExport::STATUS_FAILED, 'error_message' => 'User no longer exists.', ]); return; } $export->update(['status' => DataExport::STATUS_PROCESSING, 'error_message' => null]); try { $payload = $this->buildPayload($export->user, $export->tenant); $zipPath = $this->writeArchive($export, $payload); $export->update([ 'status' => DataExport::STATUS_READY, 'path' => $zipPath, 'size_bytes' => Storage::disk('local')->size($zipPath), 'expires_at' => now()->addDays(14), ]); } catch (\Throwable $exception) { $export->update([ 'status' => DataExport::STATUS_FAILED, 'error_message' => $exception->getMessage(), ]); report($exception); } } /** * @return array */ protected function buildPayload(User $user, ?Tenant $tenant): array { $profile = [ 'generated_at' => now()->toIso8601String(), 'user' => [ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'username' => $user->username, 'preferred_locale' => $user->preferred_locale, 'created_at' => optional($user->created_at)->toIso8601String(), 'email_verified_at' => optional($user->email_verified_at)->toIso8601String(), ], ]; if ($tenant) { $profile['tenant'] = [ 'id' => $tenant->id, 'name' => $tenant->name, 'slug' => $tenant->slug, 'contact_email' => $tenant->contact_email, 'contact_phone' => $tenant->contact_phone, 'subscription_status' => $tenant->subscription_status, 'subscription_expires_at' => optional($tenant->subscription_expires_at)->toIso8601String(), 'created_at' => optional($tenant->created_at)->toIso8601String(), ]; } $events = $tenant ? $this->collectEvents($tenant) : []; $invoices = $tenant ? $this->collectInvoices($tenant) : []; return [ 'profile' => $profile, 'events' => $events, 'invoices' => $invoices, ]; } /** * @return array> */ protected function collectEvents(Tenant $tenant): array { $events = Event::query() ->withCount([ 'photos as photos_total', 'photos as featured_photos_total' => fn ($query) => $query->where('is_featured', true), 'joinTokens', 'members', ]) ->where('tenant_id', $tenant->id) ->orderBy('date') ->get(); $likeCounts = DB::table('photo_likes') ->join('photos', 'photo_likes.photo_id', '=', 'photos.id') ->where('photos.tenant_id', $tenant->id) ->groupBy('photos.event_id') ->pluck(DB::raw('COUNT(*)'), 'photos.event_id'); return $events ->map(function (Event $event) use ($likeCounts): array { $likes = (int) ($likeCounts[$event->id] ?? 0); return [ 'id' => $event->id, 'slug' => $event->slug, 'status' => $event->status, 'name' => $event->name, 'location' => $event->location, 'date' => optional($event->date)->toIso8601String(), 'photos_total' => (int) ($event->photos_total ?? 0), 'featured_photos_total' => (int) ($event->featured_photos_total ?? 0), 'join_tokens_total' => (int) ($event->join_tokens_count ?? 0), 'members_total' => (int) ($event->members_count ?? 0), 'likes_total' => $likes, 'created_at' => optional($event->created_at)->toIso8601String(), 'updated_at' => optional($event->updated_at)->toIso8601String(), ]; }) ->all(); } /** * @return array> */ protected function collectInvoices(Tenant $tenant): array { return PackagePurchase::query() ->where('tenant_id', $tenant->id) ->with('package') ->latest('purchased_at') ->get() ->map(function (PackagePurchase $purchase): array { return [ 'id' => $purchase->id, 'package' => $purchase->package?->getNameForLocale(app()->getLocale()) ?? $purchase->package?->name, 'price' => $purchase->price !== null ? (float) $purchase->price : null, 'currency' => 'EUR', 'type' => $purchase->type, 'provider' => $purchase->provider, 'provider_id' => $purchase->provider_id, 'purchased_at' => optional($purchase->purchased_at)->toIso8601String(), 'refunded' => (bool) $purchase->refunded, ]; }) ->all(); } /** * @param array $payload */ protected function writeArchive(DataExport $export, array $payload): string { $directory = 'exports/user-'.$export->user_id; Storage::disk('local')->makeDirectory($directory); $filename = sprintf('data-export-%s.zip', Str::uuid()); $path = $directory.'/'.$filename; $zip = new ZipArchive; $fullPath = Storage::disk('local')->path($path); if ($zip->open($fullPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { throw new \RuntimeException('Unable to create export archive.'); } $zip->addFromString('profile.json', json_encode($payload['profile'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); $zip->addFromString('events.json', json_encode($payload['events'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); $zip->addFromString('invoices.json', json_encode($payload['invoices'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); $locale = $export->user?->preferred_locale ?? app()->getLocale(); $readme = implode("\n", [ __('profile.export.readme.title', [], $locale), '---------------------', __('profile.export.readme.description', [], $locale), __('profile.export.readme.generated', [ 'date' => now()->copy()->locale($locale)->translatedFormat('d. F Y H:i'), ], $locale), __('profile.export.readme.expiry', [], $locale), ]); $zip->addFromString('README.txt', $readme); $zip->close(); return $path; } }