feat(api): Phase 4 backend — gamification, social foundation, onboarding - #261
Conversation
…, #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>
There was a problem hiding this comment.
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.
|
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>
There was a problem hiding this comment.
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>
|



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
gamification_free_tierflag (streak/XP/level + freeze for free), infinite levels (100·L²past 10), new achievements,GET /api/gamification/recap(metrics-only, free, with shareDeepLink).User.Handle/SocialOptIn+ migration with handle backfill; request/accept/remove/list/feed/cheer/block/report endpoints +SocialAccessGuard+ rate limits; OpenAI moderation (fail-open); FriendFeedEvent write-pipeline (streak + achievement hooks, keyset read);first_cheer; FCM pushes; export + deletion purge. Backend + shared only.Validation
Build 0 errors; full suite 4,015 tests pass; migrations (
AddSocialFoundation,AddOnboardingChecklistFlags, gamification flag seed) — no model drift.🤖 Generated with Claude Code