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:
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
|
||||
@@ -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/',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user