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)
+
+ @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));
}
}