Skip to content

feat(promo-codes): domain-authorized promo codes for early registration access#525

Draft
caseylocker wants to merge 12 commits intomainfrom
feature/promo-codes-early-registration
Draft

feat(promo-codes): domain-authorized promo codes for early registration access#525
caseylocker wants to merge 12 commits intomainfrom
feature/promo-codes-early-registration

Conversation

@caseylocker
Copy link
Copy Markdown
Contributor

@caseylocker caseylocker commented Apr 9, 2026

ref: https://app.clickup.com/t/86b952pgc

Summary

  • Implement domain-based early registration access via two new promo code subtypes: DomainAuthorizedSummitRegistrationDiscountCode (with discount) and DomainAuthorizedSummitRegistrationPromoCode (access-only)
  • Add WithPromoCode audience value on SummitTicketType for promo-code-only ticket types
  • Add auto-discovery endpoint (GET /api/v1/summits/{id}/promo-codes/all/discover) that finds matching codes for the authenticated user's email
  • Add auto_apply support to domain-authorized types and existing email-linked types (Member/Speaker)
  • Add QuantityPerAccount enforcement at both discovery and checkout time
  • Comprehensive unit + integration tests covering domain matching, audience filtering, collision avoidance, serialization, discovery, and checkout enforcement

SDS Implementation (12 tasks)

All 12 tasks implemented. All review follow-ups resolved. Two open deviations remain:

  • D3 (SHOULD-FIX): allowed_email_domains validation needs custom rule (currently sometimes|json)
  • D4 (MUST-FIX): TOCTOU window in QuantityPerAccount checkout enforcement — ApplyPromoCodeTask needs to move after ReserveOrderTask in saga chain

Files changed (35 files, +3127/-53)

  • New models: DomainAuthorizedSummitRegistrationDiscountCode, DomainAuthorizedSummitRegistrationPromoCode
  • New traits: DomainAuthorizedPromoCodeTrait, AutoApplyPromoCodeTrait
  • New interface: IDomainAuthorizedPromoCode
  • New serializers, factory updates, validation rules, repository queries
  • Migration: Version20260401150000
  • Discovery endpoint: route, controller, service
  • Checkout enforcement in ApplyPromoCodeTask
  • Unit tests: DomainAuthorizedPromoCodeTest (30 tests)
  • Integration tests: 8 tests in OAuth2SummitPromoCodesApiTest

Test plan

  • php artisan test --filter=DomainAuthorizedPromoCodeTest — all unit tests pass
  • php artisan test --filter="OAuth2SummitPromoCodesApiTest::testDiscover" — discovery integration tests pass
  • Migration up and down run cleanly
  • Manual API test: create domain-authorized promo code, hit discover endpoint, verify response
  • Verify existing promo code types are unaffected (regression)

🤖 Generated with Claude Code

caseylocker and others added 10 commits April 8, 2026 14:44
…registration access

Adds two new promo code subtypes (DomainAuthorizedSummitRegistrationDiscountCode
and DomainAuthorizedSummitRegistrationPromoCode) enabling domain-based early
registration access. Adds WithPromoCode ticket type audience value for
promo-code-only distribution. Includes auto-discovery endpoint, per-account
quantity enforcement at checkout, and auto_apply support for existing
member/speaker promo code types.

SDS: doc/promo-codes-for-early-registration-access.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Task 1: Add ClassName discriminator ENUM widening in migration, add
  data guard before narrowing Audience ENUM in down()
- Task 2: Guard matchesEmailDomain() against emails missing @ to
  prevent false-positive suffix matches
- Task 3: Replace canBeAppliedTo() with direct collection membership
  check in addTicketTypeRule() (Truth #4), override removeTicketTypeRule()
  to prevent parent from re-adding to allowed_ticket_types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Task 5: accepted NITs for constant naming, interface gap, and
pre-existing edge cases.
Task 7: MUST-FIX — canBuyRegistrationTicketByType() missing
WithPromoCode branch blocks checkout for promo-code-only tickets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace 'sometimes|json' with custom AllowedEmailDomainsArray rule for
  allowed_email_domains validation — accepts pre-decoded PHP array and
  validates each entry against @domain.com/.tld/user@email formats
- Remove json_decode() from factory populate for both domain-authorized
  types — value is already a PHP array after request decoding
- Fix expand=allowed_ticket_types silently dropping field on
  DomainAuthorizedSummitRegistrationDiscountCodeSerializer — extend
  re-add guard to check both $relations and $expand
- Rename json_array → json_string_array in both new serializers

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Add WithPromoCode branch to canBuyRegistrationTicketByType() so promo-code-only
ticket types are not rejected at checkout for both invited and non-invited users.
Replace isSoldOut() with canSell() in the strategy's WithPromoCode loop to align
listing visibility with checkout enforcement. Add 5 unit tests for audience-based
filtering scenarios required by Task 7 DoD.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Task 8: wrap INSTANCE OF chain in parentheses to preserve summit
scoping, simplify speaker email matching via getOwnerEmail(), and
exclude cancelled tickets from quantity-per-account count.

Task 9: add remaining_quantity_per_account (null) to all four
member/speaker serializers, re-add allowed_ticket_types to member
and speaker discount code serializers, and declare setter/getter
on IDomainAuthorizedPromoCode interface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ity_per_account

Move ApplyPromoCodeTask after ReserveOrderTask in the saga chain so
ticket rows exist when the count query fires. Broaden
getTicketCountByMemberAndPromoCode to include 'Reserved' orders,
ensuring concurrent checkouts correctly see each other's reservations.
Remove the TOCTOU-vulnerable pre-check from PreProcessReservationTask
and relocate it inside ApplyPromoCodeTask's locked transaction, where
it naturally fires once per unique promo code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…sion, canBeAppliedTo, discovery, checkout

- Fix base class: extend Tests\TestCase instead of PHPUnit\Framework\TestCase (boots Laravel facades)
- Add 3 collision avoidance tests for DomainAuthorizedSummitRegistrationDiscountCode
- Add 2 canBeAppliedTo override tests (free ticket guard bypass)
- Add 4 auto_apply tests for existing email-linked types (Member/Speaker promo/discount)
- Fix vacuous testWithPromoCodeAudienceNoPromoCodeNotReturned (now asserts on real data)
- Add 3 serializer tests (auto_apply, remaining_quantity_per_account, email-linked type)
- Rename misleading test to testWithPromoCodeAudienceLivePromoCodeReturned
- Add 5 discovery endpoint integration tests in OAuth2SummitPromoCodesApiTest
- Add 3 checkout enforcement test stubs (2 need order pipeline harness, 1 blocked by D4)
- Mark all 9 review follow-ups complete in SDS doc

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8a759ee-8004-4f8c-84a5-07bcde220f6f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/promo-codes-early-registration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@caseylocker caseylocker self-assigned this Apr 9, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 9, 2026

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-525/

This page is automatically updated on each push to this PR.

The GET /api/v1/summits/{id}/promo-codes/all/discover route was added in
Task 12 but never seeded into the api_endpoints table. The OAuth2 bearer
token validator middleware rejects any unregistered route with a 400
"API endpoint does not exits" error, causing 5 discover-endpoint integration
tests in OAuth2SummitPromoCodesApiTest to fail.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 9, 2026

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-525/

This page is automatically updated on each push to this PR.

The discover endpoint's seeder entry intentionally omits authz_groups per
SDS Task 9 ("any authenticated user with read scope"). The auth.user
middleware requires at least one matching group, so every request fell
through to a 403. Switch to rate.limit:25,1 to match the adjacent
pre-validate-promo-code route, which has the same "any authenticated user"
profile. OAuth2 bearer auth and scope enforcement are still applied via
the parent 'api' middleware group.

All 5 discover integration tests now pass (verified locally).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 9, 2026

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-525/

This page is automatically updated on each push to this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant