@kaikybrofc/omnizap-system 2.1.8

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.
Files changed (166) hide show
  1. package/.env.example +534 -0
  2. package/LICENSE +21 -0
  3. package/README.md +431 -0
  4. package/RELEASE-v2.1.2.md +83 -0
  5. package/app/config/adminIdentity.js +87 -0
  6. package/app/config/baileysConfig.js +693 -0
  7. package/app/config/groupUtils.js +388 -0
  8. package/app/connection/socketController.js +992 -0
  9. package/app/controllers/messageController.js +354 -0
  10. package/app/modules/adminModule/groupCommandHandlers.js +1294 -0
  11. package/app/modules/adminModule/groupEventHandlers.js +355 -0
  12. package/app/modules/aiModule/catCommand.js +1006 -0
  13. package/app/modules/broadcastModule/noticeCommand.js +416 -0
  14. package/app/modules/gameModule/diceCommand.js +67 -0
  15. package/app/modules/menuModule/common.js +311 -0
  16. package/app/modules/menuModule/menus.js +59 -0
  17. package/app/modules/playModule/playCommand.js +1615 -0
  18. package/app/modules/quoteModule/quoteCommand.js +851 -0
  19. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +786 -0
  20. package/app/modules/rpgPokemonModule/rpgBattleService.js +2082 -0
  21. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +760 -0
  22. package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
  23. package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +172 -0
  24. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
  25. package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
  26. package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
  27. package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
  28. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1859 -0
  29. package/app/modules/rpgPokemonModule/rpgPokemonService.js +6738 -0
  30. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
  31. package/app/modules/statsModule/globalRankingCommand.js +65 -0
  32. package/app/modules/statsModule/noMessageCommand.js +288 -0
  33. package/app/modules/statsModule/rankingCommand.js +60 -0
  34. package/app/modules/statsModule/rankingCommon.js +889 -0
  35. package/app/modules/stickerModule/addStickerMetadata.js +239 -0
  36. package/app/modules/stickerModule/convertToWebp.js +390 -0
  37. package/app/modules/stickerModule/stickerCommand.js +454 -0
  38. package/app/modules/stickerModule/stickerConvertCommand.js +156 -0
  39. package/app/modules/stickerModule/stickerTextCommand.js +657 -0
  40. package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
  41. package/app/modules/stickerPackModule/autoPackCollectorService.js +284 -0
  42. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +466 -0
  43. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +88 -0
  44. package/app/modules/stickerPackModule/semanticThemeClusterService.js +571 -0
  45. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +449 -0
  46. package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
  47. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +180 -0
  48. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +4078 -0
  49. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +598 -0
  50. package/app/modules/stickerPackModule/stickerClassificationService.js +588 -0
  51. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +102 -0
  52. package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +7506 -0
  53. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1095 -0
  54. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +108 -0
  55. package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
  56. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +110 -0
  57. package/app/modules/stickerPackModule/stickerPackItemRepository.js +440 -0
  58. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +337 -0
  59. package/app/modules/stickerPackModule/stickerPackMessageService.js +296 -0
  60. package/app/modules/stickerPackModule/stickerPackRepository.js +442 -0
  61. package/app/modules/stickerPackModule/stickerPackService.js +788 -0
  62. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +51 -0
  63. package/app/modules/stickerPackModule/stickerPackUtils.js +97 -0
  64. package/app/modules/stickerPackModule/stickerStorageService.js +507 -0
  65. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +233 -0
  66. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +205 -0
  67. package/app/modules/systemMetricsModule/pingCommand.js +421 -0
  68. package/app/modules/tiktokModule/tiktokCommand.js +798 -0
  69. package/app/modules/userModule/userCommand.js +1217 -0
  70. package/app/modules/waifuPicsModule/waifuPicsCommand.js +177 -0
  71. package/app/observability/metrics.js +734 -0
  72. package/app/services/captchaService.js +492 -0
  73. package/app/services/dbWriteQueue.js +572 -0
  74. package/app/services/groupMetadataService.js +279 -0
  75. package/app/services/lidMapService.js +663 -0
  76. package/app/services/messagePersistenceService.js +56 -0
  77. package/app/services/newsBroadcastService.js +351 -0
  78. package/app/services/pokeApiService.js +398 -0
  79. package/app/services/queueUtils.js +57 -0
  80. package/app/services/socketState.js +7 -0
  81. package/app/store/aiPromptStore.js +38 -0
  82. package/app/store/groupConfigStore.js +58 -0
  83. package/app/store/premiumUserStore.js +36 -0
  84. package/app/utils/antiLink/antiLinkModule.js +804 -0
  85. package/app/utils/http/getImageBufferModule.js +18 -0
  86. package/app/utils/json/jsonSanitizer.js +113 -0
  87. package/app/utils/json/jsonSanitizer.test.js +40 -0
  88. package/app/utils/logger/loggerModule.js +262 -0
  89. package/app/utils/systemMetrics/systemMetricsModule.js +91 -0
  90. package/database/index.js +2052 -0
  91. package/database/init.js +516 -0
  92. package/database/migrations/20260203_0001_sticker_packs.sql +54 -0
  93. package/database/migrations/20260210_0003_rpg_pokemon.sql +58 -0
  94. package/database/migrations/20260210_0004_rpg_shiny_biome.sql +9 -0
  95. package/database/migrations/20260210_0005_rpg_missions.sql +14 -0
  96. package/database/migrations/20260210_0006_rpg_world_pokedex_traits.sql +27 -0
  97. package/database/migrations/20260210_0007_rpg_raid_pvp.sql +56 -0
  98. package/database/migrations/20260210_0008_rpg_social_system.sql +195 -0
  99. package/database/migrations/20260211_0009_rpg_social_xp.sql +36 -0
  100. package/database/migrations/20260222_0010_remove_message_xp.sql +2 -0
  101. package/database/migrations/20260226_0011_sticker_asset_classification.sql +17 -0
  102. package/database/migrations/20260226_0012_sticker_pack_engagement.sql +16 -0
  103. package/database/migrations/20260226_0013_sticker_marketplace_intelligence.sql +19 -0
  104. package/database/migrations/20260226_0014_sticker_pack_publish_flow.sql +30 -0
  105. package/database/migrations/20260226_0014_sticker_worker_queues.sql +42 -0
  106. package/database/migrations/20260226_0015_sticker_auto_pack_curation_integrity.sql +18 -0
  107. package/database/migrations/20260226_0016_sticker_web_google_auth_persistence.sql +34 -0
  108. package/database/migrations/20260226_0017_sticker_web_admin_ban.sql +22 -0
  109. package/database/migrations/20260226_0018_sticker_web_admin_moderator.sql +18 -0
  110. package/database/migrations/20260227_0019_sticker_classification_v2_signals.sql +12 -0
  111. package/database/migrations/20260227_0020_semantic_theme_clusters.sql +35 -0
  112. package/docker-compose.yml +103 -0
  113. package/ecosystem.prod.config.cjs +35 -0
  114. package/eslint.config.js +61 -0
  115. package/index.js +437 -0
  116. package/ml/clip_classifier/Dockerfile +16 -0
  117. package/ml/clip_classifier/README.md +120 -0
  118. package/ml/clip_classifier/adaptive_scoring.py +40 -0
  119. package/ml/clip_classifier/classifier.py +654 -0
  120. package/ml/clip_classifier/embedding_store.py +481 -0
  121. package/ml/clip_classifier/env_loader.py +15 -0
  122. package/ml/clip_classifier/llm_label_expander.py +144 -0
  123. package/ml/clip_classifier/main.py +213 -0
  124. package/ml/clip_classifier/requirements.txt +10 -0
  125. package/ml/clip_classifier/similarity_engine.py +74 -0
  126. package/observability/alert-rules.yml +60 -0
  127. package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
  128. package/observability/grafana/dashboards/omnizap-overview.json +170 -0
  129. package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
  130. package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
  131. package/observability/loki-config.yml +38 -0
  132. package/observability/mysql-exporter.cnf +5 -0
  133. package/observability/mysql-setup.sql +46 -0
  134. package/observability/prometheus.yml +32 -0
  135. package/observability/promtail-config.yml +84 -0
  136. package/package.json +109 -0
  137. package/public/api-docs/index.html +144 -0
  138. package/public/css/github-project-panel.css +297 -0
  139. package/public/css/stickers-admin.css +1272 -0
  140. package/public/css/styles.css +671 -0
  141. package/public/index.html +1311 -0
  142. package/public/js/apps/apiDocsApp.js +310 -0
  143. package/public/js/apps/createPackApp.js +2069 -0
  144. package/public/js/apps/homeApp.js +396 -0
  145. package/public/js/apps/stickersAdminApp.js +1744 -0
  146. package/public/js/apps/stickersApp.js +4830 -0
  147. package/public/js/catalog.js +1019 -0
  148. package/public/js/github-panel/components/CommitList.js +34 -0
  149. package/public/js/github-panel/components/ErrorState.js +16 -0
  150. package/public/js/github-panel/components/GithubProjectPanel.js +106 -0
  151. package/public/js/github-panel/components/ReleaseList.js +38 -0
  152. package/public/js/github-panel/components/SkeletonPanel.js +22 -0
  153. package/public/js/github-panel/components/StatCard.js +15 -0
  154. package/public/js/github-panel/index.js +15 -0
  155. package/public/js/github-panel/useGithubRepoData.js +154 -0
  156. package/public/js/github-panel/vendor/react.js +11 -0
  157. package/public/js/runtime/react-runtime.js +19 -0
  158. package/public/licenca/index.html +106 -0
  159. package/public/stickers/admin/index.html +23 -0
  160. package/public/stickers/create/index.html +47 -0
  161. package/public/stickers/index.html +48 -0
  162. package/public/termos-de-uso/index.html +125 -0
  163. package/scripts/cache-bust.mjs +107 -0
  164. package/scripts/deploy.sh +458 -0
  165. package/scripts/github-deploy-notify.mjs +174 -0
  166. package/scripts/release.sh +129 -0
@@ -0,0 +1,1859 @@
1
+ import { executeQuery, TABLES } from '../../../database/index.js';
2
+
3
+ const PLAYER_COLUMNS = 'jid, level, xp, xp_pool_social, gold, created_at, updated_at';
4
+ const PLAYER_POKEMON_COLUMNS = 'id, owner_jid, poke_id, nickname, level, xp, current_hp, ivs_json, moves_json, nature_key, ability_key, ability_name, is_shiny, is_active, created_at';
5
+ const BATTLE_COLUMNS = 'chat_jid, owner_jid, my_pokemon_id, enemy_snapshot_json, turn, expires_at, created_at, updated_at';
6
+
7
+ const parseJson = (value, fallback) => {
8
+ if (value === null || value === undefined) return fallback;
9
+ if (typeof value === 'object') return value;
10
+ try {
11
+ return JSON.parse(value);
12
+ } catch {
13
+ return fallback;
14
+ }
15
+ };
16
+
17
+ const normalizePlayerPokemon = (row) => {
18
+ if (!row) return null;
19
+ return {
20
+ ...row,
21
+ ivs_json: parseJson(row.ivs_json, {}),
22
+ moves_json: parseJson(row.moves_json, []),
23
+ is_shiny: Number(row.is_shiny) === 1,
24
+ is_active: Number(row.is_active) === 1,
25
+ level: Number(row.level || 1),
26
+ xp: Number(row.xp || 0),
27
+ current_hp: Number(row.current_hp || 0),
28
+ poke_id: Number(row.poke_id || 0),
29
+ nature_key: row.nature_key || null,
30
+ ability_key: row.ability_key || null,
31
+ ability_name: row.ability_name || null,
32
+ };
33
+ };
34
+
35
+ const normalizeBattle = (row) => {
36
+ if (!row) return null;
37
+ return {
38
+ ...row,
39
+ turn: Number(row.turn || 1),
40
+ enemy_snapshot_json: parseJson(row.enemy_snapshot_json, {}),
41
+ };
42
+ };
43
+
44
+ const normalizeRaidState = (row) => {
45
+ if (!row) return null;
46
+ return {
47
+ ...row,
48
+ boss_snapshot_json: parseJson(row.boss_snapshot_json, {}),
49
+ max_hp: Number(row.max_hp || 0),
50
+ current_hp: Number(row.current_hp || 0),
51
+ };
52
+ };
53
+
54
+ const normalizePvpChallenge = (row) => {
55
+ if (!row) return null;
56
+ return {
57
+ ...row,
58
+ battle_snapshot_json: parseJson(row.battle_snapshot_json, {}),
59
+ };
60
+ };
61
+
62
+ export const getPlayerByJid = async (jid, connection = null) => {
63
+ const rows = await executeQuery(
64
+ `SELECT ${PLAYER_COLUMNS}
65
+ FROM ${TABLES.RPG_PLAYER}
66
+ WHERE jid = ?
67
+ LIMIT 1`,
68
+ [jid],
69
+ connection,
70
+ );
71
+
72
+ return rows?.[0] || null;
73
+ };
74
+
75
+ export const getPlayerByJidForUpdate = async (jid, connection) => {
76
+ const rows = await executeQuery(
77
+ `SELECT ${PLAYER_COLUMNS}
78
+ FROM ${TABLES.RPG_PLAYER}
79
+ WHERE jid = ?
80
+ LIMIT 1
81
+ FOR UPDATE`,
82
+ [jid],
83
+ connection,
84
+ );
85
+
86
+ return rows?.[0] || null;
87
+ };
88
+
89
+ export const createPlayer = async ({ jid, level = 1, xp = 0, gold = 200 }, connection = null) => {
90
+ await executeQuery(
91
+ `INSERT INTO ${TABLES.RPG_PLAYER} (jid, level, xp, xp_pool_social, gold)
92
+ VALUES (?, ?, ?, 0, ?)
93
+ ON DUPLICATE KEY UPDATE jid = VALUES(jid)`,
94
+ [jid, level, xp, gold],
95
+ connection,
96
+ );
97
+
98
+ return getPlayerByJid(jid, connection);
99
+ };
100
+
101
+ export const updatePlayerProgress = async ({ jid, level, xp, gold }, connection = null) => {
102
+ await executeQuery(
103
+ `UPDATE ${TABLES.RPG_PLAYER}
104
+ SET level = ?,
105
+ xp = ?,
106
+ gold = ?,
107
+ updated_at = CURRENT_TIMESTAMP
108
+ WHERE jid = ?`,
109
+ [level, xp, gold, jid],
110
+ connection,
111
+ );
112
+ };
113
+
114
+ export const updatePlayerSocialXpPool = async ({ jid, xpPoolSocial }, connection = null) => {
115
+ await executeQuery(
116
+ `UPDATE ${TABLES.RPG_PLAYER}
117
+ SET xp_pool_social = ?,
118
+ updated_at = CURRENT_TIMESTAMP
119
+ WHERE jid = ?`,
120
+ [Math.max(0, Number(xpPoolSocial) || 0), jid],
121
+ connection,
122
+ );
123
+ };
124
+
125
+ export const updatePlayerGoldOnly = async ({ jid, gold }, connection = null) => {
126
+ await executeQuery(
127
+ `UPDATE ${TABLES.RPG_PLAYER}
128
+ SET gold = ?,
129
+ updated_at = CURRENT_TIMESTAMP
130
+ WHERE jid = ?`,
131
+ [gold, jid],
132
+ connection,
133
+ );
134
+ };
135
+
136
+ export const createPlayerPokemon = async ({ ownerJid, pokeId, nickname = null, level = 5, xp = 0, currentHp, ivsJson, movesJson, natureKey = null, abilityKey = null, abilityName = null, isShiny = false, isActive = false }, connection = null) => {
137
+ const result = await executeQuery(
138
+ `INSERT INTO ${TABLES.RPG_PLAYER_POKEMON}
139
+ (owner_jid, poke_id, nickname, level, xp, current_hp, ivs_json, moves_json, nature_key, ability_key, ability_name, is_shiny, is_active)
140
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
141
+ [ownerJid, pokeId, nickname, level, xp, currentHp, JSON.stringify(ivsJson || {}), JSON.stringify(movesJson || []), natureKey, abilityKey, abilityName, isShiny ? 1 : 0, isActive ? 1 : 0],
142
+ connection,
143
+ );
144
+
145
+ const insertedId = Number(result?.insertId || 0);
146
+ if (!insertedId) return null;
147
+ return getPlayerPokemonById(ownerJid, insertedId, connection);
148
+ };
149
+
150
+ export const listPlayerPokemons = async (ownerJid, connection = null) => {
151
+ const rows = await executeQuery(
152
+ `SELECT ${PLAYER_POKEMON_COLUMNS}
153
+ FROM ${TABLES.RPG_PLAYER_POKEMON}
154
+ WHERE owner_jid = ?
155
+ ORDER BY is_active DESC, level DESC, id ASC`,
156
+ [ownerJid],
157
+ connection,
158
+ );
159
+
160
+ return (rows || []).map(normalizePlayerPokemon);
161
+ };
162
+
163
+ export const getPlayerPokemonById = async (ownerJid, pokemonId, connection = null) => {
164
+ const rows = await executeQuery(
165
+ `SELECT ${PLAYER_POKEMON_COLUMNS}
166
+ FROM ${TABLES.RPG_PLAYER_POKEMON}
167
+ WHERE owner_jid = ?
168
+ AND id = ?
169
+ LIMIT 1`,
170
+ [ownerJid, pokemonId],
171
+ connection,
172
+ );
173
+
174
+ return normalizePlayerPokemon(rows?.[0] || null);
175
+ };
176
+
177
+ export const getPlayerPokemonByIdForUpdate = async (ownerJid, pokemonId, connection) => {
178
+ const rows = await executeQuery(
179
+ `SELECT ${PLAYER_POKEMON_COLUMNS}
180
+ FROM ${TABLES.RPG_PLAYER_POKEMON}
181
+ WHERE owner_jid = ?
182
+ AND id = ?
183
+ LIMIT 1
184
+ FOR UPDATE`,
185
+ [ownerJid, pokemonId],
186
+ connection,
187
+ );
188
+
189
+ return normalizePlayerPokemon(rows?.[0] || null);
190
+ };
191
+
192
+ export const getActivePlayerPokemon = async (ownerJid, connection = null) => {
193
+ const rows = await executeQuery(
194
+ `SELECT ${PLAYER_POKEMON_COLUMNS}
195
+ FROM ${TABLES.RPG_PLAYER_POKEMON}
196
+ WHERE owner_jid = ?
197
+ AND is_active = 1
198
+ ORDER BY id ASC
199
+ LIMIT 1`,
200
+ [ownerJid],
201
+ connection,
202
+ );
203
+
204
+ return normalizePlayerPokemon(rows?.[0] || null);
205
+ };
206
+
207
+ export const getActivePlayerPokemonForUpdate = async (ownerJid, connection) => {
208
+ const rows = await executeQuery(
209
+ `SELECT ${PLAYER_POKEMON_COLUMNS}
210
+ FROM ${TABLES.RPG_PLAYER_POKEMON}
211
+ WHERE owner_jid = ?
212
+ AND is_active = 1
213
+ ORDER BY id ASC
214
+ LIMIT 1
215
+ FOR UPDATE`,
216
+ [ownerJid],
217
+ connection,
218
+ );
219
+
220
+ return normalizePlayerPokemon(rows?.[0] || null);
221
+ };
222
+
223
+ export const setActivePokemon = async (ownerJid, pokemonId, connection = null) => {
224
+ await executeQuery(
225
+ `UPDATE ${TABLES.RPG_PLAYER_POKEMON}
226
+ SET is_active = 0
227
+ WHERE owner_jid = ?`,
228
+ [ownerJid],
229
+ connection,
230
+ );
231
+
232
+ const result = await executeQuery(
233
+ `UPDATE ${TABLES.RPG_PLAYER_POKEMON}
234
+ SET is_active = 1
235
+ WHERE owner_jid = ?
236
+ AND id = ?`,
237
+ [ownerJid, pokemonId],
238
+ connection,
239
+ );
240
+
241
+ return Number(result?.affectedRows || 0) > 0;
242
+ };
243
+
244
+ export const updatePlayerPokemonState = async ({ id, ownerJid, level, xp, currentHp, movesJson = null, pokeId = null, nickname, isShiny, natureKey, abilityKey, abilityName }, connection = null) => {
245
+ const fields = ['level = ?', 'xp = ?', 'current_hp = ?'];
246
+ const params = [level, xp, currentHp];
247
+
248
+ if (movesJson !== null) {
249
+ fields.push('moves_json = ?');
250
+ params.push(JSON.stringify(movesJson || []));
251
+ }
252
+
253
+ if (Number.isFinite(Number(pokeId)) && Number(pokeId) > 0) {
254
+ fields.push('poke_id = ?');
255
+ params.push(Number(pokeId));
256
+ }
257
+
258
+ if (nickname !== undefined) {
259
+ fields.push('nickname = ?');
260
+ params.push(nickname ?? null);
261
+ }
262
+
263
+ if (isShiny !== undefined) {
264
+ fields.push('is_shiny = ?');
265
+ params.push(isShiny ? 1 : 0);
266
+ }
267
+
268
+ if (natureKey !== undefined) {
269
+ fields.push('nature_key = ?');
270
+ params.push(natureKey ?? null);
271
+ }
272
+
273
+ if (abilityKey !== undefined) {
274
+ fields.push('ability_key = ?');
275
+ params.push(abilityKey ?? null);
276
+ }
277
+
278
+ if (abilityName !== undefined) {
279
+ fields.push('ability_name = ?');
280
+ params.push(abilityName ?? null);
281
+ }
282
+
283
+ params.push(id, ownerJid);
284
+
285
+ await executeQuery(
286
+ `UPDATE ${TABLES.RPG_PLAYER_POKEMON}
287
+ SET ${fields.join(', ')}
288
+ WHERE id = ?
289
+ AND owner_jid = ?`,
290
+ params,
291
+ connection,
292
+ );
293
+ };
294
+
295
+ export const deleteExpiredBattleStatesByOwner = async (ownerJid, connection = null) => {
296
+ const result = await executeQuery(
297
+ `DELETE FROM ${TABLES.RPG_BATTLE_STATE}
298
+ WHERE owner_jid = ?
299
+ AND expires_at <= UTC_TIMESTAMP()`,
300
+ [ownerJid],
301
+ connection,
302
+ );
303
+
304
+ return Number(result?.affectedRows || 0);
305
+ };
306
+
307
+ export const getBattleStateByOwner = async (ownerJid, connection = null) => {
308
+ const rows = await executeQuery(
309
+ `SELECT ${BATTLE_COLUMNS}
310
+ FROM ${TABLES.RPG_BATTLE_STATE}
311
+ WHERE owner_jid = ?
312
+ LIMIT 1`,
313
+ [ownerJid],
314
+ connection,
315
+ );
316
+
317
+ return normalizeBattle(rows?.[0] || null);
318
+ };
319
+
320
+ export const getBattleStateByOwnerForUpdate = async (ownerJid, connection) => {
321
+ const rows = await executeQuery(
322
+ `SELECT ${BATTLE_COLUMNS}
323
+ FROM ${TABLES.RPG_BATTLE_STATE}
324
+ WHERE owner_jid = ?
325
+ LIMIT 1
326
+ FOR UPDATE`,
327
+ [ownerJid],
328
+ connection,
329
+ );
330
+
331
+ return normalizeBattle(rows?.[0] || null);
332
+ };
333
+
334
+ export const upsertBattleState = async ({ chatJid, ownerJid, myPokemonId, battleSnapshot, turn = 1, expiresAt }, connection = null) => {
335
+ await executeQuery(
336
+ `INSERT INTO ${TABLES.RPG_BATTLE_STATE}
337
+ (chat_jid, owner_jid, my_pokemon_id, enemy_snapshot_json, turn, expires_at)
338
+ VALUES (?, ?, ?, ?, ?, ?)
339
+ ON DUPLICATE KEY UPDATE
340
+ chat_jid = VALUES(chat_jid),
341
+ owner_jid = VALUES(owner_jid),
342
+ my_pokemon_id = VALUES(my_pokemon_id),
343
+ enemy_snapshot_json = VALUES(enemy_snapshot_json),
344
+ turn = VALUES(turn),
345
+ expires_at = VALUES(expires_at),
346
+ updated_at = CURRENT_TIMESTAMP`,
347
+ [chatJid, ownerJid, myPokemonId, JSON.stringify(battleSnapshot || {}), turn, expiresAt],
348
+ connection,
349
+ );
350
+ };
351
+
352
+ export const deleteBattleStateByOwner = async (ownerJid, connection = null) => {
353
+ await executeQuery(
354
+ `DELETE FROM ${TABLES.RPG_BATTLE_STATE}
355
+ WHERE owner_jid = ?`,
356
+ [ownerJid],
357
+ connection,
358
+ );
359
+ };
360
+
361
+ export const getInventoryItems = async (ownerJid, connection = null) => {
362
+ const rows = await executeQuery(
363
+ `SELECT owner_jid, item_key, quantity, created_at, updated_at
364
+ FROM ${TABLES.RPG_PLAYER_INVENTORY}
365
+ WHERE owner_jid = ?
366
+ ORDER BY item_key ASC`,
367
+ [ownerJid],
368
+ connection,
369
+ );
370
+
371
+ return (rows || []).map((row) => ({
372
+ ...row,
373
+ quantity: Number(row.quantity || 0),
374
+ }));
375
+ };
376
+
377
+ export const getInventoryItem = async (ownerJid, itemKey, connection = null) => {
378
+ const rows = await executeQuery(
379
+ `SELECT owner_jid, item_key, quantity, created_at, updated_at
380
+ FROM ${TABLES.RPG_PLAYER_INVENTORY}
381
+ WHERE owner_jid = ?
382
+ AND item_key = ?
383
+ LIMIT 1`,
384
+ [ownerJid, itemKey],
385
+ connection,
386
+ );
387
+
388
+ const row = rows?.[0] || null;
389
+ if (!row) return null;
390
+ return {
391
+ ...row,
392
+ quantity: Number(row.quantity || 0),
393
+ };
394
+ };
395
+
396
+ export const getInventoryItemForUpdate = async (ownerJid, itemKey, connection) => {
397
+ const rows = await executeQuery(
398
+ `SELECT owner_jid, item_key, quantity, created_at, updated_at
399
+ FROM ${TABLES.RPG_PLAYER_INVENTORY}
400
+ WHERE owner_jid = ?
401
+ AND item_key = ?
402
+ LIMIT 1
403
+ FOR UPDATE`,
404
+ [ownerJid, itemKey],
405
+ connection,
406
+ );
407
+
408
+ const row = rows?.[0] || null;
409
+ if (!row) return null;
410
+ return {
411
+ ...row,
412
+ quantity: Number(row.quantity || 0),
413
+ };
414
+ };
415
+
416
+ export const addInventoryItem = async ({ ownerJid, itemKey, quantity }, connection = null) => {
417
+ await executeQuery(
418
+ `INSERT INTO ${TABLES.RPG_PLAYER_INVENTORY} (owner_jid, item_key, quantity)
419
+ VALUES (?, ?, ?)
420
+ ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity), updated_at = CURRENT_TIMESTAMP`,
421
+ [ownerJid, itemKey, quantity],
422
+ connection,
423
+ );
424
+ };
425
+
426
+ export const consumeInventoryItem = async ({ ownerJid, itemKey, quantity }, connection = null) => {
427
+ const safeQty = Number(quantity);
428
+ if (!Number.isFinite(safeQty) || safeQty <= 0) return false;
429
+
430
+ const result = await executeQuery(
431
+ `UPDATE ${TABLES.RPG_PLAYER_INVENTORY}
432
+ SET quantity = quantity - ?
433
+ WHERE owner_jid = ?
434
+ AND item_key = ?
435
+ AND quantity >= ?`,
436
+ [safeQty, ownerJid, itemKey, safeQty],
437
+ connection,
438
+ );
439
+
440
+ const consumed = Number(result?.affectedRows || 0) > 0;
441
+ if (!consumed) return false;
442
+
443
+ await executeQuery(
444
+ `DELETE FROM ${TABLES.RPG_PLAYER_INVENTORY}
445
+ WHERE owner_jid = ?
446
+ AND item_key = ?
447
+ AND quantity <= 0`,
448
+ [ownerJid, itemKey],
449
+ connection,
450
+ );
451
+
452
+ return true;
453
+ };
454
+
455
+ export const getGroupBiomeByJid = async (groupJid, connection = null) => {
456
+ const rows = await executeQuery(
457
+ `SELECT group_jid, biome_key, created_at, updated_at
458
+ FROM ${TABLES.RPG_GROUP_BIOME}
459
+ WHERE group_jid = ?
460
+ LIMIT 1`,
461
+ [groupJid],
462
+ connection,
463
+ );
464
+
465
+ return rows?.[0] || null;
466
+ };
467
+
468
+ export const upsertGroupBiome = async ({ groupJid, biomeKey }, connection = null) => {
469
+ await executeQuery(
470
+ `INSERT INTO ${TABLES.RPG_GROUP_BIOME} (group_jid, biome_key)
471
+ VALUES (?, ?)
472
+ ON DUPLICATE KEY UPDATE biome_key = VALUES(biome_key), updated_at = CURRENT_TIMESTAMP`,
473
+ [groupJid, biomeKey],
474
+ connection,
475
+ );
476
+ };
477
+
478
+ const MISSION_COLUMNS = 'owner_jid, daily_ref_date, daily_progress_json, daily_claimed_at, weekly_ref_date, weekly_progress_json, weekly_claimed_at, created_at, updated_at';
479
+ const SOCIAL_XP_DAILY_COLUMNS = 'day_ref_date, owner_jid, chat_jid, earned_xp, converted_xp, cap_hits, last_message_hash, last_earned_at, created_at, updated_at';
480
+
481
+ const normalizeMissionRow = (row) => {
482
+ if (!row) return null;
483
+ return {
484
+ ...row,
485
+ daily_progress_json: parseJson(row.daily_progress_json, {}),
486
+ weekly_progress_json: parseJson(row.weekly_progress_json, {}),
487
+ };
488
+ };
489
+
490
+ export const getMissionProgressByOwner = async (ownerJid, connection = null) => {
491
+ const rows = await executeQuery(
492
+ `SELECT ${MISSION_COLUMNS}
493
+ FROM ${TABLES.RPG_PLAYER_MISSION_PROGRESS}
494
+ WHERE owner_jid = ?
495
+ LIMIT 1`,
496
+ [ownerJid],
497
+ connection,
498
+ );
499
+
500
+ return normalizeMissionRow(rows?.[0] || null);
501
+ };
502
+
503
+ export const getMissionProgressByOwnerForUpdate = async (ownerJid, connection) => {
504
+ const rows = await executeQuery(
505
+ `SELECT ${MISSION_COLUMNS}
506
+ FROM ${TABLES.RPG_PLAYER_MISSION_PROGRESS}
507
+ WHERE owner_jid = ?
508
+ LIMIT 1
509
+ FOR UPDATE`,
510
+ [ownerJid],
511
+ connection,
512
+ );
513
+
514
+ return normalizeMissionRow(rows?.[0] || null);
515
+ };
516
+
517
+ export const createMissionProgress = async ({ ownerJid, dailyRefDate, dailyProgressJson, weeklyRefDate, weeklyProgressJson }, connection = null) => {
518
+ await executeQuery(
519
+ `INSERT INTO ${TABLES.RPG_PLAYER_MISSION_PROGRESS}
520
+ (owner_jid, daily_ref_date, daily_progress_json, daily_claimed_at, weekly_ref_date, weekly_progress_json, weekly_claimed_at)
521
+ VALUES (?, ?, ?, NULL, ?, ?, NULL)
522
+ ON DUPLICATE KEY UPDATE owner_jid = VALUES(owner_jid)`,
523
+ [ownerJid, dailyRefDate, JSON.stringify(dailyProgressJson || {}), weeklyRefDate, JSON.stringify(weeklyProgressJson || {})],
524
+ connection,
525
+ );
526
+
527
+ return getMissionProgressByOwner(ownerJid, connection);
528
+ };
529
+
530
+ export const updateMissionProgress = async ({ ownerJid, dailyRefDate, dailyProgressJson, dailyClaimedAt, weeklyRefDate, weeklyProgressJson, weeklyClaimedAt }, connection = null) => {
531
+ await executeQuery(
532
+ `UPDATE ${TABLES.RPG_PLAYER_MISSION_PROGRESS}
533
+ SET daily_ref_date = ?,
534
+ daily_progress_json = ?,
535
+ daily_claimed_at = ?,
536
+ weekly_ref_date = ?,
537
+ weekly_progress_json = ?,
538
+ weekly_claimed_at = ?,
539
+ updated_at = CURRENT_TIMESTAMP
540
+ WHERE owner_jid = ?`,
541
+ [dailyRefDate, JSON.stringify(dailyProgressJson || {}), dailyClaimedAt || null, weeklyRefDate, JSON.stringify(weeklyProgressJson || {}), weeklyClaimedAt || null, ownerJid],
542
+ connection,
543
+ );
544
+ };
545
+
546
+ export const upsertPokedexEntry = async ({ ownerJid, pokeId }, connection = null) => {
547
+ const result = await executeQuery(
548
+ `INSERT IGNORE INTO ${TABLES.RPG_PLAYER_POKEDEX} (owner_jid, poke_id)
549
+ VALUES (?, ?)`,
550
+ [ownerJid, pokeId],
551
+ connection,
552
+ );
553
+
554
+ return Number(result?.affectedRows || 0) > 0;
555
+ };
556
+
557
+ export const countPokedexEntries = async (ownerJid, connection = null) => {
558
+ const rows = await executeQuery(
559
+ `SELECT COUNT(*) AS total
560
+ FROM ${TABLES.RPG_PLAYER_POKEDEX}
561
+ WHERE owner_jid = ?`,
562
+ [ownerJid],
563
+ connection,
564
+ );
565
+
566
+ return Number(rows?.[0]?.total || 0);
567
+ };
568
+
569
+ export const listPokedexEntries = async (ownerJid, limit = 12, connection = null) => {
570
+ const safeLimit = Math.max(1, Math.min(50, Number(limit) || 12));
571
+ const rows = await executeQuery(
572
+ `SELECT owner_jid, poke_id, first_captured_at
573
+ FROM ${TABLES.RPG_PLAYER_POKEDEX}
574
+ WHERE owner_jid = ?
575
+ ORDER BY first_captured_at DESC
576
+ LIMIT ${safeLimit}`,
577
+ [ownerJid],
578
+ connection,
579
+ );
580
+
581
+ return (rows || []).map((row) => ({
582
+ ...row,
583
+ poke_id: Number(row.poke_id || 0),
584
+ }));
585
+ };
586
+
587
+ export const getTravelStateByOwner = async (ownerJid, connection = null) => {
588
+ const rows = await executeQuery(
589
+ `SELECT owner_jid, region_key, location_key, location_area_key, created_at, updated_at
590
+ FROM ${TABLES.RPG_PLAYER_TRAVEL}
591
+ WHERE owner_jid = ?
592
+ LIMIT 1`,
593
+ [ownerJid],
594
+ connection,
595
+ );
596
+
597
+ return rows?.[0] || null;
598
+ };
599
+
600
+ export const getTravelStateByOwnerForUpdate = async (ownerJid, connection) => {
601
+ const rows = await executeQuery(
602
+ `SELECT owner_jid, region_key, location_key, location_area_key, created_at, updated_at
603
+ FROM ${TABLES.RPG_PLAYER_TRAVEL}
604
+ WHERE owner_jid = ?
605
+ LIMIT 1
606
+ FOR UPDATE`,
607
+ [ownerJid],
608
+ connection,
609
+ );
610
+
611
+ return rows?.[0] || null;
612
+ };
613
+
614
+ export const upsertTravelState = async ({ ownerJid, regionKey = null, locationKey = null, locationAreaKey = null }, connection = null) => {
615
+ await executeQuery(
616
+ `INSERT INTO ${TABLES.RPG_PLAYER_TRAVEL} (owner_jid, region_key, location_key, location_area_key)
617
+ VALUES (?, ?, ?, ?)
618
+ ON DUPLICATE KEY UPDATE
619
+ region_key = VALUES(region_key),
620
+ location_key = VALUES(location_key),
621
+ location_area_key = VALUES(location_area_key),
622
+ updated_at = CURRENT_TIMESTAMP`,
623
+ [ownerJid, regionKey, locationKey, locationAreaKey],
624
+ connection,
625
+ );
626
+ };
627
+
628
+ export const getRaidStateByChat = async (chatJid, connection = null) => {
629
+ const rows = await executeQuery(
630
+ `SELECT chat_jid, created_by_jid, biome_key, boss_snapshot_json, max_hp, current_hp, started_at, ends_at, created_at, updated_at
631
+ FROM ${TABLES.RPG_RAID_STATE}
632
+ WHERE chat_jid = ?
633
+ LIMIT 1`,
634
+ [chatJid],
635
+ connection,
636
+ );
637
+
638
+ return normalizeRaidState(rows?.[0] || null);
639
+ };
640
+
641
+ export const getRaidStateByChatForUpdate = async (chatJid, connection) => {
642
+ const rows = await executeQuery(
643
+ `SELECT chat_jid, created_by_jid, biome_key, boss_snapshot_json, max_hp, current_hp, started_at, ends_at, created_at, updated_at
644
+ FROM ${TABLES.RPG_RAID_STATE}
645
+ WHERE chat_jid = ?
646
+ LIMIT 1
647
+ FOR UPDATE`,
648
+ [chatJid],
649
+ connection,
650
+ );
651
+
652
+ return normalizeRaidState(rows?.[0] || null);
653
+ };
654
+
655
+ export const upsertRaidState = async ({ chatJid, createdByJid, biomeKey = null, bossSnapshot, maxHp, currentHp, startedAt, endsAt }, connection = null) => {
656
+ await executeQuery(
657
+ `INSERT INTO ${TABLES.RPG_RAID_STATE}
658
+ (chat_jid, created_by_jid, biome_key, boss_snapshot_json, max_hp, current_hp, started_at, ends_at)
659
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
660
+ ON DUPLICATE KEY UPDATE
661
+ created_by_jid = VALUES(created_by_jid),
662
+ biome_key = VALUES(biome_key),
663
+ boss_snapshot_json = VALUES(boss_snapshot_json),
664
+ max_hp = VALUES(max_hp),
665
+ current_hp = VALUES(current_hp),
666
+ started_at = VALUES(started_at),
667
+ ends_at = VALUES(ends_at),
668
+ updated_at = CURRENT_TIMESTAMP`,
669
+ [chatJid, createdByJid, biomeKey, JSON.stringify(bossSnapshot || {}), maxHp, currentHp, startedAt, endsAt],
670
+ connection,
671
+ );
672
+ };
673
+
674
+ export const deleteRaidStateByChat = async (chatJid, connection = null) => {
675
+ await executeQuery(
676
+ `DELETE FROM ${TABLES.RPG_RAID_STATE}
677
+ WHERE chat_jid = ?`,
678
+ [chatJid],
679
+ connection,
680
+ );
681
+ };
682
+
683
+ export const deleteExpiredRaidStates = async (connection = null) => {
684
+ const result = await executeQuery(
685
+ `DELETE FROM ${TABLES.RPG_RAID_STATE}
686
+ WHERE ends_at <= UTC_TIMESTAMP()`,
687
+ [],
688
+ connection,
689
+ );
690
+ return Number(result?.affectedRows || 0);
691
+ };
692
+
693
+ export const upsertRaidParticipant = async ({ chatJid, ownerJid }, connection = null) => {
694
+ await executeQuery(
695
+ `INSERT INTO ${TABLES.RPG_RAID_PARTICIPANT} (chat_jid, owner_jid, total_damage)
696
+ VALUES (?, ?, 0)
697
+ ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP`,
698
+ [chatJid, ownerJid],
699
+ connection,
700
+ );
701
+ };
702
+
703
+ export const addRaidParticipantDamage = async ({ chatJid, ownerJid, damage }, connection = null) => {
704
+ await executeQuery(
705
+ `INSERT INTO ${TABLES.RPG_RAID_PARTICIPANT} (chat_jid, owner_jid, total_damage)
706
+ VALUES (?, ?, ?)
707
+ ON DUPLICATE KEY UPDATE total_damage = total_damage + VALUES(total_damage), updated_at = CURRENT_TIMESTAMP`,
708
+ [chatJid, ownerJid, Math.max(0, Number(damage) || 0)],
709
+ connection,
710
+ );
711
+ };
712
+
713
+ export const listRaidParticipants = async (chatJid, connection = null) => {
714
+ const rows = await executeQuery(
715
+ `SELECT chat_jid, owner_jid, total_damage, joined_at, updated_at
716
+ FROM ${TABLES.RPG_RAID_PARTICIPANT}
717
+ WHERE chat_jid = ?
718
+ ORDER BY total_damage DESC, joined_at ASC`,
719
+ [chatJid],
720
+ connection,
721
+ );
722
+
723
+ return (rows || []).map((row) => ({
724
+ ...row,
725
+ total_damage: Number(row.total_damage || 0),
726
+ }));
727
+ };
728
+
729
+ export const getRaidParticipant = async (chatJid, ownerJid, connection = null) => {
730
+ const rows = await executeQuery(
731
+ `SELECT chat_jid, owner_jid, total_damage, joined_at, updated_at
732
+ FROM ${TABLES.RPG_RAID_PARTICIPANT}
733
+ WHERE chat_jid = ?
734
+ AND owner_jid = ?
735
+ LIMIT 1`,
736
+ [chatJid, ownerJid],
737
+ connection,
738
+ );
739
+
740
+ const row = rows?.[0] || null;
741
+ if (!row) return null;
742
+ return {
743
+ ...row,
744
+ total_damage: Number(row.total_damage || 0),
745
+ };
746
+ };
747
+
748
+ export const deleteRaidParticipantsByChat = async (chatJid, connection = null) => {
749
+ await executeQuery(
750
+ `DELETE FROM ${TABLES.RPG_RAID_PARTICIPANT}
751
+ WHERE chat_jid = ?`,
752
+ [chatJid],
753
+ connection,
754
+ );
755
+ };
756
+
757
+ export const createPvpChallenge = async ({ chatJid = null, challengerJid, opponentJid, status = 'pending', turnJid = null, winnerJid = null, battleSnapshot, startedAt = null, expiresAt }, connection = null) => {
758
+ const result = await executeQuery(
759
+ `INSERT INTO ${TABLES.RPG_PVP_CHALLENGE}
760
+ (chat_jid, challenger_jid, opponent_jid, status, turn_jid, winner_jid, battle_snapshot_json, started_at, expires_at)
761
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
762
+ [chatJid, challengerJid, opponentJid, status, turnJid, winnerJid, JSON.stringify(battleSnapshot || {}), startedAt, expiresAt],
763
+ connection,
764
+ );
765
+
766
+ const insertId = Number(result?.insertId || 0);
767
+ if (!insertId) return null;
768
+ return getPvpChallengeById(insertId, connection);
769
+ };
770
+
771
+ export const getPvpChallengeById = async (challengeId, connection = null) => {
772
+ const rows = await executeQuery(
773
+ `SELECT id, chat_jid, challenger_jid, opponent_jid, status, turn_jid, winner_jid, battle_snapshot_json, started_at, expires_at, created_at, updated_at
774
+ FROM ${TABLES.RPG_PVP_CHALLENGE}
775
+ WHERE id = ?
776
+ LIMIT 1`,
777
+ [challengeId],
778
+ connection,
779
+ );
780
+
781
+ return normalizePvpChallenge(rows?.[0] || null);
782
+ };
783
+
784
+ export const getPvpChallengeByIdForUpdate = async (challengeId, connection) => {
785
+ const rows = await executeQuery(
786
+ `SELECT id, chat_jid, challenger_jid, opponent_jid, status, turn_jid, winner_jid, battle_snapshot_json, started_at, expires_at, created_at, updated_at
787
+ FROM ${TABLES.RPG_PVP_CHALLENGE}
788
+ WHERE id = ?
789
+ LIMIT 1
790
+ FOR UPDATE`,
791
+ [challengeId],
792
+ connection,
793
+ );
794
+
795
+ return normalizePvpChallenge(rows?.[0] || null);
796
+ };
797
+
798
+ export const listOpenPvpChallengesByPlayer = async (ownerJid, connection = null) => {
799
+ const rows = await executeQuery(
800
+ `SELECT id, chat_jid, challenger_jid, opponent_jid, status, turn_jid, winner_jid, battle_snapshot_json, started_at, expires_at, created_at, updated_at
801
+ FROM ${TABLES.RPG_PVP_CHALLENGE}
802
+ WHERE (challenger_jid = ? OR opponent_jid = ?)
803
+ AND status IN ('pending', 'active')
804
+ AND expires_at > UTC_TIMESTAMP()
805
+ ORDER BY id DESC`,
806
+ [ownerJid, ownerJid],
807
+ connection,
808
+ );
809
+
810
+ return (rows || []).map(normalizePvpChallenge);
811
+ };
812
+
813
+ export const getActivePvpChallengeByPlayerForUpdate = async (ownerJid, connection) => {
814
+ const rows = await executeQuery(
815
+ `SELECT id, chat_jid, challenger_jid, opponent_jid, status, turn_jid, winner_jid, battle_snapshot_json, started_at, expires_at, created_at, updated_at
816
+ FROM ${TABLES.RPG_PVP_CHALLENGE}
817
+ WHERE (challenger_jid = ? OR opponent_jid = ?)
818
+ AND status = 'active'
819
+ AND expires_at > UTC_TIMESTAMP()
820
+ ORDER BY id DESC
821
+ LIMIT 1
822
+ FOR UPDATE`,
823
+ [ownerJid, ownerJid],
824
+ connection,
825
+ );
826
+
827
+ return normalizePvpChallenge(rows?.[0] || null);
828
+ };
829
+
830
+ export const updatePvpChallengeState = async ({ id, status, turnJid, winnerJid, battleSnapshot, startedAt, expiresAt }, connection = null) => {
831
+ const fields = ['updated_at = CURRENT_TIMESTAMP'];
832
+ const params = [];
833
+
834
+ if (status !== undefined) {
835
+ fields.push('status = ?');
836
+ params.push(status);
837
+ }
838
+ if (turnJid !== undefined) {
839
+ fields.push('turn_jid = ?');
840
+ params.push(turnJid ?? null);
841
+ }
842
+ if (winnerJid !== undefined) {
843
+ fields.push('winner_jid = ?');
844
+ params.push(winnerJid ?? null);
845
+ }
846
+ if (battleSnapshot !== undefined) {
847
+ fields.push('battle_snapshot_json = ?');
848
+ params.push(JSON.stringify(battleSnapshot || {}));
849
+ }
850
+ if (startedAt !== undefined) {
851
+ fields.push('started_at = ?');
852
+ params.push(startedAt ?? null);
853
+ }
854
+ if (expiresAt !== undefined) {
855
+ fields.push('expires_at = ?');
856
+ params.push(expiresAt);
857
+ }
858
+
859
+ params.push(id);
860
+ await executeQuery(
861
+ `UPDATE ${TABLES.RPG_PVP_CHALLENGE}
862
+ SET ${fields.join(', ')}
863
+ WHERE id = ?`,
864
+ params,
865
+ connection,
866
+ );
867
+ };
868
+
869
+ export const expireOldPvpChallenges = async (connection = null) => {
870
+ const result = await executeQuery(
871
+ `UPDATE ${TABLES.RPG_PVP_CHALLENGE}
872
+ SET status = 'expired',
873
+ updated_at = CURRENT_TIMESTAMP
874
+ WHERE status IN ('pending', 'active')
875
+ AND expires_at <= UTC_TIMESTAMP()`,
876
+ [],
877
+ connection,
878
+ );
879
+ return Number(result?.affectedRows || 0);
880
+ };
881
+
882
+ const normalizePvpQueueRow = (row) => {
883
+ if (!row) return null;
884
+ return {
885
+ ...row,
886
+ id: Number(row.id || 0),
887
+ matched_challenge_id: row.matched_challenge_id ? Number(row.matched_challenge_id) : null,
888
+ };
889
+ };
890
+
891
+ const normalizeTradeOfferRow = (row) => {
892
+ if (!row) return null;
893
+ return {
894
+ ...row,
895
+ id: Number(row.id || 0),
896
+ proposer_offer_json: parseJson(row.proposer_offer_json, {}),
897
+ receiver_offer_json: parseJson(row.receiver_offer_json, {}),
898
+ };
899
+ };
900
+
901
+ const normalizeSocialLinkRow = (row) => {
902
+ if (!row) return null;
903
+ return {
904
+ ...row,
905
+ friendship_score: Number(row.friendship_score || 0),
906
+ rivalry_score: Number(row.rivalry_score || 0),
907
+ interactions_count: Number(row.interactions_count || 0),
908
+ };
909
+ };
910
+
911
+ const normalizeWeeklyStatsRow = (row) => {
912
+ if (!row) return null;
913
+ return {
914
+ ...row,
915
+ matches_played: Number(row.matches_played || 0),
916
+ wins: Number(row.wins || 0),
917
+ losses: Number(row.losses || 0),
918
+ points: Number(row.points || 0),
919
+ };
920
+ };
921
+
922
+ const normalizeCoopWeeklyRow = (row) => {
923
+ if (!row) return null;
924
+ return {
925
+ ...row,
926
+ capture_target: Number(row.capture_target || 0),
927
+ raid_target: Number(row.raid_target || 0),
928
+ capture_progress: Number(row.capture_progress || 0),
929
+ raid_progress: Number(row.raid_progress || 0),
930
+ };
931
+ };
932
+
933
+ const normalizeCoopMemberRow = (row) => {
934
+ if (!row) return null;
935
+ return {
936
+ ...row,
937
+ capture_contribution: Number(row.capture_contribution || 0),
938
+ raid_contribution: Number(row.raid_contribution || 0),
939
+ };
940
+ };
941
+
942
+ const normalizeGroupEventRow = (row) => {
943
+ if (!row) return null;
944
+ return {
945
+ ...row,
946
+ target_value: Number(row.target_value || 0),
947
+ progress_value: Number(row.progress_value || 0),
948
+ };
949
+ };
950
+
951
+ const normalizeGroupEventMemberRow = (row) => {
952
+ if (!row) return null;
953
+ return {
954
+ ...row,
955
+ contribution: Number(row.contribution || 0),
956
+ };
957
+ };
958
+
959
+ const normalizeKarmaProfileRow = (row) => {
960
+ if (!row) return null;
961
+ return {
962
+ ...row,
963
+ karma_score: Number(row.karma_score || 0),
964
+ positive_votes: Number(row.positive_votes || 0),
965
+ negative_votes: Number(row.negative_votes || 0),
966
+ };
967
+ };
968
+
969
+ const normalizeSocialXpDailyRow = (row) => {
970
+ if (!row) return null;
971
+ return {
972
+ ...row,
973
+ earned_xp: Number(row.earned_xp || 0),
974
+ converted_xp: Number(row.converted_xp || 0),
975
+ cap_hits: Number(row.cap_hits || 0),
976
+ };
977
+ };
978
+
979
+ const buildPairUsers = (jidA, jidB) => {
980
+ const a = String(jidA || '').trim();
981
+ const b = String(jidB || '').trim();
982
+ if (!a || !b || a === b) return null;
983
+ return a < b ? [a, b] : [b, a];
984
+ };
985
+
986
+ const buildPairKey = (jidA, jidB) => {
987
+ const pair = buildPairUsers(jidA, jidB);
988
+ if (!pair) return null;
989
+ return `${pair[0]}::${pair[1]}`;
990
+ };
991
+
992
+ export const expirePvpQueue = async (connection = null) => {
993
+ const result = await executeQuery(
994
+ `UPDATE ${TABLES.RPG_PVP_QUEUE}
995
+ SET status = 'expired',
996
+ updated_at = CURRENT_TIMESTAMP
997
+ WHERE status = 'queued'
998
+ AND expires_at <= UTC_TIMESTAMP()`,
999
+ [],
1000
+ connection,
1001
+ );
1002
+ return Number(result?.affectedRows || 0);
1003
+ };
1004
+
1005
+ export const enqueuePvpQueue = async ({ chatJid, ownerJid, expiresAt }, connection = null) => {
1006
+ await executeQuery(
1007
+ `INSERT INTO ${TABLES.RPG_PVP_QUEUE} (chat_jid, owner_jid, status, matched_challenge_id, expires_at)
1008
+ VALUES (?, ?, 'queued', NULL, ?)
1009
+ ON DUPLICATE KEY UPDATE
1010
+ expires_at = VALUES(expires_at),
1011
+ matched_challenge_id = NULL,
1012
+ updated_at = CURRENT_TIMESTAMP`,
1013
+ [chatJid, ownerJid, expiresAt],
1014
+ connection,
1015
+ );
1016
+ };
1017
+
1018
+ export const getQueuedPvpByOwnerForUpdate = async (chatJid, ownerJid, connection) => {
1019
+ const rows = await executeQuery(
1020
+ `SELECT id, chat_jid, owner_jid, status, matched_challenge_id, expires_at, created_at, updated_at
1021
+ FROM ${TABLES.RPG_PVP_QUEUE}
1022
+ WHERE chat_jid = ?
1023
+ AND owner_jid = ?
1024
+ AND status = 'queued'
1025
+ LIMIT 1
1026
+ FOR UPDATE`,
1027
+ [chatJid, ownerJid],
1028
+ connection,
1029
+ );
1030
+ return normalizePvpQueueRow(rows?.[0] || null);
1031
+ };
1032
+
1033
+ export const listQueuedPvpByChatForUpdate = async (chatJid, limit = 10, connection = null) => {
1034
+ const safeLimit = Math.max(1, Math.min(50, Number(limit) || 10));
1035
+ const rows = await executeQuery(
1036
+ `SELECT id, chat_jid, owner_jid, status, matched_challenge_id, expires_at, created_at, updated_at
1037
+ FROM ${TABLES.RPG_PVP_QUEUE}
1038
+ WHERE chat_jid = ?
1039
+ AND status = 'queued'
1040
+ AND expires_at > UTC_TIMESTAMP()
1041
+ ORDER BY created_at ASC
1042
+ LIMIT ${safeLimit}
1043
+ FOR UPDATE`,
1044
+ [chatJid],
1045
+ connection,
1046
+ );
1047
+ return (rows || []).map(normalizePvpQueueRow);
1048
+ };
1049
+
1050
+ export const listQueuedPvpByChat = async (chatJid, limit = 10, connection = null) => {
1051
+ const safeLimit = Math.max(1, Math.min(50, Number(limit) || 10));
1052
+ const rows = await executeQuery(
1053
+ `SELECT id, chat_jid, owner_jid, status, matched_challenge_id, expires_at, created_at, updated_at
1054
+ FROM ${TABLES.RPG_PVP_QUEUE}
1055
+ WHERE chat_jid = ?
1056
+ AND status = 'queued'
1057
+ AND expires_at > UTC_TIMESTAMP()
1058
+ ORDER BY created_at ASC
1059
+ LIMIT ${safeLimit}`,
1060
+ [chatJid],
1061
+ connection,
1062
+ );
1063
+ return (rows || []).map(normalizePvpQueueRow);
1064
+ };
1065
+
1066
+ export const cancelQueuedPvpByOwner = async (chatJid, ownerJid, connection = null) => {
1067
+ const result = await executeQuery(
1068
+ `UPDATE ${TABLES.RPG_PVP_QUEUE}
1069
+ SET status = 'cancelled',
1070
+ updated_at = CURRENT_TIMESTAMP
1071
+ WHERE chat_jid = ?
1072
+ AND owner_jid = ?
1073
+ AND status = 'queued'`,
1074
+ [chatJid, ownerJid],
1075
+ connection,
1076
+ );
1077
+ return Number(result?.affectedRows || 0);
1078
+ };
1079
+
1080
+ export const markPvpQueueMatchedByIds = async (queueIds = [], challengeId = null, connection = null) => {
1081
+ const ids = (queueIds || []).map((value) => Number(value)).filter((value) => Number.isFinite(value) && value > 0);
1082
+ if (!ids.length) return 0;
1083
+ const placeholders = ids.map(() => '?').join(', ');
1084
+ const result = await executeQuery(
1085
+ `UPDATE ${TABLES.RPG_PVP_QUEUE}
1086
+ SET status = 'matched',
1087
+ matched_challenge_id = ?,
1088
+ updated_at = CURRENT_TIMESTAMP
1089
+ WHERE id IN (${placeholders})`,
1090
+ [challengeId, ...ids],
1091
+ connection,
1092
+ );
1093
+ return Number(result?.affectedRows || 0);
1094
+ };
1095
+
1096
+ export const getLatestFinishedPvpByPlayer = async (ownerJid, connection = null) => {
1097
+ const rows = await executeQuery(
1098
+ `SELECT id, chat_jid, challenger_jid, opponent_jid, status, turn_jid, winner_jid, battle_snapshot_json, started_at, expires_at, created_at, updated_at
1099
+ FROM ${TABLES.RPG_PVP_CHALLENGE}
1100
+ WHERE status = 'finished'
1101
+ AND (challenger_jid = ? OR opponent_jid = ?)
1102
+ ORDER BY updated_at DESC, id DESC
1103
+ LIMIT 1`,
1104
+ [ownerJid, ownerJid],
1105
+ connection,
1106
+ );
1107
+ return normalizePvpChallenge(rows?.[0] || null);
1108
+ };
1109
+
1110
+ export const listRecentFinishedPvpByPlayer = async (ownerJid, limit = 20, connection = null) => {
1111
+ const safeLimit = Math.max(1, Math.min(100, Number(limit) || 20));
1112
+ const rows = await executeQuery(
1113
+ `SELECT id, chat_jid, challenger_jid, opponent_jid, status, turn_jid, winner_jid, battle_snapshot_json, started_at, expires_at, created_at, updated_at
1114
+ FROM ${TABLES.RPG_PVP_CHALLENGE}
1115
+ WHERE status = 'finished'
1116
+ AND (challenger_jid = ? OR opponent_jid = ?)
1117
+ ORDER BY updated_at DESC, id DESC
1118
+ LIMIT ${safeLimit}`,
1119
+ [ownerJid, ownerJid],
1120
+ connection,
1121
+ );
1122
+ return (rows || []).map(normalizePvpChallenge);
1123
+ };
1124
+
1125
+ export const upsertPvpWeeklyStatsDelta = async ({ weekRefDate, ownerJid, matchesPlayedDelta = 0, winsDelta = 0, lossesDelta = 0, pointsDelta = 0 }, connection = null) => {
1126
+ await executeQuery(
1127
+ `INSERT INTO ${TABLES.RPG_PVP_WEEKLY_STATS}
1128
+ (week_ref_date, owner_jid, matches_played, wins, losses, points)
1129
+ VALUES (?, ?, ?, ?, ?, ?)
1130
+ ON DUPLICATE KEY UPDATE
1131
+ matches_played = GREATEST(0, matches_played + VALUES(matches_played)),
1132
+ wins = GREATEST(0, wins + VALUES(wins)),
1133
+ losses = GREATEST(0, losses + VALUES(losses)),
1134
+ points = GREATEST(0, points + VALUES(points)),
1135
+ updated_at = CURRENT_TIMESTAMP`,
1136
+ [weekRefDate, ownerJid, matchesPlayedDelta, winsDelta, lossesDelta, pointsDelta],
1137
+ connection,
1138
+ );
1139
+ };
1140
+
1141
+ export const getPvpWeeklyStatsByOwner = async (weekRefDate, ownerJid, connection = null) => {
1142
+ const rows = await executeQuery(
1143
+ `SELECT week_ref_date, owner_jid, matches_played, wins, losses, points, created_at, updated_at
1144
+ FROM ${TABLES.RPG_PVP_WEEKLY_STATS}
1145
+ WHERE week_ref_date = ?
1146
+ AND owner_jid = ?
1147
+ LIMIT 1`,
1148
+ [weekRefDate, ownerJid],
1149
+ connection,
1150
+ );
1151
+ return normalizeWeeklyStatsRow(rows?.[0] || null);
1152
+ };
1153
+
1154
+ export const getPvpWeeklyRankByOwner = async (weekRefDate, ownerJid, connection = null) => {
1155
+ const stats = await getPvpWeeklyStatsByOwner(weekRefDate, ownerJid, connection);
1156
+ if (!stats) return null;
1157
+
1158
+ const rows = await executeQuery(
1159
+ `SELECT COUNT(*) + 1 AS rank_position
1160
+ FROM ${TABLES.RPG_PVP_WEEKLY_STATS}
1161
+ WHERE week_ref_date = ?
1162
+ AND (
1163
+ points > ?
1164
+ OR (points = ? AND wins > ?)
1165
+ OR (points = ? AND wins = ? AND matches_played > ?)
1166
+ OR (points = ? AND wins = ? AND matches_played = ? AND owner_jid < ?)
1167
+ )`,
1168
+ [
1169
+ weekRefDate,
1170
+ stats.points,
1171
+ stats.points,
1172
+ stats.wins,
1173
+ stats.points,
1174
+ stats.wins,
1175
+ stats.matches_played,
1176
+ stats.points,
1177
+ stats.wins,
1178
+ stats.matches_played,
1179
+ ownerJid,
1180
+ ],
1181
+ connection,
1182
+ );
1183
+
1184
+ const rank = Number(rows?.[0]?.rank_position || 0);
1185
+ return rank > 0 ? rank : null;
1186
+ };
1187
+
1188
+ export const getPvpLifetimeStatsByOwner = async (ownerJid, connection = null) => {
1189
+ const rows = await executeQuery(
1190
+ `SELECT
1191
+ COALESCE(SUM(matches_played), 0) AS matches_played,
1192
+ COALESCE(SUM(wins), 0) AS wins,
1193
+ COALESCE(SUM(losses), 0) AS losses,
1194
+ COALESCE(SUM(points), 0) AS points
1195
+ FROM ${TABLES.RPG_PVP_WEEKLY_STATS}
1196
+ WHERE owner_jid = ?`,
1197
+ [ownerJid],
1198
+ connection,
1199
+ );
1200
+
1201
+ const row = rows?.[0] || {};
1202
+ return {
1203
+ matches_played: Number(row.matches_played || 0),
1204
+ wins: Number(row.wins || 0),
1205
+ losses: Number(row.losses || 0),
1206
+ points: Number(row.points || 0),
1207
+ };
1208
+ };
1209
+
1210
+ export const listPvpWeeklyRanking = async (weekRefDate, limit = 10, connection = null) => {
1211
+ const safeLimit = Math.max(1, Math.min(30, Number(limit) || 10));
1212
+ const rows = await executeQuery(
1213
+ `SELECT week_ref_date, owner_jid, matches_played, wins, losses, points, created_at, updated_at
1214
+ FROM ${TABLES.RPG_PVP_WEEKLY_STATS}
1215
+ WHERE week_ref_date = ?
1216
+ ORDER BY points DESC, wins DESC, matches_played DESC, owner_jid ASC
1217
+ LIMIT ${safeLimit}`,
1218
+ [weekRefDate],
1219
+ connection,
1220
+ );
1221
+ return (rows || []).map(normalizeWeeklyStatsRow);
1222
+ };
1223
+
1224
+ export const getSocialLinkByUsers = async (jidA, jidB, connection = null) => {
1225
+ const pair = buildPairUsers(jidA, jidB);
1226
+ if (!pair) return null;
1227
+ const pairKey = buildPairKey(pair[0], pair[1]);
1228
+ const rows = await executeQuery(
1229
+ `SELECT pair_key, user_a_jid, user_b_jid, friendship_score, rivalry_score, interactions_count, last_interaction_at, created_at, updated_at
1230
+ FROM ${TABLES.RPG_SOCIAL_LINK}
1231
+ WHERE pair_key = ?
1232
+ LIMIT 1`,
1233
+ [pairKey],
1234
+ connection,
1235
+ );
1236
+ return normalizeSocialLinkRow(rows?.[0] || null);
1237
+ };
1238
+
1239
+ export const upsertSocialLinkDelta = async ({ jidA, jidB, friendshipDelta = 0, rivalryDelta = 0, interactionsDelta = 1 }, connection = null) => {
1240
+ const pair = buildPairUsers(jidA, jidB);
1241
+ if (!pair) return;
1242
+ const pairKey = buildPairKey(pair[0], pair[1]);
1243
+ await executeQuery(
1244
+ `INSERT INTO ${TABLES.RPG_SOCIAL_LINK}
1245
+ (pair_key, user_a_jid, user_b_jid, friendship_score, rivalry_score, interactions_count, last_interaction_at)
1246
+ VALUES (?, ?, ?, ?, ?, ?, UTC_TIMESTAMP())
1247
+ ON DUPLICATE KEY UPDATE
1248
+ friendship_score = friendship_score + VALUES(friendship_score),
1249
+ rivalry_score = rivalry_score + VALUES(rivalry_score),
1250
+ interactions_count = interactions_count + VALUES(interactions_count),
1251
+ last_interaction_at = UTC_TIMESTAMP(),
1252
+ updated_at = CURRENT_TIMESTAMP`,
1253
+ [pairKey, pair[0], pair[1], friendshipDelta, rivalryDelta, interactionsDelta],
1254
+ connection,
1255
+ );
1256
+ };
1257
+
1258
+ export const listSocialLinksByOwner = async (ownerJid, mode = 'friendship', limit = 10, connection = null) => {
1259
+ const safeLimit = Math.max(1, Math.min(30, Number(limit) || 10));
1260
+ const orderField = mode === 'rivalry' ? 'rivalry_score' : 'friendship_score';
1261
+ const rows = await executeQuery(
1262
+ `SELECT pair_key, user_a_jid, user_b_jid, friendship_score, rivalry_score, interactions_count, last_interaction_at, created_at, updated_at
1263
+ FROM ${TABLES.RPG_SOCIAL_LINK}
1264
+ WHERE user_a_jid = ?
1265
+ OR user_b_jid = ?
1266
+ ORDER BY ${orderField} DESC, interactions_count DESC, updated_at DESC
1267
+ LIMIT ${safeLimit}`,
1268
+ [ownerJid, ownerJid],
1269
+ connection,
1270
+ );
1271
+ return (rows || []).map(normalizeSocialLinkRow);
1272
+ };
1273
+
1274
+ export const getSocialSummaryByOwner = async (ownerJid, connection = null) => {
1275
+ const rows = await executeQuery(
1276
+ `SELECT
1277
+ COUNT(*) AS links_total,
1278
+ COALESCE(SUM(interactions_count), 0) AS interactions_total,
1279
+ COALESCE(MAX(friendship_score), 0) AS top_friendship,
1280
+ COALESCE(MAX(rivalry_score), 0) AS top_rivalry
1281
+ FROM ${TABLES.RPG_SOCIAL_LINK}
1282
+ WHERE user_a_jid = ?
1283
+ OR user_b_jid = ?`,
1284
+ [ownerJid, ownerJid],
1285
+ connection,
1286
+ );
1287
+
1288
+ const row = rows?.[0] || {};
1289
+ return {
1290
+ linksTotal: Number(row.links_total || 0),
1291
+ interactionsTotal: Number(row.interactions_total || 0),
1292
+ topFriendship: Number(row.top_friendship || 0),
1293
+ topRivalry: Number(row.top_rivalry || 0),
1294
+ };
1295
+ };
1296
+
1297
+ export const createTradeOffer = async ({ chatJid = null, proposerJid, receiverJid, proposerOffer, receiverOffer, expiresAt }, connection = null) => {
1298
+ const result = await executeQuery(
1299
+ `INSERT INTO ${TABLES.RPG_TRADE_OFFER}
1300
+ (chat_jid, proposer_jid, receiver_jid, proposer_offer_json, receiver_offer_json, status, accepted_at, expires_at)
1301
+ VALUES (?, ?, ?, ?, ?, 'pending', NULL, ?)`,
1302
+ [chatJid, proposerJid, receiverJid, JSON.stringify(proposerOffer || {}), JSON.stringify(receiverOffer || {}), expiresAt],
1303
+ connection,
1304
+ );
1305
+ const id = Number(result?.insertId || 0);
1306
+ if (!id) return null;
1307
+ return getTradeOfferById(id, connection);
1308
+ };
1309
+
1310
+ export const getTradeOfferById = async (offerId, connection = null) => {
1311
+ const rows = await executeQuery(
1312
+ `SELECT id, chat_jid, proposer_jid, receiver_jid, proposer_offer_json, receiver_offer_json, status, accepted_at, expires_at, created_at, updated_at
1313
+ FROM ${TABLES.RPG_TRADE_OFFER}
1314
+ WHERE id = ?
1315
+ LIMIT 1`,
1316
+ [offerId],
1317
+ connection,
1318
+ );
1319
+ return normalizeTradeOfferRow(rows?.[0] || null);
1320
+ };
1321
+
1322
+ export const getTradeOfferByIdForUpdate = async (offerId, connection) => {
1323
+ const rows = await executeQuery(
1324
+ `SELECT id, chat_jid, proposer_jid, receiver_jid, proposer_offer_json, receiver_offer_json, status, accepted_at, expires_at, created_at, updated_at
1325
+ FROM ${TABLES.RPG_TRADE_OFFER}
1326
+ WHERE id = ?
1327
+ LIMIT 1
1328
+ FOR UPDATE`,
1329
+ [offerId],
1330
+ connection,
1331
+ );
1332
+ return normalizeTradeOfferRow(rows?.[0] || null);
1333
+ };
1334
+
1335
+ export const listOpenTradeOffersByUser = async (ownerJid, connection = null) => {
1336
+ const rows = await executeQuery(
1337
+ `SELECT id, chat_jid, proposer_jid, receiver_jid, proposer_offer_json, receiver_offer_json, status, accepted_at, expires_at, created_at, updated_at
1338
+ FROM ${TABLES.RPG_TRADE_OFFER}
1339
+ WHERE status = 'pending'
1340
+ AND expires_at > UTC_TIMESTAMP()
1341
+ AND (proposer_jid = ? OR receiver_jid = ?)
1342
+ ORDER BY id DESC
1343
+ LIMIT 30`,
1344
+ [ownerJid, ownerJid],
1345
+ connection,
1346
+ );
1347
+ return (rows || []).map(normalizeTradeOfferRow);
1348
+ };
1349
+
1350
+ export const updateTradeOfferState = async ({ id, status, acceptedAt = undefined }, connection = null) => {
1351
+ const fields = ['status = ?', 'updated_at = CURRENT_TIMESTAMP'];
1352
+ const params = [status];
1353
+ if (acceptedAt !== undefined) {
1354
+ fields.push('accepted_at = ?');
1355
+ params.push(acceptedAt);
1356
+ }
1357
+ params.push(id);
1358
+ await executeQuery(
1359
+ `UPDATE ${TABLES.RPG_TRADE_OFFER}
1360
+ SET ${fields.join(', ')}
1361
+ WHERE id = ?`,
1362
+ params,
1363
+ connection,
1364
+ );
1365
+ };
1366
+
1367
+ export const expireOldTradeOffers = async (connection = null) => {
1368
+ const result = await executeQuery(
1369
+ `UPDATE ${TABLES.RPG_TRADE_OFFER}
1370
+ SET status = 'expired',
1371
+ updated_at = CURRENT_TIMESTAMP
1372
+ WHERE status = 'pending'
1373
+ AND expires_at <= UTC_TIMESTAMP()`,
1374
+ [],
1375
+ connection,
1376
+ );
1377
+ return Number(result?.affectedRows || 0);
1378
+ };
1379
+
1380
+ export const countPlayerPokemons = async (ownerJid, connection = null) => {
1381
+ const rows = await executeQuery(
1382
+ `SELECT COUNT(*) AS total
1383
+ FROM ${TABLES.RPG_PLAYER_POKEMON}
1384
+ WHERE owner_jid = ?`,
1385
+ [ownerJid],
1386
+ connection,
1387
+ );
1388
+ return Number(rows?.[0]?.total || 0);
1389
+ };
1390
+
1391
+ export const transferPlayerPokemon = async ({ pokemonId, fromOwnerJid, toOwnerJid }, connection = null) => {
1392
+ const result = await executeQuery(
1393
+ `UPDATE ${TABLES.RPG_PLAYER_POKEMON}
1394
+ SET owner_jid = ?,
1395
+ is_active = 0
1396
+ WHERE id = ?
1397
+ AND owner_jid = ?`,
1398
+ [toOwnerJid, pokemonId, fromOwnerJid],
1399
+ connection,
1400
+ );
1401
+ return Number(result?.affectedRows || 0) > 0;
1402
+ };
1403
+
1404
+ export const getFirstPlayerPokemon = async (ownerJid, connection = null) => {
1405
+ const rows = await executeQuery(
1406
+ `SELECT ${PLAYER_POKEMON_COLUMNS}
1407
+ FROM ${TABLES.RPG_PLAYER_POKEMON}
1408
+ WHERE owner_jid = ?
1409
+ ORDER BY id ASC
1410
+ LIMIT 1`,
1411
+ [ownerJid],
1412
+ connection,
1413
+ );
1414
+ return normalizePlayerPokemon(rows?.[0] || null);
1415
+ };
1416
+
1417
+ export const getGroupCoopWeekly = async (chatJid, weekRefDate, connection = null) => {
1418
+ const rows = await executeQuery(
1419
+ `SELECT chat_jid, week_ref_date, capture_target, raid_target, capture_progress, raid_progress, status, completed_at, created_at, updated_at
1420
+ FROM ${TABLES.RPG_GROUP_COOP_WEEKLY}
1421
+ WHERE chat_jid = ?
1422
+ AND week_ref_date = ?
1423
+ LIMIT 1`,
1424
+ [chatJid, weekRefDate],
1425
+ connection,
1426
+ );
1427
+ return normalizeCoopWeeklyRow(rows?.[0] || null);
1428
+ };
1429
+
1430
+ export const getGroupCoopWeeklyForUpdate = async (chatJid, weekRefDate, connection) => {
1431
+ const rows = await executeQuery(
1432
+ `SELECT chat_jid, week_ref_date, capture_target, raid_target, capture_progress, raid_progress, status, completed_at, created_at, updated_at
1433
+ FROM ${TABLES.RPG_GROUP_COOP_WEEKLY}
1434
+ WHERE chat_jid = ?
1435
+ AND week_ref_date = ?
1436
+ LIMIT 1
1437
+ FOR UPDATE`,
1438
+ [chatJid, weekRefDate],
1439
+ connection,
1440
+ );
1441
+ return normalizeCoopWeeklyRow(rows?.[0] || null);
1442
+ };
1443
+
1444
+ export const upsertGroupCoopWeekly = async ({ chatJid, weekRefDate, captureTarget = 20, raidTarget = 3 }, connection = null) => {
1445
+ await executeQuery(
1446
+ `INSERT INTO ${TABLES.RPG_GROUP_COOP_WEEKLY}
1447
+ (chat_jid, week_ref_date, capture_target, raid_target, capture_progress, raid_progress, status, completed_at)
1448
+ VALUES (?, ?, ?, ?, 0, 0, 'active', NULL)
1449
+ ON DUPLICATE KEY UPDATE
1450
+ capture_target = capture_target,
1451
+ raid_target = raid_target,
1452
+ updated_at = CURRENT_TIMESTAMP`,
1453
+ [chatJid, weekRefDate, captureTarget, raidTarget],
1454
+ connection,
1455
+ );
1456
+ };
1457
+
1458
+ export const addGroupCoopContribution = async ({ chatJid, weekRefDate, ownerJid, captureDelta = 0, raidDelta = 0 }, connection = null) => {
1459
+ await executeQuery(
1460
+ `UPDATE ${TABLES.RPG_GROUP_COOP_WEEKLY}
1461
+ SET capture_progress = capture_progress + ?,
1462
+ raid_progress = raid_progress + ?,
1463
+ updated_at = CURRENT_TIMESTAMP
1464
+ WHERE chat_jid = ?
1465
+ AND week_ref_date = ?`,
1466
+ [captureDelta, raidDelta, chatJid, weekRefDate],
1467
+ connection,
1468
+ );
1469
+
1470
+ await executeQuery(
1471
+ `INSERT INTO ${TABLES.RPG_GROUP_COOP_MEMBER}
1472
+ (chat_jid, week_ref_date, owner_jid, capture_contribution, raid_contribution, reward_claimed_at, last_contribution_at)
1473
+ VALUES (?, ?, ?, ?, ?, NULL, UTC_TIMESTAMP())
1474
+ ON DUPLICATE KEY UPDATE
1475
+ capture_contribution = capture_contribution + VALUES(capture_contribution),
1476
+ raid_contribution = raid_contribution + VALUES(raid_contribution),
1477
+ last_contribution_at = UTC_TIMESTAMP(),
1478
+ updated_at = CURRENT_TIMESTAMP`,
1479
+ [chatJid, weekRefDate, ownerJid, captureDelta, raidDelta],
1480
+ connection,
1481
+ );
1482
+ };
1483
+
1484
+ export const listGroupCoopMembers = async (chatJid, weekRefDate, connection = null) => {
1485
+ const rows = await executeQuery(
1486
+ `SELECT chat_jid, week_ref_date, owner_jid, capture_contribution, raid_contribution, reward_claimed_at, last_contribution_at, created_at, updated_at
1487
+ FROM ${TABLES.RPG_GROUP_COOP_MEMBER}
1488
+ WHERE chat_jid = ?
1489
+ AND week_ref_date = ?
1490
+ ORDER BY (capture_contribution + raid_contribution) DESC, updated_at DESC`,
1491
+ [chatJid, weekRefDate],
1492
+ connection,
1493
+ );
1494
+ return (rows || []).map(normalizeCoopMemberRow);
1495
+ };
1496
+
1497
+ export const getGroupCoopMember = async (chatJid, weekRefDate, ownerJid, connection = null) => {
1498
+ const rows = await executeQuery(
1499
+ `SELECT chat_jid, week_ref_date, owner_jid, capture_contribution, raid_contribution, reward_claimed_at, last_contribution_at, created_at, updated_at
1500
+ FROM ${TABLES.RPG_GROUP_COOP_MEMBER}
1501
+ WHERE chat_jid = ?
1502
+ AND week_ref_date = ?
1503
+ AND owner_jid = ?
1504
+ LIMIT 1`,
1505
+ [chatJid, weekRefDate, ownerJid],
1506
+ connection,
1507
+ );
1508
+ return normalizeCoopMemberRow(rows?.[0] || null);
1509
+ };
1510
+
1511
+ export const listUnrewardedGroupCoopMembersForUpdate = async (chatJid, weekRefDate, connection) => {
1512
+ const rows = await executeQuery(
1513
+ `SELECT chat_jid, week_ref_date, owner_jid, capture_contribution, raid_contribution, reward_claimed_at, last_contribution_at, created_at, updated_at
1514
+ FROM ${TABLES.RPG_GROUP_COOP_MEMBER}
1515
+ WHERE chat_jid = ?
1516
+ AND week_ref_date = ?
1517
+ AND reward_claimed_at IS NULL
1518
+ FOR UPDATE`,
1519
+ [chatJid, weekRefDate],
1520
+ connection,
1521
+ );
1522
+ return (rows || []).map(normalizeCoopMemberRow);
1523
+ };
1524
+
1525
+ export const markGroupCoopCompleted = async (chatJid, weekRefDate, connection = null) => {
1526
+ await executeQuery(
1527
+ `UPDATE ${TABLES.RPG_GROUP_COOP_WEEKLY}
1528
+ SET status = 'completed',
1529
+ completed_at = COALESCE(completed_at, UTC_TIMESTAMP()),
1530
+ updated_at = CURRENT_TIMESTAMP
1531
+ WHERE chat_jid = ?
1532
+ AND week_ref_date = ?`,
1533
+ [chatJid, weekRefDate],
1534
+ connection,
1535
+ );
1536
+ };
1537
+
1538
+ export const markGroupCoopMemberRewardClaimed = async (chatJid, weekRefDate, ownerJid, connection = null) => {
1539
+ await executeQuery(
1540
+ `UPDATE ${TABLES.RPG_GROUP_COOP_MEMBER}
1541
+ SET reward_claimed_at = COALESCE(reward_claimed_at, UTC_TIMESTAMP()),
1542
+ updated_at = CURRENT_TIMESTAMP
1543
+ WHERE chat_jid = ?
1544
+ AND week_ref_date = ?
1545
+ AND owner_jid = ?`,
1546
+ [chatJid, weekRefDate, ownerJid],
1547
+ connection,
1548
+ );
1549
+ };
1550
+
1551
+ export const getGroupEventWeekly = async (chatJid, weekRefDate, connection = null) => {
1552
+ const rows = await executeQuery(
1553
+ `SELECT chat_jid, week_ref_date, event_key, target_value, progress_value, status, expires_at, completed_at, created_at, updated_at
1554
+ FROM ${TABLES.RPG_GROUP_EVENT_WEEKLY}
1555
+ WHERE chat_jid = ?
1556
+ AND week_ref_date = ?
1557
+ LIMIT 1`,
1558
+ [chatJid, weekRefDate],
1559
+ connection,
1560
+ );
1561
+ return normalizeGroupEventRow(rows?.[0] || null);
1562
+ };
1563
+
1564
+ export const getGroupEventWeeklyForUpdate = async (chatJid, weekRefDate, connection) => {
1565
+ const rows = await executeQuery(
1566
+ `SELECT chat_jid, week_ref_date, event_key, target_value, progress_value, status, expires_at, completed_at, created_at, updated_at
1567
+ FROM ${TABLES.RPG_GROUP_EVENT_WEEKLY}
1568
+ WHERE chat_jid = ?
1569
+ AND week_ref_date = ?
1570
+ LIMIT 1
1571
+ FOR UPDATE`,
1572
+ [chatJid, weekRefDate],
1573
+ connection,
1574
+ );
1575
+ return normalizeGroupEventRow(rows?.[0] || null);
1576
+ };
1577
+
1578
+ export const upsertGroupEventWeekly = async ({ chatJid, weekRefDate, eventKey, targetValue, expiresAt }, connection = null) => {
1579
+ await executeQuery(
1580
+ `INSERT INTO ${TABLES.RPG_GROUP_EVENT_WEEKLY}
1581
+ (chat_jid, week_ref_date, event_key, target_value, progress_value, status, expires_at, completed_at)
1582
+ VALUES (?, ?, ?, ?, 0, 'active', ?, NULL)
1583
+ ON DUPLICATE KEY UPDATE
1584
+ event_key = event_key,
1585
+ target_value = target_value,
1586
+ expires_at = expires_at,
1587
+ updated_at = CURRENT_TIMESTAMP`,
1588
+ [chatJid, weekRefDate, eventKey, targetValue, expiresAt],
1589
+ connection,
1590
+ );
1591
+ };
1592
+
1593
+ export const addGroupEventContribution = async ({ chatJid, weekRefDate, ownerJid, contributionDelta = 0 }, connection = null) => {
1594
+ await executeQuery(
1595
+ `UPDATE ${TABLES.RPG_GROUP_EVENT_WEEKLY}
1596
+ SET progress_value = progress_value + ?,
1597
+ updated_at = CURRENT_TIMESTAMP
1598
+ WHERE chat_jid = ?
1599
+ AND week_ref_date = ?`,
1600
+ [contributionDelta, chatJid, weekRefDate],
1601
+ connection,
1602
+ );
1603
+
1604
+ await executeQuery(
1605
+ `INSERT INTO ${TABLES.RPG_GROUP_EVENT_MEMBER}
1606
+ (chat_jid, week_ref_date, owner_jid, contribution, reward_claimed_at, last_contribution_at)
1607
+ VALUES (?, ?, ?, ?, NULL, UTC_TIMESTAMP())
1608
+ ON DUPLICATE KEY UPDATE
1609
+ contribution = contribution + VALUES(contribution),
1610
+ last_contribution_at = UTC_TIMESTAMP(),
1611
+ updated_at = CURRENT_TIMESTAMP`,
1612
+ [chatJid, weekRefDate, ownerJid, contributionDelta],
1613
+ connection,
1614
+ );
1615
+ };
1616
+
1617
+ export const listGroupEventMembers = async (chatJid, weekRefDate, limit = 10, connection = null) => {
1618
+ const safeLimit = Math.max(1, Math.min(50, Number(limit) || 10));
1619
+ const rows = await executeQuery(
1620
+ `SELECT chat_jid, week_ref_date, owner_jid, contribution, reward_claimed_at, last_contribution_at, created_at, updated_at
1621
+ FROM ${TABLES.RPG_GROUP_EVENT_MEMBER}
1622
+ WHERE chat_jid = ?
1623
+ AND week_ref_date = ?
1624
+ ORDER BY contribution DESC, updated_at DESC
1625
+ LIMIT ${safeLimit}`,
1626
+ [chatJid, weekRefDate],
1627
+ connection,
1628
+ );
1629
+ return (rows || []).map(normalizeGroupEventMemberRow);
1630
+ };
1631
+
1632
+ export const getGroupEventMember = async (chatJid, weekRefDate, ownerJid, connection = null) => {
1633
+ const rows = await executeQuery(
1634
+ `SELECT chat_jid, week_ref_date, owner_jid, contribution, reward_claimed_at, last_contribution_at, created_at, updated_at
1635
+ FROM ${TABLES.RPG_GROUP_EVENT_MEMBER}
1636
+ WHERE chat_jid = ?
1637
+ AND week_ref_date = ?
1638
+ AND owner_jid = ?
1639
+ LIMIT 1`,
1640
+ [chatJid, weekRefDate, ownerJid],
1641
+ connection,
1642
+ );
1643
+ return normalizeGroupEventMemberRow(rows?.[0] || null);
1644
+ };
1645
+
1646
+ export const getGroupEventMemberForUpdate = async (chatJid, weekRefDate, ownerJid, connection) => {
1647
+ const rows = await executeQuery(
1648
+ `SELECT chat_jid, week_ref_date, owner_jid, contribution, reward_claimed_at, last_contribution_at, created_at, updated_at
1649
+ FROM ${TABLES.RPG_GROUP_EVENT_MEMBER}
1650
+ WHERE chat_jid = ?
1651
+ AND week_ref_date = ?
1652
+ AND owner_jid = ?
1653
+ LIMIT 1
1654
+ FOR UPDATE`,
1655
+ [chatJid, weekRefDate, ownerJid],
1656
+ connection,
1657
+ );
1658
+ return normalizeGroupEventMemberRow(rows?.[0] || null);
1659
+ };
1660
+
1661
+ export const markGroupEventCompleted = async (chatJid, weekRefDate, connection = null) => {
1662
+ await executeQuery(
1663
+ `UPDATE ${TABLES.RPG_GROUP_EVENT_WEEKLY}
1664
+ SET status = 'completed',
1665
+ completed_at = COALESCE(completed_at, UTC_TIMESTAMP()),
1666
+ updated_at = CURRENT_TIMESTAMP
1667
+ WHERE chat_jid = ?
1668
+ AND week_ref_date = ?`,
1669
+ [chatJid, weekRefDate],
1670
+ connection,
1671
+ );
1672
+ };
1673
+
1674
+ export const markGroupEventMemberRewardClaimed = async (chatJid, weekRefDate, ownerJid, connection = null) => {
1675
+ await executeQuery(
1676
+ `UPDATE ${TABLES.RPG_GROUP_EVENT_MEMBER}
1677
+ SET reward_claimed_at = COALESCE(reward_claimed_at, UTC_TIMESTAMP()),
1678
+ updated_at = CURRENT_TIMESTAMP
1679
+ WHERE chat_jid = ?
1680
+ AND week_ref_date = ?
1681
+ AND owner_jid = ?`,
1682
+ [chatJid, weekRefDate, ownerJid],
1683
+ connection,
1684
+ );
1685
+ };
1686
+
1687
+ export const getKarmaProfile = async (ownerJid, connection = null) => {
1688
+ const rows = await executeQuery(
1689
+ `SELECT owner_jid, karma_score, positive_votes, negative_votes, created_at, updated_at
1690
+ FROM ${TABLES.RPG_KARMA_PROFILE}
1691
+ WHERE owner_jid = ?
1692
+ LIMIT 1`,
1693
+ [ownerJid],
1694
+ connection,
1695
+ );
1696
+ return normalizeKarmaProfileRow(rows?.[0] || null);
1697
+ };
1698
+
1699
+ export const getKarmaVoteByWeekForUpdate = async (weekRefDate, voterJid, targetJid, connection) => {
1700
+ const rows = await executeQuery(
1701
+ `SELECT id, week_ref_date, voter_jid, target_jid, vote_value, created_at
1702
+ FROM ${TABLES.RPG_KARMA_VOTE_HISTORY}
1703
+ WHERE week_ref_date = ?
1704
+ AND voter_jid = ?
1705
+ AND target_jid = ?
1706
+ LIMIT 1
1707
+ FOR UPDATE`,
1708
+ [weekRefDate, voterJid, targetJid],
1709
+ connection,
1710
+ );
1711
+ return rows?.[0] || null;
1712
+ };
1713
+
1714
+ export const createKarmaVote = async ({ weekRefDate, voterJid, targetJid, voteValue }, connection = null) => {
1715
+ const result = await executeQuery(
1716
+ `INSERT INTO ${TABLES.RPG_KARMA_VOTE_HISTORY}
1717
+ (week_ref_date, voter_jid, target_jid, vote_value)
1718
+ VALUES (?, ?, ?, ?)`,
1719
+ [weekRefDate, voterJid, targetJid, voteValue],
1720
+ connection,
1721
+ );
1722
+ return Number(result?.insertId || 0) > 0;
1723
+ };
1724
+
1725
+ export const applyKarmaDelta = async ({ ownerJid, karmaDelta = 0, positiveDelta = 0, negativeDelta = 0 }, connection = null) => {
1726
+ await executeQuery(
1727
+ `INSERT INTO ${TABLES.RPG_KARMA_PROFILE}
1728
+ (owner_jid, karma_score, positive_votes, negative_votes)
1729
+ VALUES (?, ?, ?, ?)
1730
+ ON DUPLICATE KEY UPDATE
1731
+ karma_score = karma_score + VALUES(karma_score),
1732
+ positive_votes = positive_votes + VALUES(positive_votes),
1733
+ negative_votes = negative_votes + VALUES(negative_votes),
1734
+ updated_at = CURRENT_TIMESTAMP`,
1735
+ [ownerJid, karmaDelta, positiveDelta, negativeDelta],
1736
+ connection,
1737
+ );
1738
+ };
1739
+
1740
+ export const listTopKarmaProfiles = async (limit = 10, connection = null) => {
1741
+ const safeLimit = Math.max(1, Math.min(30, Number(limit) || 10));
1742
+ const rows = await executeQuery(
1743
+ `SELECT owner_jid, karma_score, positive_votes, negative_votes, created_at, updated_at
1744
+ FROM ${TABLES.RPG_KARMA_PROFILE}
1745
+ ORDER BY karma_score DESC, positive_votes DESC, updated_at DESC
1746
+ LIMIT ${safeLimit}`,
1747
+ [],
1748
+ connection,
1749
+ );
1750
+ return (rows || []).map(normalizeKarmaProfileRow);
1751
+ };
1752
+
1753
+ export const upsertGroupActivityDaily = async ({ dayRefDate, chatJid, ownerJid, actionsDelta = 0, pvpCreatedDelta = 0, pvpCompletedDelta = 0, coopCompletedDelta = 0 }, connection = null) => {
1754
+ await executeQuery(
1755
+ `INSERT INTO ${TABLES.RPG_GROUP_ACTIVITY_DAILY}
1756
+ (day_ref_date, chat_jid, owner_jid, actions_count, pvp_created_count, pvp_completed_count, coop_completed_count)
1757
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1758
+ ON DUPLICATE KEY UPDATE
1759
+ actions_count = actions_count + VALUES(actions_count),
1760
+ pvp_created_count = pvp_created_count + VALUES(pvp_created_count),
1761
+ pvp_completed_count = pvp_completed_count + VALUES(pvp_completed_count),
1762
+ coop_completed_count = coop_completed_count + VALUES(coop_completed_count),
1763
+ updated_at = CURRENT_TIMESTAMP`,
1764
+ [dayRefDate, chatJid, ownerJid, actionsDelta, pvpCreatedDelta, pvpCompletedDelta, coopCompletedDelta],
1765
+ connection,
1766
+ );
1767
+ };
1768
+
1769
+ export const getSocialXpDailyByKeyForUpdate = async (dayRefDate, ownerJid, chatJid, connection) => {
1770
+ await executeQuery(
1771
+ `INSERT INTO ${TABLES.RPG_SOCIAL_XP_DAILY}
1772
+ (day_ref_date, owner_jid, chat_jid, earned_xp, converted_xp, cap_hits, last_message_hash, last_earned_at)
1773
+ VALUES (?, ?, ?, 0, 0, 0, NULL, NULL)
1774
+ ON DUPLICATE KEY UPDATE updated_at = updated_at`,
1775
+ [dayRefDate, ownerJid, chatJid],
1776
+ connection,
1777
+ );
1778
+
1779
+ const rows = await executeQuery(
1780
+ `SELECT ${SOCIAL_XP_DAILY_COLUMNS}
1781
+ FROM ${TABLES.RPG_SOCIAL_XP_DAILY}
1782
+ WHERE day_ref_date = ?
1783
+ AND owner_jid = ?
1784
+ AND chat_jid = ?
1785
+ LIMIT 1
1786
+ FOR UPDATE`,
1787
+ [dayRefDate, ownerJid, chatJid],
1788
+ connection,
1789
+ );
1790
+
1791
+ return normalizeSocialXpDailyRow(rows?.[0] || null);
1792
+ };
1793
+
1794
+ export const upsertSocialXpDailyDelta = async ({ dayRefDate, ownerJid, chatJid, earnedDelta = 0, convertedDelta = 0, capHitsDelta = 0, lastMessageHash = null, lastEarnedAt = null }, connection = null) => {
1795
+ await executeQuery(
1796
+ `INSERT INTO ${TABLES.RPG_SOCIAL_XP_DAILY}
1797
+ (day_ref_date, owner_jid, chat_jid, earned_xp, converted_xp, cap_hits, last_message_hash, last_earned_at)
1798
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1799
+ ON DUPLICATE KEY UPDATE
1800
+ earned_xp = GREATEST(0, earned_xp + VALUES(earned_xp)),
1801
+ converted_xp = GREATEST(0, converted_xp + VALUES(converted_xp)),
1802
+ cap_hits = GREATEST(0, cap_hits + VALUES(cap_hits)),
1803
+ last_message_hash = COALESCE(VALUES(last_message_hash), last_message_hash),
1804
+ last_earned_at = COALESCE(VALUES(last_earned_at), last_earned_at),
1805
+ updated_at = CURRENT_TIMESTAMP`,
1806
+ [dayRefDate, ownerJid, chatJid, Math.max(0, Number(earnedDelta) || 0), Math.max(0, Number(convertedDelta) || 0), Math.max(0, Number(capHitsDelta) || 0), lastMessageHash || null, lastEarnedAt || null],
1807
+ connection,
1808
+ );
1809
+ };
1810
+
1811
+ export const getGroupActiveUsersByDay = async (chatJid, dayRefDate, connection = null) => {
1812
+ const rows = await executeQuery(
1813
+ `SELECT COUNT(*) AS total
1814
+ FROM ${TABLES.RPG_GROUP_ACTIVITY_DAILY}
1815
+ WHERE chat_jid = ?
1816
+ AND day_ref_date = ?`,
1817
+ [chatJid, dayRefDate],
1818
+ connection,
1819
+ );
1820
+ return Number(rows?.[0]?.total || 0);
1821
+ };
1822
+
1823
+ export const getGroupRetentionByDays = async (chatJid, currentDayRefDate, previousDayRefDate, connection = null) => {
1824
+ const rows = await executeQuery(
1825
+ `SELECT COUNT(*) AS retained
1826
+ FROM ${TABLES.RPG_GROUP_ACTIVITY_DAILY} cur
1827
+ INNER JOIN ${TABLES.RPG_GROUP_ACTIVITY_DAILY} prev
1828
+ ON prev.chat_jid = cur.chat_jid
1829
+ AND prev.owner_jid = cur.owner_jid
1830
+ AND prev.day_ref_date = ?
1831
+ WHERE cur.chat_jid = ?
1832
+ AND cur.day_ref_date = ?`,
1833
+ [previousDayRefDate, chatJid, currentDayRefDate],
1834
+ connection,
1835
+ );
1836
+ return Number(rows?.[0]?.retained || 0);
1837
+ };
1838
+
1839
+ export const getGroupActivitySummaryByDay = async (chatJid, dayRefDate, connection = null) => {
1840
+ const rows = await executeQuery(
1841
+ `SELECT
1842
+ COALESCE(SUM(actions_count), 0) AS actions_total,
1843
+ COALESCE(SUM(pvp_created_count), 0) AS pvp_created_total,
1844
+ COALESCE(SUM(pvp_completed_count), 0) AS pvp_completed_total,
1845
+ COALESCE(SUM(coop_completed_count), 0) AS coop_completed_total
1846
+ FROM ${TABLES.RPG_GROUP_ACTIVITY_DAILY}
1847
+ WHERE chat_jid = ?
1848
+ AND day_ref_date = ?`,
1849
+ [chatJid, dayRefDate],
1850
+ connection,
1851
+ );
1852
+ const row = rows?.[0] || {};
1853
+ return {
1854
+ actionsTotal: Number(row.actions_total || 0),
1855
+ pvpCreatedTotal: Number(row.pvp_created_total || 0),
1856
+ pvpCompletedTotal: Number(row.pvp_completed_total || 0),
1857
+ coopCompletedTotal: Number(row.coop_completed_total || 0),
1858
+ };
1859
+ };