A types, enums & statuses, U product models, I shopping trait, A booking support, R tests
This commit is contained in:
parent
2008a16a53
commit
c5004158eb
|
|
@ -20,7 +20,7 @@ return new class extends Migration
|
|||
$table->string('sku')->nullable()->unique();
|
||||
$table->text('short_description')->nullable();
|
||||
$table->longText('description')->nullable();
|
||||
$table->string('type')->default('simple'); // simple, variable, grouped, external
|
||||
$table->string('type')->default('simple'); // simple, variable, grouped, external, booking
|
||||
$table->string('stripe_product_id')->nullable();
|
||||
$table->timestamp('sale_start')->nullable();
|
||||
$table->timestamp('sale_end')->nullable();
|
||||
|
|
@ -240,6 +240,8 @@ return new class extends Migration
|
|||
$table->decimal('amount', 10, 8)->nullable();
|
||||
$table->decimal('amount_paid', 10, 8)->default(0);
|
||||
$table->string('charge_id')->nullable();
|
||||
$table->timestamp('from')->nullable();
|
||||
$table->timestamp('until')->nullable();
|
||||
$table->json('meta')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Blax\Shop\Console\Commands;
|
||||
|
||||
use Blax\Shop\Enums\ProductStatus;
|
||||
use Blax\Shop\Enums\ProductType;
|
||||
use Blax\Shop\Models\Product;
|
||||
use Blax\Shop\Models\ProductAction;
|
||||
use Blax\Shop\Models\ProductAttribute;
|
||||
|
|
@ -131,20 +133,20 @@ class ShopAddExampleProducts extends Command
|
|||
'slug' => $slug,
|
||||
'name' => $productName,
|
||||
'sku' => 'EX-' . strtoupper($this->faker->bothify('??-####')),
|
||||
'type' => $type,
|
||||
'status' => $this->faker->randomElement(['published', 'published', 'published', 'draft']),
|
||||
'type' => ProductType::from($type),
|
||||
'status' => $this->faker->randomElement([ProductStatus::PUBLISHED, ProductStatus::PUBLISHED, ProductStatus::PUBLISHED, ProductStatus::DRAFT]),
|
||||
'is_visible' => true,
|
||||
'featured' => $this->faker->boolean(20),
|
||||
'sale_start' => $saleStart,
|
||||
'sale_end' => $saleEnd,
|
||||
'manage_stock' => $type !== 'external',
|
||||
'low_stock_threshold' => $type !== 'external' ? 5 : null,
|
||||
'manage_stock' => $type !== ProductType::EXTERNAL->value,
|
||||
'low_stock_threshold' => $type !== ProductType::EXTERNAL->value ? 5 : null,
|
||||
'weight' => $type === 'virtual' ? null : $this->faker->randomFloat(2, 0.1, 50),
|
||||
'length' => $type === 'virtual' ? null : $this->faker->randomFloat(2, 5, 100),
|
||||
'width' => $type === 'virtual' ? null : $this->faker->randomFloat(2, 5, 100),
|
||||
'height' => $type === 'virtual' ? null : $this->faker->randomFloat(2, 5, 100),
|
||||
'virtual' => $type === 'variable' ? $this->faker->boolean(20) : false,
|
||||
'downloadable' => $type === 'simple' ? $this->faker->boolean(15) : false,
|
||||
'virtual' => $type === ProductType::VARIABLE->value ? $this->faker->boolean(20) : false,
|
||||
'downloadable' => $type === ProductType::SIMPLE->value ? $this->faker->boolean(15) : false,
|
||||
'published_at' => now(),
|
||||
'sort_order' => $this->faker->numberBetween(0, 100),
|
||||
'tax_class' => $this->faker->randomElement(['standard', 'reduced', 'zero']),
|
||||
|
|
@ -184,7 +186,7 @@ class ShopAddExampleProducts extends Command
|
|||
$this->addAttributes($product, $type);
|
||||
|
||||
// Add additional prices (multi-currency or subscription)
|
||||
if ($type === 'simple' || $type === 'variable') {
|
||||
if ($type === ProductType::SIMPLE->value || $type === ProductType::VARIABLE->value) {
|
||||
$this->addAdditionalPrices($product, $baseUnitAmount);
|
||||
}
|
||||
|
||||
|
|
@ -192,12 +194,12 @@ class ShopAddExampleProducts extends Command
|
|||
$this->addExampleActions($product);
|
||||
|
||||
// For variable products, add variations
|
||||
if ($type === 'variable') {
|
||||
if ($type === ProductType::VARIABLE->value) {
|
||||
$this->addVariations($product, $baseUnitAmount);
|
||||
}
|
||||
|
||||
// For grouped products, add child products
|
||||
if ($type === 'grouped') {
|
||||
if ($type === ProductType::GROUPED->value) {
|
||||
$this->addGroupedProducts($product);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace Blax\Shop\Enums;
|
||||
|
||||
enum BillingScheme: string
|
||||
{
|
||||
case PER_UNIT = 'per_unit';
|
||||
case TIERED = 'tiered';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::PER_UNIT => 'Per Unit',
|
||||
self::TIERED => 'Tiered',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Blax\Shop\Enums;
|
||||
|
||||
enum CartStatus: string
|
||||
{
|
||||
case ACTIVE = 'active';
|
||||
case ABANDONED = 'abandoned';
|
||||
case CONVERTED = 'converted';
|
||||
case EXPIRED = 'expired';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::ACTIVE => 'Active',
|
||||
self::ABANDONED => 'Abandoned',
|
||||
self::CONVERTED => 'Converted',
|
||||
self::EXPIRED => 'Expired',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace Blax\Shop\Enums;
|
||||
|
||||
enum PriceType: string
|
||||
{
|
||||
case ONE_TIME = 'one_time';
|
||||
case RECURRING = 'recurring';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::ONE_TIME => 'One Time',
|
||||
self::RECURRING => 'Recurring',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Blax\Shop\Enums;
|
||||
|
||||
enum ProductAttributeType: string
|
||||
{
|
||||
case TEXT = 'text';
|
||||
case SELECT = 'select';
|
||||
case COLOR = 'color';
|
||||
case IMAGE = 'image';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::TEXT => 'Text',
|
||||
self::SELECT => 'Select',
|
||||
self::COLOR => 'Color',
|
||||
self::IMAGE => 'Image',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace Blax\Shop\Enums;
|
||||
|
||||
enum ProductStatus: string
|
||||
{
|
||||
case DRAFT = 'draft';
|
||||
case PUBLISHED = 'published';
|
||||
case ARCHIVED = 'archived';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DRAFT => 'Draft',
|
||||
self::PUBLISHED => 'Published',
|
||||
self::ARCHIVED => 'Archived',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace Blax\Shop\Enums;
|
||||
|
||||
enum ProductType: string
|
||||
{
|
||||
case SIMPLE = 'simple';
|
||||
case VARIABLE = 'variable';
|
||||
case GROUPED = 'grouped';
|
||||
case EXTERNAL = 'external';
|
||||
case BOOKING = 'booking';
|
||||
case VARIATION = 'variation';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::SIMPLE => 'Simple',
|
||||
self::VARIABLE => 'Variable',
|
||||
self::GROUPED => 'Grouped',
|
||||
self::EXTERNAL => 'External',
|
||||
self::BOOKING => 'Booking',
|
||||
self::VARIATION => 'Variation',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace Blax\Shop\Enums;
|
||||
|
||||
enum PurchaseStatus: string
|
||||
{
|
||||
case PENDING = 'pending';
|
||||
case UNPAID = 'unpaid';
|
||||
case COMPLETED = 'completed';
|
||||
case REFUNDED = 'refunded';
|
||||
case CART = 'cart';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::PENDING => 'Pending',
|
||||
self::UNPAID => 'Unpaid',
|
||||
self::COMPLETED => 'Completed',
|
||||
self::REFUNDED => 'Refunded',
|
||||
self::CART => 'Cart',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace Blax\Shop\Enums;
|
||||
|
||||
enum RecurringInterval: string
|
||||
{
|
||||
case DAY = 'day';
|
||||
case WEEK = 'week';
|
||||
case MONTH = 'month';
|
||||
case YEAR = 'year';
|
||||
case QUARTER = 'quarter';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DAY => 'Day',
|
||||
self::WEEK => 'Week',
|
||||
self::MONTH => 'Month',
|
||||
self::QUARTER => 'Quarter',
|
||||
self::YEAR => 'Year',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Blax\Shop\Enums;
|
||||
|
||||
enum StockStatus: string
|
||||
{
|
||||
case PENDING = 'pending';
|
||||
case COMPLETED = 'completed';
|
||||
case CANCELLED = 'cancelled';
|
||||
case EXPIRED = 'expired';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::PENDING => 'Pending',
|
||||
self::COMPLETED => 'Completed',
|
||||
self::CANCELLED => 'Cancelled',
|
||||
self::EXPIRED => 'Expired',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace Blax\Shop\Enums;
|
||||
|
||||
enum StockType: string
|
||||
{
|
||||
case RESERVATION = 'reservation';
|
||||
case ADJUSTMENT = 'adjustment';
|
||||
case SALE = 'sale';
|
||||
case RETURN = 'return';
|
||||
case INCREASE = 'increase';
|
||||
case DECREASE = 'decrease';
|
||||
case RELEASE = 'release';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::RESERVATION => 'Reservation',
|
||||
self::ADJUSTMENT => 'Adjustment',
|
||||
self::SALE => 'Sale',
|
||||
self::RETURN => 'Return',
|
||||
self::INCREASE => 'Increase',
|
||||
self::DECREASE => 'Decrease',
|
||||
self::RELEASE => 'Release',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
namespace Blax\Shop\Models;
|
||||
|
||||
use Blax\Shop\Contracts\Cartable;
|
||||
use Blax\Shop\Enums\CartStatus;
|
||||
use Blax\Shop\Enums\ProductType;
|
||||
use Blax\Workkit\Traits\HasExpiration;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -27,6 +29,7 @@ class Cart extends Model
|
|||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => CartStatus::class,
|
||||
'expires_at' => 'datetime',
|
||||
'converted_at' => 'datetime',
|
||||
'last_activity_at' => 'datetime',
|
||||
|
|
@ -174,10 +177,30 @@ class Cart extends Model
|
|||
foreach ($items as $item) {
|
||||
$product = $item->purchasable;
|
||||
$quantity = $item->quantity;
|
||||
|
||||
// Extract booking dates from parameters if this is a booking product
|
||||
$from = null;
|
||||
$until = null;
|
||||
if ($product->type === ProductType::BOOKING && $item->parameters) {
|
||||
$params = is_array($item->parameters) ? $item->parameters : (array) $item->parameters;
|
||||
$from = $params['from'] ?? null;
|
||||
$until = $params['until'] ?? null;
|
||||
|
||||
// Convert to Carbon instances if they're strings
|
||||
if ($from && is_string($from)) {
|
||||
$from = \Carbon\Carbon::parse($from);
|
||||
}
|
||||
if ($until && is_string($until)) {
|
||||
$until = \Carbon\Carbon::parse($until);
|
||||
}
|
||||
}
|
||||
|
||||
$purchase = $this->customer->purchase(
|
||||
$product->prices()->first(),
|
||||
$quantity
|
||||
$quantity,
|
||||
null,
|
||||
$from,
|
||||
$until
|
||||
);
|
||||
|
||||
$purchase->update([
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ use Blax\Workkit\Traits\HasMetaTranslation;
|
|||
use Blax\Shop\Events\ProductCreated;
|
||||
use Blax\Shop\Events\ProductUpdated;
|
||||
use Blax\Shop\Contracts\Purchasable;
|
||||
use Blax\Shop\Enums\ProductStatus;
|
||||
use Blax\Shop\Enums\ProductType;
|
||||
use Blax\Shop\Enums\StockStatus;
|
||||
use Blax\Shop\Enums\StockType;
|
||||
use Blax\Shop\Traits\HasPrices;
|
||||
use Blax\Shop\Traits\HasStocks;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
|
|
@ -53,6 +57,8 @@ class Product extends Model implements Purchasable, Cartable
|
|||
'manage_stock' => 'boolean',
|
||||
'virtual' => 'boolean',
|
||||
'downloadable' => 'boolean',
|
||||
'type' => ProductType::class,
|
||||
'status' => ProductStatus::class,
|
||||
'meta' => 'object',
|
||||
'sale_start' => 'datetime',
|
||||
'sale_end' => 'datetime',
|
||||
|
|
@ -163,7 +169,7 @@ class Product extends Model implements Purchasable, Cartable
|
|||
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('status', 'published');
|
||||
return $query->where('status', ProductStatus::PUBLISHED->value);
|
||||
}
|
||||
|
||||
public function scopeFeatured($query)
|
||||
|
|
@ -228,7 +234,7 @@ class Product extends Model implements Purchasable, Cartable
|
|||
public function scopeVisible($query)
|
||||
{
|
||||
return $query->where('is_visible', true)
|
||||
->where('status', 'published')
|
||||
->where('status', ProductStatus::PUBLISHED->value)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('published_at')
|
||||
->orWhere('published_at', '<=', now());
|
||||
|
|
@ -253,7 +259,7 @@ class Product extends Model implements Purchasable, Cartable
|
|||
|
||||
public function isVisible(): bool
|
||||
{
|
||||
if (!$this->is_visible || $this->status !== 'published') {
|
||||
if (!$this->is_visible || $this->status !== ProductStatus::PUBLISHED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -331,4 +337,53 @@ class Product extends Model implements Purchasable, Cartable
|
|||
|
||||
return parent::newInstance($attributes, $exists);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a booking product
|
||||
*/
|
||||
public function isBooking(): bool
|
||||
{
|
||||
return $this->type === ProductType::BOOKING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check stock availability for a booking period
|
||||
*/
|
||||
public function isAvailableForBooking(\DateTimeInterface $from, \DateTimeInterface $until, int $quantity = 1): bool
|
||||
{
|
||||
if (!$this->manage_stock) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get stock reservations that overlap with the requested period
|
||||
$overlappingReservations = $this->stocks()
|
||||
->where('type', StockType::RESERVATION->value)
|
||||
->where('status', StockStatus::PENDING->value)
|
||||
->where(function ($query) use ($from, $until) {
|
||||
$query->where(function ($q) use ($from, $until) {
|
||||
// Reservation starts during the requested period
|
||||
$q->whereBetween('created_at', [$from, $until]);
|
||||
})->orWhere(function ($q) use ($from, $until) {
|
||||
// Reservation ends during the requested period
|
||||
$q->whereBetween('expires_at', [$from, $until]);
|
||||
})->orWhere(function ($q) use ($from, $until) {
|
||||
// Reservation encompasses the entire requested period
|
||||
$q->where('created_at', '<=', $from)
|
||||
->where('expires_at', '>=', $until);
|
||||
});
|
||||
})
|
||||
->sum('quantity');
|
||||
|
||||
$availableStock = $this->getAvailableStock() - abs($overlappingReservations);
|
||||
|
||||
return $availableStock >= $quantity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for booking products
|
||||
*/
|
||||
public function scopeBookings($query)
|
||||
{
|
||||
return $query->where('type', ProductType::BOOKING->value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ namespace Blax\Shop\Models;
|
|||
|
||||
use Blax\Shop\Contracts\Cartable;
|
||||
use Blax\Shop\Contracts\Purchasable;
|
||||
use Blax\Shop\Enums\BillingScheme;
|
||||
use Blax\Shop\Enums\PriceType;
|
||||
use Blax\Shop\Enums\RecurringInterval;
|
||||
use Blax\Workkit\Traits\HasMetaTranslation;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -34,6 +37,9 @@ class ProductPrice extends Model implements Cartable
|
|||
protected $casts = [
|
||||
'is_default' => 'boolean',
|
||||
'active' => 'boolean',
|
||||
'type' => PriceType::class,
|
||||
'billing_scheme' => BillingScheme::class,
|
||||
'interval' => RecurringInterval::class,
|
||||
'meta' => 'object',
|
||||
'unit_amount' => 'float',
|
||||
'sale_unit_amount' => 'float',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Blax\Shop\Models;
|
||||
|
||||
use Blax\Shop\Enums\PurchaseStatus;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
|
|
@ -21,13 +22,18 @@ class ProductPurchase extends Model
|
|||
'amount',
|
||||
'amount_paid',
|
||||
'charge_id',
|
||||
'from',
|
||||
'until',
|
||||
'meta',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => PurchaseStatus::class,
|
||||
'quantity' => 'integer',
|
||||
'amount' => 'integer',
|
||||
'amount_paid' => 'integer',
|
||||
'from' => 'datetime',
|
||||
'until' => 'datetime',
|
||||
'meta' => 'object',
|
||||
];
|
||||
|
||||
|
|
@ -67,12 +73,12 @@ class ProductPurchase extends Model
|
|||
|
||||
public static function scopeInCart($query)
|
||||
{
|
||||
return $query->where('status', 'cart');
|
||||
return $query->where('status', PurchaseStatus::CART->value);
|
||||
}
|
||||
|
||||
public static function scopeCompleted($query)
|
||||
{
|
||||
return $query->where('status', 'completed');
|
||||
return $query->where('status', PurchaseStatus::COMPLETED->value);
|
||||
}
|
||||
|
||||
protected static function booted()
|
||||
|
|
@ -86,7 +92,7 @@ class ProductPurchase extends Model
|
|||
? $productPurchase->purchasable?->product
|
||||
: $product;
|
||||
|
||||
if ($productPurchase->status === 'completed' && $product) {
|
||||
if ($productPurchase->status === PurchaseStatus::COMPLETED && $product) {
|
||||
$product->callActions('purchased', $productPurchase);
|
||||
}
|
||||
});
|
||||
|
|
@ -102,7 +108,7 @@ class ProductPurchase extends Model
|
|||
: $product;
|
||||
|
||||
|
||||
if ($productPurchase->status === 'completed' && $product) {
|
||||
if ($productPurchase->status === PurchaseStatus::COMPLETED && $product) {
|
||||
$product->callActions('purchased', $productPurchase);
|
||||
}
|
||||
});
|
||||
|
|
@ -119,4 +125,40 @@ class ProductPurchase extends Model
|
|||
'id' // Local key on ProductAction table...
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a booking purchase
|
||||
*/
|
||||
public function isBooking(): bool
|
||||
{
|
||||
return !is_null($this->from) && !is_null($this->until);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the booking has ended
|
||||
*/
|
||||
public function isBookingEnded(): bool
|
||||
{
|
||||
if (!$this->isBooking()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return now()->isAfter($this->until);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for booking purchases
|
||||
*/
|
||||
public function scopeBookings($query)
|
||||
{
|
||||
return $query->whereNotNull('from')->whereNotNull('until');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for ended bookings
|
||||
*/
|
||||
public function scopeEndedBookings($query)
|
||||
{
|
||||
return $query->bookings()->where('until', '<', now());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Blax\Shop\Models;
|
||||
|
||||
use Blax\Shop\Enums\StockStatus;
|
||||
use Blax\Shop\Enums\StockType;
|
||||
use Blax\Shop\Models\Product;
|
||||
use Blax\Workkit\Traits\HasExpiration;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
|
|
@ -27,6 +29,8 @@ class ProductStock extends Model
|
|||
|
||||
protected $casts = [
|
||||
'quantity' => 'integer',
|
||||
'type' => StockType::class,
|
||||
'status' => StockStatus::class,
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
|
|
@ -43,7 +47,7 @@ class ProductStock extends Model
|
|||
});
|
||||
|
||||
static::updated(function ($model) {
|
||||
if ($model->wasChanged('status') && $model->status === 'completed') {
|
||||
if ($model->wasChanged('status') && $model->status === StockStatus::COMPLETED) {
|
||||
$model->releaseStock();
|
||||
}
|
||||
});
|
||||
|
|
@ -61,12 +65,12 @@ class ProductStock extends Model
|
|||
|
||||
public function scopePending($query)
|
||||
{
|
||||
return $query->where('status', 'pending');
|
||||
return $query->where('status', StockStatus::PENDING->value);
|
||||
}
|
||||
|
||||
public function scopeReleased($query)
|
||||
{
|
||||
return $query->where('status', 'completed');
|
||||
return $query->where('status', StockStatus::COMPLETED->value);
|
||||
}
|
||||
|
||||
public function scopeTemporary($query)
|
||||
|
|
@ -82,7 +86,7 @@ class ProductStock extends Model
|
|||
// Backward compatibility accessors
|
||||
public function getReleasedAtAttribute()
|
||||
{
|
||||
return $this->status === 'completed' ? $this->updated_at : null;
|
||||
return $this->status === StockStatus::COMPLETED ? $this->updated_at : null;
|
||||
}
|
||||
|
||||
public function getUntilAtAttribute()
|
||||
|
|
@ -105,8 +109,8 @@ class ProductStock extends Model
|
|||
return self::create([
|
||||
'product_id' => $product->id,
|
||||
'quantity' => $quantity,
|
||||
'type' => 'reservation',
|
||||
'status' => 'pending',
|
||||
'type' => StockType::RESERVATION,
|
||||
'status' => StockStatus::PENDING,
|
||||
'reference_type' => $reference ? get_class($reference) : null,
|
||||
'reference_id' => $reference?->id,
|
||||
'expires_at' => $until,
|
||||
|
|
@ -117,12 +121,12 @@ class ProductStock extends Model
|
|||
|
||||
public function release(): bool
|
||||
{
|
||||
if ($this->status !== 'pending') {
|
||||
if ($this->status !== StockStatus::PENDING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return DB::transaction(function () {
|
||||
$this->status = 'completed';
|
||||
$this->status = StockStatus::COMPLETED;
|
||||
$this->save();
|
||||
|
||||
return true;
|
||||
|
|
@ -142,13 +146,13 @@ class ProductStock extends Model
|
|||
public function isExpired(): bool
|
||||
{
|
||||
return $this->isTemporary()
|
||||
&& $this->status === 'pending'
|
||||
&& $this->status === StockStatus::PENDING
|
||||
&& $this->expires_at->isPast();
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->status === 'pending';
|
||||
return $this->status === StockStatus::PENDING;
|
||||
}
|
||||
|
||||
protected function logStockChange(): void
|
||||
|
|
@ -180,7 +184,7 @@ class ProductStock extends Model
|
|||
'product_id' => $this->product_id,
|
||||
'quantity_change' => $this->quantity,
|
||||
'quantity_after' => $this->product->stock_quantity,
|
||||
'type' => 'release',
|
||||
'type' => StockType::RELEASE->value,
|
||||
'note' => 'Stock released from reservation',
|
||||
'reference_type' => $this->reference_type,
|
||||
'reference_id' => $this->reference_id,
|
||||
|
|
@ -205,12 +209,12 @@ class ProductStock extends Model
|
|||
|
||||
public static function scopeAvailable($query)
|
||||
{
|
||||
return $query->where('status', 'completed');
|
||||
return $query->where('status', StockStatus::COMPLETED->value);
|
||||
}
|
||||
|
||||
public static function scopeAvailableReservations($query)
|
||||
{
|
||||
return $query->where('type', 'reservation')->where('status', 'pending');
|
||||
return $query->where('type', StockType::RESERVATION->value)->where('status', StockStatus::PENDING->value);
|
||||
}
|
||||
|
||||
public static function reservations()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
namespace Blax\Shop\Traits;
|
||||
|
||||
use Blax\Shop\Contracts\Purchasable;
|
||||
use Blax\Shop\Enums\ProductType;
|
||||
use Blax\Shop\Enums\PurchaseStatus;
|
||||
use Blax\Shop\Exceptions\MultiplePurchaseOptions;
|
||||
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||
use Blax\Shop\Exceptions\NotPurchasable;
|
||||
|
|
@ -36,7 +38,7 @@ trait HasShoppingCapabilities
|
|||
*/
|
||||
public function completedPurchases(): MorphMany
|
||||
{
|
||||
return $this->purchases()->where('status', 'completed');
|
||||
return $this->purchases()->where('status', PurchaseStatus::COMPLETED->value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -44,6 +46,9 @@ trait HasShoppingCapabilities
|
|||
*
|
||||
* @param Product|Product $product_or_price
|
||||
* @param int $quantity
|
||||
* @param array|object|null $meta
|
||||
* @param \DateTimeInterface|null $from Booking start date (for booking products)
|
||||
* @param \DateTimeInterface|null $until Booking end date (for booking products)
|
||||
*
|
||||
* @return ProductPurchase
|
||||
* @throws \Exception
|
||||
|
|
@ -51,7 +56,9 @@ trait HasShoppingCapabilities
|
|||
public function purchase(
|
||||
ProductPrice|Product $product_or_price,
|
||||
int $quantity = 1,
|
||||
array|object|null $meta = null
|
||||
array|object|null $meta = null,
|
||||
?\DateTimeInterface $from = null,
|
||||
?\DateTimeInterface $until = null
|
||||
): ProductPurchase {
|
||||
|
||||
if ($product_or_price instanceof Product) {
|
||||
|
|
@ -98,8 +105,15 @@ trait HasShoppingCapabilities
|
|||
throw new \Exception("Product is not available for purchase");
|
||||
}
|
||||
|
||||
// Decrease stock
|
||||
if (!$product->decreaseStock($quantity)) {
|
||||
// Handle booking products
|
||||
$isBooking = $product->type === ProductType::BOOKING;
|
||||
|
||||
if ($isBooking && (!$from || !$until)) {
|
||||
throw new \Exception("Booking products require 'from' and 'until' dates");
|
||||
}
|
||||
|
||||
// Decrease stock (for bookings, pass the until date)
|
||||
if (!$product->decreaseStock($quantity, $isBooking ? $until : null)) {
|
||||
throw new \Exception("Unable to decrease stock");
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +124,9 @@ trait HasShoppingCapabilities
|
|||
'purchaser_id' => $this->getKey(),
|
||||
'purchaser_type' => get_class($this),
|
||||
'quantity' => $quantity,
|
||||
'status' => 'unpaid',
|
||||
'status' => PurchaseStatus::UNPAID,
|
||||
'from' => $from,
|
||||
'until' => $until,
|
||||
'meta' => $meta,
|
||||
'amount' => $price->unit_amount * $quantity,
|
||||
]);
|
||||
|
|
@ -192,7 +208,7 @@ trait HasShoppingCapabilities
|
|||
*/
|
||||
public function refundPurchase(ProductPurchase $purchase, array $options = []): bool
|
||||
{
|
||||
if ($purchase->status !== 'completed') {
|
||||
if ($purchase->status !== PurchaseStatus::COMPLETED) {
|
||||
throw new \Exception("Can only refund completed purchases");
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +219,7 @@ trait HasShoppingCapabilities
|
|||
|
||||
// Update purchase
|
||||
$purchase->update([
|
||||
'status' => 'refunded',
|
||||
'status' => PurchaseStatus::REFUNDED,
|
||||
]);
|
||||
|
||||
// Trigger refund actions
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Blax\Shop\Traits;
|
||||
|
||||
use Blax\Shop\Enums\StockStatus;
|
||||
use Blax\Shop\Enums\StockType;
|
||||
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
|
@ -46,8 +48,8 @@ trait HasStocks
|
|||
|
||||
$this->stocks()->create([
|
||||
'quantity' => -$quantity,
|
||||
'type' => 'decrease',
|
||||
'status' => 'completed',
|
||||
'type' => StockType::DECREASE,
|
||||
'status' => StockStatus::COMPLETED,
|
||||
'expires_at' => $until,
|
||||
]);
|
||||
|
||||
|
|
@ -66,8 +68,8 @@ trait HasStocks
|
|||
|
||||
$this->stocks()->create([
|
||||
'quantity' => $quantity,
|
||||
'type' => 'increase',
|
||||
'status' => 'completed',
|
||||
'type' => StockType::INCREASE,
|
||||
'status' => StockStatus::COMPLETED,
|
||||
]);
|
||||
|
||||
$this->logStockChange($quantity, 'increase');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,377 @@
|
|||
<?php
|
||||
|
||||
namespace Blax\Shop\Tests\Feature;
|
||||
|
||||
use Blax\Shop\Enums\ProductType;
|
||||
use Blax\Shop\Models\Product;
|
||||
use Blax\Shop\Models\ProductPrice;
|
||||
use Blax\Shop\Models\ProductPurchase;
|
||||
use Blax\Shop\Models\Cart;
|
||||
use Blax\Shop\Tests\TestCase;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Workbench\App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class BookingFeatureTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
protected Product $bookingProduct;
|
||||
protected ProductPrice $price;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->user = User::factory()->create();
|
||||
|
||||
// Create a booking product
|
||||
$this->bookingProduct = Product::factory()->create([
|
||||
'name' => 'Hotel Room',
|
||||
'slug' => 'hotel-room',
|
||||
'type' => ProductType::BOOKING,
|
||||
'manage_stock' => true,
|
||||
'stock_quantity' => 0,
|
||||
]);
|
||||
|
||||
// Initialize stock
|
||||
$this->bookingProduct->increaseStock(10);
|
||||
|
||||
// Create a price
|
||||
$this->price = ProductPrice::factory()->create([
|
||||
'purchasable_id' => $this->bookingProduct->id,
|
||||
'purchasable_type' => Product::class,
|
||||
'unit_amount' => 15000, // $150.00
|
||||
'currency' => 'USD',
|
||||
'is_default' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_create_a_booking_product()
|
||||
{
|
||||
$this->assertNotNull($this->bookingProduct);
|
||||
$this->assertEquals(ProductType::BOOKING, $this->bookingProduct->type);
|
||||
$this->assertTrue($this->bookingProduct->isBooking());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_purchase_a_booking_with_dates()
|
||||
{
|
||||
$from = Carbon::now()->addDays(1);
|
||||
$until = Carbon::now()->addDays(3);
|
||||
|
||||
$purchase = $this->user->purchase(
|
||||
$this->price,
|
||||
1,
|
||||
null,
|
||||
$from,
|
||||
$until
|
||||
);
|
||||
|
||||
$this->assertNotNull($purchase);
|
||||
$this->assertEquals($this->bookingProduct->id, $purchase->purchasable_id);
|
||||
$this->assertTrue($purchase->isBooking());
|
||||
$this->assertEquals($from->format('Y-m-d H:i:s'), $purchase->from->format('Y-m-d H:i:s'));
|
||||
$this->assertEquals($until->format('Y-m-d H:i:s'), $purchase->until->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_throws_exception_when_booking_without_dates()
|
||||
{
|
||||
$this->expectException(\Exception::class);
|
||||
$this->expectExceptionMessage("Booking products require 'from' and 'until' dates");
|
||||
|
||||
$this->user->purchase($this->price, 1);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_decreases_stock_for_booking_duration()
|
||||
{
|
||||
$from = Carbon::now()->addDays(1);
|
||||
$until = Carbon::now()->addDays(3);
|
||||
|
||||
$initialStock = $this->bookingProduct->getAvailableStock();
|
||||
|
||||
$purchase = $this->user->purchase(
|
||||
$this->price,
|
||||
2,
|
||||
null,
|
||||
$from,
|
||||
$until
|
||||
);
|
||||
|
||||
// Stock should be decreased
|
||||
$this->bookingProduct->refresh();
|
||||
$remainingStock = $this->bookingProduct->getAvailableStock();
|
||||
$this->assertEquals($initialStock - 2, $remainingStock);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_releases_stock_after_booking_period()
|
||||
{
|
||||
$from = Carbon::now()->subDays(3);
|
||||
$until = Carbon::now()->subDays(1); // Booking ended yesterday
|
||||
|
||||
$initialStock = $this->bookingProduct->getAvailableStock();
|
||||
|
||||
$purchase = $this->user->purchase(
|
||||
$this->price,
|
||||
2,
|
||||
null,
|
||||
$from,
|
||||
$until
|
||||
);
|
||||
|
||||
// Find the stock reservation
|
||||
$reservation = $this->bookingProduct->stocks()
|
||||
->where('type', 'decrease')
|
||||
->where('expires_at', $until)
|
||||
->first();
|
||||
|
||||
$this->assertNotNull($reservation);
|
||||
$this->assertEquals($until->format('Y-m-d H:i:s'), $reservation->expires_at->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_check_booking_availability()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5);
|
||||
$until = Carbon::now()->addDays(7);
|
||||
|
||||
// Should be available (stock is 10)
|
||||
$this->assertTrue($this->bookingProduct->isAvailableForBooking($from, $until, 5));
|
||||
|
||||
// Should not be available (requesting more than available)
|
||||
$this->assertFalse($this->bookingProduct->isAvailableForBooking($from, $until, 15));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_add_booking_to_cart_with_dates()
|
||||
{
|
||||
$cart = $this->user->currentCart();
|
||||
|
||||
$from = Carbon::now()->addDays(10);
|
||||
$until = Carbon::now()->addDays(12);
|
||||
|
||||
$cartItem = $cart->addToCart(
|
||||
$this->bookingProduct,
|
||||
1,
|
||||
[
|
||||
'from' => $from->toDateTimeString(),
|
||||
'until' => $until->toDateTimeString(),
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertNotNull($cartItem);
|
||||
$this->assertEquals($this->bookingProduct->id, $cartItem->purchasable_id);
|
||||
$this->assertNotNull($cartItem->parameters);
|
||||
$this->assertIsArray($cartItem->parameters);
|
||||
$this->assertEquals($from->toDateTimeString(), $cartItem->parameters['from']);
|
||||
$this->assertEquals($until->toDateTimeString(), $cartItem->parameters['until']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_checkout_cart_with_booking_product()
|
||||
{
|
||||
$cart = $this->user->currentCart();
|
||||
|
||||
$from = Carbon::now()->addDays(15);
|
||||
$until = Carbon::now()->addDays(17);
|
||||
|
||||
$cart->addToCart(
|
||||
$this->bookingProduct,
|
||||
2,
|
||||
[
|
||||
'from' => $from->toDateTimeString(),
|
||||
'until' => $until->toDateTimeString(),
|
||||
]
|
||||
);
|
||||
|
||||
$initialStock = $this->bookingProduct->getAvailableStock();
|
||||
|
||||
$cart->checkout();
|
||||
|
||||
$this->assertTrue($cart->isConverted());
|
||||
|
||||
// Check that purchase was created with dates
|
||||
$purchase = ProductPurchase::where('cart_id', $cart->id)->first();
|
||||
$this->assertNotNull($purchase);
|
||||
$this->assertTrue($purchase->isBooking());
|
||||
$this->assertEquals($from->format('Y-m-d H:i:s'), $purchase->from->format('Y-m-d H:i:s'));
|
||||
$this->assertEquals($until->format('Y-m-d H:i:s'), $purchase->until->format('Y-m-d H:i:s'));
|
||||
|
||||
// Stock should be decreased
|
||||
$this->bookingProduct->refresh();
|
||||
$this->assertEquals($initialStock - 2, $this->bookingProduct->getAvailableStock());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_prevents_overbooking()
|
||||
{
|
||||
$from = Carbon::now()->addDays(20);
|
||||
$until = Carbon::now()->addDays(22);
|
||||
|
||||
// Book 8 units
|
||||
$this->user->purchase(
|
||||
$this->price,
|
||||
8,
|
||||
null,
|
||||
$from,
|
||||
$until
|
||||
);
|
||||
|
||||
// Try to book 5 more units (would exceed stock of 10)
|
||||
$this->expectException(\Exception::class);
|
||||
|
||||
$this->user->purchase(
|
||||
$this->price,
|
||||
5,
|
||||
null,
|
||||
$from,
|
||||
$until
|
||||
);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_scope_booking_purchases()
|
||||
{
|
||||
$from = Carbon::now()->addDays(25);
|
||||
$until = Carbon::now()->addDays(27);
|
||||
|
||||
// Create a regular product purchase
|
||||
$regularProduct = Product::factory()->create([
|
||||
'type' => ProductType::SIMPLE,
|
||||
'manage_stock' => false,
|
||||
]);
|
||||
$regularPrice = ProductPrice::factory()->create([
|
||||
'purchasable_id' => $regularProduct->id,
|
||||
'purchasable_type' => Product::class,
|
||||
'unit_amount' => 5000,
|
||||
'is_default' => true,
|
||||
]);
|
||||
$this->user->purchase($regularPrice, 1);
|
||||
|
||||
// Create a booking purchase
|
||||
$this->user->purchase(
|
||||
$this->price,
|
||||
1,
|
||||
null,
|
||||
$from,
|
||||
$until
|
||||
);
|
||||
|
||||
$bookingPurchases = ProductPurchase::bookings()->get();
|
||||
$this->assertCount(1, $bookingPurchases);
|
||||
$this->assertTrue($bookingPurchases->first()->isBooking());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_scope_ended_bookings()
|
||||
{
|
||||
$pastFrom = Carbon::now()->subDays(5);
|
||||
$pastUntil = Carbon::now()->subDays(2);
|
||||
|
||||
$futureFrom = Carbon::now()->addDays(1);
|
||||
$futureUntil = Carbon::now()->addDays(3);
|
||||
|
||||
// Create past booking
|
||||
$pastPurchase = $this->user->purchase(
|
||||
$this->price,
|
||||
1,
|
||||
null,
|
||||
$pastFrom,
|
||||
$pastUntil
|
||||
);
|
||||
|
||||
// Create future booking
|
||||
$futurePurchase = $this->user->purchase(
|
||||
$this->price,
|
||||
1,
|
||||
null,
|
||||
$futureFrom,
|
||||
$futureUntil
|
||||
);
|
||||
|
||||
$endedBookings = ProductPurchase::endedBookings()->get();
|
||||
|
||||
$this->assertCount(1, $endedBookings);
|
||||
$this->assertEquals($pastPurchase->id, $endedBookings->first()->id);
|
||||
$this->assertTrue($endedBookings->first()->isBookingEnded());
|
||||
$this->assertFalse($futurePurchase->isBookingEnded());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_scope_booking_products()
|
||||
{
|
||||
// Create regular product
|
||||
Product::factory()->create([
|
||||
'type' => ProductType::SIMPLE,
|
||||
]);
|
||||
|
||||
$bookingProducts = Product::bookings()->get();
|
||||
$this->assertCount(1, $bookingProducts);
|
||||
$this->assertEquals($this->bookingProduct->id, $bookingProducts->first()->id);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function booking_stock_expires_after_until_date()
|
||||
{
|
||||
$from = Carbon::now()->addDays(1);
|
||||
$until = Carbon::now()->addDays(2);
|
||||
|
||||
$purchase = $this->user->purchase(
|
||||
$this->price,
|
||||
3,
|
||||
null,
|
||||
$from,
|
||||
$until
|
||||
);
|
||||
|
||||
// Find the stock decrease record
|
||||
$stockRecord = $this->bookingProduct->stocks()
|
||||
->where('type', 'decrease')
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
$this->assertNotNull($stockRecord);
|
||||
$this->assertNotNull($stockRecord->expires_at);
|
||||
$this->assertEquals($until->format('Y-m-d H:i:s'), $stockRecord->expires_at->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function multiple_bookings_with_different_dates_work_independently()
|
||||
{
|
||||
$booking1From = Carbon::now()->addDays(1);
|
||||
$booking1Until = Carbon::now()->addDays(3);
|
||||
|
||||
$booking2From = Carbon::now()->addDays(5);
|
||||
$booking2Until = Carbon::now()->addDays(7);
|
||||
|
||||
$purchase1 = $this->user->purchase(
|
||||
$this->price,
|
||||
2,
|
||||
null,
|
||||
$booking1From,
|
||||
$booking1Until
|
||||
);
|
||||
|
||||
$purchase2 = $this->user->purchase(
|
||||
$this->price,
|
||||
3,
|
||||
null,
|
||||
$booking2From,
|
||||
$booking2Until
|
||||
);
|
||||
|
||||
$this->assertNotNull($purchase1);
|
||||
$this->assertNotNull($purchase2);
|
||||
$this->assertNotEquals($purchase1->from, $purchase2->from);
|
||||
$this->assertNotEquals($purchase1->until, $purchase2->until);
|
||||
|
||||
// Both should have decreased stock
|
||||
$this->bookingProduct->refresh();
|
||||
$this->assertEquals(5, $this->bookingProduct->getAvailableStock()); // 10 - 2 - 3
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Blax\Shop\Tests\Feature;
|
||||
|
||||
use Blax\Shop\Enums\PurchaseStatus;
|
||||
use Blax\Shop\Exceptions\MultiplePurchaseOptions;
|
||||
use Blax\Shop\Exceptions\NotPurchasable;
|
||||
use Blax\Shop\Models\Product;
|
||||
|
|
@ -123,7 +124,7 @@ class HasShoppingCapabilitiesTest extends TestCase
|
|||
$completed = $user->completedPurchases;
|
||||
|
||||
$this->assertCount(1, $completed);
|
||||
$this->assertEquals('completed', $completed->first()->status);
|
||||
$this->assertEquals(PurchaseStatus::COMPLETED, $completed->first()->status);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Blax\Shop\Tests\Feature;
|
||||
|
||||
use Blax\Shop\Enums\ProductStatus;
|
||||
use Blax\Shop\Enums\ProductType;
|
||||
use Blax\Shop\Models\Product;
|
||||
use Blax\Shop\Models\ProductCategory;
|
||||
use Blax\Shop\Models\ProductAttribute;
|
||||
|
|
@ -130,8 +132,8 @@ class ProductManagementTest extends TestCase
|
|||
ProductPrice::create([
|
||||
'purchasable_id' => $product->id,
|
||||
'purchasable_type' => get_class($product),
|
||||
'type' => 'one-time',
|
||||
'price' => 9999,
|
||||
'type' => 'one_time',
|
||||
'unit_amount' => 9999,
|
||||
'currency' => 'USD',
|
||||
'active' => true,
|
||||
]);
|
||||
|
|
@ -197,7 +199,7 @@ class ProductManagementTest extends TestCase
|
|||
$published = Product::published()->get();
|
||||
|
||||
$this->assertCount(1, $published);
|
||||
$this->assertEquals('published', $published->first()->status);
|
||||
$this->assertEquals(ProductStatus::PUBLISHED, $published->first()->status);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -243,11 +245,11 @@ class ProductManagementTest extends TestCase
|
|||
public function it_can_have_parent_child_relationships()
|
||||
{
|
||||
$parent = Product::factory()->create([
|
||||
'type' => 'variable',
|
||||
'type' => ProductType::VARIABLE,
|
||||
]);
|
||||
|
||||
$child = Product::factory()->create([
|
||||
'type' => 'variation',
|
||||
'type' => ProductType::VARIABLE,
|
||||
'parent_id' => $parent->id,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
namespace Blax\Shop\Tests\Feature;
|
||||
|
||||
use Blax\Shop\Enums\BillingScheme;
|
||||
use Blax\Shop\Enums\PriceType;
|
||||
use Blax\Shop\Enums\RecurringInterval;
|
||||
use Blax\Shop\Models\Product;
|
||||
use Blax\Shop\Models\ProductPrice;
|
||||
use Blax\Shop\Tests\TestCase;
|
||||
|
|
@ -90,8 +93,8 @@ class ProductPriceTest extends TestCase
|
|||
'trial_period_days' => 14,
|
||||
]);
|
||||
|
||||
$this->assertEquals('recurring', $price->type);
|
||||
$this->assertEquals('month', $price->interval);
|
||||
$this->assertEquals(PriceType::RECURRING, $price->type);
|
||||
$this->assertEquals(RecurringInterval::MONTH, $price->interval);
|
||||
$this->assertEquals(1, $price->interval_count);
|
||||
$this->assertEquals(14, $price->trial_period_days);
|
||||
}
|
||||
|
|
@ -109,7 +112,7 @@ class ProductPriceTest extends TestCase
|
|||
'type' => 'one_time',
|
||||
]);
|
||||
|
||||
$this->assertEquals('one_time', $price->type);
|
||||
$this->assertEquals(PriceType::ONE_TIME, $price->type);
|
||||
$this->assertNull($price->interval);
|
||||
}
|
||||
|
||||
|
|
@ -274,7 +277,7 @@ class ProductPriceTest extends TestCase
|
|||
'billing_scheme' => 'per_unit',
|
||||
]);
|
||||
|
||||
$this->assertEquals('tiered', $tieredPrice->billing_scheme);
|
||||
$this->assertEquals('per_unit', $perUnitPrice->billing_scheme);
|
||||
$this->assertEquals(BillingScheme::TIERED, $tieredPrice->billing_scheme);
|
||||
$this->assertEquals(BillingScheme::PER_UNIT, $perUnitPrice->billing_scheme);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Blax\Shop\Tests\Feature;
|
||||
|
||||
use Blax\Shop\Enums\PurchaseStatus;
|
||||
use Blax\Shop\Models\Product;
|
||||
use Blax\Shop\Models\ProductPurchase;
|
||||
use Blax\Shop\Tests\TestCase;
|
||||
|
|
@ -109,8 +110,8 @@ class ProductPurchaseTest extends TestCase
|
|||
'status' => 'completed',
|
||||
]);
|
||||
|
||||
$this->assertEquals('unpaid', $unpaidPurchase->status);
|
||||
$this->assertEquals('completed', $completedPurchase->status);
|
||||
$this->assertEquals(PurchaseStatus::UNPAID, $unpaidPurchase->status);
|
||||
$this->assertEquals(PurchaseStatus::COMPLETED, $completedPurchase->status);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -144,7 +145,7 @@ class ProductPurchaseTest extends TestCase
|
|||
$completed = ProductPurchase::completed()->get();
|
||||
|
||||
$this->assertCount(1, $completed);
|
||||
$this->assertEquals('completed', $completed->first()->status);
|
||||
$this->assertEquals(PurchaseStatus::COMPLETED, $completed->first()->status);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -178,7 +179,7 @@ class ProductPurchaseTest extends TestCase
|
|||
$inCart = ProductPurchase::inCart()->get();
|
||||
|
||||
$this->assertCount(1, $inCart);
|
||||
$this->assertEquals('cart', $inCart->first()->status);
|
||||
$this->assertEquals(PurchaseStatus::CART, $inCart->first()->status);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -235,7 +236,7 @@ class ProductPurchaseTest extends TestCase
|
|||
]);
|
||||
|
||||
$this->assertEquals(5000, $purchase->fresh()->amount_paid);
|
||||
$this->assertEquals('completed', $purchase->fresh()->status);
|
||||
$this->assertEquals(PurchaseStatus::COMPLETED, $purchase->fresh()->status);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Blax\Shop\Tests\Feature;
|
||||
|
||||
use Blax\Shop\Enums\ProductType;
|
||||
use Blax\Shop\Models\Product;
|
||||
use Blax\Shop\Models\ProductCategory;
|
||||
use Blax\Shop\Tests\TestCase;
|
||||
|
|
@ -275,12 +276,12 @@ class ProductScopeTest extends TestCase
|
|||
/** @test */
|
||||
public function it_can_scope_products_by_type()
|
||||
{
|
||||
Product::factory()->create(['type' => 'simple']);
|
||||
Product::factory()->create(['type' => 'simple']);
|
||||
Product::factory()->create(['type' => 'variable']);
|
||||
Product::factory()->create(['type' => 'variation']);
|
||||
Product::factory()->create(['type' => ProductType::SIMPLE]);
|
||||
Product::factory()->create(['type' => ProductType::SIMPLE]);
|
||||
Product::factory()->create(['type' => ProductType::VARIABLE]);
|
||||
Product::factory()->create(['type' => ProductType::VARIABLE]);
|
||||
|
||||
$simpleProducts = Product::where('type', 'simple')->get();
|
||||
$simpleProducts = Product::where('type', ProductType::SIMPLE)->get();
|
||||
|
||||
$this->assertCount(2, $simpleProducts);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Blax\Shop\Tests\Feature;
|
||||
|
||||
use Blax\Shop\Enums\StockStatus;
|
||||
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||
use Blax\Shop\Models\Product;
|
||||
use Blax\Shop\Models\ProductStock;
|
||||
|
|
@ -216,7 +217,7 @@ class ProductStockTest extends TestCase
|
|||
|
||||
$stock = $product->stocks()->first();
|
||||
|
||||
$this->assertEquals('completed', $stock->status);
|
||||
$this->assertEquals(StockStatus::COMPLETED, $stock->status);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Blax\Shop\Tests\Feature;
|
||||
|
||||
use Blax\Shop\Enums\PurchaseStatus;
|
||||
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||
use Blax\Shop\Models\Product;
|
||||
use Blax\Shop\Models\ProductPurchase;
|
||||
|
|
@ -37,7 +38,7 @@ class PurchaseFlowTest extends TestCase
|
|||
$this->assertEquals($product->id, $purchase->purchasable_id);
|
||||
$this->assertEquals($user->id, $purchase->purchaser_id);
|
||||
$this->assertEquals(1, $purchase->quantity);
|
||||
$this->assertEquals('unpaid', $purchase->status);
|
||||
$this->assertEquals(PurchaseStatus::UNPAID, $purchase->status);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -126,8 +127,8 @@ class PurchaseFlowTest extends TestCase
|
|||
$purchases = $cart->purchases;
|
||||
|
||||
$this->assertCount(2, $purchases);
|
||||
$this->assertEquals('unpaid', $purchases[0]->status);
|
||||
$this->assertEquals('unpaid', $purchases[1]->status);
|
||||
$this->assertEquals(PurchaseStatus::UNPAID, $purchases[0]->status);
|
||||
$this->assertEquals(PurchaseStatus::UNPAID, $purchases[1]->status);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Blax\Shop\Tests\Unit;
|
||||
|
||||
use Blax\Shop\Enums\CartStatus;
|
||||
use Blax\Shop\Models\Cart;
|
||||
use Blax\Shop\Models\Product;
|
||||
use Blax\Shop\Models\ProductPrice;
|
||||
|
|
@ -52,8 +53,8 @@ class CartTest extends TestCase
|
|||
public function cart_respects_sale_prices()
|
||||
{
|
||||
$cart = Cart::create();
|
||||
$product = Product::factory()->withPrices(1,50)->create([
|
||||
'sale_start' => now()->subDay(),
|
||||
$product = Product::factory()->withPrices(1, 50)->create([
|
||||
'sale_start' => now()->subDay(),
|
||||
'sale_end' => now()->addDay(),
|
||||
]);
|
||||
|
||||
|
|
@ -109,7 +110,7 @@ class CartTest extends TestCase
|
|||
public function cart_total_sums_all_items()
|
||||
{
|
||||
$cart = Cart::create();
|
||||
|
||||
|
||||
$product1 = Product::factory()->create();
|
||||
$price1 = ProductPrice::create([
|
||||
'purchasable_id' => $product1->id,
|
||||
|
|
@ -225,14 +226,14 @@ class CartTest extends TestCase
|
|||
public function cart_can_have_status()
|
||||
{
|
||||
$cart = Cart::create([
|
||||
'status' => 'pending',
|
||||
'status' => CartStatus::ACTIVE,
|
||||
]);
|
||||
|
||||
$this->assertEquals('pending', $cart->status);
|
||||
$this->assertEquals(CartStatus::ACTIVE, $cart->status);
|
||||
|
||||
$cart->update(['status' => 'completed']);
|
||||
$cart->update(['status' => CartStatus::CONVERTED]);
|
||||
|
||||
$this->assertEquals('completed', $cart->fresh()->status);
|
||||
$this->assertEquals(CartStatus::CONVERTED, $cart->fresh()->status);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
|||
Loading…
Reference in New Issue