feat(contact): Contact model for public-ticket dedupe (Pattern B)#17
Draft
feat(contact): Contact model for public-ticket dedupe (Pattern B)#17
Conversation
.NET port of the Pattern B design shipped in escalated-nestjs PR #17 and companion PRs for Laravel / Rails / Django / Adonis. Changes: - src/Escalated/Models/Contact.cs — EF Core entity mapped to escalated_contacts. Unique email (case-normalized before save by application code), nullable UserId, JSON Metadata string with GetMetadata / SetMetadata helpers. Exposes pure static helpers NormalizeEmail and DecideAction so FindOrCreateByEmail's branch selection is testable without EF. - src/Escalated/Models/Ticket.cs — adds nullable ContactId + Contact navigation property (FK on delete SetNull). - src/Escalated/Data/EscalatedDbContext.cs — registers DbSet<Contact> with unique email index + user_id index; wires ticket.Contact relation with SetNull cascade. - tests/Escalated.Tests/Models/ContactModelTests.cs — 8 xUnit cases covering NormalizeEmail (3), DecideAction (4), Metadata round-trip (2), and default construction. Inline GuestName / GuestEmail / GuestToken fields preserved on Ticket for backwards compatibility; follow-up PRs will migrate controllers. Local verification: .NET SDK unavailable in author's environment; CI on this PR will run the tests. See escalated-dev/escalated docs/superpowers/plans/2026-04-24-public-tickets-rollout-status.md
2 tasks
Follow-up on the Contact entity added earlier on this branch. TicketService.CreateAsync now resolves/creates a Contact by email when a guest email is supplied (and no requesterId was provided), and sets Ticket.ContactId to link them. Private FindOrCreateContactAsync uses the Pattern B pure statics (Contact.NormalizeEmail + Contact.DecideAction) to select between return-existing, update-name, and create. Matches the reference impl used in NestJS/Laravel/Rails/Django/Adonis/Symfony/Go. Inline GuestName / GuestEmail / GuestToken fields preserved for backwards-compat dual-read. Tests: 4 new integration-style xUnit cases using the in-memory EF provider: - CreateAsync_WithGuestEmail_CreatesContactAndLinksTicket - CreateAsync_RepeatGuestEmail_DedupesOntoSameContact (casing variant) - CreateAsync_NoGuestEmail_LeavesContactIdNull (authenticated path) - CreateAsync_FillsBlankNameOnExistingContact Local verification: no .NET SDK in author's env; CI on this PR runs the tests.
Member
Author
Wire-up commit pushed
Private 4 new xUnit tests using the in-memory EF provider:
Local .NET SDK unavailable; CI on this PR verifies. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
.NET port of Pattern B. Reference implementations:
Rollout strategy: https://github.com/escalated-dev/escalated/blob/feat/public-ticket-system/docs/superpowers/plans/2026-04-24-public-tickets-rollout-status.md
Changes
Pure-function helpers (`NormalizeEmail`, `DecideAction`) let the branch logic be tested without EF — matching the NestJS `message-id.ts` and Adonis `contact_model.test.js` patterns.
Test plan
Backwards compatibility
Inline GuestName / GuestEmail / GuestToken fields preserved. Follow-up PRs migrate controllers and deprecate inline fields.