|  | 
| 2 | 2 | 
 | 
| 3 | 3 | declare(strict_types=1); | 
| 4 | 4 | 
 | 
|  | 5 | +use ComplexHeart\Domain\Model\Contracts\Aggregatable; | 
| 5 | 6 | use ComplexHeart\Domain\Model\Errors\ImmutabilityError; | 
| 6 | 7 | use ComplexHeart\Domain\Model\Exceptions\InvariantViolation; | 
| 7 | 8 | use ComplexHeart\Domain\Model\Test\Fixtures\OrderManagement\Domain\Errors\InvalidPriceError; | 
|  | 
| 17 | 18 | 
 | 
| 18 | 19 | test('Object with HasImmutability should expose primitive values.', function () { | 
| 19 | 20 |     $price = new Price(100.0, 'EUR'); | 
| 20 |  | -    expect($price->amount)->toBeFloat(); | 
| 21 |  | -    expect($price->currency)->toBeString(); | 
|  | 21 | +    expect($price->amount)->toBeFloat() | 
|  | 22 | +        ->and($price->currency)->toBeString(); | 
| 22 | 23 | }) | 
| 23 | 24 |     ->group('Unit'); | 
| 24 | 25 | 
 | 
| 25 | 26 | test('Object with HasImmutability should return new instance with override values.', function () { | 
| 26 | 27 |     $price = new Price(100.0, 'EUR'); | 
| 27 | 28 |     $newPrice = $price->applyDiscount(10.0); | 
| 28 | 29 | 
 | 
| 29 |  | -    expect($newPrice)->toBeInstanceOf(Price::class); | 
| 30 |  | -    expect($newPrice->amount)->toBe(90.0); | 
|  | 30 | +    expect($newPrice)->toBeInstanceOf(Price::class) | 
|  | 31 | +        ->and($newPrice->amount)->toBe(90.0); | 
| 31 | 32 | }) | 
| 32 | 33 |     ->group('Unit'); | 
| 33 | 34 | 
 | 
| @@ -165,3 +166,112 @@ protected function invariantSingleFailure(): bool | 
| 165 | 166 |         ->and($exception->getMessage())->toBe('Single error message'); | 
| 166 | 167 | }) | 
| 167 | 168 |     ->group('Unit'); | 
|  | 169 | + | 
|  | 170 | +test('Custom non-aggregatable exception should be thrown immediately', function () { | 
|  | 171 | +    new class () { | 
|  | 172 | +        use HasInvariants; | 
|  | 173 | + | 
|  | 174 | +        public function __construct() | 
|  | 175 | +        { | 
|  | 176 | +            $this->check(); | 
|  | 177 | +        } | 
|  | 178 | + | 
|  | 179 | +        protected function invariantCustomError(): bool | 
|  | 180 | +        { | 
|  | 181 | +            throw new DomainException('Custom domain error'); | 
|  | 182 | +        } | 
|  | 183 | +    }; | 
|  | 184 | +}) | 
|  | 185 | +    ->group('Unit') | 
|  | 186 | +    ->throws(DomainException::class, 'Custom domain error'); | 
|  | 187 | + | 
|  | 188 | +test('Custom non-aggregatable exception stops invariant checking', function () { | 
|  | 189 | +    new class () { | 
|  | 190 | +        use HasInvariants; | 
|  | 191 | + | 
|  | 192 | +        public function __construct() | 
|  | 193 | +        { | 
|  | 194 | +            $this->check(); | 
|  | 195 | +        } | 
|  | 196 | + | 
|  | 197 | +        protected function invariantFirstCheck(): bool | 
|  | 198 | +        { | 
|  | 199 | +            // This should throw immediately | 
|  | 200 | +            throw new RuntimeException('First error'); | 
|  | 201 | +        } | 
|  | 202 | + | 
|  | 203 | +        protected function invariantSecondCheck(): bool | 
|  | 204 | +        { | 
|  | 205 | +            // This should NEVER be reached | 
|  | 206 | +            throw new DomainException('Second error - should not be reached'); | 
|  | 207 | +        } | 
|  | 208 | +    }; | 
|  | 209 | +}) | 
|  | 210 | +    ->group('Unit') | 
|  | 211 | +    ->throws(RuntimeException::class, 'First error'); | 
|  | 212 | + | 
|  | 213 | +test('Custom aggregatable exception should be aggregated', function () { | 
|  | 214 | +    try { | 
|  | 215 | +        new class () { | 
|  | 216 | +            use HasInvariants; | 
|  | 217 | + | 
|  | 218 | +            public function __construct() | 
|  | 219 | +            { | 
|  | 220 | +                $this->check(); | 
|  | 221 | +            } | 
|  | 222 | + | 
|  | 223 | +            protected function invariantFirst(): bool | 
|  | 224 | +            { | 
|  | 225 | +                // Create aggregatable exception inline | 
|  | 226 | +                $exception = new class ('Aggregatable error') extends \Exception implements Aggregatable {}; | 
|  | 227 | +                throw $exception; | 
|  | 228 | +            } | 
|  | 229 | + | 
|  | 230 | +            protected function invariantSecond(): bool | 
|  | 231 | +            { | 
|  | 232 | +                return false; // Regular InvariantViolation | 
|  | 233 | +            } | 
|  | 234 | +        }; | 
|  | 235 | +    } catch (InvariantViolation $e) { | 
|  | 236 | +        expect($e->hasMultipleViolations())->toBeTrue() | 
|  | 237 | +            ->and($e->getViolationCount())->toBe(2) | 
|  | 238 | +            ->and($e->getViolations())->toContain('Aggregatable error') | 
|  | 239 | +            ->and($e->getViolations())->toContain('second'); | 
|  | 240 | +    } | 
|  | 241 | +}) | 
|  | 242 | +    ->group('Unit'); | 
|  | 243 | + | 
|  | 244 | +test('InvariantViolation implements Aggregatable', function () { | 
|  | 245 | +    $exception = InvariantViolation::fromViolations(['Test']); | 
|  | 246 | + | 
|  | 247 | +    expect($exception)->toBeInstanceOf(Aggregatable::class); | 
|  | 248 | +}) | 
|  | 249 | +    ->group('Unit'); | 
|  | 250 | + | 
|  | 251 | +test('Mix of custom non-aggregatable throws immediately before aggregation', function () { | 
|  | 252 | +    new class () { | 
|  | 253 | +        use HasInvariants; | 
|  | 254 | + | 
|  | 255 | +        public function __construct() | 
|  | 256 | +        { | 
|  | 257 | +            $this->check(); | 
|  | 258 | +        } | 
|  | 259 | + | 
|  | 260 | +        protected function invariantFirstAggregatable(): bool | 
|  | 261 | +        { | 
|  | 262 | +            return false; // InvariantViolation | 
|  | 263 | +        } | 
|  | 264 | + | 
|  | 265 | +        protected function invariantCustomNonAggregatable(): bool | 
|  | 266 | +        { | 
|  | 267 | +            throw new RuntimeException('Non-aggregatable error'); | 
|  | 268 | +        } | 
|  | 269 | + | 
|  | 270 | +        protected function invariantThirdAggregatable(): bool | 
|  | 271 | +        { | 
|  | 272 | +            return false; // Should not be reached | 
|  | 273 | +        } | 
|  | 274 | +    }; | 
|  | 275 | +}) | 
|  | 276 | +    ->group('Unit') | 
|  | 277 | +    ->throws(RuntimeException::class, 'Non-aggregatable error'); | 
0 commit comments