From 89e7a69049021ed3ef82805343ada30f11e657d8 Mon Sep 17 00:00:00 2001 From: neckfire Date: Tue, 14 Jul 2026 12:30:11 +0200 Subject: [PATCH] feat(insurance,fortuna): payout on death + Fortuna Pass claim safety - CompleteInventoryUpdate: insured items lost in a raid (present in ItemsToRemove) are recovered back to the stash and the insurance is consumed. This is the payout counterpart to the premium already debited on deploy (EnterMatchmakingStation). Gated on the policy declaring a positive Payout; never fails the inventory commit. - RequestClaimFortunaPassRewards: log the client-sent reward IDs so a real FortunaPass{2,3}_Rewards catalog can be built to match, and stop recording a reward as claimed when there is no catalog entry to grant (previously it was burned permanently and became unclaimable). Co-Authored-By: Claude Opus 4.8 --- .../Functions/CompleteInventoryUpdate.cs | 44 ++++++++++++++++++- .../RequestClaimFortunaPassRewards.cs | 19 ++++++-- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/CompleteInventoryUpdate.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/CompleteInventoryUpdate.cs index d8bb2f4..b4b3398 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/CompleteInventoryUpdate.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/CompleteInventoryUpdate.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Prospect.Server.Api.Services.Auth.Extensions; using Prospect.Server.Api.Services.CloudScript; +using Prospect.Server.Api.Services.CloudScript.Functions; using Prospect.Server.Api.Services.CloudScript.Models; using Prospect.Server.Api.Services.UserData; @@ -45,12 +46,14 @@ public class CompleteInventoryUpdateFunction : ICloudScriptFunction _logger; private readonly IHttpContextAccessor _httpContextAccessor; private readonly UserDataService _userDataService; + private readonly TitleDataService _titleDataService; - public CompleteInventoryUpdateFunction(ILogger logger, IHttpContextAccessor httpContextAccessor, UserDataService userDataService) + public CompleteInventoryUpdateFunction(ILogger logger, IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService) { _logger = logger; _httpContextAccessor = httpContextAccessor; _userDataService = userDataService; + _titleDataService = titleDataService; } public async Task ExecuteAsync(CompleteInventoryUpdateRequest request) @@ -64,6 +67,42 @@ public class CompleteInventoryUpdateFunction : ICloudScriptFunction{"Inventory"}); var inventory = JsonSerializer.Deserialize>(userData["Inventory"].Value); + // Insurance payout: this request also reports the end-of-match inventory, so items + // the player lost (death / left behind) arrive in ItemsToRemove. Any of those that + // were insured on deploy (EnterMatchmakingStation) are recovered back to the stash — + // the premium was already paid up-front — and the insurance is consumed (one raid). + // Recovery is only performed if the item's policy declares a positive Payout. + var recoveredInsured = new HashSet(); + if (request.ItemsToRemove is { Count: > 0 }) + { + try + { + var td = _titleDataService.Find(new List { "InventoryInsurance" }); + if (td.TryGetValue("InventoryInsurance", out var rawPolicies) && !string.IsNullOrEmpty(rawPolicies)) + { + var policies = JsonSerializer.Deserialize>(rawPolicies); + foreach (var item in inventory) + { + if (!request.ItemsToRemove.Contains(item.ItemId)) continue; + if (string.IsNullOrEmpty(item.Insurance) || item.InsuranceOwnerPlayfabId != userId) continue; + if (policies == null || !policies.TryGetValue(item.Insurance, out var policy) || policy.Payout <= 0) continue; + recoveredInsured.Add(item.ItemId); + item.Insurance = ""; // consumed for this raid + item.InsuranceOwnerPlayfabId = ""; + } + if (recoveredInsured.Count > 0) + { + _logger.LogInformation("Recovered {Count} insured item(s) for user {User}", recoveredInsured.Count, userId); + } + } + } + catch (Exception e) + { + // Never fail the inventory commit because of insurance handling. + _logger.LogWarning(e, "Failed to process insurance payout"); + } + } + // TODO: Optimize // TODO: Check deleted items to see if other stacks/mods were updated correctly // Process inventory update. The player may: @@ -74,7 +113,8 @@ public class CompleteInventoryUpdateFunction : ICloudScriptFunction(inventory.Count); foreach (var item in inventory) { - if (!request.ItemsToRemove.Contains(item.ItemId)) { + // Keep items that weren't removed, plus insured items recovered above. + if (!request.ItemsToRemove.Contains(item.ItemId) || recoveredInsured.Contains(item.ItemId)) { newInventory.Add(item); } } diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestClaimFortunaPassRewards.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestClaimFortunaPassRewards.cs index 1f0a7a2..59529d7 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestClaimFortunaPassRewards.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/RequestClaimFortunaPassRewards.cs @@ -45,12 +45,14 @@ public class RequestClaimFortunaPassRewardsFunction : ICloudScriptFunction _logger; - public RequestClaimFortunaPassRewardsFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService) + public RequestClaimFortunaPassRewardsFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService, ILogger logger) { _httpContextAccessor = httpContextAccessor; _userDataService = userDataService; _titleDataService = titleDataService; + _logger = logger; } public async Task ExecuteAsync(RequestClaimFortunaPassRewardsRequest request) @@ -96,9 +98,20 @@ public class RequestClaimFortunaPassRewardsFunction : ICloudScriptFunction {ItemId} x{Amount}", rewardId, def.ItemId, def.Amount); switch (def.ItemId) {