@sonatel-os/strapi-plugin-tcg 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/_chunks/{App-DvvgeCGs.js → App-B-WZGNkc.js} +17 -3
- package/dist/_chunks/{App-j9qu2-To.mjs → App-CS_cVYEI.mjs} +17 -3
- package/dist/_chunks/{SettingsPage-Bxa_71iT.js → SettingsPage-DNWXuY8j.js} +2 -2
- package/dist/_chunks/{SettingsPage-BcJXkWnW.mjs → SettingsPage-fGiEcX6l.mjs} +2 -2
- package/dist/_chunks/{api-DACN35Y5.mjs → api-BgR-fpDp.mjs} +1 -1
- package/dist/_chunks/{api-Aney2S9e.js → api-hTExOv_J.js} +1 -1
- package/dist/_chunks/{index-DXuaf32J.js → index-CRbzJppw.js} +2 -2
- package/dist/_chunks/{index-DSm3eEtn.mjs → index-ClDgxnnH.mjs} +2 -2
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +745 -173
- package/dist/server/index.mjs +745 -173
- package/package.json +2 -2
package/dist/server/index.mjs
CHANGED
|
@@ -44,20 +44,46 @@ const RBAC = Object.freeze({
|
|
|
44
44
|
SETTINGS_READ: "settings.read",
|
|
45
45
|
SETTINGS_WRITE: "settings.write"
|
|
46
46
|
});
|
|
47
|
-
const SEASON_STATUS = Object.freeze({
|
|
48
|
-
|
|
47
|
+
const SEASON_STATUS = Object.freeze({
|
|
48
|
+
DRAFT: "draft",
|
|
49
|
+
ACTIVE: "active",
|
|
50
|
+
ARCHIVED: "archived"
|
|
51
|
+
});
|
|
52
|
+
const PACK_KIND = Object.freeze({
|
|
53
|
+
LOCATION: "location",
|
|
54
|
+
BOOSTER: "booster",
|
|
55
|
+
EVENT: "event"
|
|
56
|
+
});
|
|
49
57
|
const ACQUISITION = Object.freeze({ PULL: "pull", TRADE: "trade", GRANT: "grant" });
|
|
50
58
|
const TRADE_STATUS = Object.freeze({
|
|
51
59
|
PENDING: "pending",
|
|
60
|
+
// the QR is shown, nobody has joined
|
|
61
|
+
JOINED: "joined",
|
|
62
|
+
// a receiver read the offer and is choosing what to give
|
|
63
|
+
PROPOSED: "proposed",
|
|
64
|
+
// the receiver proposed cards; the initiator confirms or declines
|
|
52
65
|
ACCEPTED: "accepted",
|
|
53
66
|
REJECTED: "rejected",
|
|
54
67
|
EXPIRED: "expired",
|
|
55
68
|
CANCELLED: "cancelled"
|
|
56
69
|
});
|
|
57
|
-
const
|
|
70
|
+
const TRADE_OPEN_STATUSES = Object.freeze([
|
|
71
|
+
TRADE_STATUS.PENDING,
|
|
72
|
+
TRADE_STATUS.JOINED,
|
|
73
|
+
TRADE_STATUS.PROPOSED
|
|
74
|
+
]);
|
|
75
|
+
const REDEMPTION_STATUS = Object.freeze({
|
|
76
|
+
PENDING: "pending",
|
|
77
|
+
FULFILLED: "fulfilled",
|
|
78
|
+
CANCELLED: "cancelled"
|
|
79
|
+
});
|
|
58
80
|
const NONCE_KIND = Object.freeze({ PACK: "pack", TRADE: "trade", DROP: "drop" });
|
|
59
81
|
const DROP_MODE = Object.freeze({ STATIC: "static", ROTATING: "rotating" });
|
|
60
|
-
const POOL_MODE = Object.freeze({
|
|
82
|
+
const POOL_MODE = Object.freeze({
|
|
83
|
+
INHERIT: "inherit",
|
|
84
|
+
EXTEND: "extend",
|
|
85
|
+
OVERRIDE: "override"
|
|
86
|
+
});
|
|
61
87
|
const DEFAULT_DROP_NAME = "Entrée";
|
|
62
88
|
const MAX_PULL_RETRIES = 3;
|
|
63
89
|
const COST_RULE = Object.freeze({
|
|
@@ -95,10 +121,24 @@ const BADGE_RULE = Object.freeze({
|
|
|
95
121
|
// { type:'scan_drop', drop:<slug> }
|
|
96
122
|
});
|
|
97
123
|
const DEFAULT_RARITIES = Object.freeze([
|
|
98
|
-
{
|
|
124
|
+
{
|
|
125
|
+
key: "common",
|
|
126
|
+
label: "Common",
|
|
127
|
+
probability: 0.5,
|
|
128
|
+
color: "#7A7A86",
|
|
129
|
+
scoreMin: 10,
|
|
130
|
+
scoreMax: 30
|
|
131
|
+
},
|
|
99
132
|
{ key: "rare", label: "Rare", probability: 0.3, color: "#00BFFF", scoreMin: 35, scoreMax: 55 },
|
|
100
133
|
{ key: "epic", label: "Epic", probability: 0.15, color: "#A020F0", scoreMin: 60, scoreMax: 80 },
|
|
101
|
-
{
|
|
134
|
+
{
|
|
135
|
+
key: "legendary",
|
|
136
|
+
label: "Legendary",
|
|
137
|
+
probability: 0.05,
|
|
138
|
+
color: "#FFD700",
|
|
139
|
+
scoreMin: 85,
|
|
140
|
+
scoreMax: 100
|
|
141
|
+
}
|
|
102
142
|
]);
|
|
103
143
|
const DEFAULT_EDITIONS = Object.freeze([
|
|
104
144
|
{ key: "standard", label: "Standard" },
|
|
@@ -567,6 +607,12 @@ const attributes$f = {
|
|
|
567
607
|
cooldownMs: {
|
|
568
608
|
type: "integer"
|
|
569
609
|
},
|
|
610
|
+
cardsPerPull: {
|
|
611
|
+
type: "integer",
|
|
612
|
+
min: 1,
|
|
613
|
+
max: 10,
|
|
614
|
+
"default": 1
|
|
615
|
+
},
|
|
570
616
|
scanCount: {
|
|
571
617
|
type: "integer",
|
|
572
618
|
"default": 0,
|
|
@@ -1423,6 +1469,10 @@ const attributes$4 = {
|
|
|
1423
1469
|
},
|
|
1424
1470
|
mintNumber: {
|
|
1425
1471
|
type: "integer"
|
|
1472
|
+
},
|
|
1473
|
+
isExtra: {
|
|
1474
|
+
type: "boolean",
|
|
1475
|
+
"default": false
|
|
1426
1476
|
}
|
|
1427
1477
|
};
|
|
1428
1478
|
const schema$4 = {
|
|
@@ -1440,7 +1490,7 @@ const info$3 = {
|
|
|
1440
1490
|
singularName: "trade-offer",
|
|
1441
1491
|
pluralName: "trade-offers",
|
|
1442
1492
|
displayName: "TCG Trade Offer",
|
|
1443
|
-
description: "A
|
|
1493
|
+
description: "A P2P exchange: the initiator's QR, a receiver who joins and proposes cards, the initiator's confirmation."
|
|
1444
1494
|
};
|
|
1445
1495
|
const options$3 = {
|
|
1446
1496
|
draftAndPublish: false
|
|
@@ -1468,6 +1518,8 @@ const attributes$3 = {
|
|
|
1468
1518
|
type: "enumeration",
|
|
1469
1519
|
"enum": [
|
|
1470
1520
|
"pending",
|
|
1521
|
+
"joined",
|
|
1522
|
+
"proposed",
|
|
1471
1523
|
"accepted",
|
|
1472
1524
|
"rejected",
|
|
1473
1525
|
"expired",
|
|
@@ -1487,6 +1539,23 @@ const attributes$3 = {
|
|
|
1487
1539
|
expiresAt: {
|
|
1488
1540
|
type: "datetime",
|
|
1489
1541
|
required: true
|
|
1542
|
+
},
|
|
1543
|
+
receiver: {
|
|
1544
|
+
type: "relation",
|
|
1545
|
+
relation: "manyToOne",
|
|
1546
|
+
target: "plugin::tcg.player"
|
|
1547
|
+
},
|
|
1548
|
+
receiverCardIds: {
|
|
1549
|
+
type: "json"
|
|
1550
|
+
},
|
|
1551
|
+
joinedAt: {
|
|
1552
|
+
type: "datetime"
|
|
1553
|
+
},
|
|
1554
|
+
proposedAt: {
|
|
1555
|
+
type: "datetime"
|
|
1556
|
+
},
|
|
1557
|
+
declinedAt: {
|
|
1558
|
+
type: "datetime"
|
|
1490
1559
|
}
|
|
1491
1560
|
};
|
|
1492
1561
|
const schema$3 = {
|
|
@@ -1775,7 +1844,10 @@ const game = ({ strapi }) => {
|
|
|
1775
1844
|
return {
|
|
1776
1845
|
// ── Player ───────────────────────────────────────────────────────────
|
|
1777
1846
|
me: handle(strapi, async (ctx) => {
|
|
1778
|
-
ok(ctx, {
|
|
1847
|
+
ok(ctx, {
|
|
1848
|
+
...svc("format").player(ctx.state.tcgPlayer),
|
|
1849
|
+
isNew: Boolean(ctx.state.tcgPlayerIsNew)
|
|
1850
|
+
});
|
|
1779
1851
|
}),
|
|
1780
1852
|
updateMe: handle(strapi, async (ctx) => {
|
|
1781
1853
|
const { displayName, avatarUrl } = body(ctx);
|
|
@@ -1793,12 +1865,19 @@ const game = ({ strapi }) => {
|
|
|
1793
1865
|
packs: handle(strapi, async (ctx) => {
|
|
1794
1866
|
const season2 = await seasonOf(ctx);
|
|
1795
1867
|
const packs = await svc("pack").list(season2);
|
|
1796
|
-
ok(
|
|
1868
|
+
ok(
|
|
1869
|
+
ctx,
|
|
1870
|
+
packs.map((p) => svc("format").pack(p))
|
|
1871
|
+
);
|
|
1797
1872
|
}),
|
|
1798
1873
|
pack: handle(strapi, async (ctx) => {
|
|
1799
1874
|
const season2 = await seasonOf(ctx);
|
|
1800
1875
|
const pack2 = await svc("pack").findOne(season2, ctx.params.slug);
|
|
1801
|
-
const [pool, drops, exclusiveIds] = await Promise.all([
|
|
1876
|
+
const [pool, drops, exclusiveIds] = await Promise.all([
|
|
1877
|
+
svc("pack").pool(pack2),
|
|
1878
|
+
svc("drop").list(pack2, { includeInactive: false }),
|
|
1879
|
+
svc("drop").exclusiveCardIds(season2)
|
|
1880
|
+
]);
|
|
1802
1881
|
ok(ctx, {
|
|
1803
1882
|
...svc("format").pack(pack2),
|
|
1804
1883
|
cards: pool.filter((c) => !exclusiveIds.has(c.id) || c.isExclusive).map((c) => ({ ...svc("format").card(c, season2), isExclusive: c.isExclusive })),
|
|
@@ -1818,7 +1897,11 @@ const game = ({ strapi }) => {
|
|
|
1818
1897
|
windows.push({ ...v, pack: { id: pack2.documentId, name: pack2.name, slug: pack2.slug } });
|
|
1819
1898
|
}
|
|
1820
1899
|
}
|
|
1821
|
-
ok(ctx, {
|
|
1900
|
+
ok(ctx, {
|
|
1901
|
+
boosts: await svc("boost").liveNow(season2),
|
|
1902
|
+
drops: windows,
|
|
1903
|
+
serverTime: /* @__PURE__ */ new Date()
|
|
1904
|
+
});
|
|
1822
1905
|
}),
|
|
1823
1906
|
sets: handle(strapi, async (ctx) => {
|
|
1824
1907
|
const season2 = await seasonOf(ctx);
|
|
@@ -1833,8 +1916,13 @@ const game = ({ strapi }) => {
|
|
|
1833
1916
|
rewards: handle(strapi, async (ctx) => {
|
|
1834
1917
|
const season2 = await seasonOf(ctx);
|
|
1835
1918
|
const rewards = await svc("reward").list(season2, { includeInactive: false });
|
|
1836
|
-
const sets = Object.fromEntries(
|
|
1837
|
-
|
|
1919
|
+
const sets = Object.fromEntries(
|
|
1920
|
+
(await svc("set").list(season2)).map((x) => [x.slug, { name: x.name }])
|
|
1921
|
+
);
|
|
1922
|
+
ok(
|
|
1923
|
+
ctx,
|
|
1924
|
+
rewards.map((r) => svc("reward").format(r, season2, sets))
|
|
1925
|
+
);
|
|
1838
1926
|
}),
|
|
1839
1927
|
rewardEligibility: handle(strapi, async (ctx) => {
|
|
1840
1928
|
const season2 = await seasonOf(ctx);
|
|
@@ -1843,8 +1931,16 @@ const game = ({ strapi }) => {
|
|
|
1843
1931
|
cards: handle(strapi, async (ctx) => {
|
|
1844
1932
|
const season2 = await seasonOf(ctx);
|
|
1845
1933
|
const page = parsePagination(ctx.query);
|
|
1846
|
-
const { items, total } = await svc("card").list(
|
|
1847
|
-
|
|
1934
|
+
const { items, total } = await svc("card").list(
|
|
1935
|
+
season2,
|
|
1936
|
+
{ rarity: ctx.query.rarity, edition: ctx.query.edition, isActive: true },
|
|
1937
|
+
page
|
|
1938
|
+
);
|
|
1939
|
+
ctx.body = paged(
|
|
1940
|
+
items.map((c) => svc("format").card(c, season2)),
|
|
1941
|
+
total,
|
|
1942
|
+
page
|
|
1943
|
+
);
|
|
1848
1944
|
}),
|
|
1849
1945
|
card: handle(strapi, async (ctx) => {
|
|
1850
1946
|
const card2 = await svc("card").findOne(ctx.params.id);
|
|
@@ -1855,7 +1951,12 @@ const game = ({ strapi }) => {
|
|
|
1855
1951
|
collection: handle(strapi, async (ctx) => {
|
|
1856
1952
|
const season2 = await seasonOf(ctx);
|
|
1857
1953
|
const page = parsePagination(ctx.query);
|
|
1858
|
-
const { items, total } = await svc("collection").list(
|
|
1954
|
+
const { items, total } = await svc("collection").list(
|
|
1955
|
+
ctx.state.tcgPlayer,
|
|
1956
|
+
season2,
|
|
1957
|
+
{ rarity: ctx.query.rarity },
|
|
1958
|
+
page
|
|
1959
|
+
);
|
|
1859
1960
|
ctx.body = paged(items, total, page);
|
|
1860
1961
|
}),
|
|
1861
1962
|
collectionStats: handle(strapi, async (ctx) => {
|
|
@@ -1880,17 +1981,34 @@ const game = ({ strapi }) => {
|
|
|
1880
1981
|
// ── Trades ───────────────────────────────────────────────────────────
|
|
1881
1982
|
createOffer: handle(strapi, async (ctx) => {
|
|
1882
1983
|
const season2 = await seasonOf(ctx);
|
|
1883
|
-
created(
|
|
1984
|
+
created(
|
|
1985
|
+
ctx,
|
|
1986
|
+
await svc("trade").createOffer(ctx.state.tcgPlayer, season2, { cardIds: body(ctx).cardIds })
|
|
1987
|
+
);
|
|
1884
1988
|
}),
|
|
1885
1989
|
scanOffer: handle(strapi, async (ctx) => {
|
|
1886
1990
|
ok(ctx, await svc("trade").scan(ctx.state.tcgPlayer, String(body(ctx).qrToken ?? "")));
|
|
1887
1991
|
}),
|
|
1888
1992
|
getOffer: handle(strapi, async (ctx) => {
|
|
1889
1993
|
const offer = await svc("trade").findOffer(ctx.params.id);
|
|
1890
|
-
ok(ctx, await svc("trade").describe(offer, offer.season));
|
|
1994
|
+
ok(ctx, await svc("trade").describe(offer, offer.season, ctx.state.tcgPlayer));
|
|
1995
|
+
}),
|
|
1996
|
+
joinOffer: handle(strapi, async (ctx) => {
|
|
1997
|
+
ok(ctx, await svc("trade").join(ctx.state.tcgPlayer, ctx.params.id));
|
|
1998
|
+
}),
|
|
1999
|
+
proposeOffer: handle(strapi, async (ctx) => {
|
|
2000
|
+
ok(
|
|
2001
|
+
ctx,
|
|
2002
|
+
await svc("trade").propose(ctx.state.tcgPlayer, ctx.params.id, {
|
|
2003
|
+
cardIds: body(ctx).cardIds
|
|
2004
|
+
})
|
|
2005
|
+
);
|
|
1891
2006
|
}),
|
|
1892
|
-
|
|
1893
|
-
ok(ctx, await svc("trade").
|
|
2007
|
+
confirmOffer: handle(strapi, async (ctx) => {
|
|
2008
|
+
ok(ctx, await svc("trade").confirm(ctx.state.tcgPlayer, ctx.params.id));
|
|
2009
|
+
}),
|
|
2010
|
+
declineOffer: handle(strapi, async (ctx) => {
|
|
2011
|
+
ok(ctx, await svc("trade").decline(ctx.state.tcgPlayer, ctx.params.id));
|
|
1894
2012
|
}),
|
|
1895
2013
|
rejectOffer: handle(strapi, async (ctx) => {
|
|
1896
2014
|
ok(ctx, await svc("trade").reject(ctx.state.tcgPlayer, ctx.params.id));
|
|
@@ -1912,10 +2030,15 @@ const game = ({ strapi }) => {
|
|
|
1912
2030
|
createRedemption: handle(strapi, async (ctx) => {
|
|
1913
2031
|
const season2 = await seasonOf(ctx);
|
|
1914
2032
|
const { cardIds, rewardId } = body(ctx);
|
|
1915
|
-
created(
|
|
2033
|
+
created(
|
|
2034
|
+
ctx,
|
|
2035
|
+
await svc("redemption").create(ctx.state.tcgPlayer, season2, { cardIds, rewardId })
|
|
2036
|
+
);
|
|
1916
2037
|
}),
|
|
1917
2038
|
getRedemption: handle(strapi, async (ctx) => {
|
|
1918
|
-
const rec = await svc("redemption").findOne(ctx.params.id, {
|
|
2039
|
+
const rec = await svc("redemption").findOne(ctx.params.id, {
|
|
2040
|
+
forPlayer: ctx.state.tcgPlayer
|
|
2041
|
+
});
|
|
1919
2042
|
ok(ctx, svc("format").redemption(rec));
|
|
1920
2043
|
}),
|
|
1921
2044
|
redemptionHistory: handle(strapi, async (ctx) => {
|
|
@@ -2304,7 +2427,12 @@ const contentApi = [
|
|
|
2304
2427
|
{ method: "GET", path: "/cards/:id", handler: "game.card", config: CATALOG },
|
|
2305
2428
|
{ method: "GET", path: "/live", handler: "game.live", config: CATALOG },
|
|
2306
2429
|
{ method: "GET", path: "/rewards", handler: "game.rewards", config: CATALOG },
|
|
2307
|
-
{
|
|
2430
|
+
{
|
|
2431
|
+
method: "GET",
|
|
2432
|
+
path: "/rewards/eligibility",
|
|
2433
|
+
handler: "game.rewardEligibility",
|
|
2434
|
+
config: PLAYER
|
|
2435
|
+
},
|
|
2308
2436
|
{ method: "GET", path: "/sets", handler: "game.sets", config: PLAYER },
|
|
2309
2437
|
{ method: "GET", path: "/badges", handler: "game.badges", config: PLAYER },
|
|
2310
2438
|
// collection
|
|
@@ -2320,12 +2448,35 @@ const contentApi = [
|
|
|
2320
2448
|
{ method: "GET", path: "/trades/active", handler: "game.activeOffers", config: PLAYER },
|
|
2321
2449
|
{ method: "GET", path: "/trades/history", handler: "game.tradeHistory", config: PLAYER },
|
|
2322
2450
|
{ method: "GET", path: "/trades/offer/:id", handler: "game.getOffer", config: PLAYER },
|
|
2323
|
-
{ method: "POST", path: "/trades/offer/:id/
|
|
2451
|
+
{ method: "POST", path: "/trades/offer/:id/join", handler: "game.joinOffer", config: PLAYER },
|
|
2452
|
+
{
|
|
2453
|
+
method: "POST",
|
|
2454
|
+
path: "/trades/offer/:id/propose",
|
|
2455
|
+
handler: "game.proposeOffer",
|
|
2456
|
+
config: PLAYER
|
|
2457
|
+
},
|
|
2458
|
+
{
|
|
2459
|
+
method: "POST",
|
|
2460
|
+
path: "/trades/offer/:id/confirm",
|
|
2461
|
+
handler: "game.confirmOffer",
|
|
2462
|
+
config: PLAYER
|
|
2463
|
+
},
|
|
2464
|
+
{
|
|
2465
|
+
method: "POST",
|
|
2466
|
+
path: "/trades/offer/:id/decline",
|
|
2467
|
+
handler: "game.declineOffer",
|
|
2468
|
+
config: PLAYER
|
|
2469
|
+
},
|
|
2324
2470
|
{ method: "POST", path: "/trades/offer/:id/reject", handler: "game.rejectOffer", config: PLAYER },
|
|
2325
2471
|
{ method: "POST", path: "/trades/offer/:id/cancel", handler: "game.cancelOffer", config: PLAYER },
|
|
2326
2472
|
// redemptions
|
|
2327
2473
|
{ method: "POST", path: "/redemptions", handler: "game.createRedemption", config: PLAYER },
|
|
2328
|
-
{
|
|
2474
|
+
{
|
|
2475
|
+
method: "GET",
|
|
2476
|
+
path: "/redemptions/history",
|
|
2477
|
+
handler: "game.redemptionHistory",
|
|
2478
|
+
config: PLAYER
|
|
2479
|
+
},
|
|
2329
2480
|
{ method: "GET", path: "/redemptions/:id", handler: "game.getRedemption", config: PLAYER }
|
|
2330
2481
|
];
|
|
2331
2482
|
const can = (...actions) => ({
|
|
@@ -2494,34 +2645,55 @@ const qr = ({ strapi }) => ({
|
|
|
2494
2645
|
},
|
|
2495
2646
|
/** @param {Object} drop @param {number} ttlMs */
|
|
2496
2647
|
forDrop(drop2, ttlMs) {
|
|
2497
|
-
if (drop2.mode === DROP_MODE.STATIC)
|
|
2498
|
-
|
|
2648
|
+
if (drop2.mode === DROP_MODE.STATIC)
|
|
2649
|
+
return {
|
|
2650
|
+
...signStaticToken(drop2.documentId, this.secretOf(drop2)),
|
|
2651
|
+
mode: DROP_MODE.STATIC,
|
|
2652
|
+
expiresAt: null
|
|
2653
|
+
};
|
|
2654
|
+
return {
|
|
2655
|
+
...signDropToken(drop2.documentId, this.secretOf(drop2), ttlMs),
|
|
2656
|
+
mode: DROP_MODE.ROTATING
|
|
2657
|
+
};
|
|
2499
2658
|
},
|
|
2500
2659
|
forTrade(offer, player2, ttlMs) {
|
|
2501
|
-
return signTradeToken(
|
|
2660
|
+
return signTradeToken(
|
|
2661
|
+
offer.documentId,
|
|
2662
|
+
player2.documentId,
|
|
2663
|
+
strapi.plugin("tcg").service("settings").qrGlobalSecret(),
|
|
2664
|
+
ttlMs
|
|
2665
|
+
);
|
|
2502
2666
|
},
|
|
2503
2667
|
/**
|
|
2504
|
-
* Verifies signature + expiry; burns the nonce for rotating/trade tokens
|
|
2668
|
+
* Verifies signature + expiry; burns the nonce for rotating/trade tokens — per player for a drop code, so
|
|
2669
|
+
* the same displayed code serves everyone around the screen, once each. Throws TcgError.
|
|
2505
2670
|
* @param {string} token
|
|
2506
2671
|
* @param {string} secret
|
|
2507
2672
|
* @param {'drop'|'trade'} kind
|
|
2673
|
+
* @param {{ playerId?: string }} [scope] - the scanning player's documentId (drops)
|
|
2508
2674
|
*/
|
|
2509
|
-
async consume(token, secret, kind2) {
|
|
2675
|
+
async consume(token, secret, kind2, scope = {}) {
|
|
2510
2676
|
const result = verifyToken(token, secret);
|
|
2511
2677
|
if (!result.valid) {
|
|
2512
|
-
if (result.reason === "expired")
|
|
2678
|
+
if (result.reason === "expired")
|
|
2679
|
+
throw badRequest(ERROR_CODES.QR_EXPIRED, "Ce QR code a expiré.");
|
|
2513
2680
|
throw badRequest(ERROR_CODES.QR_INVALID, "QR code invalide.");
|
|
2514
2681
|
}
|
|
2515
2682
|
if (result.kind !== kind2) throw badRequest(ERROR_CODES.QR_INVALID, "QR code invalide.");
|
|
2516
2683
|
if (!result.nonce) return result;
|
|
2517
|
-
const
|
|
2518
|
-
|
|
2684
|
+
const key = kind2 === "drop" && scope.playerId ? `${result.nonce}:${scope.playerId}` : result.nonce;
|
|
2685
|
+
const seen = await strapi.db.query(UID.NONCE).findOne({ where: { nonce: key }, select: ["id"] });
|
|
2686
|
+
if (seen) throw badRequest(ERROR_CODES.QR_REUSED, "Vous avez déjà scanné ce QR code.");
|
|
2519
2687
|
try {
|
|
2520
2688
|
await strapi.db.query(UID.NONCE).create({
|
|
2521
|
-
data: {
|
|
2689
|
+
data: {
|
|
2690
|
+
nonce: key,
|
|
2691
|
+
kind: kind2 === "drop" ? NONCE_KIND.DROP : NONCE_KIND.TRADE,
|
|
2692
|
+
expiresAt: new Date(result.expiresAt)
|
|
2693
|
+
}
|
|
2522
2694
|
});
|
|
2523
2695
|
} catch (err) {
|
|
2524
|
-
throw badRequest(ERROR_CODES.QR_REUSED, "
|
|
2696
|
+
throw badRequest(ERROR_CODES.QR_REUSED, "Vous avez déjà scanné ce QR code.");
|
|
2525
2697
|
}
|
|
2526
2698
|
return result;
|
|
2527
2699
|
},
|
|
@@ -3040,44 +3212,91 @@ const pull = ({ strapi }) => ({
|
|
|
3040
3212
|
async scan(player2, qrToken) {
|
|
3041
3213
|
const plugin = strapi.plugin("tcg");
|
|
3042
3214
|
const peek = peekToken(qrToken);
|
|
3043
|
-
if (!peek || peek.kind !== "drop")
|
|
3215
|
+
if (!peek || peek.kind !== "drop")
|
|
3216
|
+
throw badRequest(ERROR_CODES.QR_INVALID, "QR code invalide.");
|
|
3044
3217
|
const drop2 = await plugin.service("drop").byDocumentId(peek.dropId);
|
|
3045
3218
|
const pack2 = drop2.pack;
|
|
3046
3219
|
const season2 = pack2?.season;
|
|
3047
|
-
if (!pack2 || !pack2.isActive)
|
|
3048
|
-
|
|
3220
|
+
if (!pack2 || !pack2.isActive)
|
|
3221
|
+
throw unprocessable(ERROR_CODES.PACK_INACTIVE, "Ce pack n'est plus disponible.");
|
|
3222
|
+
if (!season2 || season2.status !== "active")
|
|
3223
|
+
throw unprocessable(ERROR_CODES.SEASON_INACTIVE, "Cette saison n'est pas en cours.");
|
|
3049
3224
|
if (peek.mode !== drop2.mode) throw badRequest(ERROR_CODES.QR_INVALID, "QR code invalide.");
|
|
3050
|
-
if (!plugin.service("drop").isOpen(drop2))
|
|
3051
|
-
|
|
3225
|
+
if (!plugin.service("drop").isOpen(drop2))
|
|
3226
|
+
throw unprocessable(ERROR_CODES.DROP_CLOSED, "Ce point n'est pas ouvert en ce moment.", {
|
|
3227
|
+
startsAt: drop2.startsAt,
|
|
3228
|
+
endsAt: drop2.endsAt
|
|
3229
|
+
});
|
|
3230
|
+
if (drop2.maxScansTotal != null && drop2.scanCount >= drop2.maxScansTotal)
|
|
3231
|
+
throw unprocessable(
|
|
3232
|
+
ERROR_CODES.DROP_SOLD_OUT,
|
|
3233
|
+
"Toutes les cartes de ce point ont été distribuées."
|
|
3234
|
+
);
|
|
3052
3235
|
const limits = plugin.service("season").limitsOf(season2);
|
|
3053
|
-
await plugin.service("qr").consume(qrToken, plugin.service("qr").secretOf(drop2), "drop"
|
|
3236
|
+
await plugin.service("qr").consume(qrToken, plugin.service("qr").secretOf(drop2), "drop", {
|
|
3237
|
+
playerId: player2.documentId
|
|
3238
|
+
});
|
|
3054
3239
|
const now = /* @__PURE__ */ new Date();
|
|
3055
3240
|
const owned = await strapi.db.query(UID.PLAYER_CARD).count({ where: { player: player2.id, card: { season: season2.id } } });
|
|
3056
|
-
if (owned >= limits.maxCollectionSize)
|
|
3241
|
+
if (owned >= limits.maxCollectionSize)
|
|
3242
|
+
throw unprocessable(
|
|
3243
|
+
ERROR_CODES.COLLECTION_FULL,
|
|
3244
|
+
`Collection pleine (${limits.maxCollectionSize} cartes max).`
|
|
3245
|
+
);
|
|
3057
3246
|
const cooldownMs = drop2.cooldownMs ?? (drop2.mode === DROP_MODE.STATIC ? Math.max(limits.pullCooldownMs, limits.staticScanCooldownMs) : limits.pullCooldownMs);
|
|
3058
|
-
const lastHere = await strapi.db.query(UID.PULL).findOne({
|
|
3247
|
+
const lastHere = await strapi.db.query(UID.PULL).findOne({
|
|
3248
|
+
where: { player: player2.id, drop: drop2.id, isExtra: false },
|
|
3249
|
+
orderBy: { pulledAt: "desc" },
|
|
3250
|
+
select: ["pulledAt"]
|
|
3251
|
+
});
|
|
3059
3252
|
if (lastHere) {
|
|
3060
3253
|
const elapsed = now.getTime() - new Date(lastHere.pulledAt).getTime();
|
|
3061
3254
|
if (elapsed < cooldownMs) {
|
|
3062
3255
|
const remainingSeconds = Math.ceil((cooldownMs - elapsed) / 1e3);
|
|
3063
|
-
throw unprocessable(
|
|
3256
|
+
throw unprocessable(
|
|
3257
|
+
ERROR_CODES.COOLDOWN_ACTIVE,
|
|
3258
|
+
remainingSeconds > 3600 ? `Revenez dans ${Math.ceil(remainingSeconds / 3600)} h.` : `Patientez encore ${remainingSeconds} s.`,
|
|
3259
|
+
{ remainingSeconds }
|
|
3260
|
+
);
|
|
3064
3261
|
}
|
|
3065
3262
|
}
|
|
3066
3263
|
if (drop2.maxScansPerPlayer != null) {
|
|
3067
|
-
const mine = await strapi.db.query(UID.PULL).count({ where: { player: player2.id, drop: drop2.id } });
|
|
3068
|
-
if (mine >= drop2.maxScansPerPlayer)
|
|
3264
|
+
const mine = await strapi.db.query(UID.PULL).count({ where: { player: player2.id, drop: drop2.id, isExtra: false } });
|
|
3265
|
+
if (mine >= drop2.maxScansPerPlayer)
|
|
3266
|
+
throw unprocessable(
|
|
3267
|
+
ERROR_CODES.DROP_PLAYER_LIMIT,
|
|
3268
|
+
`Vous avez déjà obtenu vos ${drop2.maxScansPerPlayer} carte(s) ici.`
|
|
3269
|
+
);
|
|
3069
3270
|
}
|
|
3070
3271
|
const since = startOfToday$1();
|
|
3071
3272
|
const [packToday, totalToday] = await Promise.all([
|
|
3072
|
-
strapi.db.query(UID.PULL).count({
|
|
3073
|
-
|
|
3273
|
+
strapi.db.query(UID.PULL).count({
|
|
3274
|
+
where: { player: player2.id, pack: pack2.id, pulledAt: { $gte: since }, isExtra: false }
|
|
3275
|
+
}),
|
|
3276
|
+
strapi.db.query(UID.PULL).count({
|
|
3277
|
+
where: {
|
|
3278
|
+
player: player2.id,
|
|
3279
|
+
season: season2.id,
|
|
3280
|
+
pulledAt: { $gte: since },
|
|
3281
|
+
isExtra: false
|
|
3282
|
+
}
|
|
3283
|
+
})
|
|
3074
3284
|
]);
|
|
3075
|
-
if (packToday >= limits.maxDailyPullsPerPack)
|
|
3076
|
-
|
|
3285
|
+
if (packToday >= limits.maxDailyPullsPerPack)
|
|
3286
|
+
throw unprocessable(
|
|
3287
|
+
ERROR_CODES.DAILY_LIMIT_PACK,
|
|
3288
|
+
`Limite atteinte pour ce pack aujourd'hui (${limits.maxDailyPullsPerPack}).`
|
|
3289
|
+
);
|
|
3290
|
+
if (totalToday >= limits.maxDailyPullsTotal)
|
|
3291
|
+
throw unprocessable(
|
|
3292
|
+
ERROR_CODES.DAILY_LIMIT_TOTAL,
|
|
3293
|
+
`Limite quotidienne atteinte (${limits.maxDailyPullsTotal}).`
|
|
3294
|
+
);
|
|
3077
3295
|
const raw = await plugin.service("drop").rawPool(drop2, pack2);
|
|
3078
3296
|
const exclusiveIds = await plugin.service("drop").exclusiveCardIds(season2);
|
|
3079
3297
|
let pool = prunePool(raw, exclusiveIds);
|
|
3080
|
-
if (pool.length === 0)
|
|
3298
|
+
if (pool.length === 0)
|
|
3299
|
+
throw unprocessable(ERROR_CODES.PACK_EMPTY, "Aucune carte disponible ici pour le moment.");
|
|
3081
3300
|
const boosts = await plugin.service("boost").activeFor(season2, pack2, drop2);
|
|
3082
3301
|
const multipliers = plugin.service("boost").combine(boosts);
|
|
3083
3302
|
const recent = await strapi.db.query(UID.PULL).findMany({
|
|
@@ -3087,65 +3306,113 @@ const pull = ({ strapi }) => ({
|
|
|
3087
3306
|
select: ["rarity"]
|
|
3088
3307
|
});
|
|
3089
3308
|
const minRarity = pityFloor(recent, season2.rarities ?? [], limits);
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
mintNumber =
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
}
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
pity: result.pity,
|
|
3127
|
-
mintNumber
|
|
3309
|
+
const count = Math.max(1, Math.min(10, Number(drop2.cardsPerPull) || 1));
|
|
3310
|
+
const format2 = plugin.service("format");
|
|
3311
|
+
const cards = [];
|
|
3312
|
+
for (let n = 0; n < count; n += 1) {
|
|
3313
|
+
let result = null;
|
|
3314
|
+
let mintNumber = null;
|
|
3315
|
+
for (let attempt = 0; attempt < MAX_PULL_RETRIES && pool.length; attempt += 1) {
|
|
3316
|
+
result = pull$1({
|
|
3317
|
+
playerId: player2.documentId,
|
|
3318
|
+
dropId: drop2.documentId,
|
|
3319
|
+
pullNumber: totalToday + 1 + n * MAX_PULL_RETRIES + attempt,
|
|
3320
|
+
pool,
|
|
3321
|
+
rarities: season2.rarities ?? [],
|
|
3322
|
+
multipliers,
|
|
3323
|
+
minRarity: n === 0 ? minRarity : null
|
|
3324
|
+
});
|
|
3325
|
+
if (!result.card) break;
|
|
3326
|
+
mintNumber = await this.claimSlot(UID.CARD, result.card.id, "minted", "supplyCap");
|
|
3327
|
+
if (mintNumber != null) break;
|
|
3328
|
+
pool = pool.filter((c) => c.id !== result.card.id);
|
|
3329
|
+
result = null;
|
|
3330
|
+
}
|
|
3331
|
+
if (!result?.card || mintNumber == null) {
|
|
3332
|
+
if (n === 0) throw unprocessable(ERROR_CODES.PACK_EMPTY, "Plus de cartes disponibles ici.");
|
|
3333
|
+
break;
|
|
3334
|
+
}
|
|
3335
|
+
if (n === 0) {
|
|
3336
|
+
if (drop2.maxScansTotal != null) {
|
|
3337
|
+
const slot = await this.claimSlot(UID.DROP, drop2.id, "scanCount", "maxScansTotal");
|
|
3338
|
+
if (slot == null)
|
|
3339
|
+
throw unprocessable(
|
|
3340
|
+
ERROR_CODES.DROP_SOLD_OUT,
|
|
3341
|
+
"Toutes les cartes de ce point ont été distribuées."
|
|
3342
|
+
);
|
|
3343
|
+
} else {
|
|
3344
|
+
await this.claimSlot(UID.DROP, drop2.id, "scanCount", null);
|
|
3128
3345
|
}
|
|
3346
|
+
}
|
|
3347
|
+
const alreadyOwned = await strapi.db.query(UID.PLAYER_CARD).count({ where: { player: player2.id, card: result.card.id } });
|
|
3348
|
+
const playerCard2 = await strapi.db.transaction(async () => {
|
|
3349
|
+
const pc = await strapi.db.query(UID.PLAYER_CARD).create({
|
|
3350
|
+
data: {
|
|
3351
|
+
player: player2.id,
|
|
3352
|
+
card: result.card.id,
|
|
3353
|
+
pack: pack2.id,
|
|
3354
|
+
acquiredFrom: ACQUISITION.PULL,
|
|
3355
|
+
acquiredAt: now,
|
|
3356
|
+
mintNumber
|
|
3357
|
+
}
|
|
3358
|
+
});
|
|
3359
|
+
await strapi.db.query(UID.PULL).create({
|
|
3360
|
+
data: {
|
|
3361
|
+
player: player2.id,
|
|
3362
|
+
pack: pack2.id,
|
|
3363
|
+
drop: drop2.id,
|
|
3364
|
+
card: result.card.id,
|
|
3365
|
+
season: season2.id,
|
|
3366
|
+
seed: String(result.seed),
|
|
3367
|
+
roll: result.roll,
|
|
3368
|
+
rarity: result.rarity,
|
|
3369
|
+
verificationHash: result.verificationHash,
|
|
3370
|
+
qrToken: peek.mode === DROP_MODE.STATIC ? null : qrToken,
|
|
3371
|
+
pulledAt: now,
|
|
3372
|
+
odds: result.odds,
|
|
3373
|
+
pity: result.pity,
|
|
3374
|
+
mintNumber,
|
|
3375
|
+
isExtra: n > 0
|
|
3376
|
+
}
|
|
3377
|
+
});
|
|
3378
|
+
await strapi.db.query(UID.PLAYER).update({
|
|
3379
|
+
where: { id: player2.id },
|
|
3380
|
+
data: { totalCards: (player2.totalCards ?? 0) + n + 1, lastActiveAt: now }
|
|
3381
|
+
});
|
|
3382
|
+
return pc;
|
|
3129
3383
|
});
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
strapi.log.info(`[tcg] pull ${player2.code} ← ${result.card.name} #${mintNumber} (${result.rarity}${result.pity ? ", pity" : ""}) @ ${pack2.slug}/${drop2.slug}${boosts.length ? ` boosts=${boosts.map((b) => b.name).join("+")}` : ""}`);
|
|
3134
|
-
const format2 = plugin.service("format");
|
|
3135
|
-
const unlocked = await plugin.service("badge").evaluate(player2, season2, { event: "pull", card: result.card.documentId });
|
|
3136
|
-
return {
|
|
3137
|
-
playerCardId: playerCard2.documentId,
|
|
3138
|
-
card: { ...format2.card(result.card, season2), mintNumber },
|
|
3139
|
-
pull: {
|
|
3384
|
+
cards.push({
|
|
3385
|
+
playerCardId: playerCard2.documentId,
|
|
3386
|
+
card: { ...format2.card(result.card, season2), mintNumber },
|
|
3140
3387
|
rarity: result.rarity,
|
|
3141
|
-
isNew: alreadyOwned === 0,
|
|
3388
|
+
isNew: alreadyOwned === 0 && !cards.some((c) => c.card.id === result.card.documentId),
|
|
3142
3389
|
pity: result.pity,
|
|
3143
3390
|
odds: result.odds,
|
|
3144
|
-
boosts: boosts.map((b) => ({ name: b.name, multipliers: b.multipliers })),
|
|
3145
3391
|
isExclusive: Boolean(result.card.isExclusive),
|
|
3392
|
+
verificationHash: result.verificationHash,
|
|
3393
|
+
mintNumber
|
|
3394
|
+
});
|
|
3395
|
+
strapi.log.info(
|
|
3396
|
+
`[tcg] pull ${player2.code} ← ${result.card.name} #${mintNumber} (${result.rarity}${result.pity ? ", pity" : ""}${n > 0 ? ", extra" : ""}) @ ${pack2.slug}/${drop2.slug}${boosts.length ? ` boosts=${boosts.map((b) => b.name).join("+")}` : ""}`
|
|
3397
|
+
);
|
|
3398
|
+
}
|
|
3399
|
+
const first = cards[0];
|
|
3400
|
+
const unlocked = await plugin.service("badge").evaluate(player2, season2, { event: "pull", card: first.card.id });
|
|
3401
|
+
return {
|
|
3402
|
+
playerCardId: first.playerCardId,
|
|
3403
|
+
card: first.card,
|
|
3404
|
+
cards,
|
|
3405
|
+
pull: {
|
|
3406
|
+
rarity: first.rarity,
|
|
3407
|
+
isNew: first.isNew,
|
|
3408
|
+
pity: first.pity,
|
|
3409
|
+
odds: first.odds,
|
|
3410
|
+
boosts: boosts.map((b) => ({ name: b.name, multipliers: b.multipliers })),
|
|
3411
|
+
isExclusive: first.isExclusive,
|
|
3412
|
+
cardCount: cards.length,
|
|
3146
3413
|
pack: { id: pack2.documentId, name: pack2.name, slug: pack2.slug },
|
|
3147
3414
|
drop: { id: drop2.documentId, name: drop2.name, slug: drop2.slug },
|
|
3148
|
-
verificationHash:
|
|
3415
|
+
verificationHash: first.verificationHash,
|
|
3149
3416
|
pulledAt: now
|
|
3150
3417
|
},
|
|
3151
3418
|
unlocked
|
|
@@ -3154,7 +3421,13 @@ const pull = ({ strapi }) => ({
|
|
|
3154
3421
|
async history(player2, season2, page) {
|
|
3155
3422
|
const where = { player: player2.id, season: season2.id };
|
|
3156
3423
|
const [items, total] = await Promise.all([
|
|
3157
|
-
strapi.db.query(UID.PULL).findMany({
|
|
3424
|
+
strapi.db.query(UID.PULL).findMany({
|
|
3425
|
+
where,
|
|
3426
|
+
populate: { card: { populate: { image: true } }, pack: true, drop: true },
|
|
3427
|
+
orderBy: { pulledAt: "desc" },
|
|
3428
|
+
offset: page.offset,
|
|
3429
|
+
limit: page.limit
|
|
3430
|
+
}),
|
|
3158
3431
|
strapi.db.query(UID.PULL).count({ where })
|
|
3159
3432
|
]);
|
|
3160
3433
|
const format2 = strapi.plugin("tcg").service("format");
|
|
@@ -3179,7 +3452,11 @@ const pull = ({ strapi }) => ({
|
|
|
3179
3452
|
const limits = plugin.service("season").limitsOf(season2);
|
|
3180
3453
|
const packs = await plugin.service("pack").list(season2);
|
|
3181
3454
|
const since = startOfToday$1();
|
|
3182
|
-
const todays = await strapi.db.query(UID.PULL).findMany({
|
|
3455
|
+
const todays = await strapi.db.query(UID.PULL).findMany({
|
|
3456
|
+
where: { player: player2.id, season: season2.id, pulledAt: { $gte: since }, isExtra: false },
|
|
3457
|
+
populate: { pack: { select: ["id"] } },
|
|
3458
|
+
select: ["pulledAt"]
|
|
3459
|
+
});
|
|
3183
3460
|
const countByPack = {};
|
|
3184
3461
|
for (const p of todays) countByPack[p.pack?.id] = (countByPack[p.pack?.id] ?? 0) + 1;
|
|
3185
3462
|
const totalToday = todays.length;
|
|
@@ -3189,10 +3466,21 @@ const pull = ({ strapi }) => ({
|
|
|
3189
3466
|
const drops = await plugin.service("drop").list(pack2, { includeInactive: false });
|
|
3190
3467
|
const dropStates = [];
|
|
3191
3468
|
for (const drop2 of drops) {
|
|
3192
|
-
const last = await strapi.db.query(UID.PULL).findOne({
|
|
3469
|
+
const last = await strapi.db.query(UID.PULL).findOne({
|
|
3470
|
+
where: { player: player2.id, drop: drop2.id, isExtra: false },
|
|
3471
|
+
orderBy: { pulledAt: "desc" },
|
|
3472
|
+
select: ["pulledAt"]
|
|
3473
|
+
});
|
|
3193
3474
|
const cooldownMs = drop2.cooldownMs ?? (drop2.mode === DROP_MODE.STATIC ? Math.max(limits.pullCooldownMs, limits.staticScanCooldownMs) : limits.pullCooldownMs);
|
|
3194
3475
|
const remaining = last ? Math.max(0, cooldownMs - (now - new Date(last.pulledAt).getTime())) : 0;
|
|
3195
|
-
dropStates.push({
|
|
3476
|
+
dropStates.push({
|
|
3477
|
+
drop: { id: drop2.documentId, name: drop2.name, slug: drop2.slug },
|
|
3478
|
+
isOpen: plugin.service("drop").isOpen(drop2),
|
|
3479
|
+
cooldownRemainingMs: remaining,
|
|
3480
|
+
cooldownRemainingSeconds: Math.ceil(remaining / 1e3),
|
|
3481
|
+
cooldownUntil: remaining > 0 ? new Date(now + remaining) : null,
|
|
3482
|
+
cardsPerPull: Math.max(1, Number(drop2.cardsPerPull) || 1)
|
|
3483
|
+
});
|
|
3196
3484
|
}
|
|
3197
3485
|
const pullsToday = countByPack[pack2.id] ?? 0;
|
|
3198
3486
|
const anyReady = dropStates.some((d) => d.isOpen && d.cooldownRemainingMs === 0);
|
|
@@ -3330,28 +3618,46 @@ const trade = ({ strapi }) => ({
|
|
|
3330
3618
|
/** Player-card rows owned by `player` among `documentIds`; throws if any is missing. */
|
|
3331
3619
|
async ownedRows(player2, documentIds, season2) {
|
|
3332
3620
|
const ids = [...new Set((documentIds ?? []).map(String))];
|
|
3333
|
-
if (ids.length === 0)
|
|
3621
|
+
if (ids.length === 0)
|
|
3622
|
+
throw badRequest(ERROR_CODES.VALIDATION, "Sélectionnez au moins une carte.");
|
|
3334
3623
|
const rows = await strapi.db.query(UID.PLAYER_CARD).findMany({
|
|
3335
|
-
where: {
|
|
3624
|
+
where: {
|
|
3625
|
+
player: player2.id,
|
|
3626
|
+
documentId: { $in: ids },
|
|
3627
|
+
...season2 ? { card: { season: season2.id } } : {}
|
|
3628
|
+
},
|
|
3336
3629
|
populate: { card: { populate: { image: true } } }
|
|
3337
3630
|
});
|
|
3338
|
-
if (rows.length !== ids.length)
|
|
3631
|
+
if (rows.length !== ids.length)
|
|
3632
|
+
throw badRequest(ERROR_CODES.CARD_NOT_OWNED, "Vous ne possédez pas toutes ces cartes.");
|
|
3339
3633
|
return rows;
|
|
3340
3634
|
},
|
|
3341
3635
|
async createOffer(player2, season2, { cardIds }) {
|
|
3342
3636
|
const plugin = strapi.plugin("tcg");
|
|
3343
3637
|
const limits = plugin.service("season").limitsOf(season2);
|
|
3344
3638
|
if ((cardIds ?? []).length > limits.maxCardsPerTrade) {
|
|
3345
|
-
throw unprocessable(
|
|
3639
|
+
throw unprocessable(
|
|
3640
|
+
ERROR_CODES.TRADE_TOO_MANY_CARDS,
|
|
3641
|
+
`Maximum ${limits.maxCardsPerTrade} cartes par échange.`
|
|
3642
|
+
);
|
|
3346
3643
|
}
|
|
3347
|
-
const active = await strapi.db.query(UID.TRADE_OFFER).count({ where: { initiator: player2.id, status:
|
|
3644
|
+
const active = await strapi.db.query(UID.TRADE_OFFER).count({ where: { initiator: player2.id, status: { $in: TRADE_OPEN_STATUSES } } });
|
|
3348
3645
|
if (active >= limits.maxActiveTradeOffers) {
|
|
3349
|
-
throw unprocessable(
|
|
3646
|
+
throw unprocessable(
|
|
3647
|
+
ERROR_CODES.TRADE_LIMIT,
|
|
3648
|
+
`Maximum ${limits.maxActiveTradeOffers} offres actives.`
|
|
3649
|
+
);
|
|
3350
3650
|
}
|
|
3351
3651
|
const rows = await this.ownedRows(player2, cardIds, season2);
|
|
3352
3652
|
const expiresAt = new Date(Date.now() + limits.tradeOfferExpiryMs);
|
|
3353
3653
|
const offer = await strapi.db.query(UID.TRADE_OFFER).create({
|
|
3354
|
-
data: {
|
|
3654
|
+
data: {
|
|
3655
|
+
initiator: player2.id,
|
|
3656
|
+
season: season2.id,
|
|
3657
|
+
status: TRADE_STATUS.PENDING,
|
|
3658
|
+
offeredCardIds: rows.map((r) => r.documentId),
|
|
3659
|
+
expiresAt
|
|
3660
|
+
}
|
|
3355
3661
|
});
|
|
3356
3662
|
const { token } = plugin.service("qr").forTrade(offer, player2, limits.tradeOfferExpiryMs);
|
|
3357
3663
|
await strapi.db.query(UID.TRADE_OFFER).update({ where: { id: offer.id }, data: { qrToken: token } });
|
|
@@ -3367,68 +3673,184 @@ const trade = ({ strapi }) => ({
|
|
|
3367
3673
|
async findOffer(documentId) {
|
|
3368
3674
|
const offer = await strapi.db.query(UID.TRADE_OFFER).findOne({
|
|
3369
3675
|
where: { documentId },
|
|
3370
|
-
populate: { initiator: true, season: true }
|
|
3676
|
+
populate: { initiator: true, receiver: true, season: true }
|
|
3371
3677
|
});
|
|
3372
3678
|
if (!offer) throw notFound(ERROR_CODES.TRADE_NOT_FOUND, "Offre introuvable.");
|
|
3373
3679
|
return offer;
|
|
3374
3680
|
},
|
|
3681
|
+
/** Alive and not past its expiry; otherwise the status names why. */
|
|
3375
3682
|
assertOpen(offer) {
|
|
3376
|
-
if (
|
|
3377
|
-
|
|
3683
|
+
if (!TRADE_OPEN_STATUSES.includes(offer.status))
|
|
3684
|
+
throw unprocessable(ERROR_CODES.TRADE_INVALID_STATUS, `Cette offre est ${offer.status}.`);
|
|
3685
|
+
if (/* @__PURE__ */ new Date() > new Date(offer.expiresAt))
|
|
3686
|
+
throw unprocessable(ERROR_CODES.TRADE_EXPIRED, "Cette offre a expiré.");
|
|
3378
3687
|
},
|
|
3379
|
-
async
|
|
3688
|
+
async cardsOf(ids, season2) {
|
|
3380
3689
|
const format2 = strapi.plugin("tcg").service("format");
|
|
3690
|
+
if (!ids?.length) return [];
|
|
3381
3691
|
const rows = await strapi.db.query(UID.PLAYER_CARD).findMany({
|
|
3382
|
-
where: { documentId: { $in:
|
|
3692
|
+
where: { documentId: { $in: ids } },
|
|
3383
3693
|
populate: { card: { populate: { image: true } } }
|
|
3384
3694
|
});
|
|
3695
|
+
return rows.map((r) => format2.playerCard(r, season2));
|
|
3696
|
+
},
|
|
3697
|
+
/**
|
|
3698
|
+
* The offer as both players see it: who is on each side, what each side puts in, where the
|
|
3699
|
+
* handshake stands. `role` is the caller's seat when a player is given.
|
|
3700
|
+
*/
|
|
3701
|
+
async describe(offer, season2, viewer = null) {
|
|
3702
|
+
const [offeredCards, receiverCards] = await Promise.all([
|
|
3703
|
+
this.cardsOf(offer.offeredCardIds, season2),
|
|
3704
|
+
this.cardsOf(offer.receiverCardIds, season2)
|
|
3705
|
+
]);
|
|
3706
|
+
const who = (p) => p ? { code: p.code, displayName: p.displayName } : null;
|
|
3707
|
+
const role = !viewer ? null : offer.initiator?.id === viewer.id ? "initiator" : offer.receiver?.id === viewer.id ? "receiver" : null;
|
|
3385
3708
|
return {
|
|
3386
3709
|
id: offer.documentId,
|
|
3387
3710
|
status: offer.status,
|
|
3388
3711
|
expiresAt: offer.expiresAt,
|
|
3389
|
-
initiator: offer.initiator
|
|
3390
|
-
|
|
3712
|
+
initiator: who(offer.initiator),
|
|
3713
|
+
receiver: who(offer.receiver),
|
|
3714
|
+
offeredCards,
|
|
3715
|
+
receiverCards,
|
|
3716
|
+
joinedAt: offer.joinedAt ?? null,
|
|
3717
|
+
proposedAt: offer.proposedAt ?? null,
|
|
3718
|
+
declinedAt: offer.declinedAt ?? null,
|
|
3719
|
+
role
|
|
3391
3720
|
};
|
|
3392
3721
|
},
|
|
3393
|
-
/** The receiver scans the initiator's QR
|
|
3722
|
+
/** The receiver scans the initiator's signed QR: same as joining by id. */
|
|
3394
3723
|
async scan(player2, qrToken) {
|
|
3395
3724
|
const peek = peekToken(qrToken);
|
|
3396
|
-
if (!peek || peek.kind !== "trade")
|
|
3397
|
-
|
|
3725
|
+
if (!peek || peek.kind !== "trade")
|
|
3726
|
+
throw badRequest(ERROR_CODES.QR_INVALID, "QR code invalide.");
|
|
3398
3727
|
const offer = await this.findOffer(peek.offerId);
|
|
3399
3728
|
if (offer.qrToken !== qrToken) throw badRequest(ERROR_CODES.QR_INVALID, "QR code invalide.");
|
|
3400
|
-
this.
|
|
3401
|
-
return this.describe(offer, offer.season);
|
|
3729
|
+
return this.join(player2, offer.documentId);
|
|
3402
3730
|
},
|
|
3403
3731
|
/**
|
|
3404
|
-
*
|
|
3732
|
+
* A receiver takes the seat opposite the initiator. Idempotent for the same receiver; refused
|
|
3733
|
+
* while another receiver holds the seat, so two phones cannot answer one QR at once.
|
|
3405
3734
|
*/
|
|
3406
|
-
async
|
|
3735
|
+
async join(player2, offerDocumentId) {
|
|
3736
|
+
const offer = await this.findOffer(offerDocumentId);
|
|
3737
|
+
if (offer.initiator?.id === player2.id)
|
|
3738
|
+
throw badRequest(ERROR_CODES.TRADE_SELF, "Vous ne pouvez pas rejoindre votre propre offre.");
|
|
3739
|
+
this.assertOpen(offer);
|
|
3740
|
+
if (offer.receiver && offer.receiver.id !== player2.id) {
|
|
3741
|
+
throw unprocessable(
|
|
3742
|
+
ERROR_CODES.TRADE_INVALID_STATUS,
|
|
3743
|
+
"Un autre joueur est déjà sur cette offre."
|
|
3744
|
+
);
|
|
3745
|
+
}
|
|
3746
|
+
if (!offer.receiver) {
|
|
3747
|
+
await strapi.db.query(UID.TRADE_OFFER).update({
|
|
3748
|
+
where: { id: offer.id },
|
|
3749
|
+
data: {
|
|
3750
|
+
receiver: player2.id,
|
|
3751
|
+
status: TRADE_STATUS.JOINED,
|
|
3752
|
+
joinedAt: /* @__PURE__ */ new Date(),
|
|
3753
|
+
receiverCardIds: null,
|
|
3754
|
+
declinedAt: null
|
|
3755
|
+
}
|
|
3756
|
+
});
|
|
3757
|
+
}
|
|
3758
|
+
return this.describe(await this.findOffer(offerDocumentId), offer.season, player2);
|
|
3759
|
+
},
|
|
3760
|
+
/** The receiver says what they give. Replaces a previous proposal; the initiator must confirm. */
|
|
3761
|
+
async propose(player2, offerDocumentId, { cardIds }) {
|
|
3407
3762
|
const plugin = strapi.plugin("tcg");
|
|
3408
3763
|
const offer = await this.findOffer(offerDocumentId);
|
|
3764
|
+
if (offer.initiator?.id === player2.id)
|
|
3765
|
+
throw badRequest(
|
|
3766
|
+
ERROR_CODES.TRADE_SELF,
|
|
3767
|
+
"Vous ne pouvez pas proposer sur votre propre offre."
|
|
3768
|
+
);
|
|
3409
3769
|
this.assertOpen(offer);
|
|
3410
|
-
if (offer.
|
|
3770
|
+
if (offer.receiver && offer.receiver.id !== player2.id) {
|
|
3771
|
+
throw unprocessable(
|
|
3772
|
+
ERROR_CODES.TRADE_INVALID_STATUS,
|
|
3773
|
+
"Un autre joueur est déjà sur cette offre."
|
|
3774
|
+
);
|
|
3775
|
+
}
|
|
3411
3776
|
const season2 = offer.season;
|
|
3412
3777
|
const limits = plugin.service("season").limitsOf(season2);
|
|
3413
3778
|
if ((cardIds ?? []).length > limits.maxCardsPerTrade) {
|
|
3414
|
-
throw unprocessable(
|
|
3779
|
+
throw unprocessable(
|
|
3780
|
+
ERROR_CODES.TRADE_TOO_MANY_CARDS,
|
|
3781
|
+
`Maximum ${limits.maxCardsPerTrade} cartes par échange.`
|
|
3782
|
+
);
|
|
3783
|
+
}
|
|
3784
|
+
const rows = await this.ownedRows(player2, cardIds, season2);
|
|
3785
|
+
await strapi.db.query(UID.TRADE_OFFER).update({
|
|
3786
|
+
where: { id: offer.id },
|
|
3787
|
+
data: {
|
|
3788
|
+
receiver: player2.id,
|
|
3789
|
+
status: TRADE_STATUS.PROPOSED,
|
|
3790
|
+
receiverCardIds: rows.map((r) => r.documentId),
|
|
3791
|
+
joinedAt: offer.joinedAt ?? /* @__PURE__ */ new Date(),
|
|
3792
|
+
proposedAt: /* @__PURE__ */ new Date(),
|
|
3793
|
+
declinedAt: null
|
|
3794
|
+
}
|
|
3795
|
+
});
|
|
3796
|
+
return this.describe(await this.findOffer(offerDocumentId), season2, player2);
|
|
3797
|
+
},
|
|
3798
|
+
/** The initiator sends the proposal back; the receiver keeps the seat and can propose again. */
|
|
3799
|
+
async decline(player2, offerDocumentId) {
|
|
3800
|
+
const offer = await this.findOffer(offerDocumentId);
|
|
3801
|
+
if (offer.initiator?.id !== player2.id)
|
|
3802
|
+
throw forbidden(
|
|
3803
|
+
ERROR_CODES.TRADE_FORBIDDEN,
|
|
3804
|
+
"Seul l’auteur de l’offre peut refuser une proposition."
|
|
3805
|
+
);
|
|
3806
|
+
this.assertOpen(offer);
|
|
3807
|
+
if (offer.status !== TRADE_STATUS.PROPOSED)
|
|
3808
|
+
throw unprocessable(ERROR_CODES.TRADE_INVALID_STATUS, "Aucune proposition à refuser.");
|
|
3809
|
+
await strapi.db.query(UID.TRADE_OFFER).update({
|
|
3810
|
+
where: { id: offer.id },
|
|
3811
|
+
data: { status: TRADE_STATUS.JOINED, receiverCardIds: null, declinedAt: /* @__PURE__ */ new Date() }
|
|
3812
|
+
});
|
|
3813
|
+
return this.describe(await this.findOffer(offerDocumentId), offer.season, player2);
|
|
3814
|
+
},
|
|
3815
|
+
/**
|
|
3816
|
+
* The initiator confirms the proposal: the atomic swap. Both players have said yes by now —
|
|
3817
|
+
* the receiver by proposing, the initiator here.
|
|
3818
|
+
*/
|
|
3819
|
+
async confirm(initiator, offerDocumentId) {
|
|
3820
|
+
const plugin = strapi.plugin("tcg");
|
|
3821
|
+
const offer = await this.findOffer(offerDocumentId);
|
|
3822
|
+
if (offer.initiator?.id !== initiator.id)
|
|
3823
|
+
throw forbidden(
|
|
3824
|
+
ERROR_CODES.TRADE_FORBIDDEN,
|
|
3825
|
+
"Seul l’auteur de l’offre peut confirmer l’échange."
|
|
3826
|
+
);
|
|
3827
|
+
this.assertOpen(offer);
|
|
3828
|
+
if (offer.status !== TRADE_STATUS.PROPOSED || !offer.receiver)
|
|
3829
|
+
throw unprocessable(ERROR_CODES.TRADE_INVALID_STATUS, "Aucune proposition à confirmer.");
|
|
3830
|
+
const receiver = offer.receiver;
|
|
3831
|
+
const season2 = offer.season;
|
|
3832
|
+
const receiverRows = await strapi.db.query(UID.PLAYER_CARD).findMany({
|
|
3833
|
+
where: { player: receiver.id, documentId: { $in: offer.receiverCardIds ?? [] } }
|
|
3834
|
+
});
|
|
3835
|
+
if (receiverRows.length !== (offer.receiverCardIds ?? []).length || receiverRows.length === 0) {
|
|
3836
|
+
throw unprocessable(ERROR_CODES.CARD_NOT_OWNED, "L'autre joueur ne possède plus ces cartes.");
|
|
3415
3837
|
}
|
|
3416
|
-
const receiverRows = await this.ownedRows(receiver, cardIds, season2);
|
|
3417
3838
|
const initiatorRows = await strapi.db.query(UID.PLAYER_CARD).findMany({
|
|
3418
|
-
where: { player:
|
|
3839
|
+
where: { player: initiator.id, documentId: { $in: offer.offeredCardIds ?? [] } }
|
|
3419
3840
|
});
|
|
3420
3841
|
if (initiatorRows.length !== (offer.offeredCardIds ?? []).length) {
|
|
3421
|
-
throw unprocessable(ERROR_CODES.CARD_NOT_OWNED, "
|
|
3842
|
+
throw unprocessable(ERROR_CODES.CARD_NOT_OWNED, "Vous ne possédez plus ces cartes.");
|
|
3422
3843
|
}
|
|
3423
3844
|
const now = /* @__PURE__ */ new Date();
|
|
3424
3845
|
const trade2 = await strapi.db.transaction(async () => {
|
|
3425
3846
|
const fresh = await strapi.db.query(UID.TRADE_OFFER).findOne({ where: { id: offer.id }, select: ["status"] });
|
|
3426
|
-
if (fresh.status !== TRADE_STATUS.
|
|
3847
|
+
if (fresh.status !== TRADE_STATUS.PROPOSED)
|
|
3848
|
+
throw unprocessable(ERROR_CODES.TRADE_INVALID_STATUS, "Offre déjà traitée.");
|
|
3427
3849
|
await strapi.db.query(UID.TRADE_OFFER).update({ where: { id: offer.id }, data: { status: TRADE_STATUS.ACCEPTED } });
|
|
3428
3850
|
const t = await strapi.db.query(UID.TRADE).create({
|
|
3429
3851
|
data: {
|
|
3430
3852
|
offer: offer.id,
|
|
3431
|
-
initiator:
|
|
3853
|
+
initiator: initiator.id,
|
|
3432
3854
|
receiver: receiver.id,
|
|
3433
3855
|
season: season2.id,
|
|
3434
3856
|
initiatorCardIds: initiatorRows.map((r) => r.documentId),
|
|
@@ -3437,53 +3859,127 @@ const trade = ({ strapi }) => ({
|
|
|
3437
3859
|
}
|
|
3438
3860
|
});
|
|
3439
3861
|
for (const row of initiatorRows) {
|
|
3440
|
-
await strapi.db.query(UID.PLAYER_CARD).update({
|
|
3862
|
+
await strapi.db.query(UID.PLAYER_CARD).update({
|
|
3863
|
+
where: { id: row.id },
|
|
3864
|
+
data: {
|
|
3865
|
+
player: receiver.id,
|
|
3866
|
+
acquiredFrom: ACQUISITION.TRADE,
|
|
3867
|
+
acquiredAt: now,
|
|
3868
|
+
sourceTrade: t.id
|
|
3869
|
+
}
|
|
3870
|
+
});
|
|
3441
3871
|
}
|
|
3442
3872
|
for (const row of receiverRows) {
|
|
3443
|
-
await strapi.db.query(UID.PLAYER_CARD).update({
|
|
3873
|
+
await strapi.db.query(UID.PLAYER_CARD).update({
|
|
3874
|
+
where: { id: row.id },
|
|
3875
|
+
data: {
|
|
3876
|
+
player: initiator.id,
|
|
3877
|
+
acquiredFrom: ACQUISITION.TRADE,
|
|
3878
|
+
acquiredAt: now,
|
|
3879
|
+
sourceTrade: t.id
|
|
3880
|
+
}
|
|
3881
|
+
});
|
|
3444
3882
|
}
|
|
3445
3883
|
const delta = receiverRows.length - initiatorRows.length;
|
|
3446
3884
|
await strapi.db.query(UID.PLAYER).update({
|
|
3447
|
-
where: { id:
|
|
3448
|
-
data: {
|
|
3885
|
+
where: { id: initiator.id },
|
|
3886
|
+
data: {
|
|
3887
|
+
totalTrades: (initiator.totalTrades ?? 0) + 1,
|
|
3888
|
+
totalCards: Math.max(0, (initiator.totalCards ?? 0) + delta),
|
|
3889
|
+
lastActiveAt: now
|
|
3890
|
+
}
|
|
3449
3891
|
});
|
|
3450
3892
|
await strapi.db.query(UID.PLAYER).update({
|
|
3451
3893
|
where: { id: receiver.id },
|
|
3452
|
-
data: {
|
|
3894
|
+
data: {
|
|
3895
|
+
totalTrades: (receiver.totalTrades ?? 0) + 1,
|
|
3896
|
+
totalCards: Math.max(0, (receiver.totalCards ?? 0) - delta),
|
|
3897
|
+
lastActiveAt: now
|
|
3898
|
+
}
|
|
3453
3899
|
});
|
|
3454
3900
|
return t;
|
|
3455
3901
|
});
|
|
3456
|
-
strapi.log.info(
|
|
3902
|
+
strapi.log.info(
|
|
3903
|
+
`[tcg] trade ${initiator.code} ⇄ ${receiver.code} (${initiatorRows.length}:${receiverRows.length})`
|
|
3904
|
+
);
|
|
3457
3905
|
const badge2 = plugin.service("badge");
|
|
3458
|
-
const [unlocked] = await Promise.all([
|
|
3459
|
-
|
|
3906
|
+
const [unlocked, receiverUnlocked] = await Promise.all([
|
|
3907
|
+
badge2.evaluate(initiator, season2, { event: "trade" }),
|
|
3908
|
+
badge2.evaluate(receiver, season2, { event: "trade" })
|
|
3909
|
+
]);
|
|
3910
|
+
return {
|
|
3911
|
+
id: trade2.documentId,
|
|
3912
|
+
offerId: offer.documentId,
|
|
3913
|
+
completedAt: now,
|
|
3914
|
+
received: receiverRows.length,
|
|
3915
|
+
given: initiatorRows.length,
|
|
3916
|
+
unlocked,
|
|
3917
|
+
receiverUnlocked
|
|
3918
|
+
};
|
|
3460
3919
|
},
|
|
3920
|
+
/** The receiver leaves: the seat frees up, the initiator's QR stays valid for someone else. */
|
|
3461
3921
|
async reject(player2, offerDocumentId) {
|
|
3462
3922
|
const offer = await this.findOffer(offerDocumentId);
|
|
3923
|
+
if (offer.initiator?.id === player2.id)
|
|
3924
|
+
throw badRequest(ERROR_CODES.TRADE_SELF, "Utilisez « annuler » pour votre propre offre.");
|
|
3925
|
+
if (!offer.receiver || offer.receiver.id !== player2.id)
|
|
3926
|
+
throw forbidden(ERROR_CODES.TRADE_FORBIDDEN, "Vous n’êtes pas sur cette offre.");
|
|
3463
3927
|
this.assertOpen(offer);
|
|
3464
|
-
|
|
3465
|
-
|
|
3928
|
+
await strapi.db.query(UID.TRADE_OFFER).update({
|
|
3929
|
+
where: { id: offer.id },
|
|
3930
|
+
data: {
|
|
3931
|
+
status: TRADE_STATUS.PENDING,
|
|
3932
|
+
receiver: null,
|
|
3933
|
+
receiverCardIds: null,
|
|
3934
|
+
joinedAt: null,
|
|
3935
|
+
proposedAt: null,
|
|
3936
|
+
declinedAt: null
|
|
3937
|
+
}
|
|
3938
|
+
});
|
|
3466
3939
|
return { ok: true };
|
|
3467
3940
|
},
|
|
3468
3941
|
async cancel(player2, offerDocumentId) {
|
|
3469
3942
|
const offer = await this.findOffer(offerDocumentId);
|
|
3470
|
-
if (offer.initiator?.id !== player2.id)
|
|
3471
|
-
|
|
3943
|
+
if (offer.initiator?.id !== player2.id)
|
|
3944
|
+
throw forbidden(ERROR_CODES.TRADE_FORBIDDEN, "Vous ne pouvez annuler que vos offres.");
|
|
3945
|
+
if (!TRADE_OPEN_STATUSES.includes(offer.status))
|
|
3946
|
+
throw unprocessable(
|
|
3947
|
+
ERROR_CODES.TRADE_INVALID_STATUS,
|
|
3948
|
+
`Cette offre est déjà ${offer.status}.`
|
|
3949
|
+
);
|
|
3472
3950
|
await strapi.db.query(UID.TRADE_OFFER).update({ where: { id: offer.id }, data: { status: TRADE_STATUS.CANCELLED } });
|
|
3473
3951
|
return { ok: true };
|
|
3474
3952
|
},
|
|
3953
|
+
/** The player's live offers, on either seat, newest first — what a reopened screen resumes. */
|
|
3475
3954
|
async activeOffers(player2, season2) {
|
|
3476
3955
|
const offers = await strapi.db.query(UID.TRADE_OFFER).findMany({
|
|
3477
|
-
where: {
|
|
3478
|
-
|
|
3956
|
+
where: {
|
|
3957
|
+
season: season2.id,
|
|
3958
|
+
status: { $in: TRADE_OPEN_STATUSES },
|
|
3959
|
+
expiresAt: { $gt: /* @__PURE__ */ new Date() },
|
|
3960
|
+
$or: [{ initiator: player2.id }, { receiver: player2.id }]
|
|
3961
|
+
},
|
|
3962
|
+
populate: { initiator: true, receiver: true },
|
|
3479
3963
|
orderBy: { createdAt: "desc" }
|
|
3480
3964
|
});
|
|
3481
|
-
return Promise.all(
|
|
3965
|
+
return Promise.all(
|
|
3966
|
+
offers.map(
|
|
3967
|
+
(o) => this.describe(o, season2, player2).then(
|
|
3968
|
+
(d) => o.initiator?.id === player2.id ? { ...d, qrToken: o.qrToken } : d
|
|
3969
|
+
)
|
|
3970
|
+
)
|
|
3971
|
+
);
|
|
3482
3972
|
},
|
|
3483
3973
|
async history(player2, season2, page) {
|
|
3484
3974
|
const where = { season: season2.id, $or: [{ initiator: player2.id }, { receiver: player2.id }] };
|
|
3485
3975
|
const [items, total] = await Promise.all([
|
|
3486
|
-
strapi.db.query(UID.TRADE).findMany({
|
|
3976
|
+
strapi.db.query(UID.TRADE).findMany({
|
|
3977
|
+
where,
|
|
3978
|
+
populate: { initiator: true, receiver: true },
|
|
3979
|
+
orderBy: { completedAt: "desc" },
|
|
3980
|
+
offset: page.offset,
|
|
3981
|
+
limit: page.limit
|
|
3982
|
+
}),
|
|
3487
3983
|
strapi.db.query(UID.TRADE).count({ where })
|
|
3488
3984
|
]);
|
|
3489
3985
|
return {
|
|
@@ -3491,17 +3987,20 @@ const trade = ({ strapi }) => ({
|
|
|
3491
3987
|
id: t.documentId,
|
|
3492
3988
|
completedAt: t.completedAt,
|
|
3493
3989
|
role: t.initiator?.id === player2.id ? "initiator" : "receiver",
|
|
3494
|
-
counterpart: (t.initiator?.id === player2.id ? t.receiver : t.initiator) ? {
|
|
3990
|
+
counterpart: (t.initiator?.id === player2.id ? t.receiver : t.initiator) ? {
|
|
3991
|
+
code: (t.initiator?.id === player2.id ? t.receiver : t.initiator).code,
|
|
3992
|
+
displayName: (t.initiator?.id === player2.id ? t.receiver : t.initiator).displayName
|
|
3993
|
+
} : null,
|
|
3495
3994
|
given: (t.initiator?.id === player2.id ? t.initiatorCardIds : t.receiverCardIds) ?? [],
|
|
3496
3995
|
received: (t.initiator?.id === player2.id ? t.receiverCardIds : t.initiatorCardIds) ?? []
|
|
3497
3996
|
})),
|
|
3498
3997
|
total
|
|
3499
3998
|
};
|
|
3500
3999
|
},
|
|
3501
|
-
/** Marks
|
|
4000
|
+
/** Marks live offers past their expiry. */
|
|
3502
4001
|
async sweep() {
|
|
3503
4002
|
const { count } = await strapi.db.query(UID.TRADE_OFFER).updateMany({
|
|
3504
|
-
where: { status:
|
|
4003
|
+
where: { status: { $in: TRADE_OPEN_STATUSES }, expiresAt: { $lt: /* @__PURE__ */ new Date() } },
|
|
3505
4004
|
data: { status: TRADE_STATUS.EXPIRED }
|
|
3506
4005
|
});
|
|
3507
4006
|
return count ?? 0;
|
|
@@ -3783,10 +4282,16 @@ const drop = ({ strapi }) => ({
|
|
|
3783
4282
|
return drop2;
|
|
3784
4283
|
},
|
|
3785
4284
|
validate(data) {
|
|
3786
|
-
if (data.mode && !Object.values(DROP_MODE).includes(data.mode))
|
|
3787
|
-
|
|
4285
|
+
if (data.mode && !Object.values(DROP_MODE).includes(data.mode))
|
|
4286
|
+
throw badRequest(ERROR_CODES.VALIDATION, "Mode invalide.");
|
|
4287
|
+
if (data.poolMode && !Object.values(POOL_MODE).includes(data.poolMode))
|
|
4288
|
+
throw badRequest(ERROR_CODES.VALIDATION, "Mode de pool invalide.");
|
|
3788
4289
|
for (const k of ["maxScansTotal", "maxScansPerPlayer", "cooldownMs"]) {
|
|
3789
|
-
if (data[k] != null && data[k] !== "" && !(Number(data[k]) >= 0))
|
|
4290
|
+
if (data[k] != null && data[k] !== "" && !(Number(data[k]) >= 0))
|
|
4291
|
+
throw badRequest(ERROR_CODES.VALIDATION, `${k} invalide.`);
|
|
4292
|
+
}
|
|
4293
|
+
if (data.cardsPerPull != null && data.cardsPerPull !== "" && !(Number.isInteger(Number(data.cardsPerPull)) && Number(data.cardsPerPull) >= 1 && Number(data.cardsPerPull) <= 10)) {
|
|
4294
|
+
throw badRequest(ERROR_CODES.VALIDATION, "cardsPerPull doit être entre 1 et 10.");
|
|
3790
4295
|
}
|
|
3791
4296
|
},
|
|
3792
4297
|
async create(pack2, data) {
|
|
@@ -3809,13 +4314,19 @@ const drop = ({ strapi }) => ({
|
|
|
3809
4314
|
async ensureDefault(pack2) {
|
|
3810
4315
|
const existing = await strapi.db.query(UID.DROP).findOne({ where: { pack: pack2.id, isDefault: true } });
|
|
3811
4316
|
if (existing) return existing;
|
|
3812
|
-
return this.create(pack2, {
|
|
4317
|
+
return this.create(pack2, {
|
|
4318
|
+
name: DEFAULT_DROP_NAME,
|
|
4319
|
+
mode: DROP_MODE.STATIC,
|
|
4320
|
+
isDefault: true,
|
|
4321
|
+
poolMode: POOL_MODE.INHERIT
|
|
4322
|
+
});
|
|
3813
4323
|
},
|
|
3814
4324
|
async update(pack2, ref, data) {
|
|
3815
4325
|
const drop2 = await this.findOne(pack2, ref);
|
|
3816
4326
|
this.validate(data);
|
|
3817
4327
|
const { cards, pack: _p, secret: _s, scanCount: _c, isDefault: _d, ...rest } = data;
|
|
3818
|
-
if (rest.slug && rest.slug !== drop2.slug)
|
|
4328
|
+
if (rest.slug && rest.slug !== drop2.slug)
|
|
4329
|
+
rest.slug = await uniqueSlug(strapi, UID.DROP, rest.slug, { pack: pack2.id });
|
|
3819
4330
|
const updated = await strapi.documents(UID.DROP).update({ documentId: drop2.documentId, data: rest });
|
|
3820
4331
|
if (Array.isArray(cards)) await this.setCards(pack2, drop2.documentId, cards);
|
|
3821
4332
|
return updated;
|
|
@@ -3827,9 +4338,17 @@ const drop = ({ strapi }) => ({
|
|
|
3827
4338
|
},
|
|
3828
4339
|
async remove(pack2, ref) {
|
|
3829
4340
|
const drop2 = await this.findOne(pack2, ref);
|
|
3830
|
-
if (drop2.isDefault)
|
|
4341
|
+
if (drop2.isDefault)
|
|
4342
|
+
throw badRequest(
|
|
4343
|
+
ERROR_CODES.VALIDATION,
|
|
4344
|
+
"Le point de scan par défaut ne peut pas être supprimé : désactivez-le."
|
|
4345
|
+
);
|
|
3831
4346
|
const pulls = await strapi.db.query(UID.PULL).count({ where: { drop: drop2.id } });
|
|
3832
|
-
if (pulls > 0)
|
|
4347
|
+
if (pulls > 0)
|
|
4348
|
+
throw badRequest(
|
|
4349
|
+
ERROR_CODES.VALIDATION,
|
|
4350
|
+
"Ce point a des tirages : désactivez-le au lieu de le supprimer."
|
|
4351
|
+
);
|
|
3833
4352
|
await strapi.db.query(UID.DROP_CARD).deleteMany({ where: { drop: drop2.id } });
|
|
3834
4353
|
await strapi.documents(UID.DROP).delete({ documentId: drop2.documentId });
|
|
3835
4354
|
return { ok: true };
|
|
@@ -3838,15 +4357,28 @@ const drop = ({ strapi }) => ({
|
|
|
3838
4357
|
const drop2 = await this.findOne(pack2, dropRef);
|
|
3839
4358
|
const season2 = pack2.season?.id ? pack2.season : await strapi.db.query(UID.PACK).findOne({ where: { id: pack2.id }, populate: { season: true } }).then((p) => p.season);
|
|
3840
4359
|
const docIds = [...new Set(entries.map((e) => typeof e === "string" ? e : e.card))];
|
|
3841
|
-
const cards = docIds.length ? await strapi.db.query(UID.CARD).findMany({
|
|
3842
|
-
|
|
4360
|
+
const cards = docIds.length ? await strapi.db.query(UID.CARD).findMany({
|
|
4361
|
+
where: { documentId: { $in: docIds }, season: season2.id },
|
|
4362
|
+
select: ["id", "documentId"]
|
|
4363
|
+
}) : [];
|
|
4364
|
+
if (cards.length !== docIds.length)
|
|
4365
|
+
throw badRequest(
|
|
4366
|
+
ERROR_CODES.VALIDATION,
|
|
4367
|
+
"Certaines cartes ne font pas partie de cette saison."
|
|
4368
|
+
);
|
|
3843
4369
|
const idByDoc = Object.fromEntries(cards.map((c) => [c.documentId, c.id]));
|
|
3844
4370
|
await strapi.db.transaction(async () => {
|
|
3845
4371
|
await strapi.db.query(UID.DROP_CARD).deleteMany({ where: { drop: drop2.id } });
|
|
3846
4372
|
for (const e of entries) {
|
|
3847
4373
|
const entry = typeof e === "string" ? { card: e } : e;
|
|
3848
4374
|
await strapi.db.query(UID.DROP_CARD).create({
|
|
3849
|
-
data: {
|
|
4375
|
+
data: {
|
|
4376
|
+
drop: drop2.id,
|
|
4377
|
+
card: idByDoc[entry.card],
|
|
4378
|
+
weight: Number.isFinite(Number(entry.weight)) ? Number(entry.weight) : 100,
|
|
4379
|
+
isExclusive: Boolean(entry.isExclusive),
|
|
4380
|
+
isActive: entry.isActive !== false
|
|
4381
|
+
}
|
|
3850
4382
|
});
|
|
3851
4383
|
}
|
|
3852
4384
|
});
|
|
@@ -3854,8 +4386,18 @@ const drop = ({ strapi }) => ({
|
|
|
3854
4386
|
},
|
|
3855
4387
|
async cards(pack2, ref) {
|
|
3856
4388
|
const drop2 = await this.findOne(pack2, ref);
|
|
3857
|
-
const rows = await strapi.db.query(UID.DROP_CARD).findMany({
|
|
3858
|
-
|
|
4389
|
+
const rows = await strapi.db.query(UID.DROP_CARD).findMany({
|
|
4390
|
+
where: { drop: drop2.id },
|
|
4391
|
+
populate: { card: { populate: { image: true } } },
|
|
4392
|
+
orderBy: { id: "asc" }
|
|
4393
|
+
});
|
|
4394
|
+
return rows.filter((r) => r.card).map((r) => ({
|
|
4395
|
+
id: r.documentId,
|
|
4396
|
+
weight: r.weight,
|
|
4397
|
+
isExclusive: r.isExclusive,
|
|
4398
|
+
isActive: r.isActive,
|
|
4399
|
+
card: r.card
|
|
4400
|
+
}));
|
|
3859
4401
|
},
|
|
3860
4402
|
/**
|
|
3861
4403
|
* Effective raw pool for a drop (before exclusivity/supply pruning): pack pool, drop pool, or both.
|
|
@@ -3869,25 +4411,46 @@ const drop = ({ strapi }) => ({
|
|
|
3869
4411
|
});
|
|
3870
4412
|
const ownEntries = own.filter((r) => r.card).map((r) => ({ ...r.card, weight: r.weight, isExclusive: r.isExclusive, source: "drop" }));
|
|
3871
4413
|
const seen = new Set(ownEntries.map((c) => c.id));
|
|
3872
|
-
return [
|
|
4414
|
+
return [
|
|
4415
|
+
...ownEntries,
|
|
4416
|
+
...packPool.filter((c) => !seen.has(c.id)).map((c) => ({ ...c, source: "pack" }))
|
|
4417
|
+
];
|
|
3873
4418
|
},
|
|
3874
4419
|
/** Card ids flagged exclusive on any pack-card or drop-card of a season. */
|
|
3875
4420
|
async exclusiveCardIds(season2) {
|
|
3876
4421
|
const [pc, dc] = await Promise.all([
|
|
3877
|
-
strapi.db.query(UID.PACK_CARD).findMany({
|
|
3878
|
-
|
|
4422
|
+
strapi.db.query(UID.PACK_CARD).findMany({
|
|
4423
|
+
where: { isExclusive: true, isActive: true, pack: { season: season2.id } },
|
|
4424
|
+
populate: { card: { select: ["id"] } }
|
|
4425
|
+
}),
|
|
4426
|
+
strapi.db.query(UID.DROP_CARD).findMany({
|
|
4427
|
+
where: { isExclusive: true, isActive: true, drop: { pack: { season: season2.id } } },
|
|
4428
|
+
populate: { card: { select: ["id"] } }
|
|
4429
|
+
})
|
|
3879
4430
|
]);
|
|
3880
4431
|
return new Set([...pc, ...dc].map((r) => r.card?.id).filter(Boolean));
|
|
3881
4432
|
},
|
|
3882
4433
|
/** Where a card is exclusive, for the admin: [{ kind:'pack'|'drop', name, slug }]. */
|
|
3883
4434
|
async exclusivityOf(cardId) {
|
|
3884
4435
|
const [pc, dc] = await Promise.all([
|
|
3885
|
-
strapi.db.query(UID.PACK_CARD).findMany({
|
|
3886
|
-
|
|
4436
|
+
strapi.db.query(UID.PACK_CARD).findMany({
|
|
4437
|
+
where: { card: cardId, isExclusive: true },
|
|
4438
|
+
populate: { pack: { select: ["name", "slug"] } }
|
|
4439
|
+
}),
|
|
4440
|
+
strapi.db.query(UID.DROP_CARD).findMany({
|
|
4441
|
+
where: { card: cardId, isExclusive: true },
|
|
4442
|
+
populate: {
|
|
4443
|
+
drop: { select: ["name", "slug"], populate: { pack: { select: ["name"] } } }
|
|
4444
|
+
}
|
|
4445
|
+
})
|
|
3887
4446
|
]);
|
|
3888
4447
|
return [
|
|
3889
4448
|
...pc.filter((r) => r.pack).map((r) => ({ kind: "pack", name: r.pack.name, slug: r.pack.slug })),
|
|
3890
|
-
...dc.filter((r) => r.drop).map((r) => ({
|
|
4449
|
+
...dc.filter((r) => r.drop).map((r) => ({
|
|
4450
|
+
kind: "drop",
|
|
4451
|
+
name: `${r.drop.pack?.name ?? ""} · ${r.drop.name}`,
|
|
4452
|
+
slug: r.drop.slug
|
|
4453
|
+
}))
|
|
3891
4454
|
];
|
|
3892
4455
|
},
|
|
3893
4456
|
/** Booth / poster token. */
|
|
@@ -3900,7 +4463,10 @@ const drop = ({ strapi }) => ({
|
|
|
3900
4463
|
/** Public shape, exclusives revealed as count (+ names unless the drop is secret). */
|
|
3901
4464
|
async publicView(drop2, pack2, season2) {
|
|
3902
4465
|
const format2 = strapi.plugin("tcg").service("format");
|
|
3903
|
-
const own = drop2.poolMode === POOL_MODE.INHERIT ? [] : await strapi.db.query(UID.DROP_CARD).findMany({
|
|
4466
|
+
const own = drop2.poolMode === POOL_MODE.INHERIT ? [] : await strapi.db.query(UID.DROP_CARD).findMany({
|
|
4467
|
+
where: { drop: drop2.id, isActive: true, isExclusive: true },
|
|
4468
|
+
populate: { card: true }
|
|
4469
|
+
});
|
|
3904
4470
|
return {
|
|
3905
4471
|
id: drop2.documentId,
|
|
3906
4472
|
name: drop2.name,
|
|
@@ -3909,12 +4475,18 @@ const drop = ({ strapi }) => ({
|
|
|
3909
4475
|
mode: drop2.mode,
|
|
3910
4476
|
isDefault: drop2.isDefault,
|
|
3911
4477
|
isOpen: this.isOpen(drop2),
|
|
4478
|
+
cardsPerPull: Math.max(1, Number(drop2.cardsPerPull) || 1),
|
|
3912
4479
|
startsAt: drop2.startsAt ?? null,
|
|
3913
4480
|
endsAt: drop2.endsAt ?? null,
|
|
3914
4481
|
soldOut: drop2.maxScansTotal != null && drop2.scanCount >= drop2.maxScansTotal,
|
|
3915
4482
|
remaining: drop2.maxScansTotal != null ? Math.max(0, drop2.maxScansTotal - drop2.scanCount) : null,
|
|
3916
4483
|
exclusiveCount: own.length,
|
|
3917
|
-
exclusiveCards: drop2.isSecret ? [] : own.filter((r) => r.card).map((r) => ({
|
|
4484
|
+
exclusiveCards: drop2.isSecret ? [] : own.filter((r) => r.card).map((r) => ({
|
|
4485
|
+
id: r.card.documentId,
|
|
4486
|
+
name: r.card.name,
|
|
4487
|
+
rarity: r.card.rarity,
|
|
4488
|
+
rarityColor: format2.rarityMeta(season2, r.card.rarity).color
|
|
4489
|
+
})),
|
|
3918
4490
|
geo: drop2.geo ?? null
|
|
3919
4491
|
};
|
|
3920
4492
|
}
|