geschenkgutscheine implementiert ("Paket verschenken"). Neuer Upload-Provider: Sparkbooth.

This commit is contained in:
Codex Agent
2025-12-07 16:54:58 +01:00
parent 3f3c0f1d35
commit 046e2fe3ec
50 changed files with 2422 additions and 130 deletions

View File

@@ -2377,7 +2377,7 @@ class EventPublicController extends BaseController
// MyPhotos filter
if ($filter === 'photobooth') {
$query->where('photos.ingest_source', Photo::SOURCE_PHOTOBOOTH);
$query->whereIn('photos.ingest_source', [Photo::SOURCE_PHOTOBOOTH, Photo::SOURCE_SPARKBOOTH]);
} elseif ($filter === 'myphotos' && $deviceId !== 'anon') {
$query->where('guest_name', $deviceId);
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Api\Marketing;
use App\Http\Controllers\Controller;
use App\Services\GiftVouchers\GiftVoucherCheckoutService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class GiftVoucherCheckoutController extends Controller
{
public function __construct(private readonly GiftVoucherCheckoutService $checkout) {}
public function tiers(): JsonResponse
{
return response()->json([
'data' => $this->checkout->tiers(),
]);
}
public function store(Request $request): JsonResponse
{
$data = $this->validate($request, [
'tier_key' => ['required', 'string'],
'purchaser_email' => ['required', 'email:rfc,dns'],
'recipient_email' => ['nullable', 'email:rfc,dns'],
'recipient_name' => ['nullable', 'string', 'max:191'],
'message' => ['nullable', 'string', 'max:500'],
'success_url' => ['nullable', 'url'],
'return_url' => ['nullable', 'url'],
]);
$checkout = $this->checkout->create($data);
if (! $checkout['checkout_url']) {
throw ValidationException::withMessages([
'tier_key' => __('Unable to create Paddle checkout.'),
]);
}
return response()->json($checkout);
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Event;
use App\Services\Photobooth\Exceptions\SparkboothUploadException;
use App\Services\Photobooth\SparkboothUploadService;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Http\Response;
use Illuminate\Support\Str;
class SparkboothUploadController extends Controller
{
public function __construct(private readonly SparkboothUploadService $service) {}
public function store(Request $request): Response
{
$media = $this->resolveMedia($request);
if (! $media) {
return $this->respond(null, false, 'Media is required', null, 400, $request);
}
try {
$result = $this->service->handleUpload(
$media,
$request->input('username'),
$request->input('password')
);
/** @var Event $event */
$event = $result['event'];
return $this->respond($event, true, null, null, 200, $request);
} catch (SparkboothUploadException $exception) {
return $this->respond(null, false, $exception->getMessage(), null, $exception->statusCode ?? 400, $request);
} catch (\Throwable) {
return $this->respond(null, false, 'Upload failed, please retry.', null, 500, $request);
}
}
protected function respond(?Event $event, bool $ok, ?string $message, ?string $url, int $status, Request $request): Response
{
$format = $this->resolveFormat($event, $request);
if ($format === 'xml') {
$payload = $ok
? $this->buildSuccessXml($url)
: $this->buildFailureXml($message);
return response($payload, $status, ['Content-Type' => 'application/xml']);
}
return response()->json([
'status' => $ok,
'error' => $ok ? null : $message,
'url' => $url,
], $status);
}
protected function resolveFormat(?Event $event, Request $request): string
{
$preferred = $request->input('format');
if ($preferred && in_array($preferred, ['json', 'xml'], true)) {
return $preferred;
}
$configured = $event?->photobooth_metadata['sparkbooth_response_format'] ?? null;
if ($configured && in_array($configured, ['json', 'xml'], true)) {
return $configured;
}
return config('photobooth.sparkbooth.response_format', 'json') === 'xml' ? 'xml' : 'json';
}
protected function buildSuccessXml(?string $url): string
{
$urlAttribute = $url ? ' url="'.htmlspecialchars($url, ENT_QUOTES).'"' : '';
return sprintf('<?xml version="1.0" encoding="UTF-8"?>'."\n".'<rsp status="ok"%s />', $urlAttribute);
}
protected function buildFailureXml(?string $message): string
{
$escaped = htmlspecialchars($message ?? 'Upload failed', ENT_QUOTES);
return sprintf(
'<?xml version="1.0" encoding="UTF-8"?>'."\n".'<rsp status="fail"><err msg="%s" /></rsp>',
$escaped
);
}
protected function resolveMedia(Request $request): ?UploadedFile
{
$file = $request->file('media');
if ($file instanceof UploadedFile) {
return $file;
}
$raw = $request->input('media');
if (is_string($raw) && $raw !== '') {
return $this->createUploadedFileFromBase64($raw);
}
return null;
}
protected function createUploadedFileFromBase64(string $raw): ?UploadedFile
{
$payload = $raw;
if (Str::startsWith($raw, 'data:')) {
$segments = explode(',', $raw, 2);
$payload = $segments[1] ?? '';
}
$decoded = base64_decode($payload, true);
if ($decoded === false) {
return null;
}
$tmpPath = tempnam(sys_get_temp_dir(), 'sparkbooth-');
if (! $tmpPath) {
return null;
}
file_put_contents($tmpPath, $decoded);
return new UploadedFile($tmpPath, 'upload.jpg', null, null, true);
}
}

View File

@@ -26,7 +26,10 @@ class PhotoboothController extends Controller
$this->assertEventBelongsToTenant($request, $event);
$event->loadMissing('tenant');
$updated = $this->provisioner->enable($event);
$mode = $this->resolveMode($request);
$updated = $mode === 'sparkbooth'
? $this->provisioner->enableSparkbooth($event)
: $this->provisioner->enable($event);
return response()->json([
'message' => __('Photobooth-Zugang aktiviert.'),
@@ -39,7 +42,10 @@ class PhotoboothController extends Controller
$this->assertEventBelongsToTenant($request, $event);
$event->loadMissing('tenant');
$updated = $this->provisioner->rotate($event);
$mode = $this->resolveMode($request);
$updated = $mode === 'sparkbooth'
? $this->provisioner->rotateSparkbooth($event)
: $this->provisioner->rotate($event);
return response()->json([
'message' => __('Zugangsdaten neu generiert.'),
@@ -52,7 +58,10 @@ class PhotoboothController extends Controller
$this->assertEventBelongsToTenant($request, $event);
$event->loadMissing('tenant');
$updated = $this->provisioner->disable($event);
$mode = $this->resolveMode($request);
$updated = $mode === 'sparkbooth'
? $this->provisioner->disableSparkbooth($event)
: $this->provisioner->disable($event);
return response()->json([
'message' => __('Photobooth-Zugang deaktiviert.'),
@@ -76,4 +85,11 @@ class PhotoboothController extends Controller
abort(403, 'Event gehört nicht zu diesem Tenant.');
}
}
protected function resolveMode(Request $request): string
{
$mode = strtolower((string) $request->input('mode', $request->input('type', 'ftp')));
return in_array($mode, ['sparkbooth', 'ftp'], true) ? $mode : 'ftp';
}
}

View File

@@ -11,6 +11,7 @@ use App\Models\PackagePurchase;
use App\Models\TenantPackage;
use App\Services\Checkout\CheckoutSessionService;
use App\Services\Coupons\CouponService;
use App\Services\GiftVouchers\GiftVoucherCheckoutService;
use App\Services\Paddle\PaddleCheckoutService;
use App\Support\Concerns\PresentsPackages;
use Illuminate\Http\Request;
@@ -39,6 +40,7 @@ class MarketingController extends Controller
private readonly CheckoutSessionService $checkoutSessions,
private readonly PaddleCheckoutService $paddleCheckout,
private readonly CouponService $coupons,
private readonly GiftVoucherCheckoutService $giftVouchers,
) {}
public function index()
@@ -124,6 +126,15 @@ class MarketingController extends Controller
return Inertia::render('marketing/Kontakt');
}
public function giftVouchers()
{
$tiers = $this->giftVouchers->tiers();
return Inertia::render('marketing/GiftVoucher', [
'tiers' => $tiers,
]);
}
/**
* Handle package purchase flow.
*/

View File

@@ -20,22 +20,50 @@ class PhotoboothStatusResource extends JsonResource
/** @var PhotoboothSetting $settings */
$settings = $payload['settings'];
$password = $event->getAttribute('plain_photobooth_password') ?? $event->photobooth_password;
$mode = $event->photobooth_mode ?? 'ftp';
$isSparkbooth = $mode === 'sparkbooth';
$password = $isSparkbooth
? $event->getAttribute('plain_sparkbooth_password') ?? $event->sparkbooth_password
: $event->getAttribute('plain_photobooth_password') ?? $event->photobooth_password;
$activeUsername = $isSparkbooth ? $event->sparkbooth_username : $event->photobooth_username;
$activeStatus = $isSparkbooth ? $event->sparkbooth_status : $event->photobooth_status;
$activeExpires = $isSparkbooth ? $event->sparkbooth_expires_at : $event->photobooth_expires_at;
$sparkMetrics = [
'last_upload_at' => optional($event->sparkbooth_last_upload_at)->toIso8601String(),
'uploads_24h' => (int) ($event->sparkbooth_uploads_last_24h ?? 0),
'uploads_total' => (int) ($event->sparkbooth_uploads_total ?? 0),
];
return [
'mode' => $mode,
'enabled' => (bool) $event->photobooth_enabled,
'status' => $event->photobooth_status,
'username' => $event->photobooth_username,
'status' => $activeStatus,
'username' => $activeUsername,
'password' => $password,
'path' => $event->photobooth_path,
'ftp_url' => $this->buildFtpUrl($event, $settings, $password),
'expires_at' => optional($event->photobooth_expires_at)->toIso8601String(),
'ftp_url' => $isSparkbooth ? null : $this->buildFtpUrl($event, $settings, $password),
'upload_url' => $isSparkbooth ? route('api.v1.photobooth.sparkbooth.upload') : null,
'expires_at' => optional($activeExpires)->toIso8601String(),
'rate_limit_per_minute' => (int) $settings->rate_limit_per_minute,
'ftp' => [
'host' => config('photobooth.ftp.host'),
'port' => $settings->ftp_port,
'require_ftps' => (bool) $settings->require_ftps,
],
'metrics' => $isSparkbooth ? $sparkMetrics : null,
'sparkbooth' => [
'enabled' => $mode === 'sparkbooth' && $event->photobooth_enabled,
'status' => $event->sparkbooth_status,
'username' => $event->sparkbooth_username,
'password' => $event->getAttribute('plain_sparkbooth_password') ?? $event->sparkbooth_password,
'expires_at' => optional($event->sparkbooth_expires_at)->toIso8601String(),
'upload_url' => route('api.v1.photobooth.sparkbooth.upload'),
'response_format' => $event->photobooth_metadata['sparkbooth_response_format'] ?? config('photobooth.sparkbooth.response_format', 'json'),
'metrics' => $sparkMetrics,
],
];
}