diff --git a/src/Http/Controllers/Concerns/HandlesLoans.php b/src/Http/Controllers/Concerns/HandlesLoans.php deleted file mode 100644 index 22d8335..0000000 --- a/src/Http/Controllers/Concerns/HandlesLoans.php +++ /dev/null @@ -1,190 +0,0 @@ -user(); - - $query = ProductPurchase::query() - ->where('purchaser_type', $user::class) - ->where('purchaser_id', $user->getKey()) - ->where('purchasable_type', $this->loanableModel()) - ->with('purchasable') - ->orderByDesc('from'); - - match ($request->query('status')) { - 'active' => $query->activeLoans(), - 'returned' => $query->returned(), - 'overdue' => $query->overdue(), - default => null, - }; - - $perPage = min(max((int) $request->query('per_page', 25), 1), 100); - - return response()->json(MiscService::apiPaginated( - $query->paginate($perPage), - $this->loanResource(), - )); - } - - public function store(Request $request): JsonResponse - { - // Resolve the request class lazily so subclass overrides win. - /** @var StoreLoanRequest $validated */ - $validated = app($this->storeRequest()); - $validated->setContainer(app())->setRedirector(app('redirect')); - $validated->initialize( - $request->query(), - $request->request->all(), - $request->attributes->all(), - $request->cookies->all(), - $request->files->all(), - $request->server->all(), - $request->getContent(), - ); - $validated->setUserResolver($request->getUserResolver()); - $validated->validateResolved(); - - $model = $this->loanableModel(); - /** @var \Blax\Shop\Models\Product $item */ - $item = $model::query()->findOrFail($validated->loanableId()); - - try { - $purchase = $item->checkOutTo($request->user()); - } catch (NotEnoughStockException) { - throw ValidationException::withMessages([ - $validated->validationKey() => ['No copies of this item are currently available.'], - ]); - } - - return response()->json( - MiscService::apiItem($purchase->load('purchasable'), $this->loanResource()), - Response::HTTP_CREATED, - ); - } - - public function show(Request $request, ProductPurchase $purchase): JsonResponse - { - $this->ensureLoanOwner($request, $purchase); - - return response()->json(MiscService::apiItem( - $purchase->load('purchasable'), - $this->loanResource(), - )); - } - - public function extend(Request $request, ProductPurchase $purchase): JsonResponse - { - $this->ensureLoanOwner($request, $purchase); - - if ($purchase->isReturned()) { - throw ValidationException::withMessages([ - 'loan' => ['This loan has already been returned.'], - ]); - } - - if (! $purchase->canExtend()) { - $message = $purchase->isOverdue() - ? 'Overdue loans cannot be extended — please return the item first.' - : 'This loan has reached the maximum number of extensions.'; - - throw ValidationException::withMessages(['loan' => [$message]]); - } - - $purchase->extend(); - - return response()->json(MiscService::apiItem( - $purchase->load('purchasable'), - $this->loanResource(), - )); - } - - public function returnLoan(Request $request, ProductPurchase $purchase): JsonResponse - { - $this->ensureLoanOwner($request, $purchase); - - if ($purchase->isReturned()) { - throw ValidationException::withMessages([ - 'loan' => ['This loan has already been returned.'], - ]); - } - - $purchase->markReturned(); - - $purchase->load('purchasable'); - if ($purchase->purchasable !== null && method_exists($purchase->purchasable, 'increaseStock')) { - $purchase->purchasable->increaseStock(1); - } - - return response()->json(MiscService::apiItem($purchase, $this->loanResource())); - } - - /** - * 403 unless the authenticated user is the loan's purchaser. - */ - protected function ensureLoanOwner(Request $request, ProductPurchase $purchase): void - { - $user = $request->user(); - - $isOwner = $purchase->purchaser_type === $user::class - && (string) $purchase->purchaser_id === (string) $user->getKey(); - - if (! $isOwner) { - abort(Response::HTTP_FORBIDDEN, 'This loan does not belong to you.'); - } - } -} diff --git a/src/Http/Requests/StoreLoanRequest.php b/src/Http/Requests/StoreLoanRequest.php deleted file mode 100644 index 3ad2df0..0000000 --- a/src/Http/Requests/StoreLoanRequest.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ - public function rules(): array - { - return [ - 'loanable_id' => [ - 'required', - 'uuid', - 'exists:'.config('shop.tables.products', 'products').',id', - ], - ]; - } - - /** - * The validated id of the model to be checked out. Subclasses that - * rename the field can override this to keep HandlesLoans agnostic. - */ - public function loanableId(): string - { - return $this->validated($this->validationKey()); - } - - /** - * The body / validation key used in `rules()`. HandlesLoans uses this - * to attach the "no copies available" error to the correct field, so - * subclasses that rename `loanable_id` should override here too. - */ - public function validationKey(): string - { - return 'loanable_id'; - } -} diff --git a/src/ShopServiceProvider.php b/src/ShopServiceProvider.php index d147eb0..3ee2559 100644 --- a/src/ShopServiceProvider.php +++ b/src/ShopServiceProvider.php @@ -93,13 +93,16 @@ class ShopServiceProvider extends ServiceProvider * concisely. Currently provides: * * Route::shopLoans('loans', \App\Http\Controllers\LoanController::class) - * → POST {prefix} index of caller's loans + * → GET {prefix} index of caller's loans * → POST {prefix} check out a new loan * → GET {prefix}/{purchase} show a single loan * → POST {prefix}/{purchase}/extend extend the due date * → POST {prefix}/{purchase}/return return the item * - * The controller is expected to use {@see \Blax\Shop\Http\Controllers\Concerns\HandlesLoans}. + * The controller must expose matching methods: index, store, show, + * extend, returnLoan. The {purchase} route parameter is passed as a + * raw UUID string — controllers typically resolve it through the + * authenticated user's loans relation so ownership falls out for free. */ protected function registerRouteMacros(): void {