Add Woo Manager Pro with multi-store switching and free-tier limits.
Deploy Ladill Woo Manager / deploy (push) Successful in 51s
Deploy Ladill Woo Manager / deploy (push) Successful in 51s
Free: 1 store and 20 products. Pro/Business mirrors Invoice pricing (GHS 49/149) with wallet renewals, Paystack prepay, session-based store switcher, and scoped UI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,8 +3,8 @@
|
||||
namespace App\Http\Controllers\Woo;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Woo\Concerns\ResolvesWooContext;
|
||||
use App\Models\WooCategory;
|
||||
use App\Models\WooStore;
|
||||
use App\Services\Woo\CatalogSyncService;
|
||||
use App\Services\Woo\CatalogWriteService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -13,6 +13,8 @@ use Illuminate\View\View;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
use ResolvesWooContext;
|
||||
|
||||
public function __construct(
|
||||
private CatalogWriteService $write,
|
||||
private CatalogSyncService $sync,
|
||||
@@ -20,105 +22,84 @@ class CategoryController extends Controller
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$user = $request->user();
|
||||
$storeFilter = $request->query('store');
|
||||
$store = $this->currentStore($request);
|
||||
$search = trim((string) $request->query('q', ''));
|
||||
|
||||
$storeIds = WooStore::query()->where('user_id', $user->id)->pluck('id');
|
||||
|
||||
$categories = WooCategory::query()
|
||||
->whereIn('woo_store_id', $storeIds)
|
||||
->when($storeFilter, fn ($q) => $q->where('woo_store_id', $storeFilter))
|
||||
->when($store, fn ($q) => $q->where('woo_store_id', $store->id))
|
||||
->when(! $store, fn ($q) => $q->whereRaw('1 = 0'))
|
||||
->when($search !== '', fn ($q) => $q->where('name', 'like', '%'.$search.'%'))
|
||||
->with('store')
|
||||
->orderBy('name')
|
||||
->paginate(30)
|
||||
->withQueryString();
|
||||
|
||||
$stores = WooStore::query()->where('user_id', $user->id)->orderBy('site_name')->get();
|
||||
|
||||
return view('woo.categories.index', compact('categories', 'stores', 'storeFilter', 'search'));
|
||||
return view('woo.categories.index', compact('categories', 'search', 'store'));
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$stores = $this->activeStores($request);
|
||||
$storeId = (int) $request->query('store', $stores->first()?->id ?? 0);
|
||||
$parentCategories = $storeId > 0
|
||||
? WooCategory::query()->where('woo_store_id', $storeId)->orderBy('name')->get()
|
||||
: collect();
|
||||
$store = $this->currentStoreOrFail($request);
|
||||
|
||||
return view('woo.categories.form', [
|
||||
'category' => new WooCategory,
|
||||
'stores' => $stores,
|
||||
'parentCategories' => $parentCategories,
|
||||
'selectedStoreId' => $storeId,
|
||||
'category' => new WooCategory(['woo_store_id' => $store->id]),
|
||||
'currentStore' => $store,
|
||||
'parentCategories' => $store->categories()->orderBy('name')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $this->validateCategory($request);
|
||||
$store = $this->authorizedStore($request, (int) $validated['woo_store_id']);
|
||||
$store = $this->currentStoreOrFail($request);
|
||||
$validated = $this->validateCategory($request, $store);
|
||||
|
||||
$this->write->createCategory($store, $validated);
|
||||
|
||||
return redirect()->route('woo.categories.index', ['store' => $store->id])
|
||||
return redirect()->route('woo.categories.index')
|
||||
->with('success', 'Category saved.');
|
||||
}
|
||||
|
||||
public function edit(Request $request, WooCategory $category): View
|
||||
{
|
||||
$category->load('store');
|
||||
abort_if($category->store?->user_id !== $request->user()->id, 403);
|
||||
|
||||
$stores = $this->activeStores($request);
|
||||
$parentCategories = $category->store
|
||||
? $category->store->categories()
|
||||
->where('external_id', '!=', $category->external_id)
|
||||
->orderBy('name')
|
||||
->get()
|
||||
: collect();
|
||||
$store = $this->currentStoreOrFail($request);
|
||||
abort_if($category->woo_store_id !== $store->id, 404);
|
||||
|
||||
return view('woo.categories.form', [
|
||||
'category' => $category,
|
||||
'stores' => $stores,
|
||||
'parentCategories' => $parentCategories,
|
||||
'selectedStoreId' => $category->woo_store_id,
|
||||
'currentStore' => $store,
|
||||
'parentCategories' => $store->categories()
|
||||
->where('external_id', '!=', $category->external_id)
|
||||
->orderBy('name')
|
||||
->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, WooCategory $category): RedirectResponse
|
||||
{
|
||||
$category->load('store');
|
||||
abort_if($category->store?->user_id !== $request->user()->id, 403);
|
||||
$store = $this->currentStoreOrFail($request);
|
||||
abort_if($category->woo_store_id !== $store->id, 404);
|
||||
|
||||
$validated = $this->validateCategory($request);
|
||||
$validated = $this->validateCategory($request, $store);
|
||||
$this->write->updateCategory($category, $validated);
|
||||
|
||||
return redirect()->route('woo.categories.index', ['store' => $category->woo_store_id])
|
||||
return redirect()->route('woo.categories.index')
|
||||
->with('success', 'Category updated.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, WooCategory $category): RedirectResponse
|
||||
{
|
||||
$category->load('store');
|
||||
abort_if($category->store?->user_id !== $request->user()->id, 403);
|
||||
$store = $this->currentStoreOrFail($request);
|
||||
abort_if($category->woo_store_id !== $store->id, 404);
|
||||
|
||||
$storeId = $category->woo_store_id;
|
||||
$this->write->deleteCategory($category);
|
||||
|
||||
return redirect()->route('woo.categories.index', ['store' => $storeId])
|
||||
return redirect()->route('woo.categories.index')
|
||||
->with('success', 'Category removed.');
|
||||
}
|
||||
|
||||
public function sync(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'store' => ['required', 'integer'],
|
||||
]);
|
||||
|
||||
$store = $this->authorizedStore($request, (int) $validated['store']);
|
||||
$store = $this->currentStoreOrFail($request);
|
||||
$count = $this->sync->syncCategories($store);
|
||||
|
||||
return back()->with('success', sprintf(
|
||||
@@ -129,33 +110,15 @@ class CategoryController extends Controller
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function validateCategory(Request $request): array
|
||||
private function validateCategory(Request $request, $store): array
|
||||
{
|
||||
return $request->validate([
|
||||
'woo_store_id' => ['required', 'integer'],
|
||||
return array_merge($request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'slug' => ['nullable', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string', 'max:10000'],
|
||||
'parent_external_id' => ['nullable', 'integer', 'min:0'],
|
||||
]), [
|
||||
'woo_store_id' => $store->id,
|
||||
]);
|
||||
}
|
||||
|
||||
private function authorizedStore(Request $request, int $storeId): WooStore
|
||||
{
|
||||
return WooStore::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->whereKey($storeId)
|
||||
->where('status', WooStore::STATUS_ACTIVE)
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
/** @return \Illuminate\Support\Collection<int, WooStore> */
|
||||
private function activeStores(Request $request)
|
||||
{
|
||||
return WooStore::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->where('status', WooStore::STATUS_ACTIVE)
|
||||
->orderBy('site_name')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Woo\Concerns;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\WooStore;
|
||||
use App\Support\CurrentWooStore;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait ResolvesWooContext
|
||||
{
|
||||
protected function accountUser(Request $request): User
|
||||
{
|
||||
return ladill_account() ?? $request->user();
|
||||
}
|
||||
|
||||
protected function currentStore(Request $request): ?WooStore
|
||||
{
|
||||
return app(CurrentWooStore::class)->resolve($this->accountUser($request));
|
||||
}
|
||||
|
||||
protected function currentStoreOrFail(Request $request): WooStore
|
||||
{
|
||||
$store = $this->currentStore($request);
|
||||
abort_if(! $store, 404, 'Connect a WooCommerce store to continue.');
|
||||
|
||||
return $store;
|
||||
}
|
||||
|
||||
protected function authorizedStore(Request $request, int $storeId): WooStore
|
||||
{
|
||||
return WooStore::query()
|
||||
->where('user_id', $this->accountUser($request)->id)
|
||||
->whereKey($storeId)
|
||||
->where('status', WooStore::STATUS_ACTIVE)
|
||||
->firstOrFail();
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,24 @@
|
||||
namespace App\Http\Controllers\Woo;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Woo\Concerns\ResolvesWooContext;
|
||||
use App\Http\Controllers\Woo\Concerns\ResolvesWooContext;
|
||||
use App\Models\WooStore;
|
||||
use App\Services\Woo\InstallTokenService;
|
||||
use App\Services\Woo\SubscriptionService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Support\CurrentWooStore;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ConnectWordPressController extends Controller
|
||||
{
|
||||
public function __construct(private InstallTokenService $tokens) {}
|
||||
use ResolvesWooContext;
|
||||
|
||||
public function __construct(
|
||||
private InstallTokenService $tokens,
|
||||
private SubscriptionService $subscriptions,
|
||||
) {}
|
||||
|
||||
public function start(Request $request): RedirectResponse|View
|
||||
{
|
||||
@@ -56,6 +64,11 @@ class ConnectWordPressController extends Controller
|
||||
$siteUrl = (string) ($pending['site_url'] ?? '');
|
||||
$returnUrl = (string) ($pending['return_url'] ?? '');
|
||||
|
||||
if (! $this->subscriptions->canConnectStore($user, $siteUrl)) {
|
||||
return redirect()->route('woo.pro.index')
|
||||
->with('upsell', 'Free plan includes '.(int) config('woo.free.max_stores', 1).' store. Upgrade to Pro to connect more WooCommerce sites.');
|
||||
}
|
||||
|
||||
$store = WooStore::query()->updateOrCreate(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
@@ -71,6 +84,8 @@ class ConnectWordPressController extends Controller
|
||||
$issued = $this->tokens->issue($store);
|
||||
$separator = str_contains($returnUrl, '?') ? '&' : '?';
|
||||
|
||||
session([CurrentWooStore::SESSION_KEY => $store->id]);
|
||||
|
||||
return redirect()->away($returnUrl.$separator.http_build_query([
|
||||
'ladill_connect' => '1',
|
||||
'install_token' => $issued['token'],
|
||||
|
||||
@@ -3,38 +3,64 @@
|
||||
namespace App\Http\Controllers\Woo;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Woo\Concerns\ResolvesWooContext;
|
||||
use App\Models\WooCategory;
|
||||
use App\Models\WooOrder;
|
||||
use App\Models\WooProduct;
|
||||
use App\Models\WooCategory;
|
||||
use App\Models\WooStore;
|
||||
use App\Services\Woo\SubscriptionService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
use ResolvesWooContext;
|
||||
|
||||
public function __construct(private SubscriptionService $subscriptions) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$user = $request->user();
|
||||
$storeIds = WooStore::query()->where('user_id', $user->id)->pluck('id');
|
||||
$user = $this->accountUser($request);
|
||||
$store = $this->currentStore($request);
|
||||
$storeId = $store?->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(),
|
||||
'products' => WooProduct::query()->whereIn('woo_store_id', $storeIds)->count(),
|
||||
'categories' => WooCategory::query()->whereIn('woo_store_id', $storeIds)->count(),
|
||||
'orders_new' => $storeId
|
||||
? WooOrder::query()->where('woo_store_id', $storeId)->where('fulfillment_status', WooOrder::FULFILLMENT_NEW)->count()
|
||||
: 0,
|
||||
'orders_open' => $storeId
|
||||
? WooOrder::query()->where('woo_store_id', $storeId)->whereNotIn('fulfillment_status', [
|
||||
WooOrder::FULFILLMENT_DELIVERED,
|
||||
WooOrder::FULFILLMENT_CANCELLED,
|
||||
])->count()
|
||||
: 0,
|
||||
'products' => $storeId
|
||||
? WooProduct::query()->where('woo_store_id', $storeId)->count()
|
||||
: 0,
|
||||
'categories' => $storeId
|
||||
? WooCategory::query()->where('woo_store_id', $storeId)->count()
|
||||
: 0,
|
||||
];
|
||||
|
||||
$recent = WooOrder::query()
|
||||
->whereIn('woo_store_id', $storeIds)
|
||||
->with('store')
|
||||
->latest('created_at')
|
||||
->limit(8)
|
||||
->get();
|
||||
$recent = $storeId
|
||||
? WooOrder::query()
|
||||
->where('woo_store_id', $storeId)
|
||||
->with('store')
|
||||
->latest('created_at')
|
||||
->limit(8)
|
||||
->get()
|
||||
: collect();
|
||||
|
||||
return view('woo.dashboard', compact('stats', 'recent'));
|
||||
return view('woo.dashboard', [
|
||||
'stats' => $stats,
|
||||
'recent' => $recent,
|
||||
'currentStore' => $store,
|
||||
'hasPaidPlan' => $this->subscriptions->hasPaidPlan($user),
|
||||
'productCount' => $this->subscriptions->productCount($user),
|
||||
'productLimit' => $this->subscriptions->isPro($user) ? null : (int) config('woo.free.max_products', 20),
|
||||
'storeCount' => $this->subscriptions->connectedStoreCount($user),
|
||||
'storeLimit' => $this->subscriptions->isPro($user) ? null : (int) config('woo.free.max_stores', 1),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace App\Http\Controllers\Woo;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Woo\Concerns\ResolvesWooContext;
|
||||
use App\Models\WooOrder;
|
||||
use App\Models\WooStore;
|
||||
use App\Services\Woo\FulfillmentSyncService;
|
||||
use App\Services\Woo\OrderSyncService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -14,6 +14,8 @@ use Illuminate\View\View;
|
||||
|
||||
class OrderController extends Controller
|
||||
{
|
||||
use ResolvesWooContext;
|
||||
|
||||
public function __construct(
|
||||
private FulfillmentSyncService $sync,
|
||||
private OrderSyncService $orderSync,
|
||||
@@ -21,15 +23,12 @@ class OrderController extends Controller
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$user = $request->user();
|
||||
$store = $this->currentStore($request);
|
||||
$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($store, fn ($q) => $q->where('woo_store_id', $store->id))
|
||||
->when(! $store, fn ($q) => $q->whereRaw('1 = 0'))
|
||||
->when($search !== '', function ($query) use ($search) {
|
||||
$like = '%'.$search.'%';
|
||||
$query->where(function ($inner) use ($like) {
|
||||
@@ -43,15 +42,13 @@ class OrderController extends Controller
|
||||
->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'));
|
||||
return view('woo.orders.index', compact('orders', 'search', 'store'));
|
||||
}
|
||||
|
||||
public function updateStatus(Request $request, WooOrder $order): RedirectResponse
|
||||
{
|
||||
$order->load('store');
|
||||
abort_if($order->store?->user_id !== $request->user()->id, 403);
|
||||
$store = $this->currentStoreOrFail($request);
|
||||
abort_if($order->woo_store_id !== $store->id, 404);
|
||||
|
||||
$validated = $request->validate([
|
||||
'fulfillment_status' => ['required', Rule::in(array_keys(WooOrder::FULFILLMENT_STATUSES))],
|
||||
@@ -65,16 +62,7 @@ class OrderController extends Controller
|
||||
|
||||
public function sync(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'store' => ['required', 'integer'],
|
||||
]);
|
||||
|
||||
$store = WooStore::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->whereKey((int) $validated['store'])
|
||||
->where('status', WooStore::STATUS_ACTIVE)
|
||||
->firstOrFail();
|
||||
|
||||
$store = $this->currentStoreOrFail($request);
|
||||
$count = $this->orderSync->syncOrders($store);
|
||||
|
||||
return back()->with('success', sprintf(
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Woo;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Woo\Concerns\ResolvesWooContext;
|
||||
use App\Models\User;
|
||||
use App\Models\Woo\ProSubscription;
|
||||
use App\Services\Woo\BillingClient;
|
||||
use App\Services\Woo\SubscriptionService;
|
||||
use App\Support\CurrentWooStore;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ProController extends Controller
|
||||
{
|
||||
use ResolvesWooContext;
|
||||
|
||||
public function __construct(private readonly SubscriptionService $subscriptions) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$user = $this->accountUser($request);
|
||||
$planKey = $this->subscriptions->planKey($user);
|
||||
|
||||
return view('woo.pro.index', [
|
||||
'subscription' => $this->subscriptions->subscriptionFor($user),
|
||||
'planKey' => $planKey,
|
||||
'isPro' => $planKey === ProSubscription::PLAN_PRO,
|
||||
'isEnterprise' => $planKey === ProSubscription::PLAN_ENTERPRISE,
|
||||
'hasPaidPlan' => $this->subscriptions->hasPaidPlan($user),
|
||||
'gatingActive' => $this->subscriptions->gatingActive(),
|
||||
'proPriceMinor' => $this->subscriptions->priceMinor(),
|
||||
'enterprisePriceMinor' => $this->subscriptions->enterprisePriceMinor(),
|
||||
'prepaidMonths' => (array) config('woo.prepaid_months', [6, 12, 24]),
|
||||
'currency' => (string) config('woo.pro.currency', 'GHS'),
|
||||
'storeCount' => $this->subscriptions->connectedStoreCount($user),
|
||||
'storeLimit' => $this->subscriptions->maxStores($user),
|
||||
'productCount' => $this->subscriptions->productCount($user),
|
||||
'productLimit' => min($this->subscriptions->maxProducts($user), (int) config('woo.free.max_products', 20)),
|
||||
]);
|
||||
}
|
||||
|
||||
public function subscribe(Request $request): RedirectResponse
|
||||
{
|
||||
[$ok, $message] = $this->subscriptions->subscribe($this->accountUser($request));
|
||||
|
||||
return redirect()->route('woo.pro.index')->with($ok ? 'success' : 'error', $message);
|
||||
}
|
||||
|
||||
public function subscribeEnterprise(Request $request): RedirectResponse
|
||||
{
|
||||
[$ok, $message] = $this->subscriptions->subscribeEnterprise($this->accountUser($request));
|
||||
|
||||
return redirect()->route('woo.pro.index')->with($ok ? 'success' : 'error', $message);
|
||||
}
|
||||
|
||||
public function subscribePrepaid(Request $request, BillingClient $billing): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'plan' => ['required', 'in:pro,enterprise'],
|
||||
'months' => ['required', 'integer', 'in:6,12,24'],
|
||||
]);
|
||||
|
||||
$user = $this->accountUser($request);
|
||||
if ($this->subscriptions->hasPaidPlan($user)) {
|
||||
return redirect()->route('woo.pro.index')
|
||||
->with('success', 'You already have an active subscription.');
|
||||
}
|
||||
|
||||
$plan = $validated['plan'];
|
||||
$months = (int) $validated['months'];
|
||||
$monthlyMinor = $plan === 'enterprise'
|
||||
? $this->subscriptions->enterprisePriceMinor()
|
||||
: $this->subscriptions->priceMinor();
|
||||
|
||||
try {
|
||||
$checkout = $billing->initiatePlanCheckout(
|
||||
$user,
|
||||
$plan,
|
||||
$months,
|
||||
$monthlyMinor * $months,
|
||||
route('woo.pro.paystack.callback'),
|
||||
['user_id' => $user->id],
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
return redirect()->route('woo.pro.index')
|
||||
->with('error', 'Could not start Paystack checkout. Please try again.');
|
||||
}
|
||||
|
||||
$url = trim((string) ($checkout['checkout_url'] ?? ''));
|
||||
if ($url === '') {
|
||||
return redirect()->route('woo.pro.index')
|
||||
->with('error', 'Paystack did not return a checkout URL.');
|
||||
}
|
||||
|
||||
return redirect()->away($url);
|
||||
}
|
||||
|
||||
public function paystackCallback(Request $request, BillingClient $billing): RedirectResponse
|
||||
{
|
||||
$reference = (string) $request->query('reference', '');
|
||||
if ($reference === '') {
|
||||
return redirect()->route('woo.pro.index')->with('error', 'Missing payment reference.');
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $billing->verifyPlanCheckout($reference);
|
||||
} catch (\Throwable) {
|
||||
return redirect()->route('woo.pro.index')
|
||||
->with('error', 'Could not verify payment. Contact support with reference: '.$reference);
|
||||
}
|
||||
|
||||
if (! ($result['paid'] ?? false)) {
|
||||
return redirect()->route('woo.pro.index')
|
||||
->with('error', 'Payment was not completed.');
|
||||
}
|
||||
|
||||
$metadata = (array) ($result['metadata'] ?? []);
|
||||
$user = User::query()->find((int) ($metadata['user_id'] ?? 0));
|
||||
$account = $this->accountUser($request);
|
||||
if (! $user || ! $account || $user->id !== $account->id) {
|
||||
return redirect()->route('woo.pro.index')
|
||||
->with('error', 'Account not found for this payment.');
|
||||
}
|
||||
|
||||
$plan = (string) ($result['plan'] ?? 'pro');
|
||||
$months = (int) ($result['months'] ?? 0);
|
||||
$this->subscriptions->activatePrepaid($user, $plan, $months);
|
||||
|
||||
$label = $plan === 'enterprise' ? 'Woo Manager Business' : 'Woo Manager Pro';
|
||||
|
||||
return redirect()->route('woo.pro.index')
|
||||
->with('success', "{$label} is active for {$months} months.");
|
||||
}
|
||||
|
||||
public function cancel(Request $request): RedirectResponse
|
||||
{
|
||||
[$ok, $message] = $this->subscriptions->cancel($this->accountUser($request));
|
||||
|
||||
return redirect()->route('woo.pro.index')->with($ok ? 'success' : 'error', $message);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,12 @@
|
||||
namespace App\Http\Controllers\Woo;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Woo\Concerns\ResolvesWooContext;
|
||||
use App\Models\WooProduct;
|
||||
use App\Models\WooStore;
|
||||
use App\Services\Woo\CatalogSyncService;
|
||||
use App\Services\Woo\CatalogWriteService;
|
||||
use App\Services\Woo\ProductMediaService;
|
||||
use App\Services\Woo\SubscriptionService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -15,24 +16,25 @@ use Illuminate\View\View;
|
||||
|
||||
class ProductController extends Controller
|
||||
{
|
||||
use ResolvesWooContext;
|
||||
|
||||
public function __construct(
|
||||
private CatalogWriteService $write,
|
||||
private CatalogSyncService $sync,
|
||||
private ProductMediaService $media,
|
||||
private SubscriptionService $subscriptions,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$user = $request->user();
|
||||
$user = $this->accountUser($request);
|
||||
$store = $this->currentStore($request);
|
||||
$search = trim((string) $request->query('q', ''));
|
||||
$storeFilter = $request->query('store');
|
||||
$statusFilter = $request->query('status');
|
||||
|
||||
$storeIds = WooStore::query()->where('user_id', $user->id)->pluck('id');
|
||||
|
||||
$products = WooProduct::query()
|
||||
->whereIn('woo_store_id', $storeIds)
|
||||
->when($storeFilter, fn ($q) => $q->where('woo_store_id', $storeFilter))
|
||||
->when($store, fn ($q) => $q->where('woo_store_id', $store->id))
|
||||
->when(! $store, fn ($q) => $q->whereRaw('1 = 0'))
|
||||
->when($statusFilter, fn ($q) => $q->where('status', $statusFilter))
|
||||
->when($search !== '', function ($query) use ($search) {
|
||||
$like = '%'.$search.'%';
|
||||
@@ -46,82 +48,101 @@ class ProductController extends Controller
|
||||
->paginate(20)
|
||||
->withQueryString();
|
||||
|
||||
$stores = WooStore::query()->where('user_id', $user->id)->orderBy('site_name')->get();
|
||||
|
||||
return view('woo.products.index', compact('products', 'stores', 'search', 'storeFilter', 'statusFilter'));
|
||||
return view('woo.products.index', [
|
||||
'products' => $products,
|
||||
'currentStore' => $store,
|
||||
'search' => $search,
|
||||
'statusFilter' => $statusFilter,
|
||||
'productCount' => $this->subscriptions->productCount($user),
|
||||
'productLimit' => $this->subscriptions->isPro($user) ? null : (int) config('woo.free.max_products', 20),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
public function create(Request $request): View|RedirectResponse
|
||||
{
|
||||
$stores = $this->activeStores($request);
|
||||
$user = $this->accountUser($request);
|
||||
$store = $this->currentStoreOrFail($request);
|
||||
|
||||
if (! $this->subscriptions->canCreateProduct($user)) {
|
||||
return redirect()->route('woo.pro.index')
|
||||
->with('upsell', 'You have reached the free plan limit of '.(int) config('woo.free.max_products', 20).' products. Upgrade to Pro for unlimited products.');
|
||||
}
|
||||
|
||||
return view('woo.products.form', [
|
||||
'product' => new WooProduct(['status' => WooProduct::STATUS_DRAFT]),
|
||||
'stores' => $stores,
|
||||
'categories' => collect(),
|
||||
'product' => new WooProduct(['status' => WooProduct::STATUS_DRAFT, 'woo_store_id' => $store->id]),
|
||||
'currentStore' => $store,
|
||||
'categories' => $store->categories()->orderBy('name')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $this->validateProduct($request);
|
||||
$store = $this->authorizedStore($request, (int) $validated['woo_store_id']);
|
||||
$user = $this->accountUser($request);
|
||||
$store = $this->currentStoreOrFail($request);
|
||||
|
||||
if (! $this->subscriptions->canCreateProduct($user)) {
|
||||
return redirect()->route('woo.pro.index')
|
||||
->with('upsell', 'You have reached the free plan limit of '.(int) config('woo.free.max_products', 20).' products. Upgrade to Pro for unlimited products.');
|
||||
}
|
||||
|
||||
$validated = $this->validateProduct($request, $store);
|
||||
$validated['images'] = $this->media->buildImagesFromRequest($request, $store);
|
||||
|
||||
$this->write->createProduct($store, $validated);
|
||||
|
||||
return redirect()->route('woo.products.index', ['store' => $store->id])
|
||||
return redirect()->route('woo.products.index')
|
||||
->with('success', 'Product saved.');
|
||||
}
|
||||
|
||||
public function edit(Request $request, WooProduct $product): View
|
||||
{
|
||||
$product->load('store');
|
||||
abort_if($product->store?->user_id !== $request->user()->id, 403);
|
||||
$store = $this->currentStoreOrFail($request);
|
||||
abort_if($product->woo_store_id !== $store->id, 404);
|
||||
|
||||
$stores = $this->activeStores($request);
|
||||
$categories = $product->store
|
||||
? $product->store->categories()->orderBy('name')->get()
|
||||
: collect();
|
||||
|
||||
return view('woo.products.form', compact('product', 'stores', 'categories'));
|
||||
return view('woo.products.form', [
|
||||
'product' => $product,
|
||||
'currentStore' => $store,
|
||||
'categories' => $store->categories()->orderBy('name')->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, WooProduct $product): RedirectResponse
|
||||
{
|
||||
$product->load('store');
|
||||
abort_if($product->store?->user_id !== $request->user()->id, 403);
|
||||
$store = $this->currentStoreOrFail($request);
|
||||
abort_if($product->woo_store_id !== $store->id, 404);
|
||||
|
||||
$validated = $this->validateProduct($request, $product);
|
||||
$validated['images'] = $this->media->buildImagesFromRequest($request, $product->store);
|
||||
$validated = $this->validateProduct($request, $store, $product);
|
||||
$validated['images'] = $this->media->buildImagesFromRequest($request, $store);
|
||||
$this->write->updateProduct($product, $validated);
|
||||
|
||||
return redirect()->route('woo.products.index', ['store' => $product->woo_store_id])
|
||||
return redirect()->route('woo.products.index')
|
||||
->with('success', 'Product updated.');
|
||||
}
|
||||
|
||||
public function sync(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'store' => ['required', 'integer'],
|
||||
]);
|
||||
$store = $this->currentStoreOrFail($request);
|
||||
$user = $this->accountUser($request);
|
||||
$counts = $this->sync->syncAll($store, $user);
|
||||
|
||||
$store = $this->authorizedStore($request, (int) $validated['store']);
|
||||
$counts = $this->sync->syncAll($store);
|
||||
|
||||
return back()->with('success', sprintf(
|
||||
$message = sprintf(
|
||||
'Synced %d categories and %d products from %s.',
|
||||
$counts['categories'],
|
||||
$counts['products'],
|
||||
$store->site_name ?? $store->site_url,
|
||||
));
|
||||
);
|
||||
|
||||
if (! $this->subscriptions->isPro($user) && $this->subscriptions->productCount($user) >= (int) config('woo.free.max_products', 20)) {
|
||||
$message .= ' Free plan is limited to '.(int) config('woo.free.max_products', 20).' products — upgrade to Pro to sync your full catalog.';
|
||||
}
|
||||
|
||||
return back()->with('success', $message);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function validateProduct(Request $request, ?WooProduct $product = null): array
|
||||
private function validateProduct(Request $request, $store, ?WooProduct $product = null): array
|
||||
{
|
||||
return $request->validate([
|
||||
'woo_store_id' => ['required', 'integer'],
|
||||
return array_merge($request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'sku' => ['nullable', 'string', 'max:120'],
|
||||
'status' => ['required', Rule::in(array_keys(WooProduct::STATUSES))],
|
||||
@@ -141,25 +162,8 @@ class ProductController extends Controller
|
||||
'gallery_uploads.*' => ['image', 'max:10240'],
|
||||
'gallery_image_srcs' => ['nullable', 'array'],
|
||||
'gallery_image_srcs.*' => ['url', 'max:2000'],
|
||||
]), [
|
||||
'woo_store_id' => $store->id,
|
||||
]);
|
||||
}
|
||||
|
||||
private function authorizedStore(Request $request, int $storeId): WooStore
|
||||
{
|
||||
return WooStore::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->whereKey($storeId)
|
||||
->where('status', WooStore::STATUS_ACTIVE)
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
/** @return \Illuminate\Support\Collection<int, WooStore> */
|
||||
private function activeStores(Request $request)
|
||||
{
|
||||
return WooStore::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->where('status', WooStore::STATUS_ACTIVE)
|
||||
->orderBy('site_name')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,29 +3,51 @@
|
||||
namespace App\Http\Controllers\Woo;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Woo\Concerns\ResolvesWooContext;
|
||||
use App\Models\WooStore;
|
||||
use App\Services\Woo\SubscriptionService;
|
||||
use App\Support\CurrentWooStore;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class StoreController extends Controller
|
||||
{
|
||||
use ResolvesWooContext;
|
||||
|
||||
public function __construct(
|
||||
private SubscriptionService $subscriptions,
|
||||
private CurrentWooStore $currentStore,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$user = $this->accountUser($request);
|
||||
$stores = WooStore::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->where('user_id', $user->id)
|
||||
->latest('updated_at')
|
||||
->get();
|
||||
|
||||
return view('woo.stores.index', compact('stores'));
|
||||
return view('woo.stores.index', [
|
||||
'stores' => $stores,
|
||||
'currentStore' => $this->currentStore->resolve($user),
|
||||
'hasPaidPlan' => $this->subscriptions->hasPaidPlan($user),
|
||||
'storeCount' => $this->subscriptions->connectedStoreCount($user),
|
||||
'storeLimit' => $this->subscriptions->maxStores($user),
|
||||
'canConnectMore' => $this->subscriptions->canConnectStore($user),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, WooStore $store): RedirectResponse
|
||||
{
|
||||
abort_if($store->user_id !== $request->user()->id, 403);
|
||||
abort_if($store->user_id !== $this->accountUser($request)->id, 403);
|
||||
|
||||
$store->update(['status' => WooStore::STATUS_DISCONNECTED]);
|
||||
|
||||
if ((int) session(CurrentWooStore::SESSION_KEY) === $store->id) {
|
||||
session()->forget(CurrentWooStore::SESSION_KEY);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Store disconnected. Install the Ladill plugin again to reconnect.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Woo;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Woo\Concerns\ResolvesWooContext;
|
||||
use App\Support\CurrentWooStore;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class StoreSwitchController extends Controller
|
||||
{
|
||||
use ResolvesWooContext;
|
||||
|
||||
public function __invoke(Request $request, CurrentWooStore $stores): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'store' => ['required', 'integer'],
|
||||
]);
|
||||
|
||||
$stores->switch($this->accountUser($request), (int) $validated['store']);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user