laravel-shop/docs/03-purchasing.md

737 lines
17 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 {
// For regular products
$cartItem = $user->addToCart($product, quantity: 1);
// For booking products (requires dates)
$from = Carbon::parse('2025-01-15');
$until = Carbon::parse('2025-01-20');
$cartItem = $user->addToCart($product, quantity: 1, parameters: [
'from' => $from,
'until' => $until,
]);
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
### Checkout Cart
```php
try {
// Get current cart
$cart = $user->currentCart();
// Checkout (creates purchases and order)
$cart->checkout();
// Access the order
$order = $cart->order;
return response()->json([
'success' => true,
'order' => $order,
'order_number' => $order->order_number,
]);
} catch (\Exception $e) {
return response()->json([
'error' => $e->getMessage()
], 400);
}
```
### What Happens During Checkout
1. **Validates Cart**
- Checks that cart is not empty
- Validates all items have required information
- For booking products: validates dates are set
2. **Claims Stock**
- Claims stock for booking/pool products
- Validates stock availability
3. **Creates Order**
- Generates order number
- Creates Order record linked to cart
- Copies cart total to order amounts
4. **Creates Purchases**
- Creates ProductPurchase records for each cart item
- Links purchases to order
5. **Converts Cart**
- Marks cart as CONVERTED
- Sets `converted_at` timestamp
### Important Notes
- Stock is claimed at checkout time (not add-to-cart time for bookings)
- Cart items remain in database but are marked as converted
- Order is created with PENDING status by default
## Purchase History
### 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();
```
## Order Management
### Get All Orders
```php
// Get all orders
$orders = $user->orders()->get();
// Get orders with specific status
use Blax\Shop\Enums\OrderStatus;
$pendingOrders = $user->pendingOrders()->get();
$processingOrders = $user->processingOrders()->get();
$completedOrders = $user->completedOrders()->get();
// Get active orders (not completed/cancelled/refunded)
$activeOrders = $user->activeOrders()->get();
```
### Order Status Flow
Orders progress through these statuses:
1. **PENDING** - Order received but awaiting payment confirmation
2. **PROCESSING** - Payment received and order is being processed
3. **ON_HOLD** - Order on hold, awaiting further action
4. **IN_PREPARATION** - Order being prepared (packing, manufacturing)
5. **READY_FOR_PICKUP** - Order ready for pickup (for local pickup orders)
6. **SHIPPED** - Order has been shipped and is in transit
7. **DELIVERED** - Order delivered to customer
8. **COMPLETED** - Order complete, all actions fulfilled
9. **CANCELLED** - Order was cancelled
10. **REFUNDED** - Order was refunded
11. **FAILED** - Payment or processing failed
### Get Order by Number
```php
$order = $user->findOrderByNumber('ORD-2025-0001');
if ($order) {
echo "Order found: {$order->order_number}";
}
```
### Order Details
```php
$order = Order::find($orderId);
// Order properties
$order->order_number; // Unique order number
$order->status; // OrderStatus enum
$order->amount_total; // Total amount (in cents)
$order->amount_paid; // Amount paid (in cents)
$order->amount_subtotal; // Subtotal before tax/shipping
$order->amount_tax; // Tax amount
$order->amount_shipping; // Shipping cost
$order->amount_discount; // Discount applied
$order->amount_refunded; // Amount refunded
// Dates
$order->created_at; // When order was created
$order->paid_at; // When payment was received
$order->shipped_at; // When order was shipped
$order->delivered_at; // When order was delivered
$order->completed_at; // When order was completed
$order->cancelled_at; // When order was cancelled
$order->refunded_at; // When order was refunded
// Additional info
$order->payment_method; // Payment method used
$order->payment_provider; // Payment provider (e.g., 'stripe')
$order->payment_reference; // Provider reference ID
$order->billing_address; // Billing address object
$order->shipping_address; // Shipping address object
$order->customer_note; // Customer's note
$order->internal_note; // Internal staff note
```
### Order Relationships
```php
// Get order customer
$customer = $order->customer;
// Get order purchases (line items)
$purchases = $order->purchases()->get();
// Get original cart
$cart = $order->cart;
// Get order notes
$notes = $order->notes()->get();
```
### Order Statistics
```php
// Total spent across all orders
$totalSpent = $user->total_spent; // Accessor in cents
// Number of orders
$orderCount = $user->order_count;
// Number of completed orders
$completedCount = $user->completed_order_count;
// Check if user has any orders
if ($user->hasOrders()) {
echo "Customer has placed orders";
}
// Check if user has active orders
if ($user->hasActiveOrders()) {
echo "Customer has orders in progress";
}
// Get latest order
$latestOrder = $user->latestOrder();
```
### Filter Orders by Date
```php
$from = Carbon::parse('2025-01-01');
$to = Carbon::parse('2025-12-31');
$ordersThisYear = $user->ordersBetween($from, $to)->get();
```
### Order Payment Status
```php
// Check if order is paid
if ($order->is_paid) {
echo "Order has been paid";
}
// Check if fully paid
if ($order->is_fully_paid) {
echo "Order is fully paid";
}
// Get outstanding amount
$outstanding = $order->amount_outstanding; // In cents
```
### Update Order Status
```php
use Blax\Shop\Enums\OrderStatus;
// Update order status
$order->update(['status' => OrderStatus::PROCESSING]);
// Mark as shipped
$order->update([
'status' => OrderStatus::SHIPPED,
'shipped_at' => now(),
]);
// Mark as delivered
$order->update([
'status' => OrderStatus::DELIVERED,
'delivered_at' => now(),
]);
// Mark as completed
$order->update([
'status' => OrderStatus::COMPLETED,
'completed_at' => now(),
]);
```
### Add Order Notes
```php
use Blax\Shop\Models\OrderNote;
// Add customer-visible note
OrderNote::create([
'order_id' => $order->id,
'content' => 'Your order has been shipped!',
'is_customer_note' => true,
]);
// Add internal note
OrderNote::create([
'order_id' => $order->id,
'content' => 'Customer requested gift wrapping',
'is_customer_note' => false,
]);
// Get all notes
$allNotes = $order->notes()->get();
// Get customer-visible notes only
$customerNotes = $order->notes()->where('is_customer_note', true)->get();
```
## Refunds
### Refund an Order
```php
use Blax\Shop\Enums\OrderStatus;
$order = Order::find($orderId);
// Mark order as refunded
$order->update([
'status' => OrderStatus::REFUNDED,
'refunded_at' => now(),
'amount_refunded' => $order->amount_total,
]);
// Stock will be released back from associated purchases
```
## 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 order (if converted)
$order = $cart->order;
// 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; // pending, unpaid, completed, refunded, failed
$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 (in cents)
$purchase->amount_paid; // Amount paid (in cents)
$purchase->charge_id; // Payment charge ID
$purchase->from; // Booking start date (for bookings)
$purchase->until; // Booking end date (for bookings)
$purchase->meta; // Additional metadata
```
### Purchase Relationships
```php
// Get purchased product
$product = $purchase->purchasable;
// Get purchaser (user)
$user = $purchase->purchaser;
// Get associated cart item
$cartItem = $purchase->cartItem;
// Get associated order
$order = $purchase->order;
```
### Purchase Scopes
```php
use Blax\Shop\Enums\PurchaseStatus;
// Get completed purchases
$completed = ProductPurchase::where('status', PurchaseStatus::COMPLETED)->get();
// Get pending purchases
$pending = ProductPurchase::where('status', PurchaseStatus::PENDING)->get();
```
## Stock Claims
When adding booking products to cart, stock is claimed at checkout time:
```php
// For booking products, stock is NOT claimed when adding to cart
$cartItem = $user->addToCart($bookingProduct, quantity: 1, parameters: [
'from' => Carbon::parse('2025-01-15'),
'until' => Carbon::parse('2025-01-20'),
]);
// Stock is validated and claimed during checkout
$cart = $user->currentCart();
$cart->checkout(); // Claims stock at this point
// For regular products, stock is claimed immediately when adding to cart
$cartItem = $user->addToCart($regularProduct, quantity: 2);
// Stock is claimed immediately for non-booking products
```
## 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 {
$cart = $user->currentCart();
$cart->checkout();
// Access the created order
$order = $cart->order;
return redirect()->route('orders.success', ['order' => $order->id])
->with('success', "Order {$order->order_number} placed successfully!");
} catch (\Exception $e) {
return redirect()->back()->with('error', $e->getMessage());
}
});
// Order history
Route::get('/orders', function () {
$user = auth()->user();
$orders = $user->orders()
->with(['purchases.purchasable'])
->orderBy('created_at', 'desc')
->get();
return view('orders.index', compact('orders'));
});
// View specific order
Route::get('/orders/{order}', function (Order $order) {
$user = auth()->user();
// Ensure user owns this order
if ($order->customer_id !== $user->id) {
abort(403);
}
$order->load(['purchases.purchasable', 'notes']);
return view('orders.show', compact('order'));
});
```