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