Scaffold Ladill Woo Manager for WooCommerce order fulfillment.
Deploy Ladill Woo Manager / deploy (push) Failing after 2s
Deploy Ladill Woo Manager / deploy (push) Failing after 2s
Standalone app with SSO shell, WordPress plugin connect flow, webhook ingest, fulfillment inbox, and plugin activation API — no payment processing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Woo;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\WooStore;
|
||||
use App\Services\Woo\InstallTokenService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ConnectWordPressController extends Controller
|
||||
{
|
||||
public function __construct(private InstallTokenService $tokens) {}
|
||||
|
||||
public function start(Request $request): RedirectResponse|View
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'site_url' => ['required', 'url', 'max:500'],
|
||||
'return_url' => ['required', 'url', 'max:2048'],
|
||||
'site_name' => ['nullable', 'string', 'max:160'],
|
||||
]);
|
||||
|
||||
$siteUrl = $this->normalizeSiteUrl($validated['site_url']);
|
||||
if (! $this->isAllowedReturnUrl($validated['return_url'], $siteUrl)) {
|
||||
abort(422, 'Return URL must belong to the WooCommerce site.');
|
||||
}
|
||||
|
||||
$request->session()->put('woo.connect', [
|
||||
'site_url' => $siteUrl,
|
||||
'return_url' => $validated['return_url'],
|
||||
'site_name' => $validated['site_name'] ?? null,
|
||||
]);
|
||||
|
||||
if (! $request->user()) {
|
||||
return redirect()->route('sso.connect', [
|
||||
'redirect' => $request->fullUrl(),
|
||||
'interactive' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->complete($request);
|
||||
}
|
||||
|
||||
public function complete(Request $request): RedirectResponse|View
|
||||
{
|
||||
$pending = (array) $request->session()->pull('woo.connect', []);
|
||||
if ($pending === []) {
|
||||
return redirect()->route('woo.dashboard');
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
abort_if(! $user, 401);
|
||||
|
||||
$siteUrl = (string) ($pending['site_url'] ?? '');
|
||||
$returnUrl = (string) ($pending['return_url'] ?? '');
|
||||
|
||||
$store = WooStore::query()->updateOrCreate(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'site_url' => $siteUrl,
|
||||
],
|
||||
[
|
||||
'site_name' => $pending['site_name'] ?? parse_url($siteUrl, PHP_URL_HOST),
|
||||
'return_url' => $returnUrl,
|
||||
'status' => WooStore::STATUS_PENDING,
|
||||
],
|
||||
);
|
||||
|
||||
$issued = $this->tokens->issue($store);
|
||||
$separator = str_contains($returnUrl, '?') ? '&' : '?';
|
||||
|
||||
return redirect()->away($returnUrl.$separator.http_build_query([
|
||||
'ladill_connect' => '1',
|
||||
'install_token' => $issued['token'],
|
||||
'store_id' => $store->public_id,
|
||||
]));
|
||||
}
|
||||
|
||||
private function normalizeSiteUrl(string $url): string
|
||||
{
|
||||
$parsed = parse_url($url);
|
||||
$scheme = $parsed['scheme'] ?? 'https';
|
||||
$host = strtolower((string) ($parsed['host'] ?? ''));
|
||||
|
||||
return rtrim($scheme.'://'.$host, '/');
|
||||
}
|
||||
|
||||
private function isAllowedReturnUrl(string $returnUrl, string $siteUrl): bool
|
||||
{
|
||||
$returnHost = strtolower((string) (parse_url($returnUrl, PHP_URL_HOST) ?: ''));
|
||||
$siteHost = strtolower((string) (parse_url($siteUrl, PHP_URL_HOST) ?: ''));
|
||||
|
||||
return $returnHost !== '' && $returnHost === $siteHost;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Woo;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\WooOrder;
|
||||
use App\Models\WooStore;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$user = $request->user();
|
||||
$storeIds = WooStore::query()->where('user_id', $user->id)->pluck('id');
|
||||
|
||||
$stats = [
|
||||
'stores' => WooStore::query()->where('user_id', $user->id)->where('status', WooStore::STATUS_ACTIVE)->count(),
|
||||
'orders_new' => WooOrder::query()->whereIn('woo_store_id', $storeIds)->where('fulfillment_status', WooOrder::FULFILLMENT_NEW)->count(),
|
||||
'orders_open' => WooOrder::query()->whereIn('woo_store_id', $storeIds)->whereNotIn('fulfillment_status', [
|
||||
WooOrder::FULFILLMENT_DELIVERED,
|
||||
WooOrder::FULFILLMENT_CANCELLED,
|
||||
])->count(),
|
||||
];
|
||||
|
||||
$recent = WooOrder::query()
|
||||
->whereIn('woo_store_id', $storeIds)
|
||||
->with('store')
|
||||
->latest('created_at')
|
||||
->limit(8)
|
||||
->get();
|
||||
|
||||
return view('woo.dashboard', compact('stats', 'recent'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Woo;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\WooOrder;
|
||||
use App\Models\WooStore;
|
||||
use App\Services\Woo\FulfillmentSyncService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class OrderController extends Controller
|
||||
{
|
||||
public function __construct(private FulfillmentSyncService $sync) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$user = $request->user();
|
||||
$search = trim((string) $request->query('q', ''));
|
||||
$storeFilter = $request->query('store');
|
||||
|
||||
$storeIds = WooStore::query()->where('user_id', $user->id)->pluck('id');
|
||||
|
||||
$orders = WooOrder::query()
|
||||
->whereIn('woo_store_id', $storeIds)
|
||||
->when($storeFilter, fn ($q) => $q->where('woo_store_id', $storeFilter))
|
||||
->when($search !== '', function ($query) use ($search) {
|
||||
$like = '%'.$search.'%';
|
||||
$query->where(function ($inner) use ($like) {
|
||||
$inner->where('customer_name', 'like', $like)
|
||||
->orWhere('customer_email', 'like', $like)
|
||||
->orWhere('order_number', 'like', $like);
|
||||
});
|
||||
})
|
||||
->with('store')
|
||||
->latest('created_at')
|
||||
->paginate(20)
|
||||
->withQueryString();
|
||||
|
||||
$stores = WooStore::query()->where('user_id', $user->id)->orderBy('site_name')->get();
|
||||
|
||||
return view('woo.orders.index', compact('orders', 'stores', 'search', 'storeFilter'));
|
||||
}
|
||||
|
||||
public function updateStatus(Request $request, WooOrder $order): RedirectResponse
|
||||
{
|
||||
$order->load('store');
|
||||
abort_if($order->store?->user_id !== $request->user()->id, 403);
|
||||
|
||||
$validated = $request->validate([
|
||||
'fulfillment_status' => ['required', Rule::in(array_keys(WooOrder::FULFILLMENT_STATUSES))],
|
||||
]);
|
||||
|
||||
$order->update(['fulfillment_status' => $validated['fulfillment_status']]);
|
||||
$this->sync->push($order->fresh());
|
||||
|
||||
return back()->with('success', 'Fulfillment status updated.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Woo;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\WooStore;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class StoreController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$stores = WooStore::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->latest('updated_at')
|
||||
->get();
|
||||
|
||||
return view('woo.stores.index', compact('stores'));
|
||||
}
|
||||
|
||||
public function destroy(Request $request, WooStore $store): RedirectResponse
|
||||
{
|
||||
abort_if($store->user_id !== $request->user()->id, 403);
|
||||
|
||||
$store->update(['status' => WooStore::STATUS_DISCONNECTED]);
|
||||
|
||||
return back()->with('success', 'Store disconnected. Install the Ladill plugin again to reconnect.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user