fix(gift): tutorial gift_receive ThenIncludes OwnedItemEntry.Item

Same project_ef_nav_include_pitfall as 27ebb51's tutorial pack_open fix
but in the gift path: without .ThenInclude(i => i.Item), the existing
OwnedItemEntry's Item nav defaults to a new ItemEntry() (Id=0), so
RewardGrantService.ApplyAsync's `FirstOrDefault(i => i.Item.Id == detailId)`
misses pre-existing rows. It falls through to add a new entry, and the
(ViewerId, ItemId) unique index added 2026-05-25 throws on SaveChanges →
500 to the client, no tutorial advancement, no currency grant.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-05-28 21:08:19 -04:00
parent 82d9668c9b
commit 86759125a9
2 changed files with 46 additions and 1 deletions

View File

@@ -77,8 +77,14 @@ public class GiftController : SVSimController
// the pattern in TutorialController.Update and to make the intent clear.
// AsSplitQuery is the default-safe pattern when including viewer collections
// (project memory: project_ef_split_query).
//
// ThenInclude(i => i.Item) is load-bearing: OwnedItemEntry.Item is a separate non-owned
// entity whose default initialiser is `new ItemEntry()` (Id=0). Without the explicit
// ThenInclude, RewardGrantService.ApplyAsync's `FirstOrDefault(i => i.Item.Id == ...)`
// never matches a pre-existing row → falls through to add a duplicate → (ViewerId, ItemId)
// unique index throws on SaveChanges (project_ef_nav_include_pitfall).
var viewer = await _db.Viewers
.Include(v => v.Items)
.Include(v => v.Items).ThenInclude(i => i.Item)
.Include(v => v.MissionData)
.AsSplitQuery()
.FirstAsync(v => v.Id == viewerId);