A traits, concerns, services
This commit is contained in:
parent
82ee18b0f1
commit
856686e292
|
|
@ -21,7 +21,8 @@
|
|||
"php": ">=8.0",
|
||||
"illuminate/support": "^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": {
|
||||
"orchestra/testbench": "^8.0|^9.0",
|
||||
|
|
|
|||
|
|
@ -62,12 +62,6 @@ return [
|
|||
'prefix' => 'shop:',
|
||||
],
|
||||
|
||||
// Pagination
|
||||
'pagination' => [
|
||||
'per_page' => 20,
|
||||
'max_per_page' => 100,
|
||||
],
|
||||
|
||||
// Cart configuration
|
||||
'cart' => [
|
||||
'expire_after_days' => 30,
|
||||
|
|
@ -81,4 +75,5 @@ return [
|
|||
'wrap_response' => true,
|
||||
'response_key' => 'data',
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -38,14 +38,14 @@ Update `config/shop.php`:
|
|||
### Sync Individual Product
|
||||
|
||||
```php
|
||||
use Blax\Shop\Services\ShopStripeService;
|
||||
use Blax\Shop\Services\StripeService;
|
||||
use Stripe\Product as StripeProduct;
|
||||
|
||||
// Get Stripe product
|
||||
$stripeProduct = StripeProduct::retrieve('prod_xxxxx');
|
||||
|
||||
// Sync to local database
|
||||
$product = ShopStripeService::syncProductDown($stripeProduct);
|
||||
$product = StripeService::syncProductDown($stripeProduct);
|
||||
|
||||
// This creates/updates a Product with:
|
||||
// - stripe_product_id
|
||||
|
|
@ -61,7 +61,7 @@ $product = ShopStripeService::syncProductDown($stripeProduct);
|
|||
|
||||
```php
|
||||
// Sync all prices for a product
|
||||
ShopStripeService::syncProductPricesDown($product);
|
||||
StripeService::syncProductPricesDown($product);
|
||||
|
||||
// This creates/updates ProductPrice records with:
|
||||
// - stripe_price_id
|
||||
|
|
@ -464,7 +464,7 @@ class StripeWebhookController extends Controller
|
|||
|
||||
protected function handleProductUpdate($stripeProduct)
|
||||
{
|
||||
ShopStripeService::syncProductDown($stripeProduct);
|
||||
StripeService::syncProductDown($stripeProduct);
|
||||
}
|
||||
|
||||
protected function handlePriceUpdate($stripePrice)
|
||||
|
|
@ -505,7 +505,7 @@ use Stripe\Product as StripeProduct;
|
|||
Stripe::setApiKey(config('services.stripe.secret'));
|
||||
|
||||
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
|
||||
{
|
||||
public function cart(): MorphMany
|
||||
{
|
||||
return $this->morphMany(
|
||||
config('shop.models.cart', \Blax\Shop\Models\Cart::class),
|
||||
'customer'
|
||||
);
|
||||
}
|
||||
use HasChargingOptions, HasCart;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
|
@ -145,133 +131,6 @@ trait HasShoppingCapabilities
|
|||
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
|
||||
*
|
||||
|
|
@ -422,15 +281,4 @@ trait HasShoppingCapabilities
|
|||
|
||||
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