583 lines
13 KiB
Markdown
583 lines
13 KiB
Markdown
# Purchasing Products
|
|
|
|
## Setup
|
|
|
|
First, add the `HasShoppingCapabilities` trait to your User model (or any model that should purchase products):
|
|
|
|
```php
|
|
use Blax\Shop\Traits\HasShoppingCapabilities;
|
|
|
|
class User extends Authenticatable
|
|
{
|
|
use HasShoppingCapabilities;
|
|
}
|
|
```
|
|
|
|
## Direct Purchase
|
|
|
|
### Simple Purchase
|
|
|
|
```php
|
|
$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
|
|
|
|
```php
|
|
$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
|
|
|
|
```php
|
|
// 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
|
|
|
|
```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);
|
|
}
|
|
```
|
|
|
|
### Update Cart Quantity
|
|
|
|
```php
|
|
$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
|
|
|
|
```php
|
|
$cartItem = ProductPurchase::find($cartItemId);
|
|
|
|
$user->removeFromCart($cartItem);
|
|
```
|
|
|
|
### Get Cart Information
|
|
|
|
```php
|
|
// 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
|
|
|
|
```php
|
|
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
|
|
|
|
```php
|
|
$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
|
|
|
|
```php
|
|
$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
|
|
|
|
```php
|
|
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:
|
|
|
|
```php
|
|
// 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
|
|
|
|
```php
|
|
// 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
|
|
|
|
```php
|
|
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
|
|
|
|
```php
|
|
// 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
|
|
|
|
```php
|
|
// 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
|
|
|
|
```php
|
|
// 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
|
|
|
|
```php
|
|
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:
|
|
|
|
```php
|
|
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](docs/07-product-actions.md) for more details.
|