13 KiB
13 KiB
Purchasing Products
Setup
First, add the HasShoppingCapabilities trait to your User model (or any model that should purchase products):
use Blax\Shop\Traits\HasShoppingCapabilities;
class User extends Authenticatable
{
use HasShoppingCapabilities;
}
Direct Purchase
Simple Purchase
$user = auth()->user();
$product = Product::find($productId);
try {
$purchase = $user->purchase($product, quantity: 1);
// Purchase successful
return response()->json([
'success' => true,
'purchase_id' => $purchase->id,
]);
} catch (\Exception $e) {
return response()->json([
'error' => $e->getMessage()
], 400);
}
Purchase with Options
$purchase = $user->purchase($product, quantity: 2, options: [
'price_id' => $priceId, // Use specific price
'charge_id' => $paymentId, // Associate with payment
'cart_id' => $cartId, // Associate with cart
'status' => 'pending', // Custom status
]);
Check Purchase History
// Check if user has purchased a product
if ($user->hasPurchased($product)) {
// User has purchased this product
}
// Get purchase history for a product
$history = $user->getPurchaseHistory($product);
// Get all completed purchases
$purchases = $user->completedPurchases()->get();
Shopping Cart
Add to Cart
$user = auth()->user();
$product = Product::find($productId);
try {
$cartItem = $user->addToCart($product, quantity: 1);
return response()->json([
'success' => true,
'cart_item' => $cartItem,
'cart_total' => $user->getCartTotal(),
'cart_count' => $user->getCartItemsCount(),
]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
Update Cart Quantity
$cartItem = ProductPurchase::find($cartItemId);
try {
$user->updateCartQuantity($cartItem, quantity: 3);
return response()->json([
'success' => true,
'cart_total' => $user->getCartTotal(),
]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
Remove from Cart
$cartItem = ProductPurchase::find($cartItemId);
$user->removeFromCart($cartItem);
Get Cart Information
// Get all cart items
$cartItems = $user->cartItems()->with('product')->get();
// Get cart total
$total = $user->getCartTotal();
// Get items count
$count = $user->getCartItemsCount();
// Clear cart
$user->clearCart();
Checkout
try {
$completedPurchases = $user->checkout(options: [
'charge_id' => $paymentIntent->id,
]);
return response()->json([
'success' => true,
'purchases' => $completedPurchases,
'total' => $completedPurchases->sum('amount'),
]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
Refunds
$purchase = ProductPurchase::find($purchaseId);
$user = $purchase->purchasable;
try {
$user->refundPurchase($purchase, options: [
'refund_id' => $refundId,
'reason' => 'Customer request',
]);
return response()->json(['success' => true]);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
Purchase Statistics
$stats = $user->getPurchaseStats();
// Returns:
// [
// 'total_purchases' => 10,
// 'total_spent' => 299.90,
// 'total_items' => 15,
// 'cart_items' => 2,
// 'cart_total' => 49.98,
// ]
Basic Purchase Flow
1. Check Product Availability
use Blax\Shop\Models\Product;
$product = Product::find($productId);
$quantity = 1;
// Check if product is available
if (!$product->isVisible()) {
return response()->json(['error' => 'Product not available'], 404);
}
// Check stock
if ($product->manage_stock) {
$available = $product->getAvailableStock();
if ($available < $quantity) {
return response()->json([
'error' => 'Insufficient stock',
'available' => $available
], 400);
}
}
2. Reserve Stock (Optional)
Reserve stock during checkout process:
// Reserve for 15 minutes
$reservation = $product->reserveStock(
quantity: $quantity,
reference: auth()->user(),
until: now()->addMinutes(15),
note: 'Checkout reservation'
);
if (!$reservation) {
return response()->json(['error' => 'Unable to reserve stock'], 400);
}
// Store reservation ID in session
session(['stock_reservation_id' => $reservation->id]);
3. Process Payment
// Your payment processing logic
$payment = PaymentService::process([
'amount' => $product->getCurrentPrice() * $quantity,
'currency' => 'USD',
'product_id' => $product->id,
]);
if ($payment->failed()) {
// Release reservation
$reservation->update(['status' => 'cancelled']);
return response()->json(['error' => 'Payment failed'], 400);
}
4. Complete Purchase
use Blax\Shop\Models\ProductPurchase;
// Decrease stock
$product->decreaseStock($quantity);
// Create purchase record
$purchase = ProductPurchase::create([
'product_id' => $product->id,
'purchasable_type' => get_class(auth()->user()),
'purchasable_id' => auth()->id(),
'quantity' => $quantity,
'status' => 'completed',
'meta' => [
'payment_id' => $payment->id,
'price_paid' => $product->getCurrentPrice(),
'currency' => 'USD',
],
]);
// Complete reservation
if ($reservation) {
$reservation->update(['status' => 'completed']);
}
// Trigger product actions
$product->callActions('purchased', $purchase, [
'user' => auth()->user(),
'payment' => $payment,
]);
return response()->json([
'success' => true,
'purchase_id' => $purchase->id,
]);
Shopping Cart Implementation
Cart Item Model
// app/Models/CartItem.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Blax\Shop\Models\Product;
class CartItem extends Model
{
protected $fillable = [
'cart_id',
'product_id',
'quantity',
'price',
];
protected $casts = [
'price' => 'decimal:2',
];
public function product()
{
return $this->belongsTo(Product::class);
}
public function getSubtotal()
{
return $this->price * $this->quantity;
}
}
Cart Service
// app/Services/CartService.php
namespace App\Services;
use App\Models\CartItem;
use Blax\Shop\Models\Product;
class CartService
{
public function add(Product $product, int $quantity = 1)
{
$cart = $this->getCart();
// Check stock
if ($product->manage_stock && $product->getAvailableStock() < $quantity) {
throw new \Exception('Insufficient stock');
}
// Check if item already in cart
$cartItem = $cart->items()->where('product_id', $product->id)->first();
if ($cartItem) {
$newQuantity = $cartItem->quantity + $quantity;
// Check stock for new quantity
if ($product->manage_stock && $product->getAvailableStock() < $newQuantity) {
throw new \Exception('Insufficient stock for requested quantity');
}
$cartItem->update(['quantity' => $newQuantity]);
} else {
$cartItem = $cart->items()->create([
'product_id' => $product->id,
'quantity' => $quantity,
'price' => $product->getCurrentPrice(),
]);
}
return $cartItem;
}
public function update(CartItem $cartItem, int $quantity)
{
$product = $cartItem->product;
// Check stock
if ($product->manage_stock && $product->getAvailableStock() < $quantity) {
throw new \Exception('Insufficient stock');
}
$cartItem->update(['quantity' => $quantity]);
return $cartItem;
}
public function remove(CartItem $cartItem)
{
$cartItem->delete();
}
public function clear()
{
$cart = $this->getCart();
$cart->items()->delete();
}
public function getTotal()
{
$cart = $this->getCart();
return $cart->items->sum(fn($item) => $item->getSubtotal());
}
public function checkout()
{
$cart = $this->getCart();
$items = $cart->items()->with('product')->get();
// Reserve stock for all items
$reservations = [];
foreach ($items as $item) {
$reservation = $item->product->reserveStock(
$item->quantity,
$cart,
now()->addMinutes(15)
);
if (!$reservation) {
// Rollback previous reservations
foreach ($reservations as $res) {
$res->update(['status' => 'cancelled']);
}
throw new \Exception('Unable to reserve stock for: ' . $item->product->getLocalized('name'));
}
$reservations[] = $reservation;
}
return [
'items' => $items,
'reservations' => $reservations,
'total' => $this->getTotal(),
];
}
protected function getCart()
{
// Implementation depends on your cart system
// Could be session-based or user-based
return auth()->user()->cart ?? session()->get('cart');
}
}
Cart Controller
// app/Http/Controllers/CartController.php
namespace App\Http\Controllers;
use App\Services\CartService;
use Blax\Shop\Models\Product;
use Illuminate\Http\Request;
class CartController extends Controller
{
public function __construct(
protected CartService $cartService
) {}
public function add(Request $request, Product $product)
{
$validated = $request->validate([
'quantity' => 'required|integer|min:1',
]);
try {
$cartItem = $this->cartService->add($product, $validated['quantity']);
return response()->json([
'success' => true,
'cart_item' => $cartItem,
'cart_total' => $this->cartService->getTotal(),
]);
} catch (\Exception $e) {
return response()->json([
'error' => $e->getMessage()
], 400);
}
}
public function update(Request $request, $cartItemId)
{
$validated = $request->validate([
'quantity' => 'required|integer|min:1',
]);
$cartItem = CartItem::findOrFail($cartItemId);
try {
$this->cartService->update($cartItem, $validated['quantity']);
return response()->json([
'success' => true,
'cart_total' => $this->cartService->getTotal(),
]);
} catch (\Exception $e) {
return response()->json([
'error' => $e->getMessage()
], 400);
}
}
public function remove($cartItemId)
{
$cartItem = CartItem::findOrFail($cartItemId);
$this->cartService->remove($cartItem);
return response()->json([
'success' => true,
'cart_total' => $this->cartService->getTotal(),
]);
}
public function checkout()
{
try {
$checkoutData = $this->cartService->checkout();
return response()->json([
'success' => true,
'checkout' => $checkoutData,
]);
} catch (\Exception $e) {
return response()->json([
'error' => $e->getMessage()
], 400);
}
}
}
Handling Refunds
public function refund($purchaseId)
{
$purchase = ProductPurchase::findOrFail($purchaseId);
$product = $purchase->product;
// Process refund with payment processor
$refund = PaymentService::refund($purchase->meta['payment_id']);
if ($refund->success) {
// Return stock
$product->increaseStock($purchase->quantity);
// Update purchase status
$purchase->update([
'status' => 'refunded',
'meta' => array_merge($purchase->meta, [
'refund_id' => $refund->id,
'refunded_at' => now(),
]),
]);
// Trigger refund actions
$product->callActions('refunded', $purchase, [
'refund' => $refund,
]);
return response()->json(['success' => true]);
}
return response()->json(['error' => 'Refund failed'], 400);
}
Product Actions on Purchase
Product actions allow you to execute custom logic when products are purchased:
use Blax\Shop\Models\ProductAction;
// Create action to grant access to a course
ProductAction::create([
'product_id' => $product->id,
'action_type' => 'grant_access',
'event' => 'purchased',
'config' => [
'resource_type' => 'course',
'resource_id' => 123,
],
'active' => true,
]);
// Action is automatically triggered when product is purchased
// Implement the action handler in your application
See Product Actions documentation for more details.