Three coupled correctness fixes to /tutorial/gift_receive's response:
- received_ids / total_receive_count_list / reward_list are now built
from `toClaim` (the gifts THIS call granted), not from `requestedIds`.
Echoing the client's request meant idempotent re-claims re-fired the
"+N received" popup and direct-assigned the same post-state totals
again, breaking the documented idempotency contract.
- is_unreceived_present is now `unclaimedPresents.Count > 0`. The
hardcoded false hid the inbox badge after partial claims even when
present_list still carried unclaimed gifts.
- tutorial_step echoes the persisted (max-preserved) state instead of
a hardcoded 41. A replay against a state>=41 viewer used to surface
41 on the wire and regress the client's tutorial state machine.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
The client's PlayerStaticData.UpdateHaveUserGoodsNumByJsonData does direct
assignment on each reward_list entry's reward_num, so currency/item totals
must be the new viewer balance — not the gift delta. Fresh accounts were
seeing their cached crystal/rupy balances clobbered down to the gift counts
until the next /load/index. Matches the project_wire_reward_list_post_state
memory and the prod capture (which shows 120 rupy = baseline 20 + gift 100).