Extend integrations hub with more channels, caching, and refresh.

Cache per-store status for five minutes, add TikTok/Reddit/Meta cards, and let merchants refresh without reloading WordPress on every visit.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-08 07:37:41 +00:00
co-authored by Cursor
parent 5a0022af72
commit 1b0831b176
8 changed files with 162 additions and 11 deletions
@@ -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.');
}
}
+47 -2
View File
@@ -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<array<string, mixed>>
* }
*/
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<string, array<string, mixed>> $catalog
* @return array{
* store: WooStore,
* reachable: bool,
* integrations: list<array<string, mixed>>
* }
*/
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<string, mixed> $meta
* @param array<string, mixed> $remoteStatus
* @return array<string, mixed>
@@ -111,4 +151,9 @@ class IntegrationStatusService
return $integrations;
}
private function cacheKey(WooStore $store): string
{
return 'woo.integrations.'.$store->id;
}
}
+2
View File
@@ -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),
];
+21
View File
@@ -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/',
],
];
+5 -5
View File
@@ -23,9 +23,9 @@
}
</style>
</defs>
<rect class="cls-3" x=".2" y="51.2" width="147.8" height="84.5" rx="42.1" ry="42.1"/>
<path class="cls-2" d="M105.6,51.2h0c23.4,0,42.4,19.1,42.4,42.7v172.2c0,23.6-19,42.7-42.4,42.7h0c-23.4,0-42.4-19.1-42.4-42.7V93.9c0-23.6,19-42.7,42.4-42.7Z"/>
<path class="cls-4" d="M232.7,56.9h0c20.3,11.7,27.2,37.7,15.5,57.9l-105.9,172.7c-11.7,20.3-37.7,27.2-57.9,15.5h0c-20.3-11.7-27.2-37.7-15.5-57.9l105.9-172.7c11.7-20.3,37.7-27.2,57.9-15.5h0Z"/>
<path class="cls-1" d="M211.5,51.2h0c23.4,0,42.4,19.1,42.4,42.7v172.2c0,23.6-19,42.7-42.4,42.7h0c-23.4,0-42.4-19.1-42.4-42.7V93.9c0-23.6,19-42.7,42.4-42.7Z"/>
<path class="cls-3" d="M338.6,56.9h0c20.3,11.7,27.2,37.7,15.5,57.9l-105.9,172.7c-11.7,20.3-37.7,27.2-57.9,15.5h0c-20.3-11.7-27.2-37.7-15.5-57.9l105.9-172.7c11.7-20.3,37.7-27.2,57.9-15.5Z"/>
<rect class="cls-3" x="-.4" y="83.3" width="109.1" height="62.3" rx="30.7" ry="30.7"/>
<rect class="cls-2" x="46.1" y="83.3" width="62.6" height="188.7" rx="30.8" ry="30.8"/>
<rect class="cls-4" x="109.3" y="58.7" width="62.6" height="240.4" rx="30.8" ry="30.8" transform="translate(167.6 -47.1) rotate(45)"/>
<rect class="cls-1" x="172.8" y="84.5" width="62.6" height="188.7" rx="30.8" ry="30.8"/>
<rect class="cls-3" x="235.1" y="60.2" width="62.6" height="238.8" rx="30.8" ry="30.8" transform="translate(205 -135.8) rotate(45)"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1004 B

@@ -13,6 +13,15 @@
@endif
</p>
</div>
@if($store)
<form method="post" action="{{ route('woo.integrations.refresh') }}">
@csrf
<button type="submit"
class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">
Refresh status
</button>
</form>
@endif
@if($store && ! $reachable)
<span class="rounded-full bg-amber-50 px-3 py-1 text-xs font-medium text-amber-800 ring-1 ring-amber-200">
Store unreachable update the Ladill plugin on WordPress
@@ -31,6 +40,10 @@
</div>
@endif
@if($store && ($cached ?? false))
<p class="text-xs text-slate-400">Status cached for {{ (int) config('woo.integrations_cache_ttl_seconds', 300) }} seconds. Use Refresh status for the latest from WordPress.</p>
@endif
<div class="grid gap-4 lg:grid-cols-2">
@foreach($integrations as $integration)
@php
@@ -60,6 +73,17 @@
<p class="mt-3 text-sm text-slate-500">{{ $integration['summary'] }}</p>
@endif
@if($integration['key'] === 'shipping' && ! empty($integration['details']['woocommerce_shipping']))
<p class="mt-2 text-xs text-slate-500">
WooCommerce Shipping
@if(! empty($integration['details']['labels_ready']))
· labels enabled
@else
· labels not connected
@endif
</p>
@endif
<div class="mt-5 flex flex-wrap items-center gap-2">
@if($integration['manage_url'])
<a href="{{ $integration['manage_url'] }}" target="_blank" rel="noopener noreferrer"
+1
View File
@@ -63,6 +63,7 @@ Route::middleware(['auth', 'platform.session'])->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');
+49 -4
View File
@@ -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));
}
}