BF commands, A command tests, I traits
- Implement CommandReinstallTest to verify the behavior of the shop:reinstall command, including force and fresh flags, and confirmation prompts.
- Create CommandReleaseExpiredStocksTest to test the shop:release-expired-stocks command, ensuring it correctly releases expired stock claims based on configuration.
- Add CommandStatsTest to validate the shop:stats command, checking counts and revenue calculations for products, purchases, carts, and orders.
- Introduce LoanShopCommandsTest to cover loanable products in stock management, ensuring accurate reporting of assigned, used, and available stock.
- Implement LoanStockEventsTest to verify that stock events are dispatched correctly during loan operations, including checkouts and returns.
- Add NextAvailableAtTest to ensure the nextAvailableAt method behaves correctly for loanable products, considering loans and claims.
2026-05-17 11:25:34 +00:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace Blax\Shop\Tests\Feature\Loan;
|
|
|
|
|
|
|
|
|
|
|
|
use Blax\Shop\Console\Commands\ShopAvailabilityCommand;
|
|
|
|
|
|
use Blax\Shop\Console\Commands\ShopStocksCommand;
|
|
|
|
|
|
use Blax\Shop\Enums\ProductType;
|
|
|
|
|
|
use Blax\Shop\Models\Product;
|
|
|
|
|
|
use Blax\Shop\Tests\TestCase;
|
|
|
|
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
|
|
use Illuminate\Support\Carbon;
|
|
|
|
|
|
use Illuminate\Support\Facades\Artisan;
|
|
|
|
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
|
|
|
|
use Workbench\App\Models\User;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Inspector commands ({@see ShopStocksCommand}, {@see ShopAvailabilityCommand})
|
|
|
|
|
|
* read from product_stocks / product_purchases. CommandStocksTest and
|
|
|
|
|
|
* CommandAvailabilityTest already exercise them against SIMPLE products; this
|
|
|
|
|
|
* file covers the LOANABLE path — borrow + return cycles. A regression in
|
|
|
|
|
|
* loan↔stock wiring shows up here as wrong Assigned / Used / Available numbers
|
|
|
|
|
|
* earlier than any HTTP test would catch it.
|
|
|
|
|
|
*
|
|
|
|
|
|
* The Assigned-inflation bug ({@see self::assigned_for_loanable_product_must_not_inflate_after_a_return_cycle})
|
|
|
|
|
|
* is enforced here: getMaxStocksAttribute() sums every INCREASE entry — and
|
|
|
|
|
|
* the host-driven return path fires an INCREASE — so a 3-copy book that's
|
|
|
|
|
|
* been borrowed-and-returned once used to render as Assigned=4. The command
|
|
|
|
|
|
* now consults the loan-aware `total_quantity` accessor instead.
|
|
|
|
|
|
*/
|
|
|
|
|
|
class LoanShopCommandsTest extends TestCase
|
|
|
|
|
|
{
|
|
|
|
|
|
use RefreshDatabase;
|
|
|
|
|
|
|
|
|
|
|
|
private User $borrower;
|
|
|
|
|
|
|
|
|
|
|
|
protected function setUp(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
parent::setUp();
|
|
|
|
|
|
|
|
|
|
|
|
$this->borrower = User::factory()->create();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private function newBook(string $name, string $sku): CmdLoanBook
|
|
|
|
|
|
{
|
|
|
|
|
|
return CmdLoanBook::create(['name' => $name, 'sku' => $sku]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private function runOk(string $command, array $params = []): string
|
|
|
|
|
|
{
|
|
|
|
|
|
$exit = Artisan::call($command, $params);
|
|
|
|
|
|
$output = Artisan::output();
|
|
|
|
|
|
$this->assertSame(0, $exit, "{$command} returned non-zero:\n{$output}");
|
|
|
|
|
|
|
|
|
|
|
|
return $output;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ─────────────────────── shop:stocks (overview) ─────────────────────── */
|
|
|
|
|
|
|
|
|
|
|
|
#[Test]
|
|
|
|
|
|
public function shop_stocks_overview_lists_a_loaned_book_with_correct_counts(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$book = $this->newBook('Dune', 'CMD-DUNE');
|
|
|
|
|
|
$book->increaseStock(3);
|
|
|
|
|
|
$book->checkOutTo($this->borrower);
|
|
|
|
|
|
|
|
|
|
|
|
$output = $this->runOk(ShopStocksCommand::class);
|
|
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('Dune', $output);
|
|
|
|
|
|
|
|
|
|
|
|
$this->assertSame(1, $this->loanedDecreases($book), 'one DECREASE row from the loan');
|
|
|
|
|
|
$this->assertSame(2, $book->fresh()->getAvailableStock(), '3 copies − 1 loan = 2 available');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[Test]
|
|
|
|
|
|
public function shop_stocks_overview_for_a_fully_loaned_book_reports_zero_available(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$book = $this->newBook('Ember', 'CMD-EMBER');
|
|
|
|
|
|
$book->increaseStock(1);
|
|
|
|
|
|
$book->checkOutTo($this->borrower);
|
|
|
|
|
|
|
|
|
|
|
|
$output = $this->runOk(ShopStocksCommand::class);
|
|
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('Ember', $output);
|
|
|
|
|
|
$this->assertSame(0, $book->fresh()->getAvailableStock(), 'last copy is out');
|
|
|
|
|
|
$this->assertSame(1, $this->loanedDecreases($book->fresh()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ─────────────────────── shop:stocks (detail) ─────────────────────── */
|
|
|
|
|
|
|
|
|
|
|
|
#[Test]
|
|
|
|
|
|
public function shop_stocks_detail_view_renders_the_full_ledger_for_a_loaned_book(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
Carbon::setTestNow(Carbon::parse('2026-05-14 10:00:00'));
|
|
|
|
|
|
|
|
|
|
|
|
$book = $this->newBook('Hyperion', 'CMD-HYP');
|
|
|
|
|
|
$book->increaseStock(3);
|
|
|
|
|
|
|
2026-05-17 13:20:58 +00:00
|
|
|
|
// Borrow twice, return one. With the claim-based loan model the
|
|
|
|
|
|
// ledger carries: seed INCREASE, two claim cycles each writing a
|
|
|
|
|
|
// DECREASE + PHYSICALLY_CLAIMED row, plus one RETURN from the
|
|
|
|
|
|
// released claim — six rows total, but the operator only needs to
|
|
|
|
|
|
// see the standard increase/decrease verbs that the command renders.
|
BF commands, A command tests, I traits
- Implement CommandReinstallTest to verify the behavior of the shop:reinstall command, including force and fresh flags, and confirmation prompts.
- Create CommandReleaseExpiredStocksTest to test the shop:release-expired-stocks command, ensuring it correctly releases expired stock claims based on configuration.
- Add CommandStatsTest to validate the shop:stats command, checking counts and revenue calculations for products, purchases, carts, and orders.
- Introduce LoanShopCommandsTest to cover loanable products in stock management, ensuring accurate reporting of assigned, used, and available stock.
- Implement LoanStockEventsTest to verify that stock events are dispatched correctly during loan operations, including checkouts and returns.
- Add NextAvailableAtTest to ensure the nextAvailableAt method behaves correctly for loanable products, considering loans and claims.
2026-05-17 11:25:34 +00:00
|
|
|
|
$loan = $book->checkOutTo($this->borrower);
|
|
|
|
|
|
$book->checkOutTo(User::factory()->create());
|
|
|
|
|
|
$loan->markReturned();
|
|
|
|
|
|
|
|
|
|
|
|
$output = $this->runOk(ShopStocksCommand::class, ['product' => 'CMD-HYP']);
|
|
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('Hyperion', $output);
|
|
|
|
|
|
$this->assertStringContainsString('ASSIGNED', $output);
|
|
|
|
|
|
$this->assertStringContainsString('AVAILABLE', $output);
|
|
|
|
|
|
$this->assertStringContainsString('Recent stock ledger', $output);
|
|
|
|
|
|
$this->assertStringContainsString('increase', $output);
|
|
|
|
|
|
$this->assertStringContainsString('decrease', $output);
|
|
|
|
|
|
|
|
|
|
|
|
// 3 copies, 1 still out → 2 available. Verified against the model to
|
|
|
|
|
|
// sidestep ASCII-column regex fragility.
|
|
|
|
|
|
$this->assertSame(2, $book->fresh()->getAvailableStock());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[Test]
|
|
|
|
|
|
public function assigned_for_loanable_product_must_not_inflate_after_a_return_cycle(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
// Regression test for the bug where shop:stocks rendered Assigned=4
|
2026-05-17 13:20:58 +00:00
|
|
|
|
// for a 3-copy book that had been borrowed-and-returned once. With
|
|
|
|
|
|
// the new claim-based loan model the issue disappears at the source:
|
|
|
|
|
|
// checkout writes a DECREASE + PHYSICALLY_CLAIMED pair, the return
|
|
|
|
|
|
// releases the claim (status flip + RETURN row), and the net effect
|
|
|
|
|
|
// on every accessor reads exactly 3 copies — no inflation possible.
|
BF commands, A command tests, I traits
- Implement CommandReinstallTest to verify the behavior of the shop:reinstall command, including force and fresh flags, and confirmation prompts.
- Create CommandReleaseExpiredStocksTest to test the shop:release-expired-stocks command, ensuring it correctly releases expired stock claims based on configuration.
- Add CommandStatsTest to validate the shop:stats command, checking counts and revenue calculations for products, purchases, carts, and orders.
- Introduce LoanShopCommandsTest to cover loanable products in stock management, ensuring accurate reporting of assigned, used, and available stock.
- Implement LoanStockEventsTest to verify that stock events are dispatched correctly during loan operations, including checkouts and returns.
- Add NextAvailableAtTest to ensure the nextAvailableAt method behaves correctly for loanable products, considering loans and claims.
2026-05-17 11:25:34 +00:00
|
|
|
|
$book = $this->newBook('Hyperion', 'CMD-HYP-RC');
|
|
|
|
|
|
$book->increaseStock(3);
|
|
|
|
|
|
|
|
|
|
|
|
$loan = $book->checkOutTo($this->borrower);
|
|
|
|
|
|
$loan->markReturned();
|
|
|
|
|
|
|
|
|
|
|
|
$fresh = $book->fresh();
|
2026-05-17 13:20:58 +00:00
|
|
|
|
$this->assertSame(3, (int) $fresh->total_quantity, 'loan-aware accessor reads true count');
|
|
|
|
|
|
$this->assertSame(3, $fresh->getPhysicalStock(), 'physical reads true count');
|
BF commands, A command tests, I traits
- Implement CommandReinstallTest to verify the behavior of the shop:reinstall command, including force and fresh flags, and confirmation prompts.
- Create CommandReleaseExpiredStocksTest to test the shop:release-expired-stocks command, ensuring it correctly releases expired stock claims based on configuration.
- Add CommandStatsTest to validate the shop:stats command, checking counts and revenue calculations for products, purchases, carts, and orders.
- Introduce LoanShopCommandsTest to cover loanable products in stock management, ensuring accurate reporting of assigned, used, and available stock.
- Implement LoanStockEventsTest to verify that stock events are dispatched correctly during loan operations, including checkouts and returns.
- Add NextAvailableAtTest to ensure the nextAvailableAt method behaves correctly for loanable products, considering loans and claims.
2026-05-17 11:25:34 +00:00
|
|
|
|
|
|
|
|
|
|
// Detail view: ASSIGNED row must be 3, not 4.
|
|
|
|
|
|
$output = $this->runOk(ShopStocksCommand::class, ['product' => 'CMD-HYP-RC']);
|
|
|
|
|
|
|
|
|
|
|
|
// The detail view renders labels on one row and values on the next.
|
|
|
|
|
|
// Split into lines and look at the value row that follows the ASSIGNED
|
|
|
|
|
|
// label row — the first numeric token in that line is Assigned.
|
|
|
|
|
|
$assigned = $this->detailViewAssignedValue($output);
|
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
|
3,
|
|
|
|
|
|
$assigned,
|
|
|
|
|
|
"shop:stocks detail must render Assigned=3 (physical capacity), got {$assigned}",
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Overview must also be consistent.
|
|
|
|
|
|
$overviewOutput = $this->runOk(ShopStocksCommand::class);
|
|
|
|
|
|
$row = $this->overviewRow($overviewOutput, 'Hyperion');
|
|
|
|
|
|
$this->assertNotNull($row, 'Hyperion row must appear in the overview table');
|
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
|
3,
|
|
|
|
|
|
$row['assigned'],
|
|
|
|
|
|
'shop:stocks overview must report Assigned=3 for a borrowed-then-returned 3-copy book',
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ─────────────────── shop:stocks:availability (calendar) ─────────────── */
|
|
|
|
|
|
|
2026-05-17 12:09:03 +00:00
|
|
|
|
#[Test]
|
|
|
|
|
|
public function shop_stocks_availability_headline_surfaces_physical_count(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
// Coverage for the new "Physical N" headline that complements
|
|
|
|
|
|
// "Available N". For a loanable product the gap between the two is the
|
|
|
|
|
|
// loan tally — physical stays at the catalogue size while available
|
|
|
|
|
|
// drops as copies go out.
|
|
|
|
|
|
$book = $this->newBook('Hyperion', 'CMD-HYP-PHYS');
|
|
|
|
|
|
$book->increaseStock(3);
|
|
|
|
|
|
$book->checkOutTo($this->borrower);
|
|
|
|
|
|
|
|
|
|
|
|
$output = $this->runOk(ShopAvailabilityCommand::class, ['product' => 'CMD-HYP-PHYS']);
|
|
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('Physical 3', $output, 'physical = 2 on shelf + 1 active loan = 3');
|
|
|
|
|
|
$this->assertStringContainsString('Available 2', $output);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[Test]
|
|
|
|
|
|
public function shop_stocks_detail_view_carries_a_physical_box(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
// Operator-side: shop:stocks {product} renders the totals box; the
|
|
|
|
|
|
// PHYSICAL cell should sit next to ASSIGNED and reflect the loan-aware
|
|
|
|
|
|
// count.
|
|
|
|
|
|
$book = $this->newBook('Atlas', 'CMD-ATLAS-PHYS');
|
|
|
|
|
|
$book->increaseStock(4);
|
|
|
|
|
|
$book->checkOutTo($this->borrower);
|
|
|
|
|
|
|
|
|
|
|
|
$output = $this->runOk(ShopStocksCommand::class, ['product' => 'CMD-ATLAS-PHYS']);
|
|
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('PHYSICAL', $output);
|
|
|
|
|
|
$this->assertSame(4, $book->fresh()->getPhysicalStock(), 'model agrees with command');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
BF commands, A command tests, I traits
- Implement CommandReinstallTest to verify the behavior of the shop:reinstall command, including force and fresh flags, and confirmation prompts.
- Create CommandReleaseExpiredStocksTest to test the shop:release-expired-stocks command, ensuring it correctly releases expired stock claims based on configuration.
- Add CommandStatsTest to validate the shop:stats command, checking counts and revenue calculations for products, purchases, carts, and orders.
- Introduce LoanShopCommandsTest to cover loanable products in stock management, ensuring accurate reporting of assigned, used, and available stock.
- Implement LoanStockEventsTest to verify that stock events are dispatched correctly during loan operations, including checkouts and returns.
- Add NextAvailableAtTest to ensure the nextAvailableAt method behaves correctly for loanable products, considering loans and claims.
2026-05-17 11:25:34 +00:00
|
|
|
|
#[Test]
|
|
|
|
|
|
public function shop_stocks_availability_headline_reads_zero_for_a_fully_loaned_book(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
Carbon::setTestNow(Carbon::parse('2026-05-14 10:00:00'));
|
|
|
|
|
|
|
|
|
|
|
|
$book = $this->newBook('Solitaire', 'CMD-SOL');
|
|
|
|
|
|
$book->increaseStock(1);
|
|
|
|
|
|
$book->checkOutTo($this->borrower);
|
|
|
|
|
|
|
|
|
|
|
|
$output = $this->runOk(ShopAvailabilityCommand::class, [
|
|
|
|
|
|
'product' => 'CMD-SOL',
|
|
|
|
|
|
'--from' => '2026-05-01',
|
|
|
|
|
|
'--to' => '2026-05-31',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('Solitaire', $output);
|
|
|
|
|
|
$this->assertStringContainsString('May 2026', $output);
|
|
|
|
|
|
$this->assertStringContainsString('Available 0', $output);
|
|
|
|
|
|
$this->assertStringContainsString('MON', $output);
|
|
|
|
|
|
$this->assertStringContainsString('SUN', $output);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[Test]
|
|
|
|
|
|
public function shop_stocks_availability_headline_recovers_after_a_return(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
Carbon::setTestNow(Carbon::parse('2026-05-14 10:00:00'));
|
|
|
|
|
|
|
|
|
|
|
|
$book = $this->newBook('Singular', 'CMD-SIN');
|
|
|
|
|
|
$book->increaseStock(1);
|
|
|
|
|
|
$loan = $book->checkOutTo($this->borrower);
|
2026-05-17 13:20:58 +00:00
|
|
|
|
$loan->markReturned(); // releases the claim → restores available
|
BF commands, A command tests, I traits
- Implement CommandReinstallTest to verify the behavior of the shop:reinstall command, including force and fresh flags, and confirmation prompts.
- Create CommandReleaseExpiredStocksTest to test the shop:release-expired-stocks command, ensuring it correctly releases expired stock claims based on configuration.
- Add CommandStatsTest to validate the shop:stats command, checking counts and revenue calculations for products, purchases, carts, and orders.
- Introduce LoanShopCommandsTest to cover loanable products in stock management, ensuring accurate reporting of assigned, used, and available stock.
- Implement LoanStockEventsTest to verify that stock events are dispatched correctly during loan operations, including checkouts and returns.
- Add NextAvailableAtTest to ensure the nextAvailableAt method behaves correctly for loanable products, considering loans and claims.
2026-05-17 11:25:34 +00:00
|
|
|
|
|
|
|
|
|
|
$output = $this->runOk(ShopAvailabilityCommand::class, [
|
|
|
|
|
|
'product' => 'CMD-SIN',
|
|
|
|
|
|
'--from' => '2026-05-01',
|
|
|
|
|
|
'--to' => '2026-05-31',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('Available 1', $output);
|
|
|
|
|
|
$this->assertSame(1, $book->fresh()->getAvailableStock());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[Test]
|
|
|
|
|
|
public function shop_stocks_availability_day_view_for_unmanaged_book_shows_unlimited(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
// manage_stock=false on a loanable book is the moonshiner "infinite
|
|
|
|
|
|
// copy" pattern: every borrower can take a copy at any time.
|
|
|
|
|
|
$book = $this->newBook('Compendium', 'CMD-CMP');
|
|
|
|
|
|
$book->manage_stock = false;
|
|
|
|
|
|
$book->save();
|
|
|
|
|
|
|
|
|
|
|
|
$output = $this->runOk(ShopAvailabilityCommand::class, [
|
|
|
|
|
|
'product' => 'CMD-CMP',
|
|
|
|
|
|
'--day' => '2026-05-14',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString('Unlimited availability all day.', $output);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ───────────────────────────── helpers ─────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Count of loan-driven DECREASE rows on this book — mirrors what the
|
|
|
|
|
|
* "Used" column in shop:stocks renders.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function loanedDecreases(CmdLoanBook $book): int
|
|
|
|
|
|
{
|
|
|
|
|
|
return (int) abs((int) $book->stocks()
|
|
|
|
|
|
->withoutGlobalScope('willExpire')
|
|
|
|
|
|
->where('type', \Blax\Shop\Enums\StockType::DECREASE->value)
|
|
|
|
|
|
->where('status', \Blax\Shop\Enums\StockStatus::COMPLETED->value)
|
|
|
|
|
|
->sum('quantity'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Extract the first numeric token from the value row that follows the
|
|
|
|
|
|
* ASSIGNED label row in the shop:stocks detail output. The detail view
|
|
|
|
|
|
* renders a single boxed row, so the first integer after "ASSIGNED" on
|
|
|
|
|
|
* the next non-empty line is the Assigned value.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function detailViewAssignedValue(string $output): ?int
|
|
|
|
|
|
{
|
|
|
|
|
|
$lines = explode("\n", $output);
|
|
|
|
|
|
$assignedLineIndex = null;
|
|
|
|
|
|
foreach ($lines as $i => $line) {
|
|
|
|
|
|
if (str_contains($line, 'ASSIGNED')) {
|
|
|
|
|
|
$assignedLineIndex = $i;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($assignedLineIndex === null) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Walk forward to the next line that contains digits.
|
|
|
|
|
|
for ($i = $assignedLineIndex + 1; $i < count($lines); $i++) {
|
|
|
|
|
|
if (preg_match('/\b(\d+)\b/', $lines[$i], $m)) {
|
|
|
|
|
|
return (int) $m[1];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Pull the named row out of the shop:stocks overview table and parse the
|
|
|
|
|
|
* 4 trailing integer columns (Assigned / Used / Available / Claimed).
|
|
|
|
|
|
*
|
|
|
|
|
|
* @return array{assigned: int, used: int, available: int, claimed: int}|null
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function overviewRow(string $output, string $needle): ?array
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (explode("\n", $output) as $line) {
|
|
|
|
|
|
if (! str_contains($line, $needle)) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Capture the four numeric columns at the tail of the row. Type
|
|
|
|
|
|
// and ID columns precede them; we anchor on the trailing
|
|
|
|
|
|
// "n | n | n | n" shape (the only column run that's all integers).
|
|
|
|
|
|
// The overview renders via $this->table(...) which uses plain ASCII
|
|
|
|
|
|
// pipes — not the box-drawing │ used by the detail/availability
|
|
|
|
|
|
// commands.
|
|
|
|
|
|
if (preg_match('/(\d+)\s*\|\s*(\d+|—)\s*\|\s*(\d+|∞)\s*\|\s*(\d+)\s*\|\s*$/u', $line, $m)) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'assigned' => (int) $m[1],
|
|
|
|
|
|
'used' => $m[2] === '—' ? 0 : (int) $m[2],
|
|
|
|
|
|
'available' => $m[3] === '∞' ? PHP_INT_MAX : (int) $m[3],
|
|
|
|
|
|
'claimed' => (int) $m[4],
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Plug-n-pray loanable fixture, mirroring CheckOutToTest's LoanableBook but
|
|
|
|
|
|
* under a unique name so the two files can coexist in the same namespace.
|
|
|
|
|
|
*/
|
|
|
|
|
|
class CmdLoanBook extends Product
|
|
|
|
|
|
{
|
|
|
|
|
|
public const DEFAULT_TYPE = ProductType::LOANABLE;
|
|
|
|
|
|
|
|
|
|
|
|
protected $guarded = [];
|
|
|
|
|
|
}
|