feat(pack): /pack/info filters spent free-pack children and emits campaign metadata
This commit is contained in:
@@ -92,10 +92,19 @@ public class PackController : SVSimController
|
|||||||
.Select(b => new { b.PackId, b.Points })
|
.Select(b => new { b.PackId, b.Points })
|
||||||
.ToDictionaryAsync(x => x.PackId, x => x.Points);
|
.ToDictionaryAsync(x => x.PackId, x => x.Points);
|
||||||
|
|
||||||
|
// Per-viewer free-pack claim records, keyed by campaign id. Drives the
|
||||||
|
// "drop the type_detail=10 child once today's quota is spent" filter in ToDto.
|
||||||
|
// Plain projection — no owned-entity tracking needed (mirrors the items query above).
|
||||||
|
var freeClaimsByCampaignId = await _db.Viewers
|
||||||
|
.Where(v => v.Id == viewerId)
|
||||||
|
.SelectMany(v => v.FreePackClaims)
|
||||||
|
.Select(c => new { c.FreeGachaCampaignId, c.LastClaimedAt, c.ClaimCount })
|
||||||
|
.ToDictionaryAsync(x => x.FreeGachaCampaignId, x => (x.LastClaimedAt, x.ClaimCount));
|
||||||
|
|
||||||
return new PackInfoResponse
|
return new PackInfoResponse
|
||||||
{
|
{
|
||||||
PackConfigList = packs
|
PackConfigList = packs
|
||||||
.Select(p => ToDto(p, openCounts, ownedItemsByItemId, gachaPointBalancesByPackId))
|
.Select(p => ToDto(p, openCounts, ownedItemsByItemId, gachaPointBalancesByPackId, freeClaimsByCampaignId))
|
||||||
.ToList(),
|
.ToList(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -104,14 +113,31 @@ public class PackController : SVSimController
|
|||||||
PackConfigEntry p,
|
PackConfigEntry p,
|
||||||
IReadOnlyDictionary<int, ViewerPackOpenCount> openCounts,
|
IReadOnlyDictionary<int, ViewerPackOpenCount> openCounts,
|
||||||
IReadOnlyDictionary<long, int> ownedItemsByItemId,
|
IReadOnlyDictionary<long, int> ownedItemsByItemId,
|
||||||
IReadOnlyDictionary<int, int> gachaPointBalancesByPackId)
|
IReadOnlyDictionary<int, int> gachaPointBalancesByPackId,
|
||||||
|
IReadOnlyDictionary<int, (DateTime LastClaimedAt, int ClaimCount)> freeClaimsByCampaignId)
|
||||||
{
|
{
|
||||||
int openCount = openCounts.TryGetValue(p.Id, out var oc) ? oc.OpenCount : 0;
|
int openCount = openCounts.TryGetValue(p.Id, out var oc) ? oc.OpenCount : 0;
|
||||||
|
|
||||||
|
// Drop type_detail=10 (FREE_PACKS) children whose daily quota for THIS viewer is spent.
|
||||||
|
// Mirrors prod behavior: post-claim /pack/info simply omits the free child from
|
||||||
|
// child_gacha_info (verified in traffic_event_crate_free_pack.ndjson lines 28→32).
|
||||||
|
// Today's claim count >= DailyFreeGachaCount and same UTC date => hide.
|
||||||
|
var today = DateTime.UtcNow.Date;
|
||||||
|
bool ChildAvailable(PackChildGachaEntry c)
|
||||||
|
{
|
||||||
|
if (c.TypeDetail != 10) return true;
|
||||||
|
if (c.FreeGachaCampaignId is not int campaignId) return true;
|
||||||
|
if (!freeClaimsByCampaignId.TryGetValue(campaignId, out var claim)) return true;
|
||||||
|
if (claim.LastClaimedAt.Date != today) return true;
|
||||||
|
int dailyCap = c.DailyFreeGachaCount > 0 ? c.DailyFreeGachaCount : 1;
|
||||||
|
return claim.ClaimCount < dailyCap;
|
||||||
|
}
|
||||||
|
var visibleChildren = p.ChildGachas.Where(ChildAvailable).ToList();
|
||||||
|
|
||||||
// Ticket-only pack: every child is TICKET (4) or TICKET_MULTI (5). These are
|
// Ticket-only pack: every child is TICKET (4) or TICKET_MULTI (5). These are
|
||||||
// gifted-currency packs (tutorial starter, throwback) that don't participate in
|
// gifted-currency packs (tutorial starter, throwback) that don't participate in
|
||||||
// gacha-point accrual or exchange, even if GachaPointConfig is set in seed.
|
// gacha-point accrual or exchange, even if GachaPointConfig is set in seed.
|
||||||
bool isTicketOnly = p.ChildGachas.All(c => c.TypeDetail == 4 || c.TypeDetail == 5);
|
bool isTicketOnly = visibleChildren.All(c => c.TypeDetail == 4 || c.TypeDetail == 5);
|
||||||
|
|
||||||
PackGachaPointDto? gachaPointDto = null;
|
PackGachaPointDto? gachaPointDto = null;
|
||||||
if (p.GachaPointConfig is not null && !isTicketOnly)
|
if (p.GachaPointConfig is not null && !isTicketOnly)
|
||||||
@@ -145,7 +171,7 @@ public class PackController : SVSimController
|
|||||||
DialogTitle = b.DialogTitle,
|
DialogTitle = b.DialogTitle,
|
||||||
}).ToList(),
|
}).ToList(),
|
||||||
GachaDetail = p.GachaDetail,
|
GachaDetail = p.GachaDetail,
|
||||||
ChildGachaInfo = p.ChildGachas.Select(c => new PackChildGachaDto
|
ChildGachaInfo = visibleChildren.Select(c => new PackChildGachaDto
|
||||||
{
|
{
|
||||||
GachaId = c.GachaId,
|
GachaId = c.GachaId,
|
||||||
TypeDetail = c.TypeDetail,
|
TypeDetail = c.TypeDetail,
|
||||||
@@ -164,6 +190,14 @@ public class PackController : SVSimController
|
|||||||
: 0,
|
: 0,
|
||||||
IsDailySingle = c.IsDailySingle,
|
IsDailySingle = c.IsDailySingle,
|
||||||
OverrideIncreaseGachaPoint = c.OverrideIncreaseGachaPoint.ToString(CultureInfo.InvariantCulture),
|
OverrideIncreaseGachaPoint = c.OverrideIncreaseGachaPoint.ToString(CultureInfo.InvariantCulture),
|
||||||
|
CampaignName = c.CampaignName,
|
||||||
|
PurchaseLimitCount = c.PurchaseLimitCount > 0
|
||||||
|
? c.PurchaseLimitCount.ToString(CultureInfo.InvariantCulture)
|
||||||
|
: null,
|
||||||
|
DailyFreeGachaCount = c.DailyFreeGachaCount > 0
|
||||||
|
? c.DailyFreeGachaCount.ToString(CultureInfo.InvariantCulture)
|
||||||
|
: null,
|
||||||
|
FreeGachaCampaignId = c.FreeGachaCampaignId,
|
||||||
}).ToList(),
|
}).ToList(),
|
||||||
OpenCount = openCount,
|
OpenCount = openCount,
|
||||||
OpenCountLimit = p.OpenCountLimit,
|
OpenCountLimit = p.OpenCountLimit,
|
||||||
|
|||||||
@@ -210,4 +210,93 @@ public class PackControllerInfoTests
|
|||||||
"ticket-only pack must not emit a gacha_point block");
|
"ticket-only pack must not emit a gacha_point block");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Info_includes_free_pack_child_when_no_claim_today()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
long viewerId = await factory.SeedViewerAsync();
|
||||||
|
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||||
|
db.Packs.Add(new PackConfigEntry
|
||||||
|
{
|
||||||
|
Id = 80032, BasePackId = 80001, PackCategory = PackCategory.LegendCardPack,
|
||||||
|
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
|
||||||
|
GachaType = 1, GachaDetail = "throwback test", SleeveId = 5090001,
|
||||||
|
ChildGachas =
|
||||||
|
{
|
||||||
|
new PackChildGachaEntry { GachaId = 800032, TypeDetail = 5, Cost = 1, CardCount = 8, ItemId = 80001 },
|
||||||
|
new PackChildGachaEntry
|
||||||
|
{
|
||||||
|
GachaId = 780032, TypeDetail = 10, Cost = 1, CardCount = 8,
|
||||||
|
PurchaseLimitCount = 1, DailyFreeGachaCount = 1,
|
||||||
|
FreeGachaCampaignId = 49, CampaignName = "Test Campaign",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
var response = await client.PostAsync("/pack/info", JsonBody(EmptyEnvelope));
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
||||||
|
|
||||||
|
using var doc = JsonDocument.Parse(body);
|
||||||
|
var pack = doc.RootElement.GetProperty("pack_config_list").EnumerateArray()
|
||||||
|
.Single(p => p.GetProperty("parent_gacha_id").GetInt32() == 80032);
|
||||||
|
var children = pack.GetProperty("child_gacha_info").EnumerateArray().ToList();
|
||||||
|
Assert.That(children.Count, Is.EqualTo(2), "free + ticket children both visible pre-claim");
|
||||||
|
var free = children.Single(c => c.GetProperty("type_detail").GetInt32() == 10);
|
||||||
|
Assert.That(free.GetProperty("free_gacha_campaign_id").GetInt32(), Is.EqualTo(49));
|
||||||
|
Assert.That(free.GetProperty("campaign_name").GetString(), Is.EqualTo("Test Campaign"));
|
||||||
|
Assert.That(free.GetProperty("daily_free_gacha_count").GetString(), Is.EqualTo("1"));
|
||||||
|
Assert.That(free.GetProperty("purchase_limit_count").GetString(), Is.EqualTo("1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Info_drops_free_pack_child_when_claimed_today()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
long viewerId = await factory.SeedViewerAsync();
|
||||||
|
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||||
|
db.Packs.Add(new PackConfigEntry
|
||||||
|
{
|
||||||
|
Id = 80033, BasePackId = 80001, PackCategory = PackCategory.LegendCardPack,
|
||||||
|
CommenceDate = DateTime.UtcNow.AddDays(-1), CompleteDate = DateTime.UtcNow.AddDays(30),
|
||||||
|
GachaType = 1, GachaDetail = "throwback test", SleeveId = 5090001,
|
||||||
|
ChildGachas =
|
||||||
|
{
|
||||||
|
new PackChildGachaEntry { GachaId = 800033, TypeDetail = 5, Cost = 1, CardCount = 8, ItemId = 80001 },
|
||||||
|
new PackChildGachaEntry
|
||||||
|
{
|
||||||
|
GachaId = 780033, TypeDetail = 10, Cost = 1, CardCount = 8,
|
||||||
|
DailyFreeGachaCount = 1, FreeGachaCampaignId = 50, CampaignName = "X",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
var v = await db.Viewers.FirstAsync(x => x.Id == viewerId);
|
||||||
|
v.FreePackClaims.Add(new ViewerFreePackClaim
|
||||||
|
{
|
||||||
|
FreeGachaCampaignId = 50, ClaimCount = 1, LastClaimedAt = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
var response = await client.PostAsync("/pack/info", JsonBody(EmptyEnvelope));
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
using var doc = JsonDocument.Parse(body);
|
||||||
|
var pack = doc.RootElement.GetProperty("pack_config_list").EnumerateArray()
|
||||||
|
.Single(p => p.GetProperty("parent_gacha_id").GetInt32() == 80033);
|
||||||
|
var children = pack.GetProperty("child_gacha_info").EnumerateArray().ToList();
|
||||||
|
Assert.That(children.Count, Is.EqualTo(1), "Only the ticket child should remain after today's free claim");
|
||||||
|
Assert.That(children[0].GetProperty("type_detail").GetInt32(), Is.EqualTo(5));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user