A traits, concerns, services
This commit is contained in:
parent
82ee18b0f1
commit
856686e292
|
|
@ -21,7 +21,8 @@
|
||||||
"php": ">=8.0",
|
"php": ">=8.0",
|
||||||
"illuminate/support": "^10.0|^11.0|^12.0",
|
"illuminate/support": "^10.0|^11.0|^12.0",
|
||||||
"illuminate/database": "^10.0|^11.0|^12.0",
|
"illuminate/database": "^10.0|^11.0|^12.0",
|
||||||
"blax-software/laravel-workkit": "dev-master|*"
|
"blax-software/laravel-workkit": "dev-master|*",
|
||||||
|
"stripe/stripe-php": "^19.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"orchestra/testbench": "^8.0|^9.0",
|
"orchestra/testbench": "^8.0|^9.0",
|
||||||
|
|
|
||||||
|
|
@ -62,12 +62,6 @@ return [
|
||||||
'prefix' => 'shop:',
|
'prefix' => 'shop:',
|
||||||
],
|
],
|
||||||
|
|
||||||
// Pagination
|
|
||||||
'pagination' => [
|
|
||||||
'per_page' => 20,
|
|
||||||
'max_per_page' => 100,
|
|
||||||
],
|
|
||||||
|
|
||||||
// Cart configuration
|
// Cart configuration
|
||||||
'cart' => [
|
'cart' => [
|
||||||
'expire_after_days' => 30,
|
'expire_after_days' => 30,
|
||||||
|
|
@ -81,4 +75,5 @@ return [
|
||||||
'wrap_response' => true,
|
'wrap_response' => true,
|
||||||
'response_key' => 'data',
|
'response_key' => 'data',
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -38,14 +38,14 @@ Update `config/shop.php`:
|
||||||
### Sync Individual Product
|
### Sync Individual Product
|
||||||
|
|
||||||
```php
|
```php
|
||||||
use Blax\Shop\Services\ShopStripeService;
|
use Blax\Shop\Services\StripeService;
|
||||||
use Stripe\Product as StripeProduct;
|
use Stripe\Product as StripeProduct;
|
||||||
|
|
||||||
// Get Stripe product
|
// Get Stripe product
|
||||||
$stripeProduct = StripeProduct::retrieve('prod_xxxxx');
|
$stripeProduct = StripeProduct::retrieve('prod_xxxxx');
|
||||||
|
|
||||||
// Sync to local database
|
// Sync to local database
|
||||||
$product = ShopStripeService::syncProductDown($stripeProduct);
|
$product = StripeService::syncProductDown($stripeProduct);
|
||||||
|
|
||||||
// This creates/updates a Product with:
|
// This creates/updates a Product with:
|
||||||
// - stripe_product_id
|
// - stripe_product_id
|
||||||
|
|
@ -61,7 +61,7 @@ $product = ShopStripeService::syncProductDown($stripeProduct);
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// Sync all prices for a product
|
// Sync all prices for a product
|
||||||
ShopStripeService::syncProductPricesDown($product);
|
StripeService::syncProductPricesDown($product);
|
||||||
|
|
||||||
// This creates/updates ProductPrice records with:
|
// This creates/updates ProductPrice records with:
|
||||||
// - stripe_price_id
|
// - stripe_price_id
|
||||||
|
|
@ -464,7 +464,7 @@ class StripeWebhookController extends Controller
|
||||||
|
|
||||||
protected function handleProductUpdate($stripeProduct)
|
protected function handleProductUpdate($stripeProduct)
|
||||||
{
|
{
|
||||||
ShopStripeService::syncProductDown($stripeProduct);
|
StripeService::syncProductDown($stripeProduct);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function handlePriceUpdate($stripePrice)
|
protected function handlePriceUpdate($stripePrice)
|
||||||
|
|
@ -505,7 +505,7 @@ use Stripe\Product as StripeProduct;
|
||||||
Stripe::setApiKey(config('services.stripe.secret'));
|
Stripe::setApiKey(config('services.stripe.secret'));
|
||||||
|
|
||||||
Product::whereNotNull('stripe_product_id')->each(function ($product) {
|
Product::whereNotNull('stripe_product_id')->each(function ($product) {
|
||||||
ShopStripeService::syncProductPricesDown($product);
|
StripeService::syncProductPricesDown($product);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Contracts;
|
||||||
|
|
||||||
|
interface Chargable
|
||||||
|
{
|
||||||
|
public function getDefaultPaymentMethod(): ?string;
|
||||||
|
|
||||||
|
public function paymentMethods(): array;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Blax\Shop\Services;
|
||||||
|
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class StripeService
|
||||||
|
{
|
||||||
|
public $stripe;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->stripe = new \Stripe\StripeClient(config('shop.payment.stripe.secret_key'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a customer.
|
||||||
|
* $data example: ['email'=>'user@example.com','name'=>'John Doe','metadata'=>['order_id'=>123]]
|
||||||
|
*/
|
||||||
|
public function createCustomer(array $data): \Stripe\Customer
|
||||||
|
{
|
||||||
|
return $this->stripe->customers->create($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a customer.
|
||||||
|
*/
|
||||||
|
public function getCustomer(string $customerId): \Stripe\Customer
|
||||||
|
{
|
||||||
|
return $this->stripe->customers->retrieve($customerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a customer.
|
||||||
|
*/
|
||||||
|
public function updateCustomer(string $customerId, array $data): \Stripe\Customer
|
||||||
|
{
|
||||||
|
return $this->stripe->customers->update($customerId, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a product.
|
||||||
|
*/
|
||||||
|
public function createProduct(string $name, ?string $description = null, array $metadata = []): \Stripe\Product
|
||||||
|
{
|
||||||
|
return $this->stripe->products->create([
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description,
|
||||||
|
'metadata' => $metadata,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a recurring or one-time price for a product.
|
||||||
|
* $unitAmount in smallest currency unit (e.g. cents).
|
||||||
|
* For one-time price omit recurring params.
|
||||||
|
*/
|
||||||
|
public function createPriceForProduct(
|
||||||
|
string $productId,
|
||||||
|
int $unitAmount,
|
||||||
|
string $currency = 'usd',
|
||||||
|
?string $interval = 'month'
|
||||||
|
): \Stripe\Price {
|
||||||
|
$data = [
|
||||||
|
'product' => $productId,
|
||||||
|
'unit_amount' => $unitAmount,
|
||||||
|
'currency' => $currency,
|
||||||
|
];
|
||||||
|
if ($interval) {
|
||||||
|
$data['recurring'] = ['interval' => $interval];
|
||||||
|
}
|
||||||
|
return $this->stripe->prices->create($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a PaymentIntent.
|
||||||
|
* $amount in smallest currency unit.
|
||||||
|
*/
|
||||||
|
public function createPaymentIntent(
|
||||||
|
int $amount,
|
||||||
|
string $currency = 'usd',
|
||||||
|
array $paymentMethodTypes = ['card'],
|
||||||
|
array $metadata = [],
|
||||||
|
?string $customerId = null
|
||||||
|
): \Stripe\PaymentIntent {
|
||||||
|
$data = [
|
||||||
|
'amount' => $amount,
|
||||||
|
'currency' => $currency,
|
||||||
|
'payment_method_types' => $paymentMethodTypes,
|
||||||
|
'metadata' => $metadata,
|
||||||
|
];
|
||||||
|
if ($customerId) {
|
||||||
|
$data['customer'] = $customerId;
|
||||||
|
}
|
||||||
|
return $this->stripe->paymentIntents->create($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a PaymentIntent.
|
||||||
|
*/
|
||||||
|
public function getPaymentIntent(string $paymentIntentId): \Stripe\PaymentIntent
|
||||||
|
{
|
||||||
|
return $this->stripe->paymentIntents->retrieve($paymentIntentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm a PaymentIntent, optionally with a payment method.
|
||||||
|
*/
|
||||||
|
public function confirmPaymentIntent(string $paymentIntentId, ?string $paymentMethodId = null): \Stripe\PaymentIntent
|
||||||
|
{
|
||||||
|
$data = [];
|
||||||
|
if ($paymentMethodId) {
|
||||||
|
$data['payment_method'] = $paymentMethodId;
|
||||||
|
}
|
||||||
|
return $this->stripe->paymentIntents->confirm($paymentIntentId, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Checkout Session (one-time or subscription depending on price config).
|
||||||
|
* $lineItems: array of ['price'=> 'price_xxx', 'quantity'=>1]
|
||||||
|
*/
|
||||||
|
public function createCheckoutSession(
|
||||||
|
array $lineItems,
|
||||||
|
string $successUrl,
|
||||||
|
string $cancelUrl,
|
||||||
|
?string $customerId = null,
|
||||||
|
array $metadata = []
|
||||||
|
): \Stripe\Checkout\Session {
|
||||||
|
$data = [
|
||||||
|
'mode' => 'payment',
|
||||||
|
'line_items' => $lineItems,
|
||||||
|
'success_url' => $successUrl,
|
||||||
|
'cancel_url' => $cancelUrl,
|
||||||
|
'metadata' => $metadata,
|
||||||
|
];
|
||||||
|
if ($customerId) {
|
||||||
|
$data['customer'] = $customerId;
|
||||||
|
}
|
||||||
|
// If any price is recurring, Stripe auto switches mode to 'subscription' when you set 'mode'
|
||||||
|
$hasRecurring = false;
|
||||||
|
foreach ($lineItems as $li) {
|
||||||
|
if (isset($li['price']) && str_contains((string)$li['price'], 'price_')) {
|
||||||
|
// Lightweight heuristic; real check would retrieve price and inspect 'recurring'
|
||||||
|
// For brevity not retrieving each price here.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Allow caller to override mode via metadata if needed.
|
||||||
|
return $this->stripe->checkout->sessions->create($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a Checkout Session.
|
||||||
|
*/
|
||||||
|
public function getCheckoutSession(string $sessionId): \Stripe\Checkout\Session
|
||||||
|
{
|
||||||
|
return $this->stripe->checkout->sessions->retrieve($sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a subscription from price.
|
||||||
|
*/
|
||||||
|
public function createSubscription(
|
||||||
|
string $customerId,
|
||||||
|
string $priceId,
|
||||||
|
array $metadata = []
|
||||||
|
): \Stripe\Subscription {
|
||||||
|
return $this->stripe->subscriptions->create([
|
||||||
|
'customer' => $customerId,
|
||||||
|
'items' => [['price' => $priceId]],
|
||||||
|
'metadata' => $metadata,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a subscription.
|
||||||
|
* If $invoiceNow true, finalize & invoice any pending items first (simple approach).
|
||||||
|
*/
|
||||||
|
public function cancelSubscription(string $subscriptionId, bool $invoiceNow = false): \Stripe\Subscription
|
||||||
|
{
|
||||||
|
if ($invoiceNow) {
|
||||||
|
// Optional: finalize latest invoice draft before cancellation (skipped for brevity).
|
||||||
|
}
|
||||||
|
return $this->stripe->subscriptions->cancel($subscriptionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List invoices for a customer.
|
||||||
|
*/
|
||||||
|
public function listInvoices(string $customerId, int $limit = 10): \Stripe\Collection
|
||||||
|
{
|
||||||
|
return $this->stripe->invoices->all([
|
||||||
|
'customer' => $customerId,
|
||||||
|
'limit' => $limit,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Blax\Shop\Services;
|
|
||||||
|
|
||||||
use Blax\Shop\Models\Product;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
|
|
||||||
class ShopStripeService
|
|
||||||
{
|
|
||||||
public static function syncProductDown(\Stripe\Product $stripeProduct)
|
|
||||||
{
|
|
||||||
$product = Product::updateOrCreate(
|
|
||||||
['stripe_product_id' => $stripeProduct->id],
|
|
||||||
[
|
|
||||||
'slug' => str()->slug($stripeProduct->name),
|
|
||||||
'type' => $stripeProduct->type,
|
|
||||||
'virtual' => $stripeProduct->type === 'service',
|
|
||||||
'status' => $stripeProduct->active ? 'published' : 'draft',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
$product->setLocalized('name', $stripeProduct->name);
|
|
||||||
|
|
||||||
if (isset($stripeProduct->marketing_features)) {
|
|
||||||
$product->setLocalized(
|
|
||||||
'features',
|
|
||||||
collect($stripeProduct->marketing_features)->map(fn($i) => $i->name)->toArray(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$product->save();
|
|
||||||
|
|
||||||
// Sync prices
|
|
||||||
self::syncProductPricesDown($product);
|
|
||||||
|
|
||||||
if (app()->runningInConsole()) {
|
|
||||||
echo "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $product;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function syncProductPricesDown(Product $product)
|
|
||||||
{
|
|
||||||
self::getProductPrices($product->stripe_product_id)->each(function ($stripePrice) use ($product) {
|
|
||||||
if ($stripePrice->product !== $product->stripe_product_id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$price = $product->prices()->updateOrCreate(
|
|
||||||
['stripe_price_id' => $stripePrice->id],
|
|
||||||
[
|
|
||||||
'name' => $stripePrice->nickname,
|
|
||||||
'type' => $stripePrice->type,
|
|
||||||
'price' => $stripePrice->unit_amount,
|
|
||||||
'currency' => $stripePrice->currency,
|
|
||||||
'billing_scheme' => $stripePrice->billing_scheme,
|
|
||||||
'interval' => $stripePrice->recurring ? $stripePrice->recurring->interval : null,
|
|
||||||
'interval_count' => $stripePrice->recurring ? $stripePrice->recurring->interval_count : null,
|
|
||||||
'trial_period_days' => $stripePrice->recurring ? $stripePrice->recurring->trial_period_days : null,
|
|
||||||
'is_default' => false,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (app()->runningInConsole()) {
|
|
||||||
echo " - Synced price {$price->id} ({$stripePrice->id})\n";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Traits;
|
||||||
|
|
||||||
|
use Blax\Shop\Exceptions\MultiplePurchaseOptions;
|
||||||
|
use Blax\Shop\Exceptions\NotPurchasable;
|
||||||
|
use Blax\Shop\Models\CartItem;
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Models\ProductPrice;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
|
|
||||||
|
trait HasCart
|
||||||
|
{
|
||||||
|
public function cart(): MorphMany
|
||||||
|
{
|
||||||
|
return $this->morphMany(
|
||||||
|
config('shop.models.cart', \Blax\Shop\Models\Cart::class),
|
||||||
|
'customer'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cart items (purchases with status 'cart')
|
||||||
|
*/
|
||||||
|
public function cartItems(): HasMany
|
||||||
|
{
|
||||||
|
return $this->cart()->latest()->firstOrCreate()->items();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create the current cart for the entity
|
||||||
|
*
|
||||||
|
* @return Cart
|
||||||
|
*/
|
||||||
|
public function currentCart()
|
||||||
|
{
|
||||||
|
return $this->cart()
|
||||||
|
->whereNull('converted_at')
|
||||||
|
->latest()
|
||||||
|
->firstOrCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add product to cart
|
||||||
|
*
|
||||||
|
* @param Product|ProductPrice $product_or_price
|
||||||
|
* @param int $quantity
|
||||||
|
* @param array $options
|
||||||
|
* @return CartItem
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function addToCart(Product|ProductPrice $product_or_price, int $quantity = 1, array $parameters = []): CartItem
|
||||||
|
{
|
||||||
|
if ($product_or_price instanceof ProductPrice){
|
||||||
|
$product = $product_or_price->purchasable;
|
||||||
|
|
||||||
|
if ($product instanceof Product) {
|
||||||
|
$product->reserveStock($quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($product_or_price instanceof Product) {
|
||||||
|
$product_or_price->reserveStock($quantity);
|
||||||
|
|
||||||
|
$default_prices = $product_or_price->defaultPrice()->count();
|
||||||
|
|
||||||
|
if ($default_prices === 0) {
|
||||||
|
throw new NotPurchasable("Product has no default price");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($default_prices > 1) {
|
||||||
|
throw new MultiplePurchaseOptions("Product has multiple default prices, please specify a price to add to cart");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->currentCart()->addToCart(
|
||||||
|
$product_or_price,
|
||||||
|
$quantity,
|
||||||
|
$parameters
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update cart item quantity
|
||||||
|
*
|
||||||
|
* @param CartItem $cartItem
|
||||||
|
* @param int $quantity
|
||||||
|
* @return CartItem
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function updateCartQuantity(CartItem $cartItem, int $quantity): CartItem
|
||||||
|
{
|
||||||
|
$product = $cartItem->purchasable;
|
||||||
|
|
||||||
|
// Validate stock
|
||||||
|
if ($product->manage_stock && $product->getAvailableStock() < $quantity) {
|
||||||
|
throw new \Exception("Insufficient stock available");
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = (array) $cartItem->meta;
|
||||||
|
|
||||||
|
$cartItem->update([
|
||||||
|
'quantity' => $quantity,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $cartItem->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove item from cart
|
||||||
|
*
|
||||||
|
* @param CartItem $cartItem
|
||||||
|
* @return bool
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function removeFromCart(CartItem $cartItem): bool
|
||||||
|
{
|
||||||
|
return $cartItem->forceDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all cart items
|
||||||
|
*
|
||||||
|
* @param string|null $cartId (deprecated - not used)
|
||||||
|
* @return int Number of items removed
|
||||||
|
*/
|
||||||
|
public function clearCart(?string $cartId = null): int
|
||||||
|
{
|
||||||
|
return $this->cartItems()->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cart total
|
||||||
|
*
|
||||||
|
* @param string|null $cartId (deprecated - not used)
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
public function getCartTotal(?string $cartId = null): float
|
||||||
|
{
|
||||||
|
return $this->cartItems()->get()->sum(function ($item) {
|
||||||
|
return ($item->purchasable->getCurrentPrice() ?? 0) * $item->quantity;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cart items count
|
||||||
|
*
|
||||||
|
* @param string|null $cartId (deprecated - not used)
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getCartItemsCount(?string $cartId = null): int
|
||||||
|
{
|
||||||
|
return $this->cartItems()->sum('quantity') ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or generate current cart ID
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getCurrentCartId(): string
|
||||||
|
{
|
||||||
|
// Override this method if you need custom cart ID logic
|
||||||
|
return 'cart_' . $this->getKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Traits;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
|
|
||||||
|
trait HasChargingOptions
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
@ -15,13 +15,7 @@ use Illuminate\Support\Collection;
|
||||||
|
|
||||||
trait HasShoppingCapabilities
|
trait HasShoppingCapabilities
|
||||||
{
|
{
|
||||||
public function cart(): MorphMany
|
use HasChargingOptions, HasCart;
|
||||||
{
|
|
||||||
return $this->morphMany(
|
|
||||||
config('shop.models.cart', \Blax\Shop\Models\Cart::class),
|
|
||||||
'customer'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all purchases made by this entity
|
* Get all purchases made by this entity
|
||||||
|
|
@ -35,14 +29,6 @@ trait HasShoppingCapabilities
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cart items (purchases with status 'cart')
|
|
||||||
*/
|
|
||||||
public function cartItems(): HasMany
|
|
||||||
{
|
|
||||||
return $this->cart()->latest()->firstOrCreate()->items();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get completed purchases
|
* Get completed purchases
|
||||||
*/
|
*/
|
||||||
|
|
@ -145,133 +131,6 @@ trait HasShoppingCapabilities
|
||||||
return $purchase;
|
return $purchase;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get or create the current cart for the entity
|
|
||||||
*
|
|
||||||
* @return Cart
|
|
||||||
*/
|
|
||||||
public function currentCart()
|
|
||||||
{
|
|
||||||
return $this->cart()
|
|
||||||
->whereNull('converted_at')
|
|
||||||
->latest()
|
|
||||||
->firstOrCreate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add product to cart
|
|
||||||
*
|
|
||||||
* @param Product|ProductPrice $product_or_price
|
|
||||||
* @param int $quantity
|
|
||||||
* @param array $options
|
|
||||||
* @return CartItem
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
public function addToCart(Product|ProductPrice $product_or_price, int $quantity = 1, array $parameters = []): CartItem
|
|
||||||
{
|
|
||||||
if ($product_or_price instanceof ProductPrice){
|
|
||||||
$product = $product_or_price->purchasable;
|
|
||||||
|
|
||||||
if ($product instanceof Product) {
|
|
||||||
$product->reserveStock($quantity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($product_or_price instanceof Product) {
|
|
||||||
$product_or_price->reserveStock($quantity);
|
|
||||||
|
|
||||||
$default_prices = $product_or_price->defaultPrice()->count();
|
|
||||||
|
|
||||||
if ($default_prices === 0) {
|
|
||||||
throw new NotPurchasable("Product has no default price");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($default_prices > 1) {
|
|
||||||
throw new MultiplePurchaseOptions("Product has multiple default prices, please specify a price to add to cart");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return $this->currentCart()->addToCart(
|
|
||||||
$product_or_price,
|
|
||||||
$quantity,
|
|
||||||
$parameters
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update cart item quantity
|
|
||||||
*
|
|
||||||
* @param CartItem $cartItem
|
|
||||||
* @param int $quantity
|
|
||||||
* @return CartItem
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
public function updateCartQuantity(CartItem $cartItem, int $quantity): CartItem
|
|
||||||
{
|
|
||||||
$product = $cartItem->purchasable;
|
|
||||||
|
|
||||||
// Validate stock
|
|
||||||
if ($product->manage_stock && $product->getAvailableStock() < $quantity) {
|
|
||||||
throw new \Exception("Insufficient stock available");
|
|
||||||
}
|
|
||||||
|
|
||||||
$meta = (array) $cartItem->meta;
|
|
||||||
|
|
||||||
$cartItem->update([
|
|
||||||
'quantity' => $quantity,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $cartItem->fresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove item from cart
|
|
||||||
*
|
|
||||||
* @param CartItem $cartItem
|
|
||||||
* @return bool
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
public function removeFromCart(CartItem $cartItem): bool
|
|
||||||
{
|
|
||||||
return $cartItem->forceDelete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all cart items
|
|
||||||
*
|
|
||||||
* @param string|null $cartId (deprecated - not used)
|
|
||||||
* @return int Number of items removed
|
|
||||||
*/
|
|
||||||
public function clearCart(?string $cartId = null): int
|
|
||||||
{
|
|
||||||
return $this->cartItems()->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cart total
|
|
||||||
*
|
|
||||||
* @param string|null $cartId (deprecated - not used)
|
|
||||||
* @return float
|
|
||||||
*/
|
|
||||||
public function getCartTotal(?string $cartId = null): float
|
|
||||||
{
|
|
||||||
return $this->cartItems()->get()->sum(function ($item) {
|
|
||||||
return ($item->purchasable->getCurrentPrice() ?? 0) * $item->quantity;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cart items count
|
|
||||||
*
|
|
||||||
* @param string|null $cartId (deprecated - not used)
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getCartItemsCount(?string $cartId = null): int
|
|
||||||
{
|
|
||||||
return $this->cartItems()->sum('quantity') ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checkout cart - convert cart items to completed purchases
|
* Checkout cart - convert cart items to completed purchases
|
||||||
*
|
*
|
||||||
|
|
@ -422,15 +281,4 @@ trait HasShoppingCapabilities
|
||||||
|
|
||||||
return $product->getCurrentPrice();
|
return $product->getCurrentPrice();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get or generate current cart ID
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
protected function getCurrentCartId(): string
|
|
||||||
{
|
|
||||||
// Override this method if you need custom cart ID logic
|
|
||||||
return 'cart_' . $this->getKey();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue