Skip to content

Make achievements payload match Progresso - #480

Open
thomasluizon wants to merge 23 commits into
mainfrom
feature/ticket-332-achievements-progresso
Open

Make achievements payload match Progresso#480
thomasluizon wants to merge 23 commits into
mainfrom
feature/ticket-332-achievements-progresso

Conversation

@thomasluizon

@thomasluizon thomasluizon commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Closes #332

Summary

  • Return the active achievement catalog to every account whose Progresso access is enabled.
  • Retire the seven unreachable social achievements while preserving definition lookup for historical earned badges and public profiles.
  • Count active achievements plus each user's earned retired badges so the earned ratio cannot exceed 1.
  • Keep AchievementsLocked and ProTeaser in the response contract while returning false and null after unlock.
  • Remove retired grant paths and obsolete progress reads.
  • Apply the GamificationFreeTier predicate to achievement earning as well as 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.

Existing account reconciliation

  • Reconcile Liftoff, First Orbit, Mission Control, Goal Crusher, and All Systems Go from persisted habits, logs, goals, and onboarding state through IGamificationService.
  • Store historical backfill state per account in the nullable AchievementEligibilityReconciledAtUtc user column.
  • Leave feature locked free accounts unstamped and deferred. The hosted retry processes them after they unlock.
  • Select only unstamped accounts through the partial IX_Users_AchievementEligibilityReconciledAtUtc index.
  • Remove the AchievementEligibilityReconciliationComplete AppConfig marker and the singleton AchievementReconciliationState.
  • Keep the batched feature flag read and count deferred accounts directly from locked free candidates.
  • Leave new accounts unstamped until an actual reconciliation runs while gamification is unlocked.
  • Keep the reconciliation timestamp out of entitlement and visibility decisions. Reads depend only on Pro access or GamificationFreeTier.
  • Reconcile an unlocked unstamped account inline from both gamification profile and achievements reads through one shared eligibility implementation.
  • Process the fleet in bounded pages of 100 accounts plus one lookahead row. Current Pro and trial candidates sort ahead of locked accounts so deferred rows cannot starve eligible work.
  • Launch the recurring hosted worker without awaiting its first database pass during application startup.
  • Grant missing historical achievements and persist the account timestamp in the same retryable GamificationService save operation.
  • Treat a concurrent UserAchievement unique constraint loss as success, clear tracking, and recompute from persisted state so one badge and one XP award survive an inline and sweep race.

The fully reconciled path performs one indexed Users query that returns no candidates. Relational tests force concurrent inline and background scopes, surface the PostgreSQL unique violation shape to the losing save, and verify one badge row, one XP award, and a persisted stamp.

External interface evidence

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

Verification

  • dotnet build Orbit.slnx: 0 errors.
  • Focused achievement reconciliation tests: 181 passed.
  • dotnet test: 5,902 passed.
  • Architecture generation: 188 endpoints, 164 handlers, 0 orphan endpoints, and 0 untested feature folders.
  • Acceptance searches find no obsolete progress metrics, global reconciliation marker, singleton reconciliation state, retired ids outside AchievementDefinitions, or reconciliation timestamp in entitlement checks.

Assumptions

  • Applied the unified earning predicate to the bad_habit_breaker Pro condition because it encoded the same stale policy as the listed early returns; 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.
  • Deferred free accounts without GamificationFreeTier until the flag unlocks them; rejected reconciling an account while its normal earning funnel remains disabled.
  • Treated soft deleted habits, logs, and goals as historical milestone evidence; rejected current state query filters because earned achievements are not revoked when source records are deleted.
  • Treated a positive log on a non bad habit as persisted Liftoff evidence; rejected treating a bad habit slip as a completed habit.
  • Reused the User aggregates already loaded by the sweep for batch flag evaluation; rejected a second Users query because candidate identity and plan are already available in memory.
  • Prioritized current Pro and trial candidates within each bounded page; rejected arbitrary id ordering because a full page of locked free accounts could indefinitely hide eligible accounts behind it.
  • 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.

@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 permanent completion state still becomes stale across a later free-tier flag cycle, which can expose incorrect one-time achievement status.

Reviewed changes Reviewed the complete achievement availability and reconciliation branch at d2fdef7 against main, including the shipped mobile contract.

  • Achievement availability: Returns active achievement payloads to eligible free and Pro users while retaining the existing response fields.
  • Retired achievements: Marks seven definitions as retired, removes their grant and progress paths, and preserves earned historical badges.
  • Expanded earning and reconciliation: Enables active awards for unlocked free users and reconstructs five one-time milestones from persisted state through the idempotent grant funnel.
  • Rollout coordination: Gates free-user reads on in-process reconciliation state, retries deferred accounts, and persists a global completion marker.
  • Analytics and coverage: Emits achievements_viewed from the dedicated query and adds focused application and hosted-service tests.
  • Consumer contract: Keeps achievementsLocked and nullable nextReward.proTeaser compatible with the mobile Zod schema.

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

@thomasluizon

Copy link
Copy Markdown
Owner Author

Round 7 approach for #332:

  1. Add a nullable per-user achievement eligibility reconciliation timestamp with a domain mutator, configure an index for the null candidate predicate, and generate the additive EF migration.
  2. Change the reconciliation sweep to load only unstamped users. Keep the existing batched feature flag resolution and direct deferred count, process only Pro or unlocked free candidates, and stamp each processed account after its persisted eligibility is evaluated through the normal grant funnel.
  3. Remove the AchievementEligibilityReconciliationComplete AppConfig marker, the singleton reconciliation state, and the read-handler dependency on that global state. Free-user reads will require the existing feature flag plus that user's reconciliation timestamp, so a deferred account opens only after its own reconciliation completes.
  4. Simplify the hosted service to retry failures before startup and keep deferred accounts on its existing background retry path without ever short-circuiting the fleet through a global marker.
  5. Extend domain, application, infrastructure, query, and relational query-shape tests for the per-user stamp, locked then unlocked reconciliation, idempotent reruns, and the indexed zero-candidate query. Then compile and run focused tests, commit, run the full suite, push, and verify PR Make achievements payload match Progresso #480.

This makes the persistence unit match the eligibility unit. I rejected retaining an in-memory or AppConfig completion signal because either can stop work for a user whose own reconciliation is still pending.

@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 per-account reconciliation replacement leaves two paths where free accounts stay paygated even though gamification_free_tier is enabled. Accounts created after the worker exits are never stamped, and a normal grant retry can discard pending stamps, so both paths must be corrected before merge.

Reviewed changes Reviewed a662b09 and the 80e57f2 merge since the prior Pullfrog review at d2fdef7.

  • Per-account readiness: Replaced the global AppConfig and singleton completion state with User.AchievementEligibilityReconciledAtUtc, and required it on both free-user reads.
  • Indexed reconciliation: Limited sweeps to unstamped candidates, preserved deferred locked users, and added the nullable column with a partial index.
  • Hosted lifecycle and coverage: Removed marker persistence, retained startup and deferred retries, and added domain, query-shape, and hosted-service coverage.

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

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

Copy link
Copy Markdown
Owner Author

Round 8 approach for #332:

  1. Remove AchievementEligibilityReconciledAtUtc from the GetAchievementsQuery entitlement decision. Reads will depend only on Pro access or GamificationFreeTier, while the timestamp remains historical backfill state only.
  2. Initialize AchievementEligibilityReconciledAtUtc in User.Create so new accounts begin reconciled and never enter the historical candidate sweep.
  3. Refactor AchievementEligibilityReconciliationService to process and persist one candidate in a tracked scope that is reacquired after any grant retry clears tracking. This prevents detached User instances from losing their reconciliation timestamp.
  4. Add focused query and domain coverage for newly created free accounts, plus a reconciliation test that forces the existing concurrency retry and proves processed account timestamps persist and the next candidate query is empty.
  5. Compile and run focused tests, commit the coherent fix, then run the full build and test suite, push this branch, update the PR body, and verify all PR checks.

The timestamp will not appear in any entitlement check. I rejected retaining candidate entity references across TryGrantAchievementsAsync because ResetTracking can detach them during a recoverable retry.

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

@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

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𝕏

User user,
CancellationToken cancellationToken = default)
{
if (user.AchievementEligibilityReconciledAtUtc is not null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AchievementEligibilityReconciledAtUtc is permanent, but achievement earning can later be disabled by GamificationFreeTier. A reconciled free account that crosses an exact-count milestone during that disabled interval keeps this stamp, so re-enabling the flag makes both reads skip repair and leaves badges such as Liftoff or FirstOrbit and their XP permanently missing.

Technical details
# Reconcile every feature-locked interval

## Affected sites
- [`AchievementEligibilityReconciliationService.cs:121-122`](https://github.com/thomasluizon/orbit-api/blob/d6b0bc5342729d85fae5f04837830e380e0aee20/src/Orbit.Application/Gamification/Backfill/AchievementEligibilityReconciliationService.cs#L121-L122) treats the first completed reconciliation as terminal for the account.
- [`AchievementEligibilityReconciliationService.cs:57-59`](https://github.com/thomasluizon/orbit-api/blob/d6b0bc5342729d85fae5f04837830e380e0aee20/src/Orbit.Application/Gamification/Backfill/AchievementEligibilityReconciliationService.cs#L57-L59) excludes stamped accounts from every later sweep.
- [`GamificationService.cs:224-285`](https://github.com/thomasluizon/orbit-api/blob/d6b0bc5342729d85fae5f04837830e380e0aee20/src/Orbit.Application/Gamification/Services/GamificationService.cs#L224-L285) uses exact first-event counts after the unlock predicate, so a skipped event is not recovered naturally.
- [`GetAchievementsQuery.cs:53-57`](https://github.com/thomasluizon/orbit-api/blob/d6b0bc5342729d85fae5f04837830e380e0aee20/src/Orbit.Application/Gamification/Queries/GetAchievementsQuery.cs#L53-L57) and [`GetGamificationProfileQuery.cs:61-65`](https://github.com/thomasluizon/orbit-api/blob/d6b0bc5342729d85fae5f04837830e380e0aee20/src/Orbit.Application/Gamification/Queries/GetGamificationProfileQuery.cs#L61-L65) reopen immediately from the current flag and permanent stamp.

## Required outcome
- Accounts that accumulate one-time eligibility during any disabled interval must be reconciled before reads expose the catalog again.
- Cover reconcile, disable flag, cross a one-time milestone, re-enable flag, and verify one badge plus one XP award.

.ToHashSet();
var goalOwnerIds = goals.Select(goal => goal.UserId).ToHashSet();
var completedGoalOwnerIds = goals
.Where(goal => goal.Status == GoalStatus.Completed)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

GoalCrusher eligibility is derived only from the goal's current status. Goal.Reactivate, MarkAbandoned, and target-increase reopening clear completed state, so an account that completed a goal while awards were gated can later be stamped without the historical badge or XP even though that achievement should not be revoked.

Technical details
# Preserve historical goal completion evidence

## Affected sites
- [`AchievementEligibilityReconciliationService.cs:168-170`](https://github.com/thomasluizon/orbit-api/blob/d6b0bc5342729d85fae5f04837830e380e0aee20/src/Orbit.Application/Gamification/Backfill/AchievementEligibilityReconciliationService.cs#L168-L170) recognizes only goals that remain `Completed` at reconciliation time.
- [`Goal.cs:197-201`](https://github.com/thomasluizon/orbit-api/blob/d6b0bc5342729d85fae5f04837830e380e0aee20/src/Orbit.Domain/Entities/Goal.cs#L197-L201) reopens a completed goal after a target increase and clears `CompletedAtUtc`.
- [`Goal.cs:225-243`](https://github.com/thomasluizon/orbit-api/blob/d6b0bc5342729d85fae5f04837830e380e0aee20/src/Orbit.Domain/Entities/Goal.cs#L225-L243) also clears completion evidence when a completed goal is abandoned or reactivated.

## Required outcome
- Reconciliation must use durable prior-completion evidence for every goal completion and reopening path before stamping the account.
- Cover completion while earning is locked, reopening, unlock, reconciliation, and exactly one `GoalCrusher` grant and XP award.

attempt++;
continue;
}
catch (DbUpdateException exception) when (

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 23505 retry protects only GrantAchievementsAsync, but the worker can race ProcessHabitLogged or ProcessGoalCompleted while each stages the same UserAchievement. If reconciliation commits first, the normal save can bypass its concurrency-only catch and the safe caller swallows a rollback of base XP and any other awards.

Technical details
# Make normal award paths converge with reconciliation

## Affected sites
- [`GamificationService.cs:424-430`](https://github.com/thomasluizon/orbit-api/blob/d6b0bc5342729d85fae5f04837830e380e0aee20/src/Orbit.Application/Gamification/Services/GamificationService.cs#L424-L430) retries duplicate achievement inserts only inside the reconciliation grant loop.
- [`GamificationService.cs:74-84`](https://github.com/thomasluizon/orbit-api/blob/d6b0bc5342729d85fae5f04837830e380e0aee20/src/Orbit.Application/Gamification/Services/GamificationService.cs#L74-L84) and [`GamificationService.cs:505-515`](https://github.com/thomasluizon/orbit-api/blob/d6b0bc5342729d85fae5f04837830e380e0aee20/src/Orbit.Application/Gamification/Services/GamificationService.cs#L505-L515) retry concurrency exceptions but let the same unique violation escape from normal habit and goal processing.
- [`LogHabitCommand.cs:290-301`](https://github.com/thomasluizon/orbit-api/blob/d6b0bc5342729d85fae5f04837830e380e0aee20/src/Orbit.Application/Habits/Commands/LogHabitCommand.cs#L290-L301) logs and suppresses that failure after the gamification transaction has rolled back.

## Required outcome
- A normal event racing reconciliation must reload persisted achievement state and still commit its non-duplicate base XP and other eligible awards exactly once.
- Add concurrent reconciliation versus actual `ProcessHabitLogged` and `ProcessGoalCompleted` coverage rather than racing two reconciliation calls.

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