515 lines
11 KiB
Markdown
515 lines
11 KiB
Markdown
# Purchasing & Shopping Cart
|
|
|
|
## Setup
|
|
|
|
Add the `HasShoppingCapabilities` trait to your User model (or any model that should be able to purchase products):
|
|
|
|
```php
|
|
use Blax\Shop\Traits\HasShoppingCapabilities;
|
|
|
|
class User extends Authenticatable
|
|
{
|
|
use HasShoppingCapabilities;
|
|
}
|
|
```
|
|
|
|
This trait provides methods for:
|
|
- Direct product purchases
|
|
- Shopping cart management
|
|
- Purchase history
|
|
- Cart checkout
|
|
|
|
## Direct Purchase
|
|
|
|
### Purchase a Product
|
|
|
|
```php
|
|
$user = auth()->user();
|
|
$product = Product::find($productId);
|
|
|
|
// Product must have a default price
|
|
try {
|
|
$purchase = $user->purchase($product);
|
|
|
|
// Purchase successful
|
|
return response()->json([
|
|
'success' => true,
|
|
'purchase_id' => $purchase->id,
|
|
'amount' => $purchase->amount,
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'error' => $e->getMessage()
|
|
], 400);
|
|
}
|
|
```
|
|
|
|
### Purchase with Specific Price
|
|
|
|
```php
|
|
$price = ProductPrice::find($priceId);
|
|
|
|
$purchase = $user->purchase(
|
|
$price,
|
|
quantity: 2
|
|
);
|
|
```
|
|
|
|
### Purchase with Metadata
|
|
|
|
```php
|
|
$purchase = $user->purchase(
|
|
$product,
|
|
quantity: 1,
|
|
meta: [
|
|
'gift' => true,
|
|
'message' => 'Happy Birthday!',
|
|
'gift_recipient' => 'john@example.com',
|
|
]
|
|
);
|
|
```
|
|
|
|
### Important Notes
|
|
|
|
- Product must have at least one default price
|
|
- Product must not have multiple default prices (will throw `MultiplePurchaseOptions` exception)
|
|
- If stock management is enabled, sufficient stock must be available
|
|
- Product must be visible (published, visible flag, and published_at date)
|
|
- Purchase automatically decreases stock if `manage_stock` is enabled
|
|
- Product actions are automatically triggered on purchase
|
|
|
|
## Shopping Cart
|
|
|
|
### Add to Cart
|
|
|
|
```php
|
|
$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);
|
|
}
|
|
```
|
|
|
|
### Add with Parameters
|
|
|
|
```php
|
|
$cartItem = $user->addToCart(
|
|
$product,
|
|
quantity: 2,
|
|
parameters: [
|
|
'color' => 'blue',
|
|
'size' => 'large',
|
|
]
|
|
);
|
|
```
|
|
|
|
### Get Cart Items
|
|
|
|
```php
|
|
$cartItems = $user->cartItems()->get();
|
|
|
|
foreach ($cartItems as $item) {
|
|
echo $item->purchasable->getLocalized('name');
|
|
echo $item->quantity;
|
|
echo $item->price;
|
|
echo $item->subtotal;
|
|
}
|
|
```
|
|
|
|
### Update Cart Item Quantity
|
|
|
|
```php
|
|
$cartItem = CartItem::find($cartItemId);
|
|
|
|
try {
|
|
$updatedItem = $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
|
|
|
|
```php
|
|
$cartItem = CartItem::find($cartItemId);
|
|
|
|
$user->removeFromCart($cartItem);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'cart_total' => $user->getCartTotal(),
|
|
'cart_count' => $user->getCartItemsCount(),
|
|
]);
|
|
```
|
|
|
|
### Clear Cart
|
|
|
|
```php
|
|
$count = $user->clearCart();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'removed_items' => $count,
|
|
]);
|
|
```
|
|
|
|
### Get Cart Totals
|
|
|
|
```php
|
|
// Get cart total
|
|
$total = $user->getCartTotal();
|
|
|
|
// Get cart items count
|
|
$count = $user->getCartItemsCount();
|
|
|
|
// Get cart stats
|
|
$stats = [
|
|
'total' => $user->getCartTotal(),
|
|
'count' => $user->getCartItemsCount(),
|
|
'items' => $user->cartItems()->with('purchasable')->get(),
|
|
];
|
|
```
|
|
|
|
## Cart Checkout
|
|
|
|
### Convert Cart to Purchases
|
|
|
|
```php
|
|
try {
|
|
$purchases = $user->checkout();
|
|
|
|
// Checkout successful
|
|
// Cart items are now converted to completed purchases
|
|
// Cart is marked as converted
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'purchases' => $purchases,
|
|
'total_items' => $purchases->count(),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'error' => $e->getMessage()
|
|
], 400);
|
|
}
|
|
```
|
|
|
|
### Important Notes
|
|
|
|
- Checkout validates stock availability for all items
|
|
- Creates `ProductPurchase` records for each cart item
|
|
- Decreases stock for each item
|
|
- Triggers product actions
|
|
- Marks cart as converted (`converted_at` timestamp)
|
|
- Removes cart items after successful checkout
|
|
|
|
## Purchase History
|
|
|
|
### Check if User Purchased Product
|
|
|
|
```php
|
|
$product = Product::find($productId);
|
|
|
|
if ($user->hasPurchased($product)) {
|
|
// User has purchased this product
|
|
echo "You own this product!";
|
|
}
|
|
```
|
|
|
|
### Get All Purchases
|
|
|
|
```php
|
|
// Get all purchases (any status)
|
|
$allPurchases = $user->purchases()->get();
|
|
|
|
// Get only completed purchases
|
|
$completedPurchases = $user->completedPurchases()->get();
|
|
|
|
// Get purchases for specific product
|
|
$productPurchases = $user->purchases()
|
|
->where('purchasable_id', $product->id)
|
|
->where('purchasable_type', Product::class)
|
|
->get();
|
|
```
|
|
|
|
### Purchase Statistics
|
|
|
|
```php
|
|
$stats = $user->getPurchaseStats();
|
|
|
|
// Returns:
|
|
// [
|
|
// 'total_purchases' => 15,
|
|
// 'total_spent' => 450.00,
|
|
// 'total_items' => 23,
|
|
// 'cart_items' => 2,
|
|
// 'cart_total' => 89.99,
|
|
// ]
|
|
```
|
|
|
|
## Refunds
|
|
|
|
### Refund a Purchase
|
|
|
|
```php
|
|
$purchase = ProductPurchase::find($purchaseId);
|
|
|
|
try {
|
|
$success = $user->refundPurchase($purchase);
|
|
|
|
if ($success) {
|
|
// Refund successful
|
|
// Stock has been returned
|
|
// Purchase status changed to 'refunded'
|
|
// Product 'refunded' actions triggered
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Purchase refunded successfully',
|
|
]);
|
|
}
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'error' => $e->getMessage()
|
|
], 400);
|
|
}
|
|
```
|
|
|
|
### Important Notes
|
|
|
|
- Only completed purchases can be refunded
|
|
- Stock is automatically returned to inventory
|
|
- Product actions with event 'refunded' are triggered
|
|
|
|
## Cart Model
|
|
|
|
### Get Current Cart
|
|
|
|
```php
|
|
// Get or create current active cart
|
|
$cart = $user->currentCart();
|
|
|
|
// Cart properties
|
|
$cart->session_id; // Session ID for guest carts
|
|
$cart->customer_id; // User ID
|
|
$cart->customer_type; // User model class
|
|
$cart->currency; // Cart currency (default: USD)
|
|
$cart->status; // active, abandoned, converted, expired
|
|
$cart->converted_at; // When cart was checked out
|
|
$cart->expires_at; // Cart expiration date
|
|
$cart->last_activity_at; // Last activity timestamp
|
|
```
|
|
|
|
### Cart Relationships
|
|
|
|
```php
|
|
// Get cart items
|
|
$items = $cart->items()->get();
|
|
|
|
// Get cart purchases (if converted)
|
|
$purchases = $cart->purchases()->get();
|
|
|
|
// Get cart customer (user)
|
|
$customer = $cart->customer;
|
|
```
|
|
|
|
### Cart Methods
|
|
|
|
```php
|
|
// Get cart total
|
|
$total = $cart->getTotal();
|
|
|
|
// Get total items
|
|
$itemCount = $cart->getTotalItems();
|
|
|
|
// Check if cart is expired
|
|
if ($cart->isExpired()) {
|
|
// Cart has expired
|
|
}
|
|
|
|
// Check if cart is converted
|
|
if ($cart->isConverted()) {
|
|
// Cart has been checked out
|
|
}
|
|
```
|
|
|
|
### Add Items to Cart Directly
|
|
|
|
```php
|
|
use Blax\Shop\Models\Cart;
|
|
|
|
$cart = Cart::find($cartId);
|
|
|
|
$cartItem = $cart->addToCart(
|
|
$product, // or $productPrice
|
|
quantity: 2,
|
|
parameters: ['size' => 'L']
|
|
);
|
|
```
|
|
|
|
## Product Purchase Model
|
|
|
|
### Purchase Properties
|
|
|
|
```php
|
|
$purchase = ProductPurchase::find($purchaseId);
|
|
|
|
$purchase->status; // cart, pending, unpaid, completed, refunded
|
|
$purchase->cart_id; // Associated cart ID
|
|
$purchase->price_id; // Associated price ID
|
|
$purchase->purchasable_id; // Product ID
|
|
$purchase->purchasable_type; // Product class
|
|
$purchase->purchaser_id; // User ID
|
|
$purchase->purchaser_type; // User class
|
|
$purchase->quantity; // Quantity purchased
|
|
$purchase->amount; // Total amount
|
|
$purchase->amount_paid; // Amount paid
|
|
$purchase->charge_id; // Payment charge ID
|
|
$purchase->meta; // Additional metadata
|
|
```
|
|
|
|
### Purchase Relationships
|
|
|
|
```php
|
|
// Get purchased product
|
|
$product = $purchase->purchasable;
|
|
|
|
// Get purchaser (user)
|
|
$user = $purchase->purchaser;
|
|
```
|
|
|
|
### Purchase Scopes
|
|
|
|
```php
|
|
// Get purchases in cart
|
|
$cartPurchases = ProductPurchase::inCart()->get();
|
|
|
|
// Get completed purchases
|
|
$completed = ProductPurchase::completed()->get();
|
|
|
|
// Get purchases from specific cart
|
|
$cartPurchases = ProductPurchase::fromCart($cartId)->get();
|
|
```
|
|
|
|
## Stock Reservations
|
|
|
|
When adding products to cart, stock is automatically reserved:
|
|
|
|
```php
|
|
// Stock is reserved when adding to cart
|
|
$cartItem = $user->addToCart($product, quantity: 2);
|
|
|
|
// Reservation is created automatically
|
|
// It expires after configured time (default: 15 minutes)
|
|
// Stock is released back when:
|
|
// - Reservation expires
|
|
// - Cart item is removed
|
|
// - Cart is abandoned
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Common Exceptions
|
|
|
|
```php
|
|
use Blax\Shop\Exceptions\NotPurchasable;
|
|
use Blax\Shop\Exceptions\MultiplePurchaseOptions;
|
|
use Blax\Shop\Exceptions\NotEnoughStockException;
|
|
|
|
try {
|
|
$purchase = $user->purchase($product);
|
|
} catch (NotPurchasable $e) {
|
|
// Product has no default price
|
|
} catch (MultiplePurchaseOptions $e) {
|
|
// Product has multiple default prices - need to specify which one
|
|
$price = $product->prices()->where('currency', 'USD')->first();
|
|
$purchase = $user->purchase($price);
|
|
} catch (NotEnoughStockException $e) {
|
|
// Insufficient stock available
|
|
$available = $product->getAvailableStock();
|
|
echo "Only {$available} items available";
|
|
} catch (\Exception $e) {
|
|
// General error
|
|
echo $e->getMessage();
|
|
}
|
|
```
|
|
|
|
## Complete Example
|
|
|
|
```php
|
|
// Product listing
|
|
Route::get('/products', function () {
|
|
$products = Product::visible()
|
|
->inStock()
|
|
->with(['prices' => fn($q) => $q->where('is_default', true)])
|
|
->get();
|
|
|
|
return view('products.index', compact('products'));
|
|
});
|
|
|
|
// Add to cart
|
|
Route::post('/cart/add/{product}', function (Product $product) {
|
|
$user = auth()->user();
|
|
|
|
try {
|
|
$cartItem = $user->addToCart($product, quantity: 1);
|
|
|
|
return redirect()->back()->with('success', 'Product added to cart!');
|
|
} catch (\Exception $e) {
|
|
return redirect()->back()->with('error', $e->getMessage());
|
|
}
|
|
});
|
|
|
|
// View cart
|
|
Route::get('/cart', function () {
|
|
$user = auth()->user();
|
|
|
|
$cartItems = $user->cartItems()->with('purchasable')->get();
|
|
$cartTotal = $user->getCartTotal();
|
|
$cartCount = $user->getCartItemsCount();
|
|
|
|
return view('cart.index', compact('cartItems', 'cartTotal', 'cartCount'));
|
|
});
|
|
|
|
// Checkout
|
|
Route::post('/checkout', function () {
|
|
$user = auth()->user();
|
|
|
|
try {
|
|
$purchases = $user->checkout();
|
|
|
|
return redirect()->route('orders.success')
|
|
->with('success', 'Order placed successfully!');
|
|
} catch (\Exception $e) {
|
|
return redirect()->back()->with('error', $e->getMessage());
|
|
}
|
|
});
|
|
|
|
// Order history
|
|
Route::get('/orders', function () {
|
|
$user = auth()->user();
|
|
|
|
$purchases = $user->completedPurchases()
|
|
->with('purchasable')
|
|
->orderBy('created_at', 'desc')
|
|
->get();
|
|
|
|
return view('orders.index', compact('purchases'));
|
|
});
|
|
```
|