Build & Deploy / build (push) Successful in 10s
- RequestStoreRotationData: rotation daily (reset 00:00 UTC) + weekly (lundi) avec storeIDs rotatifs déterministes et timers d'expiration corrects. - EnterMatchmakingStation: traite purchaseInsuranceRequest au déploiement — marque les items assurés (Insurance + owner) et débite la prime (TitleData InventoryInsurance). - FYPurchaseInventoryInsuranceRequest: modèle complété (insuranceId + itemIds). - RequestClaimFortunaPassRewards: claim saison-correct (S2/S3 séparés, plus d'écrasement) + octroi des récompenses depuis catalogue TitleData optionnel (repli sûr sinon). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
108 lines
4.6 KiB
C#
108 lines
4.6 KiB
C#
using System.Text.Json;
|
||
using Prospect.Server.Api.Models.Data;
|
||
using Prospect.Server.Api.Services.Auth.Extensions;
|
||
using Prospect.Server.Api.Services.CloudScript.Models;
|
||
using Prospect.Server.Api.Services.CloudScript.Models.Data;
|
||
using Prospect.Server.Api.Services.UserData;
|
||
|
||
namespace Prospect.Server.Api.Services.CloudScript.Functions;
|
||
|
||
// Config d'une police d'assurance (TitleData "InventoryInsurance").
|
||
public class InventoryInsurancePolicy
|
||
{
|
||
public string InsuranceId { get; set; }
|
||
public double Cost { get; set; }
|
||
public double Payout { get; set; }
|
||
}
|
||
|
||
[CloudScriptFunction("EnterMatchmakingStation")]
|
||
public class EnterMatchmakingStationFunction : ICloudScriptFunction<FYEnterMatchAzureFunction, FYEnterMatchmakingResult>
|
||
{
|
||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||
private readonly UserDataService _userDataService;
|
||
private readonly TitleDataService _titleDataService;
|
||
private readonly ILogger<EnterMatchmakingStationFunction> _logger;
|
||
|
||
public EnterMatchmakingStationFunction(
|
||
IHttpContextAccessor httpContextAccessor,
|
||
UserDataService userDataService,
|
||
TitleDataService titleDataService,
|
||
ILogger<EnterMatchmakingStationFunction> logger)
|
||
{
|
||
_httpContextAccessor = httpContextAccessor;
|
||
_userDataService = userDataService;
|
||
_titleDataService = titleDataService;
|
||
_logger = logger;
|
||
}
|
||
|
||
public async Task<FYEnterMatchmakingResult> ExecuteAsync(FYEnterMatchAzureFunction request)
|
||
{
|
||
// Au déploiement, le client peut demander d'assurer une partie de l'inventaire.
|
||
var insurance = request.PurchaseInventoryInsurances;
|
||
var context = _httpContextAccessor.HttpContext;
|
||
if (context != null && insurance?.ItemIds is { Count: > 0 })
|
||
{
|
||
try
|
||
{
|
||
await ApplyInsuranceAsync(context.User.FindAuthUserId(), insurance);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
// Ne jamais bloquer le déploiement à cause de l'assurance.
|
||
_logger.LogWarning(e, "Failed to apply inventory insurance on deploy");
|
||
}
|
||
}
|
||
|
||
return new FYEnterMatchmakingResult
|
||
{
|
||
Success = true,
|
||
ErrorMessage = "",
|
||
SingleplayerStation = true, // NOTE: This will always travel to single player station
|
||
IsMatchTravel = false,
|
||
};
|
||
}
|
||
|
||
// Marque les items choisis comme assurés (Insurance + propriétaire) et débite la
|
||
// prime en InsuranceCurrency selon le coût de la police (TitleData InventoryInsurance).
|
||
private async Task ApplyInsuranceAsync(string userId, FYPurchaseInventoryInsuranceRequest req)
|
||
{
|
||
var titleData = _titleDataService.Find(new List<string> { "InventoryInsurance" });
|
||
var policies = JsonSerializer.Deserialize<Dictionary<string, InventoryInsurancePolicy>>(titleData["InventoryInsurance"]);
|
||
var insuranceId = string.IsNullOrEmpty(req.InsuranceId) ? "Default" : req.InsuranceId;
|
||
if (policies == null || !policies.TryGetValue(insuranceId, out var policy))
|
||
{
|
||
_logger.LogWarning("Unknown insurance policy {InsuranceId}", insuranceId);
|
||
return;
|
||
}
|
||
|
||
var userData = await _userDataService.FindAsync(userId, userId, new List<string> { "Inventory", "Balance" });
|
||
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
|
||
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
|
||
|
||
var toInsure = new HashSet<string>(req.ItemIds);
|
||
var insuredCount = 0;
|
||
foreach (var item in inventory)
|
||
{
|
||
if (!toInsure.Contains(item.ItemId)) continue;
|
||
item.Insurance = insuranceId;
|
||
item.InsuranceOwnerPlayfabId = userId;
|
||
insuredCount++;
|
||
}
|
||
if (insuredCount == 0) return;
|
||
|
||
// Prime = coût de la police × nombre d'items assurés (arrondi au supérieur),
|
||
// plafonnée au solde disponible pour ne jamais passer en négatif.
|
||
var premium = (int)Math.Ceiling(policy.Cost * insuredCount);
|
||
premium = Math.Min(premium, balance.InsuranceCurrency);
|
||
balance.InsuranceCurrency -= premium;
|
||
|
||
await _userDataService.UpdateAsync(userId, userId, new Dictionary<string, string>
|
||
{
|
||
["Inventory"] = JsonSerializer.Serialize(inventory),
|
||
["Balance"] = JsonSerializer.Serialize(balance),
|
||
});
|
||
_logger.LogInformation("Insured {Count} item(s) under policy {Policy} for user {User} (premium {Premium})",
|
||
insuredCount, insuranceId, userId, premium);
|
||
}
|
||
}
|