Introduces App\Support\DashboardSchedule (modelled on LHITS_Dashboard's pattern) as a single settings-driven registry for recurring Artisan jobs, replacing the two hard-coded Schedule::command() lines in routes/console.php. Migrated unchanged: - integrations_refresh (integrations:refresh), every_30_seconds, withoutOverlapping(5) - alerts_scan (alerts:scan), every_30_seconds, withoutOverlapping(5) New: - etsy_sync (etsy:sync), every_15_minutes - forces a fresh pull of Etsy receipts/listings into the existing IntegrationCache entries, decoupled from the bundled integrations:refresh job so it can be tuned/disabled independently of Parcel2Go/BamBuddy/dryer refreshes. - etsy_token_refresh (etsy:refresh-token), every_45_minutes - proactively refreshes the Etsy OAuth token (tokens last ~1hr) via a new public Etsy::refreshAccessToken() wrapper around the existing on-demand accessToken() refresh logic, so the connection survives quiet periods. Frequency is persisted to storage/app/dashboard-schedules.json (no app_settings table exists in this app, unlike LHITS's AppSetting-backed store) and surfaced in a new 'Scheduler' section on the existing Settings page (ScheduleSettingsController + routes), with per-job frequency dropdowns and manual 'Run now' buttons, matching the page's existing Tailwind conventions. Tests: SchedulerTest (registry/frequency behavior), ScheduleSettingsTest (settings UI + authorization), EtsySyncScheduledJobsTest (new commands, Http::fake only, no live Etsy calls).
104 lines
4.8 KiB
PHP
104 lines
4.8 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\EtsyAppCredential;
|
|
use App\Models\EtsyConnection;
|
|
use App\Models\IntegrationSetting;
|
|
use App\Services\Etsy;
|
|
use App\Services\IntegrationSettings;
|
|
use App\Services\Parcel2Go;
|
|
use App\Support\DashboardSchedule;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\View\View;
|
|
|
|
class EtsySettingsController extends Controller
|
|
{
|
|
public function index(Request $request, Etsy $etsy, IntegrationSettings $settings, Parcel2Go $parcel2Go): View
|
|
{
|
|
abort_unless($request->user()->role === 'admin', 403);
|
|
|
|
$credentials = EtsyAppCredential::find(1);
|
|
$storedIntegrations = IntegrationSetting::query()->whereIn('provider', ['parcel2go', 'bambuddy', 'dryer'])
|
|
->pluck('provider')->all();
|
|
$parcel2go = $settings->for('parcel2go');
|
|
$bambuddy = $settings->for('bambuddy');
|
|
$dryer = $settings->for('dryer');
|
|
$scheduleJobs = collect(DashboardSchedule::jobs())
|
|
->map(fn (array $job, string $key): array => [...$job, 'key' => $key, 'frequency' => DashboardSchedule::frequencyFor($key)])
|
|
->values()->all();
|
|
|
|
return view('settings.index', [
|
|
'scheduleJobs' => $scheduleJobs,
|
|
'scheduleFrequencyOptions' => DashboardSchedule::frequencyOptions(),
|
|
'storedCredentials' => $credentials !== null,
|
|
'configured' => $etsy->configured(),
|
|
'shopConnected' => EtsyConnection::query()->exists(),
|
|
'redirectUri' => $etsy->redirectUri(),
|
|
'shopName' => $credentials?->shop_name ?: config('services.etsy.shop_name', 'HickLab3D'),
|
|
'integrations' => [
|
|
'parcel2go' => [
|
|
'stored' => in_array('parcel2go', $storedIntegrations, true),
|
|
'configured' => filled($parcel2go['client_id'] ?? null) && filled($parcel2go['client_secret'] ?? null) && filled($parcel2go['auth_url'] ?? null),
|
|
'auth_url' => $parcel2go['auth_url'] ?? '',
|
|
'base_url' => $parcel2go['base_url'] ?? '',
|
|
'scope' => $parcel2go['scope'] ?? 'public-api',
|
|
'collection_postcode' => $parcel2go['collection_postcode'] ?? '',
|
|
'profile_client_configured' => $parcel2Go->profileClientConfigured(),
|
|
'profile_connected' => $parcel2Go->profileConnected(),
|
|
'callback_url' => route('parcel2go.callback'),
|
|
],
|
|
'bambuddy' => [
|
|
'stored' => in_array('bambuddy', $storedIntegrations, true),
|
|
'configured' => filled($bambuddy['url'] ?? null) && filled($bambuddy['api_key'] ?? null),
|
|
'url' => $bambuddy['url'] ?? '',
|
|
],
|
|
'dryer' => [
|
|
'stored' => in_array('dryer', $storedIntegrations, true),
|
|
'configured' => filled($dryer['url'] ?? null) && filled($dryer['api_key'] ?? null),
|
|
'url' => $dryer['url'] ?? '',
|
|
],
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function update(Request $request): RedirectResponse
|
|
{
|
|
abort_unless($request->user()->role === 'admin', 403);
|
|
|
|
$credentials = EtsyAppCredential::find(1);
|
|
$requireKeystring = $credentials === null && blank(config('services.etsy.keystring')) ? 'required' : 'nullable';
|
|
$requireSecret = $credentials === null && blank(config('services.etsy.shared_secret')) ? 'required' : 'nullable';
|
|
$validated = $request->validate([
|
|
'keystring' => [$requireKeystring, 'string', 'max:255'],
|
|
'shared_secret' => [$requireSecret, 'string', 'max:255'],
|
|
'shop_name' => ['nullable', 'string', 'max:255'],
|
|
'redirect_uri' => ['nullable', 'url:https', 'max:2048'],
|
|
]);
|
|
|
|
$previousKeystring = $credentials?->keystring ?: config('services.etsy.keystring');
|
|
$credentials ??= new EtsyAppCredential;
|
|
$credentials->id = 1;
|
|
if (! $credentials->exists) {
|
|
$credentials->fill([
|
|
'keystring' => config('services.etsy.keystring'),
|
|
'shared_secret' => config('services.etsy.shared_secret'),
|
|
'shop_name' => config('services.etsy.shop_name', 'HickLab3D'),
|
|
'redirect_uri' => config('services.etsy.redirect_uri'),
|
|
]);
|
|
}
|
|
$credentials->fill(array_filter($validated, fn (?string $value): bool => filled($value)));
|
|
$credentials->save();
|
|
|
|
$keystringChanged = $previousKeystring !== $credentials->keystring;
|
|
if ($keystringChanged) {
|
|
EtsyConnection::query()->delete();
|
|
}
|
|
|
|
return redirect()->route('settings.index')->with('status', $keystringChanged
|
|
? 'Etsy credentials saved. Reconnect your shop with the new app key.'
|
|
: 'Etsy credentials saved.');
|
|
}
|
|
}
|