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
///