diff --git a/app/Http/Controllers/Woo/IntegrationController.php b/app/Http/Controllers/Woo/IntegrationController.php index 67d142d..efe741a 100644 --- a/app/Http/Controllers/Woo/IntegrationController.php +++ b/app/Http/Controllers/Woo/IntegrationController.php @@ -22,7 +22,20 @@ class IntegrationController extends Controller return view('woo.integrations.index', [ 'store' => $store, 'reachable' => $payload['reachable'], + 'cached' => (bool) ($payload['cached'] ?? false), 'integrations' => $payload['integrations'], ]); } + + public function refresh(Request $request): \Illuminate\Http\RedirectResponse + { + $store = $this->currentStore($request); + if ($store) { + $this->integrations->forStore($store, refresh: true); + } + + return redirect() + ->route('woo.integrations.index') + ->with('status', 'Integration status refreshed.'); + } } diff --git a/app/Services/Woo/IntegrationStatusService.php b/app/Services/Woo/IntegrationStatusService.php index 22e93cb..d847e81 100644 --- a/app/Services/Woo/IntegrationStatusService.php +++ b/app/Services/Woo/IntegrationStatusService.php @@ -3,6 +3,7 @@ namespace App\Services\Woo; use App\Models\WooStore; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; class IntegrationStatusService @@ -13,10 +14,11 @@ class IntegrationStatusService * @return array{ * store: ?WooStore, * reachable: bool, + * cached: bool, * integrations: list> * } */ - public function forStore(?WooStore $store): array + public function forStore(?WooStore $store, bool $refresh = false): array { $catalog = config('woo_integrations', []); @@ -24,10 +26,48 @@ class IntegrationStatusService return [ 'store' => null, 'reachable' => false, + 'cached' => false, 'integrations' => $this->catalogOnly($catalog), ]; } + if ($refresh) { + $this->forgetCache($store); + } + + $ttl = max(0, (int) config('woo.integrations_cache_ttl_seconds', 300)); + $cacheKey = $this->cacheKey($store); + + if ($ttl === 0) { + return $this->buildPayload($store, $catalog); + } + + $cached = Cache::get($cacheKey); + if (is_array($cached)) { + return array_merge($cached, ['cached' => true]); + } + + $payload = $this->buildPayload($store, $catalog); + Cache::put($cacheKey, $payload, $ttl); + + return array_merge($payload, ['cached' => false]); + } + + public function forgetCache(WooStore $store): void + { + Cache::forget($this->cacheKey($store)); + } + + /** + * @param array> $catalog + * @return array{ + * store: WooStore, + * reachable: bool, + * integrations: list> + * } + */ + private function buildPayload(WooStore $store, array $catalog): array + { $remote = $this->client->get($store, 'integrations/status'); $reachable = is_array($remote); @@ -50,7 +90,7 @@ class IntegrationStatusService ]; } - /** + /** * @param array $meta * @param array $remoteStatus * @return array @@ -111,4 +151,9 @@ class IntegrationStatusService return $integrations; } + + private function cacheKey(WooStore $store): string + { + return 'woo.integrations.'.$store->id; + } } diff --git a/config/woo.php b/config/woo.php index 63d3dcc..52e3a08 100644 --- a/config/woo.php +++ b/config/woo.php @@ -40,4 +40,6 @@ return [ 'description' => 'Multiple stores and unlimited products — from GHS 49/mo.', 'route' => 'woo.pro.index', ], + + 'integrations_cache_ttl_seconds' => (int) env('WOO_INTEGRATIONS_CACHE_TTL', 300), ]; diff --git a/config/woo_integrations.php b/config/woo_integrations.php index 6d53528..245b249 100644 --- a/config/woo_integrations.php +++ b/config/woo_integrations.php @@ -29,4 +29,25 @@ return [ 'install_url' => 'https://wordpress.org/plugins/snapchat-for-woocommerce/', 'docs_url' => 'https://businesshelp.snapchat.com/s/article/woocommerce', ], + 'tiktok' => [ + 'name' => 'TikTok for WooCommerce', + 'description' => 'Sync your catalog to TikTok Shop and run TikTok ads from WooCommerce.', + 'category' => 'Marketing', + 'install_url' => 'https://wordpress.org/plugins/tiktok-for-business/', + 'docs_url' => 'https://woocommerce.com/document/tiktok-for-woocommerce/', + ], + 'reddit' => [ + 'name' => 'Reddit for WooCommerce', + 'description' => 'Sync products to Reddit Ads and track conversions from your store.', + 'category' => 'Marketing', + 'install_url' => 'https://wordpress.org/plugins/reddit-for-woocommerce/', + 'docs_url' => 'https://woocommerce.com/document/reddit-for-woocommerce/', + ], + 'facebook' => [ + 'name' => 'Meta for WooCommerce', + 'description' => 'Connect Facebook and Instagram catalog sync, ads, and WhatsApp order updates.', + 'category' => 'Marketing', + 'install_url' => 'https://wordpress.org/plugins/facebook-for-woocommerce/', + 'docs_url' => 'https://woocommerce.com/document/facebook-for-woocommerce/', + ], ]; diff --git a/public/images/launcher-icons/woomanager.svg b/public/images/launcher-icons/woomanager.svg index cefbb2f..9740185 100644 --- a/public/images/launcher-icons/woomanager.svg +++ b/public/images/launcher-icons/woomanager.svg @@ -23,9 +23,9 @@ } - - - - - + + + + + \ No newline at end of file diff --git a/resources/views/woo/integrations/index.blade.php b/resources/views/woo/integrations/index.blade.php index 758c4e2..76f478c 100644 --- a/resources/views/woo/integrations/index.blade.php +++ b/resources/views/woo/integrations/index.blade.php @@ -13,6 +13,15 @@ @endif

+ @if($store) +
+ @csrf + +
+ @endif @if($store && ! $reachable) Store unreachable — update the Ladill plugin on WordPress @@ -31,6 +40,10 @@ @endif + @if($store && ($cached ?? false)) +

Status cached for {{ (int) config('woo.integrations_cache_ttl_seconds', 300) }} seconds. Use Refresh status for the latest from WordPress.

+ @endif +
@foreach($integrations as $integration) @php @@ -60,6 +73,17 @@

{{ $integration['summary'] }}

@endif + @if($integration['key'] === 'shipping' && ! empty($integration['details']['woocommerce_shipping'])) +

+ WooCommerce Shipping + @if(! empty($integration['details']['labels_ready'])) + · labels enabled + @else + · labels not connected + @endif +

+ @endif +
@if($integration['manage_url']) group(function () { Route::post('/stores/switch', StoreSwitchController::class)->name('woo.stores.switch'); Route::delete('/stores/{store}', [StoreController::class, 'destroy'])->name('woo.stores.destroy'); Route::get('/integrations', [IntegrationController::class, 'index'])->name('woo.integrations.index'); + Route::post('/integrations/refresh', [IntegrationController::class, 'refresh'])->name('woo.integrations.refresh'); Route::get('/settings', [SettingsController::class, 'edit'])->name('woo.settings'); Route::post('/ai/chat', [AiController::class, 'chat'])->middleware('throttle:30,1')->name('woo.ai.chat'); diff --git a/tests/Feature/WooIntegrationsTest.php b/tests/Feature/WooIntegrationsTest.php index ae19d27..7af74db 100644 --- a/tests/Feature/WooIntegrationsTest.php +++ b/tests/Feature/WooIntegrationsTest.php @@ -5,6 +5,7 @@ namespace Tests\Feature; use App\Models\User; use App\Models\WooStore; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Tests\TestCase; @@ -29,6 +30,9 @@ class WooIntegrationsTest extends TestCase ->assertSee('Google for WooCommerce') ->assertSee('Pinterest for WooCommerce') ->assertSee('Snapchat for WooCommerce') + ->assertSee('TikTok for WooCommerce') + ->assertSee('Reddit for WooCommerce') + ->assertSee('Meta for WooCommerce') ->assertSee('Connect a WooCommerce store'); } @@ -93,9 +97,50 @@ class WooIntegrationsTest extends TestCase ->assertSee('Connected') ->assertSee('Manage in WordPress'); - Http::assertSent(function ($request) use ($store) { - return $request->url() === 'https://shop.example.com/wp-json/ladill-woo/v1/integrations/status' - && $request->hasHeader('X-Ladill-Store', $store->public_id); - }); + Http::assertSentCount(1); + } + + public function test_integrations_status_is_cached_until_refresh(): void + { + config(['woo.integrations_cache_ttl_seconds' => 300]); + + $user = User::factory()->create(); + $store = WooStore::create([ + 'user_id' => $user->id, + 'site_url' => 'https://shop.example.com', + 'site_name' => 'Example Shop', + 'status' => WooStore::STATUS_ACTIVE, + 'connected_at' => now(), + 'public_id' => 'store-public-id', + ]); + + Http::fake([ + 'https://shop.example.com/wp-json/ladill-woo/v1/integrations/status' => Http::response([ + 'integrations' => [ + 'shipping' => [ + 'state' => 'connected', + 'installed' => true, + 'connected' => true, + 'manage_url' => 'https://shop.example.com/wp-admin/admin.php?page=wc-settings&tab=shipping', + 'summary' => '1 zone, 2 methods', + 'details' => ['zones' => 1, 'methods' => 2], + ], + ], + ], 200), + ]); + + $this->actingAs($user)->get(route('woo.integrations.index'))->assertOk(); + $this->actingAs($user)->get(route('woo.integrations.index'))->assertOk(); + + Http::assertSentCount(1); + + $this->actingAs($user) + ->post(route('woo.integrations.refresh')) + ->assertRedirect(route('woo.integrations.index')); + + $this->actingAs($user)->get(route('woo.integrations.index'))->assertOk(); + + Http::assertSentCount(2); + $this->assertTrue(Cache::has('woo.integrations.'.$store->id)); } }