From 7db6f8047e04a45ebeb454db5dadcd026e03d20e Mon Sep 17 00:00:00 2001 From: "Fabian @ Blax Software" Date: Thu, 4 Dec 2025 11:16:38 +0100 Subject: [PATCH] IR stocks documentation, C github tests --- .github/workflows/tests.yml | 6 +- src/Enums/StockStatus.php | 27 +++++ src/Enums/StockType.php | 24 ++++ src/Models/ProductStock.php | 130 +++++++++++++++++++- src/Traits/HasStocks.php | 185 ++++++++++++++++++++++++++++- tests/Feature/ProductStockTest.php | 161 +++++++++++++++++++++++++ 6 files changed, 525 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a954151..01f6928 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,9 +2,9 @@ name: Tests on: push: - branches: [ master, develop ] + branches: [ master ] pull_request: - branches: [ master, develop ] + branches: [ master, dev ] jobs: test: @@ -64,4 +64,4 @@ jobs: composer run-script post-autoload-dump - name: Execute tests - run: vendor/bin/phpunit --testdox + run: vendor/bin/phpunit diff --git a/src/Enums/StockStatus.php b/src/Enums/StockStatus.php index 7e312b6..b43c2aa 100644 --- a/src/Enums/StockStatus.php +++ b/src/Enums/StockStatus.php @@ -2,6 +2,33 @@ namespace Blax\Shop\Enums; +/** + * StockStatus Enum + * + * Defines the lifecycle status of stock entries. + * + * Statuses: + * - PENDING: Stock claim is active but not yet finalized + * Used for: Active reservations, bookings, cart claims + * Can be: Released (changed to COMPLETED) or Cancelled + * Effect: Stock is allocated but tracked as claimed + * + * - COMPLETED: Stock movement is finalized + * Used for: Physical stock changes (INCREASE/DECREASE/RETURN) + * Also for: Released claims (no longer active) + * Effect: Counted as physical stock, cannot be modified + * + * - CANCELLED: Stock entry was cancelled + * Used for: Cancelled reservations, voided transactions + * Effect: Not counted in any calculations + * + * Typical Flow: + * 1. Claim created -> PENDING + * 2. Claim released -> COMPLETED + * 3. Or claim cancelled -> CANCELLED + * + * Physical stock changes (INCREASE/DECREASE) are always COMPLETED. + */ enum StockStatus: string { case PENDING = 'pending'; diff --git a/src/Enums/StockType.php b/src/Enums/StockType.php index ccb208e..7bd2b18 100644 --- a/src/Enums/StockType.php +++ b/src/Enums/StockType.php @@ -2,6 +2,30 @@ namespace Blax\Shop\Enums; +/** + * StockType Enum + * + * Defines the types of stock movements that can occur. + * + * Types: + * - CLAIMED: Stock claimed for reservation/booking (creates PENDING entry) + * Used for temporary allocations that can be released + * Examples: hotel bookings, equipment rentals, cart reservations + * + * - RETURN: Stock returned to inventory (e.g., customer returns) + * Creates a positive adjustment to physical stock + * + * - INCREASE: Stock added to inventory (e.g., new purchases, restocking) + * Creates a positive adjustment to physical stock + * + * - DECREASE: Stock removed from inventory (e.g., sales, damage, loss) + * Creates a negative adjustment to physical stock + * + * Usage Flow: + * 1. INCREASE/DECREASE: Direct physical stock changes (COMPLETED status) + * 2. CLAIMED: Temporary allocation (PENDING status, can be released) + * 3. RETURN: Special case of INCREASE for returned items + */ enum StockType: string { case CLAIMED = 'claimed'; diff --git a/src/Models/ProductStock.php b/src/Models/ProductStock.php index 25a9e82..7b77472 100644 --- a/src/Models/ProductStock.php +++ b/src/Models/ProductStock.php @@ -12,6 +12,33 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Support\Facades\DB; +/** + * ProductStock Model + * + * Represents stock movements and claims for products. This model tracks: + * - Stock increases/decreases (physical inventory changes) + * - Stock claims (reservations/bookings for future use) + * - Temporary vs permanent stock allocations + * - Date-based stock availability for bookings/rentals + * + * Stock Flow: + * 1. INCREASE/DECREASE: Physical stock changes (COMPLETED status) + * - Positive quantity = stock added to inventory + * - Negative quantity = stock removed from inventory + * + * 2. CLAIMED: Temporary allocation of stock (PENDING status) + * - Creates a DECREASE entry (negative quantity, COMPLETED) + * - Creates a CLAIMED entry (positive quantity, PENDING) + * - Net effect: stock is allocated but tracked separately + * - Can have claimed_from (when claim starts) and expires_at (when claim ends) + * - When released: CLAIMED status changes to COMPLETED + * + * Key Concepts: + * - Physical Stock: Sum of all COMPLETED status stocks (includes INCREASE/DECREASE) + * - Available Stock: Physical stock minus currently PENDING claims + * - Claimed Stock: Sum of PENDING claims (temporarily unavailable) + * - Available on Date: Available stock considering only claims active on specific date + */ class ProductStock extends Model { use HasUuids, HasExpiration; @@ -49,47 +76,87 @@ class ProductStock extends Model }); } + /** + * Get the product this stock entry belongs to + */ public function product(): BelongsTo { return $this->belongsTo(config('shop.models.product', Product::class)); } + /** + * Get the related model (e.g., Order, User, Booking) that triggered this stock change + * Used to track what caused the stock movement or claim + */ public function reference(): MorphTo { return $this->morphTo(); } + /** + * Scope: Get stock entries that are still pending (claims not yet released) + */ public function scopePending($query) { return $query->where('status', StockStatus::PENDING->value); } + /** + * Scope: Get stock entries that have been released/completed + */ public function scopeReleased($query) { return $query->where('status', StockStatus::COMPLETED->value); } + /** + * Scope: Get temporary stock entries (with expiration date) + */ public function scopeTemporary($query) { return $query->whereNotNull('expires_at'); } + /** + * Scope: Get permanent stock entries (no expiration date) + */ public function scopePermanent($query) { return $query->whereNull('expires_at'); } - // Backward compatibility accessors + /** + * Backward compatibility accessor: Get when the stock was released + * Returns updated_at if status is COMPLETED, otherwise null + */ public function getReleasedAtAttribute() { return $this->status === StockStatus::COMPLETED ? $this->updated_at : null; } + /** + * Backward compatibility accessor: Alias for expires_at + */ public function getUntilAtAttribute() { return $this->expires_at; } + /** + * Claim stock for a product (reservation/booking) + * + * This creates a two-part entry: + * 1. DECREASE entry (negative quantity, COMPLETED) - removes from physical stock + * 2. CLAIMED entry (positive quantity, PENDING) - tracks the claim + * + * @param Product $product The product to claim stock from + * @param int $quantity Amount of stock to claim + * @param mixed $reference Optional reference model (Order, Booking, etc.) + * @param \DateTimeInterface|null $from When the claim starts (null = immediately) + * @param \DateTimeInterface|null $until When the claim expires (null = permanent) + * @param string|null $note Optional note about the claim + * @return self|null The created claim entry, or null if insufficient stock + */ public static function claim( Product $product, int $quantity, @@ -117,6 +184,15 @@ class ProductStock extends Model }); } + /** + * Release a claimed stock entry + * + * Changes status from PENDING to COMPLETED, marking the claim as released. + * Note: This does NOT add stock back - the stock remains decreased. + * To return stock to inventory, use increaseStock() on the product. + * + * @return bool True if released successfully, false if not pending + */ public function release(): bool { if ($this->status !== StockStatus::PENDING) { @@ -131,16 +207,26 @@ class ProductStock extends Model }); } + /** + * Check if this is a permanent stock entry (no expiration) + */ public function isPermanent(): bool { return is_null($this->expires_at); } + /** + * Check if this is a temporary stock entry (has expiration date) + */ public function isTemporary(): bool { return !is_null($this->expires_at); } + /** + * Check if this temporary claim has expired + * Only applies to PENDING claims with past expiration dates + */ public function isExpired(): bool { return $this->isTemporary() @@ -148,11 +234,18 @@ class ProductStock extends Model && $this->expires_at->isPast(); } + /** + * Check if this claim is currently active (PENDING status) + */ public function isActive(): bool { return $this->status === StockStatus::PENDING; } + /** + * Log stock changes to the product_stock_logs table + * Provides audit trail of all stock movements + */ protected function logStockChange(): void { if (!config('shop.stock.log_changes', true)) { @@ -172,6 +265,12 @@ class ProductStock extends Model ]); } + /** + * Release all expired stock claims + * Used by scheduled command to automatically release expired claims + * + * @return int Number of claims released + */ public static function releaseExpired(): int { $expired = self::expired()->get(); @@ -186,21 +285,48 @@ class ProductStock extends Model return $count; } + /** + * Scope: Get completed/available stock entries + * These are physical stock changes (INCREASE/DECREASE) that have been finalized + */ public static function scopeAvailable($query) { return $query->where('status', StockStatus::COMPLETED->value); } + /** + * Scope: Get active (pending) claimed stock entries + * These represent stock currently claimed but not yet released + */ public static function scopeAvailableClaims($query) { return $query->where('type', StockType::CLAIMED->value)->where('status', StockStatus::PENDING->value); } + /** + * Get all active claims (alias for availableClaims) + */ public static function claims() { return self::availableClaims(); } + /** + * Scope: Get stock claims that are active on a specific date + * + * Used for date-based availability checking (bookings, rentals, etc.) + * A claim is considered active on a date if: + * - It has claimed_from <= date (or null = immediate) AND + * - It has expires_at >= date (or null = permanent) + * - Status is PENDING + * + * Examples: + * - Claim from day 5-10: Active on days 5,6,7,8,9,10 + * - Claim with no claimed_from, expires day 10: Active from creation until day 10 + * - Claim from day 5, no expires_at: Active from day 5 forever + * + * @param \DateTimeInterface $date The date to check availability for + */ public static function scopeAvailableOnDate($query, \DateTimeInterface $date) { return $query->where('type', StockType::CLAIMED->value) @@ -215,7 +341,7 @@ class ProductStock extends Model ->orWhere('expires_at', '>=', $date); }); })->orWhere(function ($subQuery) use ($date) { - // Claimed items without claimed_from (always claimed) + // Claimed items without claimed_from (immediately claimed) $subQuery->whereNull('claimed_from') ->where(function ($dateQuery) use ($date) { $dateQuery->whereNull('expires_at') diff --git a/src/Traits/HasStocks.php b/src/Traits/HasStocks.php index 8e83d10..bf06ef9 100644 --- a/src/Traits/HasStocks.php +++ b/src/Traits/HasStocks.php @@ -9,13 +9,48 @@ use Carbon\Carbon; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Facades\DB; +/** + * HasStocks Trait + * + * Provides stock management functionality to Product models. + * + * Key Features: + * - Basic stock operations (increase, decrease, adjust) + * - Stock claims for bookings/reservations + * - Date-based availability checking + * - Low stock detection + * - Stock movement logging + * + * Usage: + * - Add 'manage_stock' boolean column to products table + * - Set manage_stock = true to enable stock tracking + * - Use increaseStock/decreaseStock for inventory changes + * - Use claimStock for reservations/bookings + * - Use availableOnDate for date-based availability + * + * Stock Calculation: + * - Physical Stock = Sum of all COMPLETED entries + * - Available Stock = Physical Stock (accounts for pending claims via their DECREASE entries) + * - Claimed Stock = Sum of PENDING claims + * - Available on Date = Available Stock + All Claims - Claims Active on Date + */ trait HasStocks { + /** + * Get all stock entries for this product + */ public function stocks(): HasMany { return $this->hasMany(config('shop.models.product_stock', 'Blax\Shop\Models\ProductStock')); } + /** + * Attribute accessor: Get available physical stock + * + * Sums all COMPLETED stock entries that haven't expired. + * This includes INCREASE (+), DECREASE (-), and released claims. + * Does NOT include PENDING claims (they're tracked separately). + */ public function getAvailableStocksAttribute(): int { return $this->stocks() @@ -24,6 +59,11 @@ trait HasStocks ->sum('quantity') ?? 0; } + /** + * Check if product is in stock + * + * @return bool True if stock management is disabled OR available stock > 0 + */ public function isInStock(): bool { if (!$this->manage_stock) { @@ -33,6 +73,17 @@ trait HasStocks return $this->getAvailableStock() > 0; } + /** + * Decrease physical stock (inventory reduction) + * + * Creates a DECREASE entry with negative quantity and COMPLETED status. + * This represents actual stock leaving the inventory (sold, damaged, etc.). + * + * @param int $quantity Amount to decrease + * @param Carbon|null $until Optional expiration (for temporary decreases) + * @return bool True if successful + * @throws NotEnoughStockException If insufficient stock available + */ public function decreaseStock(int $quantity = 1, Carbon|null $until = null): bool { if (!$this->manage_stock) { @@ -57,6 +108,15 @@ trait HasStocks return true; } + /** + * Increase physical stock (inventory addition) + * + * Creates an INCREASE entry with positive quantity and COMPLETED status. + * This represents stock being added to inventory (purchased, returned, etc.). + * + * @param int $quantity Amount to increase + * @return bool True if successful, false if stock management disabled + */ public function increaseStock(int $quantity = 1): bool { if (!$this->manage_stock) { @@ -76,6 +136,20 @@ trait HasStocks return true; } + /** + * Adjust stock with custom type and status + * + * More flexible than increaseStock/decreaseStock, allows: + * - Custom stock type (INCREASE, DECREASE, RETURN) + * - Custom status (defaults to COMPLETED) + * - Optional expiration date + * + * @param StockType $type The type of adjustment + * @param int $quantity Amount to adjust (always positive, type determines direction) + * @param \DateTimeInterface|null $until Optional expiration date + * @param StockStatus|null $status Optional status (defaults to COMPLETED) + * @return bool True if successful, false if stock management disabled + */ public function adjustStock( StockType $type, int $quantity, @@ -86,20 +160,45 @@ trait HasStocks return false; } + // INCREASE and RETURN add stock (positive), DECREASE and CLAIMED remove stock (negative) + $isPositive = in_array($type, [StockType::INCREASE, StockType::RETURN]); + $adjustedQuantity = $isPositive ? $quantity : -$quantity; + $this->stocks()->create([ - 'quantity' => $type === StockType::INCREASE ? $quantity : -$quantity, + 'quantity' => $adjustedQuantity, 'type' => $type, 'status' => $status ?? StockStatus::COMPLETED, 'expires_at' => $until, ]); - $this->logStockChange($type === StockType::INCREASE ? $quantity : -$quantity, 'adjust'); + $this->logStockChange($adjustedQuantity, 'adjust'); $this->save(); return true; } + /** + * Claim stock for temporary use (reservation/booking) + * + * This is different from decreaseStock - it: + * 1. Removes stock from available inventory (via DECREASE entry) + * 2. Tracks it as a claim (via CLAIMED entry with PENDING status) + * 3. Can be released back later (changes CLAIMED to COMPLETED) + * 4. Supports date ranges for bookings (claimed_from to expires_at) + * + * Use cases: + * - Hotel room bookings (claimed_from = check-in, expires_at = check-out) + * - Equipment rentals (claimed_from = rental start, expires_at = return date) + * - Cart reservations (no claimed_from, expires_at = cart expiry) + * + * @param int $quantity Amount to claim + * @param mixed $reference Optional reference model (Order, Booking, Cart, etc.) + * @param \DateTimeInterface|null $from When claim starts (null = immediately) + * @param \DateTimeInterface|null $until When claim expires (null = permanent) + * @param string|null $note Optional note about the claim + * @return \Blax\Shop\Models\ProductStock|null The claim entry, or null if insufficient stock + */ public function claimStock( int $quantity, $reference = null, @@ -124,6 +223,14 @@ trait HasStocks ); } + /** + * Get currently available stock + * + * This is the stock available for new orders/claims. + * Calculated as: Sum of all COMPLETED entries (includes DECREASE from active claims) + * + * @return int Available quantity (PHP_INT_MAX if stock management disabled) + */ public function getAvailableStock(): int { if (!$this->manage_stock) { @@ -133,11 +240,28 @@ trait HasStocks return max(0, $this->AvailableStocks); } + /** + * Get total currently claimed stock + * + * Sum of all active (PENDING) claims. + * This stock is unavailable but tracked separately from physical inventory. + * + * @return int Total claimed quantity + */ public function getClaimedStock(): int { - return $this->activeStocks()->sum('quantity'); + return $this->stocks() + ->where('type', StockType::CLAIMED->value) + ->where('status', StockStatus::PENDING->value) + ->sum('quantity'); } + /** + * Log a stock change to the audit log + * + * @param int $quantityChange The change in quantity (positive or negative) + * @param string $type The type of change (increase, decrease, adjust) + */ protected function logStockChange(int $quantityChange, string $type): void { DB::table('product_stock_logs')->insert([ @@ -150,6 +274,13 @@ trait HasStocks ]); } + /** + * Query scope: Get products that are in stock + * + * Includes products with: + * - Stock management disabled (always in stock), OR + * - Stock management enabled AND available stock > 0 + */ public function scopeInStock($query) { return $query->where(function ($q) { @@ -161,6 +292,14 @@ trait HasStocks }); } + /** + * Query scope: Get products with low stock + * + * Returns products where: + * - Stock management is enabled + * - low_stock_threshold is set + * - Available stock <= threshold + */ public function scopeLowStock($query) { $stockTable = config('shop.tables.product_stocks', 'product_stocks'); @@ -173,6 +312,11 @@ trait HasStocks ]); } + /** + * Check if product stock is low + * + * @return bool True if stock management enabled, threshold set, and stock <= threshold + */ public function isLowStock(): bool { if (!$this->manage_stock || !$this->low_stock_threshold) { @@ -182,6 +326,17 @@ trait HasStocks return $this->getAvailableStock() <= $this->low_stock_threshold; } + /** + * Get active claims for this product + * + * Returns query builder for PENDING claims that haven't expired yet. + * Useful for: + * - Viewing current reservations + * - Checking what's claimed but not released + * - Managing active bookings + * + * @return \Illuminate\Database\Eloquent\Builder + */ public function claims() { $stockModel = config('shop.models.product_stock', 'Blax\Shop\Models\ProductStock'); @@ -191,6 +346,30 @@ trait HasStocks ->where('product_id', $this->id); } + /** + * Get available stock on a specific date + * + * This is crucial for booking/rental systems where you need to know: + * "How many units are available on date X?" + * + * Calculation: + * 1. Start with current available stock + * 2. Add back all currently pending claims (they reduce available stock) + * 3. Subtract only the claims that are active on the specific date + * + * Example with 100 units: + * - Claim 1: 20 units, days 5-10 + * - Claim 2: 30 units, days 8-15 + * - Current available: 50 (100 - 20 - 30) + * - Available on day 3: 100 (no claims active) + * - Available on day 6: 80 (only claim 1 active) + * - Available on day 9: 50 (both claims active) + * - Available on day 12: 70 (only claim 2 active) + * - Available on day 20: 100 (no claims active) + * + * @param \DateTimeInterface $date The date to check availability for + * @return int Available stock on that date (PHP_INT_MAX if stock management disabled) + */ public function availableOnDate(\DateTimeInterface $date): int { if (!$this->manage_stock) { diff --git a/tests/Feature/ProductStockTest.php b/tests/Feature/ProductStockTest.php index 38b6692..5736f26 100644 --- a/tests/Feature/ProductStockTest.php +++ b/tests/Feature/ProductStockTest.php @@ -3,6 +3,7 @@ namespace Blax\Shop\Tests\Feature; use Blax\Shop\Enums\StockStatus; +use Blax\Shop\Enums\StockType; use Blax\Shop\Exceptions\NotEnoughStockException; use Blax\Shop\Models\Product; use Blax\Shop\Models\ProductStock; @@ -481,4 +482,164 @@ class ProductStockTest extends TestCase $this->assertCount(1, $claimsOnDay12); $this->assertEquals($claim2->id, $claimsOnDay12->first()->id); } + + /** @test */ + public function it_can_get_claimed_stock_amount() + { + $product = Product::factory()->withStocks(100)->create(); + + // Claim some stock + $product->claimStock(quantity: 25); + $product->claimStock(quantity: 15); + + // Should return total claimed + $this->assertEquals(40, $product->getClaimedStock()); + } + + /** @test */ + public function it_checks_if_claim_is_active() + { + $product = Product::factory()->withStocks(100)->create(); + + $claim = $product->claimStock(quantity: 10); + + $this->assertTrue($claim->isActive()); + + $claim->release(); + + $this->assertFalse($claim->fresh()->isActive()); + } + + /** @test */ + public function it_releases_expired_claims() + { + $product = Product::factory()->withStocks(100)->create(); + + // Create expired claim + $expiredClaim = $product->claimStock( + quantity: 20, + until: now()->subHour() + ); + + // Create active claim + $activeClaim = $product->claimStock( + quantity: 15, + until: now()->addHours(2) + ); + + // Release expired claims + $count = \Blax\Shop\Models\ProductStock::releaseExpired(); + + $this->assertEquals(1, $count); + $this->assertEquals(StockStatus::COMPLETED, $expiredClaim->fresh()->status); + $this->assertEquals(StockStatus::PENDING, $activeClaim->fresh()->status); + } + + /** @test */ + public function it_has_reference_relationship() + { + $product = Product::factory()->withStocks(100)->create(); + $user = \Workbench\App\Models\User::factory()->create(); + + $claim = $product->claimStock( + quantity: 10, + reference: $user, + note: 'Reserved for user' + ); + + $this->assertNotNull($claim->reference); + $this->assertEquals($user->id, $claim->reference->id); + $this->assertEquals(get_class($user), $claim->reference_type); + } + + /** @test */ + public function it_handles_return_stock_type() + { + $product = Product::factory()->create(['manage_stock' => true]); + + $product->increaseStock(50); + $product->decreaseStock(10); + + // Use adjustStock with RETURN type (adds stock back) + $product->adjustStock( + type: StockType::RETURN, + quantity: 5 + ); + + // Refresh to get updated stock + $product = $product->fresh(); + + // Should have 45 total (50 - 10 + 5) + $this->assertEquals(45, $product->getAvailableStock()); + + // Verify the return entry exists + $returnEntry = $product->stocks()->where('type', StockType::RETURN->value)->first(); + $this->assertNotNull($returnEntry); + $this->assertEquals(5, $returnEntry->quantity); + } + + /** @test */ + public function temporary_scope_filters_correctly() + { + $product = Product::factory()->withStocks(100)->create(); + + $temporary = $product->claimStock(quantity: 10, until: now()->addDay()); + $permanent = $product->claimStock(quantity: 20); + + $temporaryStocks = \Blax\Shop\Models\ProductStock::temporary()->get(); + $permanentStocks = \Blax\Shop\Models\ProductStock::permanent()->get(); + + $this->assertTrue($temporaryStocks->contains($temporary)); + $this->assertFalse($temporaryStocks->contains($permanent)); + $this->assertTrue($permanentStocks->contains($permanent)); + $this->assertFalse($permanentStocks->contains($temporary)); + } + + /** @test */ + public function it_tracks_stock_with_custom_status() + { + $product = Product::factory()->create(['manage_stock' => true]); + + // Add stock with PENDING status + $product->adjustStock( + type: StockType::INCREASE, + quantity: 50, + status: StockStatus::PENDING + ); + + // Should not be counted in available stock (only COMPLETED counts) + $this->assertEquals(0, $product->getAvailableStock()); + + // Mark as completed + $stockEntry = $product->stocks()->where('type', StockType::INCREASE->value)->first(); + $stockEntry->status = StockStatus::COMPLETED; + $stockEntry->save(); + + // Now should be available + $this->assertEquals(50, $product->fresh()->getAvailableStock()); + } + + /** @test */ + public function backward_compatibility_accessors_work() + { + $product = Product::factory()->withStocks(100)->create(); + + $claim = $product->claimStock( + quantity: 10, + until: now()->addDays(5) + ); + + // Test released_at accessor (should be null for pending) + $this->assertNull($claim->released_at); + + // Test until_at accessor (alias for expires_at) + $this->assertEquals($claim->expires_at->format('Y-m-d'), $claim->until_at->format('Y-m-d')); + + // Release the claim + $claim->release(); + + // Now released_at should return updated_at + $this->assertNotNull($claim->fresh()->released_at); + $this->assertEquals($claim->fresh()->updated_at->format('Y-m-d H:i:s'), $claim->fresh()->released_at->format('Y-m-d H:i:s')); + } }