Add HL3D branding, Etsy Live/Draft paging and Projects lifecycle
Deploy HickLab3D Dashboard / Deploy to production (push) Failing after 23s

Replace the old touchmark/PNG identity with the supplied lockup and
monogram, segmented role-aware navigation, Live/Draft listing filters
with generation-based cache invalidation, and admin Projects covering
immutable file revisions, listing live pointers, and exact-decimal
filament/packaging/shipping costing. Additive schema only; no Etsy
publication. Isolated clone tests: 301 tests, 1162 assertions.
This commit is contained in:
2026-11-08 12:50:56 +00:00
parent c8c5822166
commit d4928789c4
83 changed files with 5808 additions and 84 deletions
+184
View File
@@ -0,0 +1,184 @@
<?php
namespace Tests\Unit;
use App\Services\ProjectCosting;
use InvalidArgumentException;
use Tests\TestCase;
class ProjectCostingTest extends TestCase
{
public function test_filament_line_converts_grams_to_kilograms_before_pricing(): void
{
// 250g at £20.00/kg = £5.00
$line = ProjectCosting::lineTotal('filament', '250', 'g', '20.000000');
$this->assertSame('5.0000', $line);
}
public function test_filament_line_includes_explicit_waste_without_double_counting(): void
{
// 250g consumed + 10g explicit purge/support waste = 260g at £20.00/kg
$line = ProjectCosting::lineTotal('filament', '250', 'g', '20.000000', waste: '10');
$this->assertSame('5.2000', $line);
}
public function test_packaging_line_is_quantity_times_unit_rate(): void
{
$line = ProjectCosting::lineTotal('packaging', '3', 'unit', '0.750000');
$this->assertSame('2.2500', $line);
}
public function test_shipping_line_total_is_quantity_times_parcel_rate(): void
{
$line = ProjectCosting::lineTotal('shipping', '1', 'parcel', '4.990000');
$this->assertSame('4.9900', $line);
}
public function test_shipping_unit_contribution_divides_rate_by_units_per_parcel_not_batch_yield(): void
{
// One £4.99 parcel covering 2 saleable units ships at £2.495 per unit,
// independent of however many units the manufacturing batch itself yielded.
$this->assertSame('2.4950', ProjectCosting::shippingUnitContribution('4.990000', 2));
}
public function test_shipping_unit_contribution_rejects_zero_or_negative_units_per_parcel(): void
{
$this->expectException(InvalidArgumentException::class);
ProjectCosting::shippingUnitContribution('4.990000', 0);
}
public function test_filament_rejects_a_quantity_unit_not_in_the_category_allowlist(): void
{
$this->expectException(InvalidArgumentException::class);
ProjectCosting::lineTotal('filament', '250', 'G', '20.000000');
}
public function test_filament_rejects_kilograms_as_the_quantity_unit(): void
{
$this->expectException(InvalidArgumentException::class);
ProjectCosting::lineTotal('filament', '0.25', 'kg', '20.000000');
}
public function test_packaging_rejects_a_quantity_unit_not_in_the_category_allowlist(): void
{
$this->expectException(InvalidArgumentException::class);
ProjectCosting::lineTotal('packaging', '1', 'units', '0.650000');
}
public function test_shipping_rejects_a_quantity_unit_not_in_the_category_allowlist(): void
{
$this->expectException(InvalidArgumentException::class);
ProjectCosting::lineTotal('shipping', '1', 'box', '4.990000');
}
public function test_aggregate_rejects_a_category_total_exceeding_the_storable_decimal_precision(): void
{
// decimal(14,4) tops out around 9,999,999,999.9999; two large-but-individually-valid
// lines can still sum past that bound, so the aggregate step must re-check it.
$this->expectException(InvalidArgumentException::class);
ProjectCosting::aggregate([
['category' => 'filament', 'line_total' => '9999999999.0000'],
['category' => 'filament', 'line_total' => '9999999999.0000'],
]);
}
public function test_aggregate_rejects_a_grand_total_exceeding_the_storable_decimal_precision(): void
{
$this->expectException(InvalidArgumentException::class);
ProjectCosting::aggregate([
['category' => 'filament', 'line_total' => '9999999999.0000'],
['category' => 'packaging', 'line_total' => '9999999999.0000'],
]);
}
public function test_zero_quantity_is_a_valid_intentional_zero(): void
{
$this->assertSame('0.0000', ProjectCosting::lineTotal('packaging', '0', 'unit', '5.000000'));
}
public function test_negative_quantity_is_rejected(): void
{
$this->expectException(InvalidArgumentException::class);
ProjectCosting::lineTotal('packaging', '-1', 'unit', '5.000000');
}
public function test_negative_rate_is_rejected(): void
{
$this->expectException(InvalidArgumentException::class);
ProjectCosting::lineTotal('packaging', '1', 'unit', '-5.000000');
}
public function test_absurd_quantity_overflows_and_is_rejected(): void
{
$this->expectException(\Throwable::class);
ProjectCosting::lineTotal('filament', '999999999999', 'g', '999999999999.000000');
}
public function test_aggregate_distinguishes_missing_category_from_intentional_zero(): void
{
$totals = ProjectCosting::aggregate([
['category' => 'filament', 'line_total' => '5.0000'],
['category' => 'packaging', 'line_total' => '0.0000'],
]);
$this->assertSame('5.0000', $totals['filament_total']);
$this->assertSame('0.0000', $totals['packaging_total']);
$this->assertNull($totals['shipping_total']);
$this->assertSame('5.0000', $totals['grand_total']);
}
public function test_aggregate_grand_total_is_null_when_nothing_costed(): void
{
$totals = ProjectCosting::aggregate([]);
$this->assertNull($totals['filament_total']);
$this->assertNull($totals['packaging_total']);
$this->assertNull($totals['shipping_total']);
$this->assertNull($totals['grand_total']);
}
public function test_aggregate_sums_multiple_lines_per_category(): void
{
$totals = ProjectCosting::aggregate([
['category' => 'filament', 'line_total' => '5.0000'],
['category' => 'filament', 'line_total' => '2.5000'],
]);
$this->assertSame('7.5000', $totals['filament_total']);
}
public function test_per_unit_divides_grand_total_by_batch_yield_with_rounding(): void
{
$this->assertSame('3.3333', ProjectCosting::perUnit('10.0000', 3));
}
public function test_per_unit_rejects_zero_or_negative_yield(): void
{
$this->expectException(InvalidArgumentException::class);
ProjectCosting::perUnit('10.0000', 0);
}
public function test_rejects_unknown_category(): void
{
$this->expectException(InvalidArgumentException::class);
ProjectCosting::lineTotal('labour', '1', 'unit', '1.000000');
}
public function test_rejects_mixed_currency_summation(): void
{
$this->expectException(InvalidArgumentException::class);
ProjectCosting::assertSameCurrency(['GBP', 'USD']);
}
public function test_allows_single_consistent_currency(): void
{
ProjectCosting::assertSameCurrency(['GBP', 'GBP', 'GBP']);
$this->addToAssertionCount(1);
}
public function test_validates_iso_currency_allowlist(): void
{
$this->assertTrue(ProjectCosting::isValidCurrency('GBP'));
$this->assertFalse(ProjectCosting::isValidCurrency('XYZ'));
}
}