From 7d02278449c8cb77f89ed2d4e14701f2142664e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:10:16 +0000 Subject: [PATCH 1/3] Initial plan From 3d29cc36179adcb6a7ed6674588ed43f232d73ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:16:08 +0000 Subject: [PATCH 2/3] feat: Add RefundByEntitlementIds endpoint (POST /game/refund/v1) Co-authored-by: kirre-bylund <4068377+kirre-bylund@users.noreply.github.com> --- Runtime/Client/LootLockerEndPoints.cs | 2 + Runtime/Game/LootLockerSDKManager.cs | 22 ++++ Runtime/Game/Requests/PurchaseRequest.cs | 147 +++++++++++++++++++++++ 3 files changed, 171 insertions(+) diff --git a/Runtime/Client/LootLockerEndPoints.cs b/Runtime/Client/LootLockerEndPoints.cs index d0d0071d..9801d627 100644 --- a/Runtime/Client/LootLockerEndPoints.cs +++ b/Runtime/Client/LootLockerEndPoints.cs @@ -218,6 +218,8 @@ public class LootLockerEndPoints public static EndPointClass querySteamPurchaseRedemptionStatus = new EndPointClass("store/steam/redeem/query", LootLockerHTTPMethod.POST); public static EndPointClass finalizeSteamPurchaseRedemption = new EndPointClass("store/steam/redeem/finalise", LootLockerHTTPMethod.POST); + public static EndPointClass refundByEntitlementIds = new EndPointClass("refund/v1", LootLockerHTTPMethod.POST); + // Triggers public static EndPointClass InvokeTriggers = new EndPointClass("triggers/cozy-crusader/v1", LootLockerHTTPMethod.POST); diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index 80ef20d5..d74c4bd3 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -6994,6 +6994,28 @@ public static void FinalizeSteamPurchaseRedemption(string entitlementId, Action< LootLockerServerRequest.CallAPI(forPlayerWithUlid, LootLockerEndPoints.finalizeSteamPurchaseRedemption.endPoint, LootLockerEndPoints.finalizeSteamPurchaseRedemption.httpMethod, body, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } + /// + /// Refund one or more entitlements by their IDs. + /// + /// Submits a refund request for the specified entitlement IDs. Assets associated with the + /// entitlements will be removed from the player's inventory where possible, the original + /// purchase currency will be credited back, and any currency rewards tied to the entitlement + /// will be clawed back. The response includes details on what was reversed and any warnings + /// for items that could not be fully reversed. + /// + /// The IDs of the entitlements to refund + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void RefundByEntitlementIds(string[] entitlementIds, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + LootLockerAPIManager.RefundByEntitlementIds(forPlayerWithUlid, entitlementIds, onComplete); + } + #endregion #region Collectables diff --git a/Runtime/Game/Requests/PurchaseRequest.cs b/Runtime/Game/Requests/PurchaseRequest.cs index 665097c5..5ff15cf8 100644 --- a/Runtime/Game/Requests/PurchaseRequest.cs +++ b/Runtime/Game/Requests/PurchaseRequest.cs @@ -243,6 +243,141 @@ public class LootLockerFinalizeSteamPurchaseRedemptionRequest /// public string entitlement_id { get; set; } } + + /// + /// Request to refund one or more entitlements by their IDs. + /// + public class LootLockerRefundByEntitlementIdsRequest + { + /// + /// The IDs of the entitlements to refund + /// + public string[] entitlement_ids { get; set; } + } + + /// + /// Represents the action taken on a player inventory item during a refund. + /// + public class LootLockerRefundPlayerInventoryEvent + { + /// + /// The legacy numeric asset ID + /// + public ulong asset_id { get; set; } + /// + /// Display name of the asset + /// + public string name { get; set; } + /// + /// "removed" if taken back from inventory, "skipped" if it could not be removed (e.g. already consumed) + /// + public string action { get; set; } + } + + /// + /// Represents a currency entry (amount credited or debited) as part of a refund + /// + public class LootLockerRefundCurrencyEntry + { + /// + /// The ULID of the currency + /// + public string currency_id { get; set; } + /// + /// Short code identifying the currency (e.g. "gold", "gems") + /// + public string currency_code { get; set; } + /// + /// The amount credited or debited, as a string to support arbitrary precision + /// + public string amount { get; set; } + } + + /// + /// Represents a non-reversible reward that was granted alongside an entitlement and could not be clawed back + /// + public class LootLockerRefundNonReversibleReward + { + /// + /// "progression_points": points were added to a progression. + /// "progression_reset": a progression was reset to its initial state. + /// + public string kind { get; set; } + /// + /// The ULID of the progression that was affected + /// + public string id { get; set; } + /// + /// Display name of the progression + /// + public string name { get; set; } + /// + /// The number of points that were granted and cannot be reversed. Only present for kind "progression_points". + /// + public string amount { get; set; } + } + + /// + /// Represents a single warning detail for a refund + /// + public class LootLockerRefundWarningDetail + { + /// + /// The warning category: + /// "non_reversible_rewards": rewards granted that cannot be automatically clawed back. + /// "insufficient_funds": the player does not have enough currency balance to cover the clawback. + /// "already_refunded": the entitlement was already refunded before this request. + /// "refund_failed": the entitlement could not be refunded due to an unexpected error. + /// + public string type { get; set; } + /// + /// Human-readable explanation of the warning + /// + public string message { get; set; } + /// + /// The specific rewards that could not be reversed. Only present when type is "non_reversible_rewards". + /// + public LootLockerRefundNonReversibleReward[] rewards { get; set; } + } + + /// + /// Warnings for a specific entitlement during refund processing + /// + public class LootLockerRefundWarning + { + /// + /// The entitlement this warning applies to + /// + public string entitlement_id { get; set; } + /// + /// One or more warning conditions for this entitlement + /// + public LootLockerRefundWarningDetail[] details { get; set; } + } + + /// + /// Response from the refund by entitlement IDs endpoint + /// + public class LootLockerRefundByEntitlementIdsResponse : LootLockerResponse + { + /// + /// Assets that were added or removed from the player's inventory as part of the refund. + /// + public LootLockerRefundPlayerInventoryEvent[] player_inventory_events { get; set; } + /// + /// Currency amounts credited back to the player's wallet (the purchase price being returned). + /// + public LootLockerRefundCurrencyEntry[] currency_refunded { get; set; } + /// + /// Currency amounts debited from the player's wallet (currency rewards from the entitlement being reclaimed). + /// + public LootLockerRefundCurrencyEntry[] currency_clawback { get; set; } + /// + /// Warnings encountered during refund processing, grouped by entitlement. + /// A non-empty warnings array does not mean the refund failed — it means some aspects could not be fully reversed. + /// + public LootLockerRefundWarning[] warnings { get; set; } + } } namespace LootLocker @@ -267,5 +402,17 @@ public static void ActivateRentalAsset(string forPlayerWithUlid, LootLockerGetRe LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, "", (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } + + public static void RefundByEntitlementIds(string forPlayerWithUlid, string[] entitlementIds, Action onComplete) + { + if (entitlementIds == null || entitlementIds.Length == 0) + { + onComplete?.Invoke(LootLockerResponseFactory.InputUnserializableError(forPlayerWithUlid)); + return; + } + EndPointClass endPoint = LootLockerEndPoints.refundByEntitlementIds; + var body = LootLockerJson.SerializeObject(new LootLockerRefundByEntitlementIdsRequest { entitlement_ids = entitlementIds }); + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint, endPoint.httpMethod, body, (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }, useAuthToken: true); + } } } \ No newline at end of file From 4318242c6c2833662d61df6499501b5fba5349d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:31:51 +0000 Subject: [PATCH 3/3] fix: Convert action/kind/type string fields to enums in refund DTOs Co-authored-by: kirre-bylund <4068377+kirre-bylund@users.noreply.github.com> --- Runtime/Game/Requests/PurchaseRequest.cs | 54 +++++++++++++++++++----- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/Runtime/Game/Requests/PurchaseRequest.cs b/Runtime/Game/Requests/PurchaseRequest.cs index 5ff15cf8..7724b65f 100644 --- a/Runtime/Game/Requests/PurchaseRequest.cs +++ b/Runtime/Game/Requests/PurchaseRequest.cs @@ -23,6 +23,43 @@ public enum SteamPurchaseRedemptionStatus RefundedSuspectedFraud, RefundedFriendlyFraud } + + /// + /// The action taken on a player inventory item as part of a refund + /// + public enum LootLockerRefundInventoryEventAction + { + /// The item was successfully removed from the player's inventory + removed = 0, + /// The item could not be removed (e.g. already consumed) and was left in place + skipped = 1, + } + + /// + /// The kind of non-reversible reward that was granted alongside an entitlement + /// + public enum LootLockerRefundNonReversibleRewardKind + { + /// Points were added to a progression and cannot be taken back + progression_points = 0, + /// A progression was reset to its initial state and cannot be undone + progression_reset = 1, + } + + /// + /// The category of a per-entitlement warning returned during a refund + /// + public enum LootLockerRefundWarningType + { + /// Rewards granted that cannot be automatically clawed back + non_reversible_rewards = 0, + /// The player does not have enough currency balance to cover the clawback + insufficient_funds = 1, + /// The entitlement was already refunded before this request + already_refunded = 2, + /// The entitlement could not be refunded due to an unexpected error + refund_failed = 3, + } } namespace LootLocker.Requests @@ -269,9 +306,9 @@ public class LootLockerRefundPlayerInventoryEvent /// public string name { get; set; } /// - /// "removed" if taken back from inventory, "skipped" if it could not be removed (e.g. already consumed) + /// The action taken on this item: removed if taken back from inventory, skipped if it could not be removed (e.g. already consumed) /// - public string action { get; set; } + public LootLockerRefundInventoryEventAction action { get; set; } } /// @@ -299,10 +336,9 @@ public class LootLockerRefundCurrencyEntry public class LootLockerRefundNonReversibleReward { /// - /// "progression_points": points were added to a progression. - /// "progression_reset": a progression was reset to its initial state. + /// The kind of non-reversible reward: progression_points if points were added to a progression, progression_reset if a progression was reset to its initial state /// - public string kind { get; set; } + public LootLockerRefundNonReversibleRewardKind kind { get; set; } /// /// The ULID of the progression that was affected /// @@ -323,13 +359,9 @@ public class LootLockerRefundNonReversibleReward public class LootLockerRefundWarningDetail { /// - /// The warning category: - /// "non_reversible_rewards": rewards granted that cannot be automatically clawed back. - /// "insufficient_funds": the player does not have enough currency balance to cover the clawback. - /// "already_refunded": the entitlement was already refunded before this request. - /// "refund_failed": the entitlement could not be refunded due to an unexpected error. + /// The warning category: non_reversible_rewards if rewards granted cannot be automatically clawed back, insufficient_funds if the player does not have enough currency balance to cover the clawback, already_refunded if the entitlement was already refunded before this request, refund_failed if the entitlement could not be refunded due to an unexpected error /// - public string type { get; set; } + public LootLockerRefundWarningType type { get; set; } /// /// Human-readable explanation of the warning ///