|
17 | 17 | use models\exceptions\ValidationException; |
18 | 18 | use models\summit\DomainAuthorizedSummitRegistrationDiscountCode; |
19 | 19 | use models\summit\DomainAuthorizedSummitRegistrationPromoCode; |
| 20 | +use models\summit\MemberSummitRegistrationDiscountCode; |
| 21 | +use models\summit\MemberSummitRegistrationPromoCode; |
| 22 | +use models\summit\SpeakerSummitRegistrationDiscountCode; |
| 23 | +use models\summit\SpeakerSummitRegistrationPromoCode; |
20 | 24 | use models\summit\Summit; |
| 25 | +use models\summit\SummitRegistrationDiscountCodeTicketTypeRule; |
21 | 26 | use models\summit\SummitRegistrationPromoCode; |
22 | 27 | use models\summit\SummitTicketType; |
23 | 28 | use models\main\Member; |
24 | | -use PHPUnit\Framework\TestCase; |
| 29 | +use ModelSerializers\SerializerRegistry; |
| 30 | +use Tests\TestCase; |
25 | 31 |
|
26 | 32 | /** |
27 | 33 | * Class DomainAuthorizedPromoCodeTest |
@@ -279,30 +285,29 @@ private function buildMockTicketType(int $id, string $audience, bool $canSell = |
279 | 285 | } |
280 | 286 |
|
281 | 287 | /** |
282 | | - * WithPromoCode ticket type + no promo code → NOT returned |
| 288 | + * WithPromoCode ticket type + no promo code → NOT returned; |
| 289 | + * Audience_All type IS returned (proves strategy returns results, but filters WithPromoCode). |
283 | 290 | */ |
284 | 291 | public function testWithPromoCodeAudienceNoPromoCodeNotReturned(): void |
285 | 292 | { |
286 | | - $summit = $this->buildMockSummit(); |
| 293 | + $allTT = $this->buildMockTicketType(30, SummitTicketType::Audience_All); |
| 294 | + $summit = $this->buildMockSummit([$allTT]); |
287 | 295 | $member = $this->buildMockMember(); |
288 | 296 |
|
289 | 297 | $strategy = new RegularPromoCodeTicketTypesStrategy($summit, $member, null); |
290 | 298 | $result = $strategy->getTicketTypes(); |
291 | 299 |
|
292 | | - // No WithPromoCode types should appear (none were in Audience_All or Audience_Without_Invitation) |
293 | | - foreach ($result as $tt) { |
294 | | - $this->assertNotEquals( |
295 | | - SummitTicketType::Audience_With_Promo_Code, |
296 | | - $tt->getAudience(), |
297 | | - 'WithPromoCode ticket types should not be returned without a promo code' |
298 | | - ); |
299 | | - } |
| 300 | + $ids = array_map(fn($tt) => $tt->getId(), $result); |
| 301 | + // Audience_All type IS returned (non-vacuous: proves the strategy produces results) |
| 302 | + $this->assertContains(30, $ids, 'Audience_All ticket type should be returned without a promo code'); |
| 303 | + // WithPromoCode type (id 99) is NOT returned — it only lives in promo_code->getAllowedTicketTypes() |
| 304 | + $this->assertNotContains(99, $ids, 'WithPromoCode ticket types should not be returned without a promo code'); |
300 | 305 | } |
301 | 306 |
|
302 | 307 | /** |
303 | | - * WithPromoCode ticket type + live domain-authorized promo code → IS returned |
| 308 | + * WithPromoCode ticket type + live promo code → IS returned |
304 | 309 | */ |
305 | | - public function testWithPromoCodeAudienceLiveDomainAuthorizedPromoCodeReturned(): void |
| 310 | + public function testWithPromoCodeAudienceLivePromoCodeReturned(): void |
306 | 311 | { |
307 | 312 | $promoCodeTicket = $this->buildMockTicketType(10, SummitTicketType::Audience_With_Promo_Code); |
308 | 313 |
|
@@ -386,4 +391,214 @@ public function testAudienceAllWithPromoCodeReturnedWithPromo(): void |
386 | 391 | $ids = array_map(fn($tt) => $tt->getId(), $result); |
387 | 392 | $this->assertContains(40, $ids, 'Audience_All ticket type should be returned with a promo code'); |
388 | 393 | } |
| 394 | + |
| 395 | + // ----------------------------------------------------------------------- |
| 396 | + // Collision avoidance — DomainAuthorizedSummitRegistrationDiscountCode |
| 397 | + // ----------------------------------------------------------------------- |
| 398 | + |
| 399 | + /** |
| 400 | + * addTicketTypeRule rejects rules for types not in allowed_ticket_types (Truth #4). |
| 401 | + */ |
| 402 | + public function testAddTicketTypeRuleRejectsWhenTypeNotInAllowedTicketTypes(): void |
| 403 | + { |
| 404 | + $code = new DomainAuthorizedSummitRegistrationDiscountCode(); |
| 405 | + |
| 406 | + $ticketType = $this->createMock(SummitTicketType::class); |
| 407 | + $ticketType->method('getId')->willReturn(1); |
| 408 | + |
| 409 | + $rule = new SummitRegistrationDiscountCodeTicketTypeRule(); |
| 410 | + $rule->setTicketType($ticketType); |
| 411 | + |
| 412 | + $this->expectException(ValidationException::class); |
| 413 | + $code->addTicketTypeRule($rule); |
| 414 | + } |
| 415 | + |
| 416 | + /** |
| 417 | + * addTicketTypeRule does NOT mutate allowed_ticket_types — override skips parent's add(). |
| 418 | + */ |
| 419 | + public function testAddTicketTypeRuleDoesNotMutateAllowedTicketTypes(): void |
| 420 | + { |
| 421 | + $code = new DomainAuthorizedSummitRegistrationDiscountCode(); |
| 422 | + |
| 423 | + $ticketType = $this->createMock(SummitTicketType::class); |
| 424 | + $ticketType->method('getId')->willReturn(1); |
| 425 | + |
| 426 | + // First add to allowed_ticket_types |
| 427 | + $code->addAllowedTicketType($ticketType); |
| 428 | + $this->assertEquals(1, $code->getAllowedTicketTypes()->count()); |
| 429 | + |
| 430 | + // Now add a discount rule — should NOT add a second entry to allowed_ticket_types |
| 431 | + $rule = new SummitRegistrationDiscountCodeTicketTypeRule(); |
| 432 | + $rule->setTicketType($ticketType); |
| 433 | + $code->addTicketTypeRule($rule); |
| 434 | + |
| 435 | + $this->assertEquals(1, $code->getAllowedTicketTypes()->count(), |
| 436 | + 'addTicketTypeRule must not mutate allowed_ticket_types'); |
| 437 | + } |
| 438 | + |
| 439 | + /** |
| 440 | + * removeTicketTypeRule does NOT mutate allowed_ticket_types. |
| 441 | + */ |
| 442 | + public function testRemoveTicketTypeRuleDoesNotMutateAllowedTicketTypes(): void |
| 443 | + { |
| 444 | + $code = new DomainAuthorizedSummitRegistrationDiscountCode(); |
| 445 | + |
| 446 | + $ticketType = $this->createMock(SummitTicketType::class); |
| 447 | + $ticketType->method('getId')->willReturn(1); |
| 448 | + |
| 449 | + $code->addAllowedTicketType($ticketType); |
| 450 | + |
| 451 | + $rule = new SummitRegistrationDiscountCodeTicketTypeRule(); |
| 452 | + $rule->setTicketType($ticketType); |
| 453 | + $code->addTicketTypeRule($rule); |
| 454 | + |
| 455 | + // Remove the rule — allowed_ticket_types must remain intact |
| 456 | + $code->removeTicketTypeRule($rule); |
| 457 | + |
| 458 | + $this->assertEquals(1, $code->getAllowedTicketTypes()->count(), |
| 459 | + 'removeTicketTypeRule must not mutate allowed_ticket_types'); |
| 460 | + } |
| 461 | + |
| 462 | + // ----------------------------------------------------------------------- |
| 463 | + // canBeAppliedTo override — DomainAuthorizedSummitRegistrationDiscountCode |
| 464 | + // ----------------------------------------------------------------------- |
| 465 | + |
| 466 | + /** |
| 467 | + * Free WithPromoCode ticket type accepted — override skips free-ticket guard (Truth #15). |
| 468 | + */ |
| 469 | + public function testCanBeAppliedToFreeWithPromoCodeTicketType(): void |
| 470 | + { |
| 471 | + $code = new DomainAuthorizedSummitRegistrationDiscountCode(); |
| 472 | + |
| 473 | + $ticketType = $this->createMock(SummitTicketType::class); |
| 474 | + $ticketType->method('getId')->willReturn(100); |
| 475 | + $ticketType->method('isFree')->willReturn(true); |
| 476 | + |
| 477 | + $code->addAllowedTicketType($ticketType); |
| 478 | + |
| 479 | + // Parent SummitRegistrationDiscountCode::canBeAppliedTo would return false |
| 480 | + // because of the free-ticket guard. The override bypasses it. |
| 481 | + $this->assertTrue($code->canBeAppliedTo($ticketType), |
| 482 | + 'Domain-authorized discount code should be applicable to free WithPromoCode ticket types'); |
| 483 | + } |
| 484 | + |
| 485 | + /** |
| 486 | + * Paid ticket type accepted — normal discount behavior preserved. |
| 487 | + */ |
| 488 | + public function testCanBeAppliedToPaidTicketType(): void |
| 489 | + { |
| 490 | + $code = new DomainAuthorizedSummitRegistrationDiscountCode(); |
| 491 | + |
| 492 | + $ticketType = $this->createMock(SummitTicketType::class); |
| 493 | + $ticketType->method('getId')->willReturn(200); |
| 494 | + $ticketType->method('isFree')->willReturn(false); |
| 495 | + |
| 496 | + $code->addAllowedTicketType($ticketType); |
| 497 | + |
| 498 | + $this->assertTrue($code->canBeAppliedTo($ticketType), |
| 499 | + 'Domain-authorized discount code should be applicable to paid ticket types'); |
| 500 | + } |
| 501 | + |
| 502 | + // ----------------------------------------------------------------------- |
| 503 | + // AutoApplyPromoCodeTrait — existing email-linked types |
| 504 | + // ----------------------------------------------------------------------- |
| 505 | + |
| 506 | + public function testAutoApplyMemberPromoCode(): void |
| 507 | + { |
| 508 | + $code = new MemberSummitRegistrationPromoCode(); |
| 509 | + $this->assertFalse($code->getAutoApply(), 'auto_apply should default to false'); |
| 510 | + $code->setAutoApply(true); |
| 511 | + $this->assertTrue($code->getAutoApply(), 'auto_apply should round-trip to true'); |
| 512 | + } |
| 513 | + |
| 514 | + public function testAutoApplyMemberDiscountCode(): void |
| 515 | + { |
| 516 | + $code = new MemberSummitRegistrationDiscountCode(); |
| 517 | + $this->assertFalse($code->getAutoApply(), 'auto_apply should default to false'); |
| 518 | + $code->setAutoApply(true); |
| 519 | + $this->assertTrue($code->getAutoApply(), 'auto_apply should round-trip to true'); |
| 520 | + } |
| 521 | + |
| 522 | + public function testAutoApplySpeakerPromoCode(): void |
| 523 | + { |
| 524 | + $code = new SpeakerSummitRegistrationPromoCode(); |
| 525 | + $this->assertFalse($code->getAutoApply(), 'auto_apply should default to false'); |
| 526 | + $code->setAutoApply(true); |
| 527 | + $this->assertTrue($code->getAutoApply(), 'auto_apply should round-trip to true'); |
| 528 | + } |
| 529 | + |
| 530 | + public function testAutoApplySpeakerDiscountCode(): void |
| 531 | + { |
| 532 | + $code = new SpeakerSummitRegistrationDiscountCode(); |
| 533 | + $this->assertFalse($code->getAutoApply(), 'auto_apply should default to false'); |
| 534 | + $code->setAutoApply(true); |
| 535 | + $this->assertTrue($code->getAutoApply(), 'auto_apply should round-trip to true'); |
| 536 | + } |
| 537 | + |
| 538 | + // ----------------------------------------------------------------------- |
| 539 | + // Serializer tests |
| 540 | + // ----------------------------------------------------------------------- |
| 541 | + |
| 542 | + /** |
| 543 | + * auto_apply field serialization for domain-authorized promo code. |
| 544 | + */ |
| 545 | + public function testSerializerAutoApplyField(): void |
| 546 | + { |
| 547 | + $code = new DomainAuthorizedSummitRegistrationPromoCode(); |
| 548 | + $code->setAutoApply(true); |
| 549 | + |
| 550 | + $serializer = SerializerRegistry::getInstance()->getSerializer($code); |
| 551 | + $data = $serializer->serialize(null, [], [], []); |
| 552 | + |
| 553 | + $this->assertArrayHasKey('auto_apply', $data); |
| 554 | + $this->assertTrue($data['auto_apply'], 'auto_apply should serialize as true'); |
| 555 | + |
| 556 | + // Also test false |
| 557 | + $code2 = new DomainAuthorizedSummitRegistrationPromoCode(); |
| 558 | + $code2->setAutoApply(false); |
| 559 | + |
| 560 | + $serializer2 = SerializerRegistry::getInstance()->getSerializer($code2); |
| 561 | + $data2 = $serializer2->serialize(null, [], [], []); |
| 562 | + |
| 563 | + $this->assertArrayHasKey('auto_apply', $data2); |
| 564 | + $this->assertFalse($data2['auto_apply'], 'auto_apply should serialize as false'); |
| 565 | + } |
| 566 | + |
| 567 | + /** |
| 568 | + * remaining_quantity_per_account transient field serialization. |
| 569 | + */ |
| 570 | + public function testSerializerRemainingQuantityPerAccount(): void |
| 571 | + { |
| 572 | + $code = new DomainAuthorizedSummitRegistrationPromoCode(); |
| 573 | + $code->setRemainingQuantityPerAccount(3); |
| 574 | + |
| 575 | + $serializer = SerializerRegistry::getInstance()->getSerializer($code); |
| 576 | + $data = $serializer->serialize(null, [], [], []); |
| 577 | + |
| 578 | + $this->assertArrayHasKey('remaining_quantity_per_account', $data); |
| 579 | + $this->assertEquals(3, $data['remaining_quantity_per_account']); |
| 580 | + |
| 581 | + // Test null (unlimited) |
| 582 | + $code2 = new DomainAuthorizedSummitRegistrationPromoCode(); |
| 583 | + $serializer2 = SerializerRegistry::getInstance()->getSerializer($code2); |
| 584 | + $data2 = $serializer2->serialize(null, [], [], []); |
| 585 | + |
| 586 | + $this->assertArrayHasKey('remaining_quantity_per_account', $data2); |
| 587 | + $this->assertNull($data2['remaining_quantity_per_account']); |
| 588 | + } |
| 589 | + |
| 590 | + /** |
| 591 | + * auto_apply field serialization for existing email-linked type (MemberSummitRegistrationPromoCode). |
| 592 | + */ |
| 593 | + public function testSerializerAutoApplyEmailLinkedType(): void |
| 594 | + { |
| 595 | + $code = new MemberSummitRegistrationPromoCode(); |
| 596 | + $code->setAutoApply(true); |
| 597 | + |
| 598 | + $serializer = SerializerRegistry::getInstance()->getSerializer($code); |
| 599 | + $data = $serializer->serialize(null, [], [], []); |
| 600 | + |
| 601 | + $this->assertArrayHasKey('auto_apply', $data); |
| 602 | + $this->assertTrue($data['auto_apply'], 'auto_apply should serialize as true for member promo code'); |
| 603 | + } |
389 | 604 | } |
0 commit comments