Skip to content

feat(api): Phase 4 backend — gamification, social foundation, onboarding - #261

Merged
thomasluizon merged 6 commits into
mainfrom
feature/phase-4
Jun 27, 2026
Merged

feat(api): Phase 4 backend — gamification, social foundation, onboarding#261
thomasluizon merged 6 commits into
mainfrom
feature/phase-4

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Phase 4 — backend (orbit-api)

Backend half of the Phase 4 growth/social batch. Pairs with the orbit-ui-mobile frontend PR (link to follow).

Issues

Validation

Build 0 errors; full suite 4,015 tests pass; migrations (AddSocialFoundation, AddOnboardingChecklistFlags, gamification flag seed) — no model drift.

🤖 Generated with Claude Code

thomasluizon and others added 4 commits June 27, 2026 00:13
…, #190)

Reversible gamification_free_tier AppFeatureFlag (default off) unlocks streak/XP/level + streak-freeze auto-activation for free users. Infinite levels via 100*L^2 curve, continuous past level 10. New half_year_hero/streak_titan achievements + reserved first_cheer. GET /api/gamification/recap returns metrics-only retrospective + referral shareDeepLink, ungated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New EncouragingToneSection (Order 150) + softened CoreIdentitySection so clarify-first coexists with direct action; destructive/bulk route through existing confirmation-card gating. Reuses shipped resolve + pendingOperations infra; gpt-4.1-mini unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on, handles (#193)

5 entities + User.Handle/SocialOptIn + migration w/ deterministic handle backfill. Endpoints (request/accept/remove/list/feed/cheer/block/report + set-handle + opt-in), SocialAccessGuard, rate limits. OpenAI moderation (fail-open on outage). FriendFeedEvent write-pipeline: streak hook (all users) + achievement hook (Pro) + keyset read. first_cheer wired; FCM pushes; export + deletion purge. Backend + shared only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ack support (#187, #189)

User onboarding-checklist completion flags (ride profile payload, auto-set from signals) + onboarding achievement hook (#189). BulkCreateHabits accepts per-item tags resolved/created by name for starter packs (#187). Migration AddOnboardingChecklistFlags.

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

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR 261 Review: REQUEST CHANGES - 2 High findings must be addressed before merge.

[HIGH-1] Missing UnblockUserCommandValidator
File: src/Orbit.Application/Social/Commands/UnblockUserCommand.cs
Every other social command has a paired validator under Orbit.Application/Social/Validators/ - six of them exist; UnblockUserCommandValidator does not. The route-bound BlockedUserId GUID has no NotEmpty guard and no self-unblock protection in the MediatR pipeline.
Fix: Add UnblockUserCommandValidator mirroring BlockUserCommandValidator - NotEmpty on both UserId and BlockedUserId, plus NotEqual(x => x.UserId) on BlockedUserId.

[HIGH-2] GetCheersQuery does not filter blocked users
File: src/Orbit.Application/Social/Queries/GetCheersQuery.cs:39-41
After a block, the cheers endpoint still returns historical cheers between the two users. GetFriendsQuery (lines 38-42) and GetFriendFeedQueryHandler.ResolveVisibleActorsAsync (lines 94-101) both apply a bilateral block filter; GetCheersQuery is the only social read path that skips it. Cheer notes can contain personal messages - post-block visibility violates the block contract.
Fix: Inject IGenericRepository, load bilateral blocks mirroring GetFriendsQuery, add !blockedIds.Contains(c.SenderId) to received-cheers and !blockedIds.Contains(c.RecipientId) to sent-cheers predicates.

[MEDIUM-1] GetFriendFeedQuery leaks empty actor records for opted-out users
If a user toggles opt-out after emitting a feed event, their rows survive in pageRows but are absent from actorMap. TryGetValue at line 67 returns (null, null), producing feed items with actorHandle='' and actorDisplayName=''. Fix: add .Where(e => actorMap.ContainsKey(e.ActorUserId)) before the projection at line 64.

[MEDIUM-2] SendFriendRequestCommandValidator missing MaximumLength on string fields
XOR constraint is enforced but no length cap on Handle or ReferralCode. Both flow into EF expression trees as unbounded strings. Fix: MaximumLength(AppConstants.HandleMaxLength) for Handle, MaximumLength(64) for ReferralCode.

[MEDIUM-3] Recap validator period set narrower than the resolver
GetRecapQueryValidator accepts only [week, month, year]; RetrospectivePeriodRange.Resolve also handles quarter and semester. Consumers assuming period-set parity receive undocumented 400s. Fix: expand the validator set to match; independently harden the resolver default branch to throw ArgumentOutOfRangeException instead of silently falling back.

Summary: Critical=0 High=2 Medium=3. Contract-aligner: not verifiable in CI (orbit-ui-mobile not checked out); all DTO changes are additive, no backward-compat breaks detected on the API side.

@thomasluizon

Copy link
Copy Markdown
Owner Author

Paired frontend PR: thomasluizon/orbit-ui-mobile#323 (squash-merge in lockstep).

…ator, feed leak, period 400 (#193, #190)

Resolves claude-review findings: add UnblockUserCommandValidator (HIGH); filter blocked users from GetCheersQuery both directions (HIGH); drop opted-out actors from the friend feed (MED); MaximumLength on friend-request handle/referralCode (MED); recap period set now matches the resolver via a shared IsKnownPeriod (single source of truth) + an invalid period returns 400 not 500 at the recap/retrospective endpoints (MED).

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

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NEEDS WORK - 2 High findings must be addressed before merge.

[HIGH-1] Block and Unblock endpoints have no rate limit - Block is a destructive write
File: src/Orbit.Api/Controllers/FriendsController.cs:120 (Block), :137 (Unblock)
Neither action carries [DistributedRateLimit]. DistributedRateLimitService.cs registers cheers, friend-requests, reports, set-handle but nothing for block or unblock. Block unconditionally tears down the friendship row (BlockUserCommand.cs:46-48), so an authenticated caller can spam-destroy friendship rows with no throttle at all.
Fix: Add [DistributedRateLimit(block)] on Block action and [DistributedRateLimit(unblock)] on Unblock. Register both policies in DistributedRateLimitService.cs e.g. [block] = new(TimeSpan.FromHours(24), PermitLimit: 50, SegmentCount: 1).

[HIGH-2] GetFriendsQuery has no FluentValidation validator
File: src/Orbit.Application/Social/Queries/ - no GetFriendsQueryValidator.cs present
Every other social query and command in this PR has a matching validator. GetFriendsQuery(Guid UserId) has none. Hard rule: Every new feature needs validators in Orbit.Application//Validators/. An MCP tool or future internal caller dispatching this query with an empty Guid hits the repository without guard, returning silent wrong data instead of a clean 400.
Fix: Add GetFriendsQueryValidator.cs with RuleFor(x => x.UserId).NotEmpty().

[MEDIUM-1] BlockUserCommand calls BlockedUser.Create before the user-existence check
File: src/Orbit.Application/Social/Commands/BlockUserCommand.cs:36 vs :40
Factory invoked before confirming target exists. Compare SendFriendRequestCommand.cs:25-26 which checks existence first. No security bypass (domain guard still fires), but wastes a DB round-trip for invalid input and is an ordering inconsistency across this PRs handlers.
Fix: Move targetExists check before BlockedUser.Create.

[MEDIUM-2] ReportUserCommand does not verify CheerId involves either party
File: src/Orbit.Application/Social/Commands/ReportUserCommand.cs:34-41
When CheerId is non-null it is persisted verbatim with no check that the referenced cheer involves the reporter or the reported user. Moderation tooling will surface the wrong cheer, potentially implicating a third partys message.
Fix: If CheerId.HasValue, verify the cheer involves request.ReportedUserId (as sender or recipient). Return Result.Failure(ErrorMessages.CheerNotFound) if it does not.

Prior review findings (UnblockUserCommandValidator, GetCheersQuery block filter, feed opted-out actor leak, MaximumLength on friend-request fields, recap period unification) are all verified fixed in commit 6b3949a - not re-flagged.

Summary: Critical=0 High=2 Medium=2. Contract-aligner: not verifiable (orbit-ui-mobile not checked out); all DTO changes are additive, no backward-compat breaks detected.

… validator, ordering, cheer-ownership

Rate-limit block/unblock (50/24h); add GetFriendsQueryValidator (UserId NotEmpty); check target existence before BlockedUser.Create; verify a reported CheerId involves the reported user (else CheerNotFound).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR 261 Round-2 Review: APPROVE (0 Critical, 0 High, 2 Medium). All round-2 HIGH fixes verified. Two remaining Medium items for awareness (GetCheersQuery sent-direction DTO gap; dead per-day cap constants). Full detail in inline comment below.

@thomasluizon
thomasluizon merged commit 4430bfb into main Jun 27, 2026
8 checks passed
@thomasluizon
thomasluizon deleted the feature/phase-4 branch June 27, 2026 17:03
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