Skip to content

Make achievements payload match Progresso - #480

Merged
thomasluizon merged 25 commits into
mainfrom
feature/ticket-332-achievements-progresso
Aug 23, 2026
Merged

thomasluizon merged 25 commits into
mainfrom
feature/ticket-332-achievements-progresso

Conversation

@thomasluizon

@thomasluizon thomasluizon commented Aug 22, 2026 •

Copy link
Copy Markdown
Owner

Closes #332

Summary

  • Ungate the achievement catalog for every account with Progresso access.
  • Let a free account with GamificationFreeTier see and earn all 32 active achievements from deploy onward.
  • Retire seven unreachable social achievements while keeping their definitions resolvable for historical earned badges and public profiles.
  • Count active definitions plus retired badges earned by the current user so AchievementsEarned cannot exceed AchievementsTotal.
  • Keep AchievementsLocked and ProTeaser in the response contract while returning false and null.
  • Remove retired grant paths, progress metrics, and database reads.
  • Emit achievements_viewed from the achievements endpoint with isPro and earnedCount. The Astra overview tool also reaches this endpoint, so the event is not yet a pure human view signal.

Historical eligibility split

This PR ungates achievements. It does not backfill history.

Five one-time achievements remain unearned for free accounts that crossed their milestone before this change: Liftoff, First Orbit, Mission Control, Goal Crusher, and All Systems Go. That work is tracked by #362, which is blocked by this ticket.

This is a pre-existing condition that this PR does not worsen. Before this change, a free account earns no achievement at all. After it, every eligible free account sees and earns active achievements from deploy onward.

The achievement eligibility reconciliation service, hosted worker, user state, migration, query hooks, repository helpers, registrations, and reconciliation-only tests were removed from this branch. XpAwardLogBackfillService remains because it is unrelated.

Concurrent grant safety

TryGrantAchievementsAsync keeps the duplicate-key retry as a standalone safety property for ordinary award paths. Two concurrent report-event requests can still attempt the same UserAchievement insert without any reconciliation worker. The retry clears tracking and recomputes against persisted state so the losing request does not duplicate XP or push delivery.

External interface evidence

The installed Npgsql 10.0.3 assembly was reflected before using its test exception shape. Npgsql.PostgresException exposes a string SqlState property and a four string constructor. The duplicate-grant test uses SqlState 23505, which the existing DbUniqueViolation helper recognizes. Reproduce by loading Npgsql.dll with System.Reflection and listing PostgresException constructors and the SqlState property.

Verification

  • dotnet build Orbit.slnx: 0 errors.
  • Focused split tests: 279 passed.
  • Post-merge overlap tests: 113 passed.
  • dotnet test: 5,923 passed.
  • Architecture generation: 188 endpoints, 164 handlers, 0 orphan endpoints, and 0 untested feature folders.
  • Acceptance searches find no achievement eligibility reconciliation code, state, migration, registration, or test.
  • The seven retired ids occur only in AchievementDefinitions.cs.
  • AchievementsLocked and ProTeaser remain in openapi.json.
  • No inline streak or social notification URL survives under src.

Assumptions

  • Applied the unified earning predicate to bad_habit_breaker because it encoded the same stale Pro-only policy as the listed gates; rejected leaving one active catalog badge unearnable to free users.
  • Treated achievements_viewed as a product analytics event through the existing PostHog abstraction; rejected log-only emission because it would not support engagement analysis.
  • Assumed the production gamification_free_tier flag remains enabled as stated in fix(api): rate-limit state-changing endpoints on five controllers #332; rejected changing the whole-profile feature gate because the ticket keeps it.

@thomasluizon

Copy link
Copy Markdown
Owner Author

Approach:

  1. Add an explicit retirement flag to achievement definitions, mark only the seven named social achievements, and filter retired definitions from target grids and totals while leaving ID lookup unchanged for historical earned badges.
  2. Remove both achievement access gates while preserving the whole profile feature flag, the AchievementsLocked response field set to false, and ProTeaser set to null.
  3. Remove retired grant paths, obsolete progress metrics, and their service dependencies and queries, while preserving live report-event grants.
  4. Update focused unit tests for free and Pro payloads, retired earned badges, progress query counts, and live report events. Regenerate OpenAPI and architecture artifacts using repository tooling.
  5. Run focused tests and compile, commit that coherent result, then run the full build and test suite before the final push.

This keeps retirement as metadata instead of deleting definitions because historical rows and public profiles still need resolvable names and descriptions. It filters by the explicit retired IDs rather than category because the Sharing category also contains live client-driven achievements.

@pullfrog pullfrog 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.

Important

The branch does not yet contain the implementation described by this pull request.

Reviewed changes Reviewed the complete branch diff and the achievement contract scope claimed by the pull request.

  • Empty implementation commit: The head contains one commit and changes zero files relative to main.
  • No contract surface: There is no API or mobile payload change to assess at this head.

⚠️ The requested behavior is not implemented

This branch changes zero files, so it cannot make achievements available to authenticated users or retire the stated targets. Merging this head would leave production behavior unchanged while presenting the ticket as completed.

Technical details
# Add the achievement implementation before merge

## Affected sites
- PR head `644b7aac74c52966eb3a0e6e13cc8b07b2955e2d`: no files differ from base `71c0240b982de47c0da3038fedace47a8727fe37`
- PR description: the summary claims behavior changes while verification still says `Pending implementation`

## Required outcome
- Implement the achievement availability and target retirement behavior described by the PR.
- Add the applicable unit coverage, then request review on the resulting diff.

Pullfrog  | Fix it ➔ | View workflow run | Using GPT Sol | 𝕏

@pullfrog pullfrog 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.

Important

The new free-user response and analytics capture both misrepresent production behavior and should be corrected before merge.

Reviewed changes Reviewed the complete implementation added since the prior Pullfrog review at 644b7aa.

  • Achievement availability: Exposed active achievement payloads to free and Pro users while retaining the whole-profile feature flag.
  • Retired definitions: Marked seven social and cooperative achievements as retired, removed their grant paths and progress queries, and preserved earned historical badges.
  • Stable response shape: Kept AchievementsLocked and ProTeaser while returning false and null.
  • Product analytics: Added achievements_viewed with isPro and earnedCount through the existing PostHog abstraction.
  • Regression coverage: Updated query, service, retirement, social-path, and analytics unit tests.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GPT Sol | 𝕏

Comment thread src/Orbit.Application/Gamification/Queries/GetGamificationProfileQuery.cs Outdated
@thomasluizon

Copy link
Copy Markdown
Owner Author

Review fix approach for #332:

  1. Update GamificationService.cs so each of the five achievement award paths uses IsGamificationUnlockedAsync, matching the existing XP, streak, and level availability controlled by GamificationFreeTier. Update the nearby contract comment to describe the unified behavior.
  2. Remove achievements_viewed from GetGamificationProfileQuery.cs and emit it from GetAchievementsQuery.cs with the same isPro and earnedCount properties. This keeps general profile fetches out of the metric while preserving the achievements endpoint and Astra overview tool path.
  3. Adjust focused application tests for free users, paid users, the disabled flag, and analytics capture location. Then build, run focused tests, commit, run the full suite, push this branch, and verify PR checks.

This keeps the reviewed retirement and response contract unchanged. I rejected adding a separate achievement flag or a client supplied view marker because the ticket requires the existing GamificationFreeTier predicate and assigns pure human view precision to the blocked UI ticket.

@pullfrog pullfrog 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.

Important

The new commit fixes the forward earning paths and narrows analytics emission, but the rollout still leaves existing free users unable to earn several badges that the payload presents as unlocked.

Reviewed changes Reviewed the substantive changes in 14c828e since the prior Pullfrog review at df0a065.

  • Expanded free-tier earning: Removed Pro-only guards from active habit, goal, bad-habit, and onboarding achievement paths while retaining the shared feature-flag predicate.
  • Narrowed analytics emission: Removed achievements_viewed from the general profile query and kept capture on the dedicated achievements query.
  • Added prospective coverage: Added focused tests proving newly qualifying free users receive representative active achievements.

⚠️ Existing free users cannot earn one-time badges

The new commit makes these award paths available to free users only when the qualifying milestone occurs after deployment. Users who already created or completed the relevant records remain permanently ineligible for five active badges even though the response now presents them as unlocked.

Technical details
# Reconcile one-time achievements when eligibility expands

## Affected sites
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:223-224`: `Liftoff` requires `TotalLogCount == 1`, so the next log cannot grant it to a user with prior logs.
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:251-255`: `FirstOrbit` requires the current habit count to equal one.
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:264-267`: `MissionControl` requires the current goal count to equal one.
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:280-284`: `GoalCrusher` requires the completed-goal count to equal one.
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:333-345`: an already-completed checklist returns before `OnboardingComplete` can be evaluated.
- `src/Orbit.Application/Gamification/Backfill/XpAwardLogBackfillService.cs`: the only gamification backfill reconstructs XP logs and does not reconcile achievements.

## Required outcome
- Idempotently grant these one-time active achievements to existing free users who satisfied their criteria while achievement earning was Pro-gated.
- Preserve the current behavior for users who have not yet satisfied each criterion and avoid duplicate achievement XP.
- Cover counts greater than one and an already-completed onboarding checklist in free-tier tests.

Pullfrog  | Fix it ➔ | View workflow run | Using GPT Sol | 𝕏

@thomasluizon

Copy link
Copy Markdown
Owner Author

Review fix approach for the existing-user reconciliation:

  1. Add an Application backfill service that scans existing free users only when GamificationFreeTier is enabled, derives the five one-time achievements from persisted habits, logs, goals, and onboarding state, and passes only missing ids through IGamificationService.TryGrantAchievementsAsync. This reuses the audited XP, level, persistence, and notification funnel and keeps repeat runs idempotent.
  2. Add a one-time Infrastructure hosted service guarded by a new AppConfig completion key. If the feature flag is disabled or not available to free users, it leaves the completion key unset so a later startup can retry.
  3. Register both services, add Application tests for counts greater than one, completed onboarding, already-earned ids, locked users, and repeat safety, plus Infrastructure tests for completion, skip, retry, and failure behavior.
  4. Update the stale IGamificationService and GamificationService contract comments that still describe the achievement catalog as Pro-gated.

I rejected query-side reconciliation because GET handlers should not mutate XP and notifications. I also rejected changing equality checks to greater-than checks alone because existing inactive users would remain stranded until another matching event.

@thomasluizon

Copy link
Copy Markdown
Owner Author

Delivery status for the merge update:

The PR now needs an approving Pullfrog review or owner dismissal of the superseded review before GitHub will report it mergeable.

@thomasluizon

Copy link
Copy Markdown
Owner Author

@pullfrog review

@pullfrog pullfrog 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.

Important

Retired badges can make the Progresso achievement counter exceed its total. The numerator and denominator need consistent semantics before merge.

Reviewed changes Reviewed the complete branch diff at bc1b119 against main, including API behavior and current mobile and web consumer assumptions.

  • Achievement availability: Returns active achievement payloads to free and Pro users while preserving the existing whole-profile feature flag.
  • Retired definitions: Marks seven social and cooperative achievements as retired, removes their grant and progress paths, and preserves earned historical badges.
  • Earning policy: Applies the free-tier gamification predicate to active habit, goal, bad-habit, and onboarding achievement paths.
  • Stable contract and analytics: Keeps AchievementsLocked and ProTeaser in the response, and emits achievements_viewed only from the dedicated achievements query.
  • Regression coverage: Updates query, service, retirement, social-path, and analytics unit tests.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GPT Sol | 𝕏

Comment thread src/Orbit.Application/Gamification/Queries/GetGamificationProfileQuery.cs Outdated

@pullfrog pullfrog 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.

ℹ️ The new commit fixes the achievement ratio without introducing new issues. The earlier rollout review remains outstanding.

Reviewed changes Reviewed the counter correction in 2417bea since the prior Pullfrog review at bc1b119.

  • Consistent achievement total: Counted the returned active and earned retired badges, preventing the displayed numerator from exceeding its denominator.
  • Regression coverage: Added a focused test proving all active badges plus one retired badge reports equal earned and total counts.

Pullfrog  | Fix it ➔ | View workflow run | Using GPT Sol | 𝕏

@thomasluizon

Copy link
Copy Markdown
Owner Author

Final review fix handoff:

  • Commit 2417bea fixes the retired achievement denominator and adds the requested overflow regression test.
  • Pullfrog confirmed that this commit fixes the achievement ratio without introducing new issues.
  • The pullfrog-approval check remains red because the earlier existing-user reconciliation review is still outstanding.
  • The later fix(api): rate-limit state-changing endpoints on five controllers #332 work order restricted this round to the denominator fix and capped review fix attempts at three, so no further code changes or review cycles will be started here.

Local verification is green with 0 build errors, 12 focused profile tests, and 5,765 tests in the full suite. This PR now needs Thomas to dismiss the superseded rollout review or make a new scope decision.

@thomasluizon

Copy link
Copy Markdown
Owner Author

@pullfrog review

@pullfrog pullfrog 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.

Important

Existing free users who crossed one-time milestones while those awards were Pro-gated remain unable to earn five active badges after this rollout.

Reviewed changes Reviewed the complete achievement availability, retirement, earning-policy, analytics, generated-artifact, and regression-test diff at 2417bea against main.

  • Achievement availability: Returns active achievement payloads to free and Pro users while preserving the whole-profile feature flag.
  • Retired achievements: Marks seven social and cooperative definitions as retired, removes their award and progress paths, and keeps earned historical badges resolvable.
  • Free-tier earning: Removes Pro-only guards from active habit, goal, bad-habit, and onboarding award paths under the existing gamification unlock predicate.
  • Response consistency: Keeps the existing contract fields and counts each returned active or earned retired badge in the total.
  • Analytics and tests: Emits achievements_viewed from the dedicated achievements query and updates focused application coverage.

⚠️ Existing free users remain stranded on one-time achievements

The removed Pro guards only help when a qualifying transition happens after deployment. A free user with existing qualifying data cannot naturally re-enter the exact-one or checklist-completion branches, so Progresso can now display Liftoff, First Orbit, Mission Control, Goal Crusher, and All Systems Go as active but unearned even though the user already satisfied them.

Technical details
# Reconcile one-time achievements when eligibility expands

## Affected sites
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:223-224`: `Liftoff` requires `TotalLogCount == 1`, so another log cannot award it to a user with multiple existing logs.
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:251-255`: `FirstOrbit` requires the top-level habit count to equal one.
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:264-267`: `MissionControl` requires the goal count to equal one.
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:280-284`: `GoalCrusher` requires the completed-goal count to equal one.
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:333-335`: an already-completed checklist returns before `OnboardingComplete` is evaluated.
- `src/Orbit.Infrastructure/Migrations/20260630175236_EnableGamificationFreeTierFlag.cs:14-19`: the free-tier flag was enabled before this rollout, so affected accounts have already accumulated qualifying state.
- `tests/Orbit.Application.Tests/Services/GamificationServiceTests.cs:523-538`, `568-583`, `1004-1021`, `1074-1098`, and `1128-1145`: the new tests cover only fresh count-one transitions and a checklist completed during the current call.

## Required outcome
- Idempotently grant these five active achievements to eligible free users who satisfied their criteria while achievement earning was Pro-gated.
- Avoid duplicate achievement XP and preserve current behavior for users who have not met each criterion.
- Cover counts greater than one and an already-completed onboarding checklist.

## Suggested approach
- Reconcile from persisted habits, logs, goals, and onboarding state through the existing `IGamificationService.TryGrantAchievementsAsync` persistence and XP funnel rather than mutating from a query handler.

Pullfrog  | Fix it ➔ | View workflow run | Using GPT Sol | 𝕏

@thomasluizon

Copy link
Copy Markdown
Owner Author

Disposition of the stranded one-time achievements finding: filed, not fixed here

The finding is correct and well evidenced. All five sites were confirmed against this branch at
2417bea5: Liftoff (GamificationService.cs:223-224), First Orbit (:251-255),
Mission Control (:264-267), Goal Crusher (:280-284) and the already-completed checklist return
before All Systems Go (:333-335). An existing free account cannot re-enter any of them.

Filed as https://github.com/thomasluizon/orbit-tickets/issues/360, blocked by #332, in the
539 Redesign milestone, carrying the five line references, the idempotency requirement and the
XP-duplication constraint.

Why it is filed rather than fixed in this pull request.

  1. It needs a reconciliation pass over persisted habits, logs, goals and onboarding state, routed
    through the IGamificationService XP funnel, with idempotency guarantees and a decision about
    whether it runs as a migration, a startup pass or an on-read repair. That is a separate deliverable
    from "make the achievements payload match Progresso", which is this ticket.
  2. This pull request is a strict improvement as it stands. Before it, a free account sees an empty
    achievements panel and earns nothing at all. After it, a free account sees all 32 active
    achievements and earns every one it qualifies for from deploy onward. Holding this to build the
    reconciliation would keep all 32 stranded in order to fix 5.
  3. The review fix budget for this pull request is spent. Three rounds have landed here: the free-tier
    earning gates, the analytics relocation, and the achievement-ratio correction. A fourth round on a
    separately shaped deliverable is the point at which it becomes its own ticket.

Nothing else in the review is outstanding. All three review threads are resolved, and the incremental
review of 2417bea5 recorded that the ratio fix introduced no new issues.

@thomasluizon

Copy link
Copy Markdown
Owner Author

@pullfrog review

@pullfrog pullfrog 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.

Important

The existing-user reconciliation gap is confirmed and still affects the payload this PR begins serving. Filing a follow-up records the defect, but it does not make this rollout correct for affected accounts.

Reviewed changes Reviewed the complete achievement availability, retirement, earning-policy, analytics, generated-artifact, test, and mobile-consumer scope at 2417bea against main.

  • Achievement availability: Returns active achievement payloads to free and Pro users while preserving the whole-profile feature flag.
  • Retired achievements: Marks seven definitions as retired, removes their award and progress paths, and keeps earned historical badges resolvable.
  • Free-tier earning: Removes Pro-only guards from active habit, goal, bad-habit, and onboarding award paths under the existing gamification unlock predicate.
  • Response consistency: Keeps the existing contract fields and counts each returned active or earned retired badge in the total.
  • Analytics and coverage: Emits achievements_viewed from the dedicated achievements query and updates focused application tests.
  • Consumer parity: The mobile shared schema remains compatible because no response field was removed, renamed, retyped, or newly required.

⚠️ Existing free users still receive incorrect one-time achievement state

The production free-tier flag was enabled before this rollout, so affected accounts have already crossed one-time milestones while awards were Pro-gated. This PR immediately exposes those active badges as unearned, but the exact-one checks and completed-checklist early return give those users no path to correct that state; a separately filed follow-up does not prevent the incorrect response between deployments.

Technical details
# Reconcile expanded one-time achievement eligibility before exposure

## Affected sites
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:223-224` — `Liftoff` only grants when the persisted log count is exactly one.
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:251-255` — `FirstOrbit` only grants when the top-level habit count is exactly one.
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:264-267` — `MissionControl` only grants when the goal count is exactly one.
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:280-284` — `GoalCrusher` only grants when the completed-goal count is exactly one.
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:333-345` — an already-completed onboarding checklist returns before `OnboardingComplete` can be evaluated.
- `src/Orbit.Infrastructure/Migrations/20260630175236_EnableGamificationFreeTierFlag.cs:14-19` — the free-tier flag was enabled before this availability change.
- `tests/Orbit.Application.Tests/Services/GamificationServiceTests.cs:523-538`, `568-583`, `1004-1021`, `1074-1098`, and `1128-1145` — the added coverage exercises fresh transitions only, not existing qualifying state.

## Required outcome
- Ensure eligible existing free users do not receive a permanently false unearned state for these five active achievements.
- Deliver idempotent reconciliation before or atomically with catalog exposure, or keep exposure gated until the filed reconciliation can deploy.
- Preserve achievement XP idempotency and cover counts greater than one plus an already-completed checklist.

Pullfrog  | Fix it ➔ | View workflow run | Using GPT Sol | 𝕏

@pullfrog pullfrog 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.

Important

The reconciliation can still expose incorrect one-time achievement state because it starts after the API and omits soft deleted historical evidence.

Reviewed changes Reviewed the reconciliation added in 12d349c since the prior Pullfrog review at 2417bea.

  • Eligibility reconstruction: Derived five one-time badges from users, habits, positive logs, goals, onboarding state, and existing grants.
  • Shared grant funnel: Routed missing ids through IGamificationService for idempotent XP, level, notification, feed, and persistence behavior.
  • Startup coordination: Added feature flag and completion marker checks around a one-time hosted sweep.
  • Regression coverage: Added application and infrastructure tests for eligible and ineligible state, idempotence, marker behavior, and failure handling.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GPT Sol | 𝕏

@pullfrog pullfrog 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.

Important

Startup reconciliation now ignores the feature flag that controls free-tier achievement earning, so a disabled rollout can still create permanent badges and XP.

Reviewed changes Reviewed the startup coordination and generated architecture refresh added after a025af6.

  • Blocking reconciliation: Replaced the delayed background sweep with startup gating and in-process retries until reconciliation succeeds or startup is cancelled.
  • Generated architecture: Refreshed the architecture outputs to include the final reconciliation service and test references.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GPT Sol | 𝕏

@thomasluizon

Copy link
Copy Markdown
Owner Author

Round 5 approach for #332:

  1. Keep the existing deferred-account and startup reconciliation state changes. Complete their focused coverage for locked accounts, retries, completion state, and query handlers that must remain gated until reconciliation finishes.
  2. Replace the per-account GetEnabledKeysForUserAsync loop with one batch resolution over the users already loaded by the reconciliation. FeatureFlagService.cs currently performs one Users query per call before consulting its feature-definition cache. The batch path will reuse the loaded User aggregates and require at most one AppFeatureFlags query on a cache miss, with no per-account database reads.
  3. Compile and run focused reconciliation, query, and feature-flag tests, then commit the coherent deferral and batching work before broader verification.
  4. Merge origin/main without rebasing, inspect any gamification conflicts, regenerate the architecture and OpenAPI artifacts, commit the merge result, then run the full build and test suite before pushing.

I rejected resolving flags one account at a time because the implementation proves that path grows one Users query per free account. I also rejected bypassing IFeatureFlagService from Application because feature-plan evaluation belongs in the existing flag abstraction.

@pullfrog pullfrog 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.

Important

Deferred legacy accounts can now read the achievement catalog before their historical reconciliation completes.

Reviewed changes Reviewed ab398524 since the prior Pullfrog review at 80e57f2, including the API behavior and mobile contract impact.

  • New-account initialization: Marked newly created users reconciled so accounts provisioned after the startup sweep are not stranded.
  • Atomic reconciliation persistence: Moved each account stamp into the retryable achievement grant save and added a relational concurrency regression test.
  • Read eligibility: Removed reconciliation timestamps from both achievement read decisions, leaving access based on Pro status or gamification_free_tier.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GPT Sol | 𝕏

Comment thread src/Orbit.Application/Gamification/Queries/GetAchievementsQuery.cs
…hievements-progresso

# Conflicts:
#	architecture.html
#	architecture.json
@thomasluizon

Copy link
Copy Markdown
Owner Author

@pullfrog review

@pullfrog pullfrog 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.

Important

The latest architecture refresh and merge do not address the existing reconciliation-readiness finding. The full branch also has two blocking rollout risks: new accounts can be permanently excluded after a feature-flag cycle, and first deployment waits on an unbounded fleet-wide history load.

Reviewed changes Reviewed the complete branch at ad85cb1, including the achievement contract, free-tier earning, retired definitions, per-account reconciliation, hosted lifecycle, persistence behavior, tests, and the current mobile schema.

  • Achievement availability: Returns the active catalog to Pro users and free users enabled for gamification_free_tier while retaining existing response fields.
  • Retired achievements: Keeps seven historical definitions resolvable, removes their grant and progress paths, and displays retired badges only when earned.
  • Free-tier earning: Applies the shared gamification unlock predicate to active achievement award paths.
  • Historical reconciliation: Adds a per-account timestamp, reconstructs five one-time milestones, and grants them through the existing XP and notification funnel.
  • Hosted rollout: Runs reconciliation during host startup and retries feature-locked accounts in the background.
  • Consumer compatibility: The current mobile Zod schema still accepts achievementsLocked, nullable proTeaser, and the returned achievement objects without a breaking field change.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GPT Sol | 𝕏

Comment thread src/Orbit.Domain/Entities/User.cs Outdated
@thomasluizon

Copy link
Copy Markdown
Owner Author

Approach for review round 9: expose one single account reconciliation path from AchievementEligibilityReconciliationService and have the fleet sweep reuse it. GetAchievementsQuery will call that path only after entitlement succeeds and only when the historical backfill stamp is absent, before loading earned badges. Tests will cover the first read grant and stamp, a no work second read, locked access, and concurrent inline and background reconciliation with idempotent XP. This keeps the existing background worker as a backstop and avoids a second eligibility implementation.

@pullfrog pullfrog 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.

Important

The new lazy repair path can return 500 when it overlaps the background sweep because its duplicate-insert conflict bypasses the existing retry.

Reviewed changes Reviewed the inline historical reconciliation added in 99ae1a3 since the prior Pullfrog review at ad85cb1.

  • Reconciled on first read: Added synchronous historical eligibility repair before the dedicated achievements query loads earned badges.
  • Extracted shared eligibility loading: Reused the same persisted habit, log, goal, onboarding, and earned-badge reconstruction for fleet and single-account repair.
  • Hardened idempotence: Skipped grant work when the retryable persistence path reloads an account whose reconciliation stamp is already present.
  • Added concurrency coverage: Exercised simultaneous inline and background repair with two contexts and asserted one badge, one XP award, and one persisted stamp.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GPT Sol | 𝕏

Comment thread src/Orbit.Application/Gamification/Queries/GetAchievementsQuery.cs Outdated
…hievements-progresso

# Conflicts:
#	architecture.html
#	architecture.json
@thomasluizon

Copy link
Copy Markdown
Owner Author

@pullfrog review

@pullfrog pullfrog 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.

Important

The current head still contains the previously anchored unbounded startup sweep, permanent flag-cycle stamp, and PostgreSQL duplicate-grant race. The mobile and web profile path also remains unreconciled, as noted inline.

Reviewed changes Reviewed the complete 51-file, 20-commit branch at 71bfd20, including the current mobile consumer contract and endpoint usage.

  • Catalog behavior: Exposes 32 active achievements to eligible free and Pro accounts, retains earned retired badges, and keeps existing response fields.
  • Earning policy: Extends active habit, goal, bad-habit, and onboarding awards to unlocked free accounts while removing retired social grant paths.
  • Historical reconciliation: Reconstructs five one-time milestones, grants through the XP funnel, and records per-account completion.
  • Rollout lifecycle: Adds startup and deferred sweeps plus a dedicated achievements-query inline repair.
  • Persistence and coverage: Adds the nullable user stamp, partial index, migration, concurrency-oriented tests, and updated application and infrastructure coverage.
  • Consumer contract: Confirms the shared Zod schema remains field-compatible and that both shipped achievements screens consume the profile endpoint.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GPT Sol | 𝕏

@thomasluizon

Copy link
Copy Markdown
Owner Author

Round 10 approach for #332:

  1. Remove User.Create stamping so only a completed reconciliation while gamification is unlocked can set AchievementEligibilityReconciledAtUtc.
  2. Expose one shared unlocked account reconciliation entry point and call it from both GetAchievementsQuery and GetGamificationProfileQuery before either builds achievement state. The existing sweep will use the same implementation.
  3. Bound the sweep to one deterministic candidate batch per pass and make the hosted service start the pass without holding application startup. Later passes will continue from the remaining unstamped accounts.
  4. Make concurrent inline and background grants idempotent at the database boundary by recognizing the existing UserAchievement unique constraint violation as an already completed grant, then reload the account state without duplicating XP.
  5. Add focused tests for flag off then on, both read handlers, bounded sweep progress, nonblocking startup, and a real concurrent duplicate insert. Compile and run focused tests, commit the coherent change, then run the full suite, push, update this PR body, and verify checks.

I am reusing the repository's DbUniqueViolation pattern and one reconciliation implementation. I rejected process locks because the inline read and hosted sweep run in separate scopes and the database uniqueness rule is the authoritative concurrency boundary.

@pullfrog pullfrog 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.

Important

The delta fixes the profile, startup, and duplicate-insert paths, but the existing feature-flag-cycle reconciliation finding remains open and still requires correction before merge.

Reviewed changes Reviewed the reconciliation simplification in b3ad8656 since the prior Pullfrog review at 71bfd20b.

  • Aligned read repair: Reconciled unlocked unstamped accounts before both the dedicated achievements response and the gamification profile consumed by mobile and web.
  • Bounded fleet processing: Limited each worker pass to 100 accounts plus one lookahead row and moved the first pass off the host startup path.
  • Convergent grant races: Retried PostgreSQL unique violations through a tracking reset and persisted-state reload so concurrent inline and background grants converge.
  • Corrected account initialization: Left new accounts unstamped until an unlocked reconciliation actually evaluates their persisted eligibility.
  • Expanded regression coverage: Added paging, nonblocking startup, profile repair, and provider-shaped unique-conflict tests while preserving the shipped mobile contract.

Pullfrog  | Fix it ➔ | View workflow run | Using GPT Sol | 𝕏

@thomasluizon

Copy link
Copy Markdown
Owner Author

Round 11 approach: merge origin/main into this branch without rebasing. Resolve the three social command conflicts by keeping main's NotificationUrls based null URL shape while preserving this branch's removal of retired achievement grants. Resolve architecture.html from one side only, then regenerate architecture.json and architecture.html with tools/arch-map.mjs and rebuild the API contract if openapi.json moves. Verify the seven retired ids remain only in AchievementDefinitions.cs, no inline streak or social notification URL returns, and round 10 reconciliation behavior remains unchanged. This shape preserves both tickets' independent behavior and avoids hand editing generated artifacts.

@pullfrog pullfrog 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.

Important

No new Critical or High issues were introduced by this synchronization. The existing feature flag cycle reconciliation thread remains open and still blocks approval.

Reviewed changes Reviewed the main synchronization in d6b0bc53 since the prior Pullfrog review at b3ad8656.

  • Integrated social access guards: Reconciled the current accountability acceptance, friendship acceptance, and cheer guard entry points without restoring retired achievement award paths.
  • Preserved consumer compatibility: Kept the achievement response contract unchanged and compatible with the shipped mobile Zod schema.
  • Validated affected commands: Confirmed all 31 focused accountability, friendship, and cheer command tests pass.

Pullfrog  | Fix it ➔ | View workflow run | Using GPT Sol | 𝕏

@thomasluizon

Copy link
Copy Markdown
Owner Author

@pullfrog review

@pullfrog pullfrog 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.

Important

Reconciliation still misses eligibility across later flag cycles and reopened goals, and it can race normal award paths in a way that rolls back earned XP.

Reviewed changes Reviewed the complete branch at d6b0bc5, the existing same-head feedback, and the current mobile consumer contract.

  • Catalog access and retirement: Exposes the active catalog to eligible free and Pro accounts, retains earned retired badges, and removes retired social grant paths.
  • Unified earning and analytics: Applies the free-tier predicate to active achievement earning and emits achievements_viewed from the dedicated query.
  • Historical reconciliation: Reconstructs five one-time milestones, repairs both achievement read paths, and records account-level completion.
  • Persistence and worker lifecycle: Adds the nullable user stamp, partial index, bounded hosted worker, and duplicate-grant retry coverage.
  • Consumer compatibility: Keeps the DTO shape append-only and compatible with the shipped orbit-ui-mobile Zod schema; no paired mobile PR is open.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using GPT Sol | 𝕏

Comment thread src/Orbit.Application/Gamification/Services/GamificationService.cs
@thomasluizon

Copy link
Copy Markdown
Owner Author

Split approach for #332:

  1. Remove the achievement eligibility reconciliation service, hosted service, registrations, user state, migration, snapshot state, query dependencies, and reconciliation-only tests.
  2. Preserve the shipping behavior: free-tier visibility and earning, seven retired definitions, ratio-safe totals, achievements_viewed placement, retained response fields, and the notification URL fixes already merged from main.
  3. Inspect the duplicate-key handling in GamificationService after removing reconciliation. I will keep it only if the ordinary award paths still use it as a complete standalone safety property; otherwise I will remove all related wiring and tests.
  4. Regenerate architecture artifacts if their source graph changes, run focused build and tests, commit the coherent split, then run the full build and test suite.
  5. Merge origin/main without rebasing if the branch remains behind, regenerate any affected artifacts, push this branch, rewrite the PR body to state the split and chore(api): quiet routine background-service logs to Debug; stop leaking AI exception detail #362 boundary, and verify the required checks.

This removes the historical backfill subsystem rather than attempting a twelfth reconciliation repair round.

@sonarqubecloud

Copy link
Copy Markdown

@pullfrog pullfrog 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.

Important

The new delta removes the only historical repair path while immediately exposing the active catalog, so existing free accounts can remain permanently incorrect. This must be resolved before merge.

Reviewed changes Reviewed the reconciliation removal and main synchronization since the prior Pullfrog review at d6b0bc5.

  • Removed historical reconciliation: Deleted the eligibility service, worker, user stamp, migration, query hooks, registrations, repository helpers, and dedicated tests, and deferred that behavior to a follow-up.
  • Preserved ordinary grant convergence: Retained the PostgreSQL unique-violation retry and focused test for concurrent TryGrantAchievementsAsync calls.
  • Integrated current base behavior: Brought in the latest main changes, including per-goal reward identity, without changing the shipped achievement DTO shape.
  • Refreshed generated architecture: Removed the deleted reconciliation components and references from the architecture artifacts.

⚠️ Historical milestones are exposed without any repair path

The current award predicates grant these badges only on the original transition, but the new commit deletes every reconciliation path before both achievement queries expose the active catalog. Existing free accounts that crossed those transitions while awards were Pro-gated will remain visibly unearned and will never receive the corresponding achievement XP.

Technical details
# Reconcile existing one-time eligibility before exposure

## Affected sites
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:223-224`: `Liftoff` requires the total log count to equal one.
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:251-255`: `FirstOrbit` requires the top-level habit count to equal one.
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:264-267`: `MissionControl` requires the goal count to equal one.
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:288-292`: `GoalCrusher` requires the completed-goal count to equal one.
- `src/Orbit.Application/Gamification/Services/GamificationService.cs:340-342`: an already-completed checklist returns before `OnboardingComplete` can be evaluated.
- `src/Orbit.Application/Gamification/Queries/GetAchievementsQuery.cs:42-58` and `src/Orbit.Application/Gamification/Queries/GetGamificationProfileQuery.cs:53-65`: both paths expose the active catalog without historical repair.
- `tests/Orbit.Application.Tests/Services/GamificationServiceTests.cs:526-540`, `571-585`, `1030-1046`, `1099-1124`, and `1153-1170`: coverage exercises only fresh transitions after enablement.
- PR body: the cited follow-up `#362` resolves to an unrelated, already-closed observability pull request in this repository.

## Required outcome
- Before or atomically with catalog exposure, idempotently grant these five badges and their XP to existing accounts with durable qualifying evidence, or keep affected reads gated until that repair deploys.
- Preserve exactly-once XP and achievement delivery.
- Cover counts greater than one, an already-completed onboarding checklist, and historical goal completion evidence.

Pullfrog  | Fix it ➔ | View workflow run | Using GPT Sol | 𝕏

@thomasluizon

Copy link
Copy Markdown
Owner Author

Disposition of the historical-repair finding: split by an explicit human decision, filed as #362

The finding is correct on its facts and it is not being dismissed. It is being sequenced, by Thomas,
deliberately, with the evidence in front of him.

Filed as https://github.com/thomasluizon/orbit-tickets/issues/362, blocked by this ticket, in the
539 Redesign milestone. It carries all five affected achievements with their line references, and the
eleven requirements these review rounds established, including the two findings just resolved above as
still-open work: the permanent stamp failing across a later flag cycle, and GoalCrusher eligibility
reading a status that Reactivate, MarkAbandoned and a target increase all clear.

Why this pull request should merge without it.

This PR is a strict improvement for every account the finding names. Today a free account sees an empty
achievements panel and earns nothing at all. After this, a free account sees all 32 active achievements
and earns every one it qualifies for from deploy onward. The residue is five one-time achievements for
accounts that crossed those milestones before this ships.

So the gap that remains is strictly narrower than the gap that is closed, and no account is worse off
than it is right now. "Existing free accounts can remain permanently incorrect" is true, and it is also
true today, for all 32 rather than for 5.

Why it is being split rather than finished here.

Eleven review rounds. Every one raised genuinely new, real defects rather than repeats, and the last
several were all in the reconciliation while the ungating had been correct for rounds. The
reconciliation is a lazy backfill crossed with a feature flag that can cycle, a paged background sweep,
an inline read path, and the normal award paths running concurrently with all of it. Each round closed
one combination and exposed the next.

That is a subsystem, not a follow-up chore. It deserves to be designed once, against a written set of
requirements, rather than discovered one review round at a time while a bounded and correct change waits
behind it. #362 is that written set.

This was escalated and decided by a human. The same split was proposed earlier in this run and
rejected on this same objection, which was the right call at the time. Eleven rounds is the new
evidence, and Thomas reviewed it and chose to split.

No reconciliation code, state, migration or test remains on this branch.

@thomasluizon

Copy link
Copy Markdown
Owner Author

@pullfrog review

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes Reviewed the complete 36-file branch at fd016ba, the current orbit-ui-mobile consumers, passing required checks, and the owner's same-head disposition of the prior historical-repair finding.

  • Catalog access: Eligible free and Pro accounts receive the active achievement catalog through both API paths used by the shipped clients.
  • Retired definitions: Seven social achievements remain resolvable for earned historical badges while their grant paths and progress reads are removed.
  • Earning and grant safety: Active achievements follow the free-tier predicate, and concurrent duplicate grants reset tracking and converge on persisted state without replaying XP or push delivery.
  • Contract compatibility: No shipped DTO field is removed, renamed, retyped, or newly required; the mobile Zod schema accepts the retained lock and teaser fields plus the additive progress fields.
  • Rollout boundary: Historical repair for five one-time milestones is deliberately sequenced into the private orbit-tickets#362 follow-up after an explicit owner decision, rather than remaining an accidental omission in this branch.

Pullfrog  | View workflow run | Using GPT Sol | 𝕏

@thomasluizon
thomasluizon merged commit bf89004 into main Aug 23, 2026
23 checks passed
@thomasluizon
thomasluizon deleted the feature/ticket-332-achievements-progresso branch August 23, 2026 16:47
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