Add QR-specific settings for notifications and new-code defaults.
Deploy Ladill QR Plus / deploy (push) Successful in 1m15s
Deploy Ladill QR Plus / deploy (push) Successful in 1m15s
Store per-account preferences in qr_settings and pre-fill the create flow with saved type and brand styling. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,7 +3,14 @@
|
||||
namespace App\Http\Controllers\Qr;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\QrSetting;
|
||||
use App\Services\Billing\BillingClient;
|
||||
use App\Support\Qr\QrCornerStyleCatalog;
|
||||
use App\Support\Qr\QrFrameStyleCatalog;
|
||||
use App\Support\Qr\QrModuleStyleCatalog;
|
||||
use App\Support\Qr\QrTypeCatalog;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AccountController extends Controller
|
||||
@@ -56,6 +63,59 @@ class AccountController extends Controller
|
||||
|
||||
public function settings(): View
|
||||
{
|
||||
return view('qr.account.settings', ['account' => ladill_account()]);
|
||||
$account = ladill_account();
|
||||
$settings = $account->getOrCreateQrSetting();
|
||||
$style = $settings->resolvedDefaultStyle();
|
||||
|
||||
return view('qr.account.settings', [
|
||||
'account' => $account,
|
||||
'settings' => $settings,
|
||||
'style' => $style,
|
||||
'types' => QrTypeCatalog::all(),
|
||||
'moduleStyles' => QrModuleStyleCatalog::visible(),
|
||||
'cornerOuterStyles' => QrCornerStyleCatalog::outerStyles(),
|
||||
'cornerInnerStyles' => QrCornerStyleCatalog::innerStyles(),
|
||||
'frameStyles' => QrFrameStyleCatalog::visible(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateSettings(Request $request): RedirectResponse
|
||||
{
|
||||
$account = ladill_account();
|
||||
|
||||
$data = $request->validate([
|
||||
'notify_email' => ['nullable', 'email', 'max:255'],
|
||||
'product_updates' => ['nullable', 'boolean'],
|
||||
'low_balance_alerts' => ['nullable', 'boolean'],
|
||||
'default_type' => ['nullable', 'in:'.implode(',', QrTypeCatalog::plusTypes())],
|
||||
'default_style' => ['nullable', 'array'],
|
||||
'default_style.foreground' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'],
|
||||
'default_style.background' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'],
|
||||
'default_style.error_correction' => ['nullable', 'in:L,M,Q,H'],
|
||||
'default_style.margin' => ['nullable', 'integer', 'min:0', 'max:10'],
|
||||
'default_style.module_style' => ['nullable', 'in:'.implode(',', QrModuleStyleCatalog::keys())],
|
||||
'default_style.finder_outer' => ['nullable', 'string', 'max:32'],
|
||||
'default_style.finder_inner' => ['nullable', 'string', 'max:32'],
|
||||
'default_style.frame_style' => ['nullable', 'string', 'max:32'],
|
||||
'default_style.frame_text' => ['nullable', 'string', 'max:100'],
|
||||
'default_style.frame_color' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'],
|
||||
'default_style.gradient_type' => ['nullable', 'in:none,linear,radial'],
|
||||
'default_style.gradient_color1' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'],
|
||||
'default_style.gradient_color2' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'],
|
||||
'default_style.gradient_rotation' => ['nullable', 'integer', 'min:0', 'max:360'],
|
||||
]);
|
||||
|
||||
QrSetting::updateOrCreate(
|
||||
['user_id' => $account->id],
|
||||
[
|
||||
'notify_email' => $data['notify_email'] ?? null,
|
||||
'product_updates' => $request->boolean('product_updates'),
|
||||
'low_balance_alerts' => $request->boolean('low_balance_alerts'),
|
||||
'default_type' => $data['default_type'] ?? null,
|
||||
'default_style' => QrSetting::sanitizeDefaultStyle($data['default_style'] ?? null),
|
||||
],
|
||||
);
|
||||
|
||||
return redirect()->route('account.settings')->with('success', 'Settings saved.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ class QrCodeController extends Controller
|
||||
{
|
||||
$account = ladill_account();
|
||||
$wallet = $this->manager->walletFor($account);
|
||||
$qrSettings = $account->getOrCreateQrSetting();
|
||||
|
||||
return view('qr-codes.create', [
|
||||
'wallet' => $wallet,
|
||||
@@ -83,6 +84,8 @@ class QrCodeController extends Controller
|
||||
'minTopup' => QrWallet::minTopupGhs(),
|
||||
'ladillWalletBalance' => $this->platformBilling->balanceMinor($account->public_id) / 100,
|
||||
'topupUrl' => 'https://'.config('app.account_domain').'/wallet',
|
||||
'defaultType' => $qrSettings->resolvedDefaultType(),
|
||||
'accountDefaultStyle' => $qrSettings->resolvedDefaultStyle(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Support\Qr\QrStyleDefaults;
|
||||
use App\Support\Qr\QrTypeCatalog;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/** Per-account QR Plus preferences (notifications and new-code defaults). */
|
||||
class QrSetting extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'notify_email',
|
||||
'product_updates',
|
||||
'low_balance_alerts',
|
||||
'default_type',
|
||||
'default_style',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'product_updates' => 'boolean',
|
||||
'low_balance_alerts' => 'boolean',
|
||||
'default_style' => 'array',
|
||||
];
|
||||
|
||||
/** @return list<string> */
|
||||
public static function storableStyleKeys(): array
|
||||
{
|
||||
return [
|
||||
'foreground',
|
||||
'background',
|
||||
'error_correction',
|
||||
'margin',
|
||||
'module_style',
|
||||
'finder_outer',
|
||||
'finder_inner',
|
||||
'frame_style',
|
||||
'frame_text',
|
||||
'frame_color',
|
||||
'gradient_type',
|
||||
'gradient_color1',
|
||||
'gradient_color2',
|
||||
'gradient_rotation',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function resolvedDefaultType(): string
|
||||
{
|
||||
$type = (string) ($this->default_type ?? QrCode::TYPE_URL);
|
||||
|
||||
return in_array($type, QrTypeCatalog::plusTypes(), true) ? $type : QrCode::TYPE_URL;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function resolvedDefaultStyle(): array
|
||||
{
|
||||
$stored = collect($this->default_style ?? [])
|
||||
->only(self::storableStyleKeys())
|
||||
->filter(fn ($value) => $value !== null && $value !== '')
|
||||
->all();
|
||||
|
||||
return QrStyleDefaults::merge($stored !== [] ? $stored : null);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed>|null $input */
|
||||
public static function sanitizeDefaultStyle(?array $input): ?array
|
||||
{
|
||||
if ($input === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$merged = QrStyleDefaults::merge(
|
||||
collect($input)->only(self::storableStyleKeys())->all(),
|
||||
);
|
||||
|
||||
return collect($merged)->only(self::storableStyleKeys())->all();
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,16 @@ class User extends Authenticatable
|
||||
return $this->hasMany(QrCode::class);
|
||||
}
|
||||
|
||||
public function qrSetting(): HasOne
|
||||
{
|
||||
return $this->hasOne(QrSetting::class);
|
||||
}
|
||||
|
||||
public function getOrCreateQrSetting(): QrSetting
|
||||
{
|
||||
return $this->qrSetting()->firstOrCreate([]);
|
||||
}
|
||||
|
||||
public function getOrCreateQrWallet(): QrWallet
|
||||
{
|
||||
return $this->qrWallet()->firstOrCreate(
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('qr_settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
|
||||
$table->string('notify_email')->nullable();
|
||||
$table->boolean('product_updates')->default(true);
|
||||
$table->boolean('low_balance_alerts')->default(true);
|
||||
$table->string('default_type', 32)->nullable();
|
||||
$table->json('default_style')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('qr_settings');
|
||||
}
|
||||
};
|
||||
@@ -74,7 +74,8 @@
|
||||
'profileName' => $navUser?->name ?? '',
|
||||
'profileSubtitle' => $navUser?->email ?? '',
|
||||
'profileMenuItems' => [
|
||||
['type' => 'link', 'label' => 'Account Settings', 'href' => route('account.settings')],
|
||||
['type' => 'link', 'label' => 'QR Settings', 'href' => route('account.settings')],
|
||||
['type' => 'link', 'label' => 'Account Settings', 'href' => ladill_account_url('account-settings')],
|
||||
['type' => 'link', 'label' => 'Overview', 'href' => route('qr.dashboard')],
|
||||
['type' => 'logout', 'label' => 'Logout', 'action' => route('logout')],
|
||||
],
|
||||
|
||||
@@ -124,7 +124,8 @@
|
||||
|
||||
<div x-show="profileOpen" x-cloak x-transition @click.outside="profileOpen = false"
|
||||
class="absolute right-0 z-20 mt-2 w-48 rounded-xl border border-slate-200 bg-white p-1 shadow-lg">
|
||||
<a href="{{ route('account.settings') }}" class="block rounded-lg px-3 py-2 text-sm text-slate-700 hover:bg-slate-100">Account Settings</a>
|
||||
<a href="{{ route('account.settings') }}" class="block rounded-lg px-3 py-2 text-sm text-slate-700 hover:bg-slate-100">QR Settings</a>
|
||||
<a href="{{ ladill_account_url('account-settings') }}" class="block rounded-lg px-3 py-2 text-sm text-slate-700 hover:bg-slate-100">Account Settings</a>
|
||||
<a href="{{ route('qr.dashboard') }}" class="block rounded-lg px-3 py-2 text-sm text-slate-700 hover:bg-slate-100">Dashboard</a>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<x-slot name="title">Create QR Code</x-slot>
|
||||
|
||||
@php
|
||||
$defaultStyle = \App\Support\Qr\QrStyleDefaults::merge(old('style'));
|
||||
$defaultStyle = \App\Support\Qr\QrStyleDefaults::merge(old('style') ?: ($accountDefaultStyle ?? null));
|
||||
@endphp
|
||||
|
||||
<div class="mx-auto max-w-6xl space-y-6"
|
||||
@@ -14,7 +14,7 @@
|
||||
balance: @js((float) $wallet->spendableBalance()),
|
||||
price: @js((float) $pricePerQr),
|
||||
topupUrl: @js($topupUrl),
|
||||
type: @js(old('type', 'url')),
|
||||
type: @js(old('type', $defaultType ?? 'url')),
|
||||
})">
|
||||
|
||||
@if(session('error'))
|
||||
|
||||
@@ -1,7 +1,160 @@
|
||||
<x-user-layout>
|
||||
<x-slot name="title">Settings</x-slot>
|
||||
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Settings</h1>
|
||||
<p class="mt-2 text-sm text-slate-500">Profile and account settings are managed on <a href="https://{{ config('app.account_domain') }}/account-settings" class="font-medium text-indigo-600 hover:text-indigo-800">account.ladill.com</a>.</p>
|
||||
<h1 class="text-xl font-semibold tracking-tight text-slate-900">Settings</h1>
|
||||
<p class="mt-0.5 text-sm text-slate-500">QR Plus notifications and defaults for new codes.</p>
|
||||
|
||||
@if (session('success'))
|
||||
<div class="mt-4 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('account.settings.update') }}" class="mt-6 space-y-4">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<h2 class="text-sm font-semibold text-slate-900">Notifications</h2>
|
||||
<p class="mt-1 text-xs text-slate-500">Where we send QR Plus billing reminders and product updates.</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<label for="notify_email" class="block text-[11px] font-medium text-slate-500">Notifications email</label>
|
||||
<input type="email" id="notify_email" name="notify_email"
|
||||
value="{{ old('notify_email', $settings->notify_email ?? $account->email) }}"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@error('notify_email') <p class="mt-1 text-xs text-rose-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<label class="mt-4 flex items-center justify-between gap-4">
|
||||
<span>
|
||||
<span class="block text-sm font-medium text-slate-800">Low balance alerts</span>
|
||||
<span class="block text-xs text-slate-400">Email when your wallet is too low to create a new code.</span>
|
||||
</span>
|
||||
<input type="checkbox" name="low_balance_alerts" value="1"
|
||||
@checked(old('low_balance_alerts', $settings->low_balance_alerts ?? true))
|
||||
class="h-5 w-5 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</label>
|
||||
|
||||
<label class="mt-4 flex items-center justify-between gap-4">
|
||||
<span>
|
||||
<span class="block text-sm font-medium text-slate-800">Product updates</span>
|
||||
<span class="block text-xs text-slate-400">Occasional emails about new QR Plus features.</span>
|
||||
</span>
|
||||
<input type="checkbox" name="product_updates" value="1"
|
||||
@checked(old('product_updates', $settings->product_updates ?? true))
|
||||
class="h-5 w-5 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<h2 class="text-sm font-semibold text-slate-900">New code defaults</h2>
|
||||
<p class="mt-1 text-xs text-slate-500">Pre-fill the create screen so every new QR starts with your brand look.</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<label for="default_type" class="block text-[11px] font-medium text-slate-500">Default type</label>
|
||||
<select id="default_type" name="default_type"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@foreach ($types as $key => $meta)
|
||||
<option value="{{ $key }}" @selected(old('default_type', $settings->default_type ?? 'url') === $key)>
|
||||
{{ $meta['label'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('default_type') <p class="mt-1 text-xs text-rose-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="foreground" class="block text-[11px] font-medium text-slate-500">Foreground</label>
|
||||
<input type="color" id="foreground" name="default_style[foreground]"
|
||||
value="{{ old('default_style.foreground', $style['foreground']) }}"
|
||||
class="mt-1 h-10 w-full cursor-pointer rounded-lg border border-slate-200 bg-white p-1">
|
||||
</div>
|
||||
<div>
|
||||
<label for="background" class="block text-[11px] font-medium text-slate-500">Background</label>
|
||||
<input type="color" id="background" name="default_style[background]"
|
||||
value="{{ old('default_style.background', $style['background']) }}"
|
||||
class="mt-1 h-10 w-full cursor-pointer rounded-lg border border-slate-200 bg-white p-1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="module_style" class="block text-[11px] font-medium text-slate-500">Module style</label>
|
||||
<select id="module_style" name="default_style[module_style]"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@foreach ($moduleStyles as $key => $meta)
|
||||
<option value="{{ $key }}" @selected(old('default_style.module_style', $style['module_style']) === $key)>{{ $meta['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="error_correction" class="block text-[11px] font-medium text-slate-500">Error correction</label>
|
||||
<select id="error_correction" name="default_style[error_correction]"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@foreach (['L' => 'Low', 'M' => 'Medium', 'Q' => 'Quartile', 'H' => 'High'] as $key => $label)
|
||||
<option value="{{ $key }}" @selected(old('default_style.error_correction', $style['error_correction']) === $key)>{{ $label }} ({{ $key }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="finder_outer" class="block text-[11px] font-medium text-slate-500">Corner outer</label>
|
||||
<select id="finder_outer" name="default_style[finder_outer]"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@foreach ($cornerOuterStyles as $key => $meta)
|
||||
<option value="{{ $key }}" @selected(old('default_style.finder_outer', $style['finder_outer']) === $key)>{{ $meta['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="finder_inner" class="block text-[11px] font-medium text-slate-500">Corner inner</label>
|
||||
<select id="finder_inner" name="default_style[finder_inner]"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@foreach ($cornerInnerStyles as $key => $meta)
|
||||
<option value="{{ $key }}" @selected(old('default_style.finder_inner', $style['finder_inner']) === $key)>{{ $meta['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="frame_style" class="block text-[11px] font-medium text-slate-500">Frame</label>
|
||||
<select id="frame_style" name="default_style[frame_style]"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
@foreach ($frameStyles as $key => $meta)
|
||||
<option value="{{ $key }}" @selected(old('default_style.frame_style', $style['frame_style']) === $key)>{{ $meta['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="frame_color" class="block text-[11px] font-medium text-slate-500">Frame color</label>
|
||||
<input type="color" id="frame_color" name="default_style[frame_color]"
|
||||
value="{{ old('default_style.frame_color', $style['frame_color']) }}"
|
||||
class="mt-1 h-10 w-full cursor-pointer rounded-lg border border-slate-200 bg-white p-1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label for="frame_text" class="block text-[11px] font-medium text-slate-500">Frame label</label>
|
||||
<input type="text" id="frame_text" name="default_style[frame_text]" maxlength="100"
|
||||
value="{{ old('default_style.frame_text', $style['frame_text']) }}"
|
||||
placeholder="SCAN ME"
|
||||
class="mt-1 w-full rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-indigo-700">
|
||||
Save settings
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="mt-6 text-xs text-slate-400">
|
||||
Profile, password, and security are managed on
|
||||
<a href="{{ ladill_account_url('account-settings') }}" class="font-medium text-indigo-600 hover:text-indigo-800">account.ladill.com</a>.
|
||||
</p>
|
||||
</div>
|
||||
</x-user-layout>
|
||||
|
||||
@@ -52,6 +52,7 @@ Route::middleware(['auth'])->group(function () {
|
||||
Route::get('/wallet', [AccountController::class, 'wallet'])->name('account.wallet');
|
||||
Route::get('/billing', [AccountController::class, 'billing'])->name('account.billing');
|
||||
Route::get('/settings', [AccountController::class, 'settings'])->name('account.settings');
|
||||
Route::put('/settings', [AccountController::class, 'updateSettings'])->name('account.settings.update');
|
||||
|
||||
Route::get('/team', [TeamController::class, 'index'])->name('account.team');
|
||||
Route::post('/team', [TeamController::class, 'store'])->name('account.team.store');
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\QrSetting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class QrSettingsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Http::preventStrayRequests();
|
||||
}
|
||||
|
||||
private function user(): User
|
||||
{
|
||||
return User::create(['public_id' => (string) Str::uuid(), 'name' => 'A', 'email' => 'a+'.uniqid().'@x.com']);
|
||||
}
|
||||
|
||||
public function test_settings_page_renders_for_authenticated_user(): void
|
||||
{
|
||||
$this->actingAs($this->user())
|
||||
->get(route('account.settings'))
|
||||
->assertOk()
|
||||
->assertSee('New code defaults')
|
||||
->assertSee('Notifications');
|
||||
}
|
||||
|
||||
public function test_can_save_qr_settings(): void
|
||||
{
|
||||
$user = $this->user();
|
||||
|
||||
$this->actingAs($user)
|
||||
->put(route('account.settings.update'), [
|
||||
'notify_email' => 'alerts@example.com',
|
||||
'product_updates' => '1',
|
||||
'low_balance_alerts' => '0',
|
||||
'default_type' => 'wifi',
|
||||
'default_style' => [
|
||||
'foreground' => '#112233',
|
||||
'background' => '#ffffff',
|
||||
'module_style' => 'dots',
|
||||
'frame_style' => 'scan_me',
|
||||
'frame_color' => '#445566',
|
||||
'frame_text' => 'SCAN ME',
|
||||
],
|
||||
])
|
||||
->assertRedirect(route('account.settings'));
|
||||
|
||||
$settings = QrSetting::where('user_id', $user->id)->first();
|
||||
|
||||
$this->assertNotNull($settings);
|
||||
$this->assertSame('alerts@example.com', $settings->notify_email);
|
||||
$this->assertTrue($settings->product_updates);
|
||||
$this->assertFalse($settings->low_balance_alerts);
|
||||
$this->assertSame('wifi', $settings->default_type);
|
||||
$this->assertSame('#112233', $settings->resolvedDefaultStyle()['foreground']);
|
||||
$this->assertSame('scan_me', $settings->resolvedDefaultStyle()['frame_style']);
|
||||
}
|
||||
|
||||
public function test_create_page_uses_saved_defaults(): void
|
||||
{
|
||||
$user = $this->user();
|
||||
QrSetting::create([
|
||||
'user_id' => $user->id,
|
||||
'default_type' => 'business',
|
||||
'default_style' => ['foreground' => '#aabbcc', 'module_style' => 'dots'],
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
config('billing.api_url').'/balance*' => Http::response(['balance_minor' => 50000]),
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('user.qr-codes.create'))
|
||||
->assertOk()
|
||||
->assertSee('#aabbcc', false)
|
||||
->assertSee('"business"', false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user