From eec10aeedad9fa97ea87ee102acc3213060a7e93 Mon Sep 17 00:00:00 2001 From: krokosik Date: Tue, 20 Oct 2026 21:02:41 +0200 Subject: [PATCH 1/4] Add date-filterable balance contribution view (#604) --- .../migration.sql | 19 +++++++++++++++++++ .../migration.sql | 5 +++++ prisma/schema.prisma | 17 +++++++++++++++++ prisma/views/public/BalanceEntryView.sql | 11 +++++++++++ 4 files changed, 52 insertions(+) create mode 100644 prisma/migrations/20261019220000_date_filterable_balances/migration.sql create mode 100644 prisma/migrations/20261019223000_prisma_managed_balance_index/migration.sql create mode 100644 prisma/views/public/BalanceEntryView.sql diff --git a/prisma/migrations/20261019220000_date_filterable_balances/migration.sql b/prisma/migrations/20261019220000_date_filterable_balances/migration.sql new file mode 100644 index 000000000..f4f380806 --- /dev/null +++ b/prisma/migrations/20261019220000_date_filterable_balances/migration.sql @@ -0,0 +1,19 @@ +-- #604: filter expense-date contributions before reducing balances. +CREATE VIEW public."BalanceEntryView" AS +SELECT ep."expenseId", ep."userId", e."paidBy" AS "friendId", e."groupId", + e.currency, ep.amount, e."expenseDate", e."createdAt", e."updatedAt" +FROM public."ExpenseParticipant" ep +JOIN public."Expense" e ON e.id = ep."expenseId" +WHERE ep."userId" <> e."paidBy" AND e."deletedAt" IS NULL +UNION ALL +SELECT ep."expenseId", e."paidBy", ep."userId", e."groupId", + e.currency, -ep.amount, e."expenseDate", e."createdAt", e."updatedAt" +FROM public."ExpenseParticipant" ep +JOIN public."Expense" e ON e.id = ep."expenseId" +WHERE ep."userId" <> e."paidBy" AND e."deletedAt" IS NULL; + +CREATE INDEX "ExpenseParticipant_userId_expenseId_idx" +ON public."ExpenseParticipant" ("userId", "expenseId") INCLUDE (amount); + +CREATE INDEX "Expense_expenseDate_live_idx" +ON public."Expense" ("expenseDate") WHERE "deletedAt" IS NULL; diff --git a/prisma/migrations/20261019223000_prisma_managed_balance_index/migration.sql b/prisma/migrations/20261019223000_prisma_managed_balance_index/migration.sql new file mode 100644 index 000000000..c39795f49 --- /dev/null +++ b/prisma/migrations/20261019223000_prisma_managed_balance_index/migration.sql @@ -0,0 +1,5 @@ +-- Prisma cannot represent PostgreSQL INCLUDE indexes. Keep the same covering +-- access path with amount as the final key so future migrations retain it. +CREATE INDEX "ExpenseParticipant_userId_expenseId_amount_idx" +ON public."ExpenseParticipant" ("userId", "expenseId", amount); +DROP INDEX public."ExpenseParticipant_userId_expenseId_idx"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 85e6dfaf2..f11afc01f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -198,6 +198,7 @@ model Expense { @@index([groupId]) @@index([paidBy]) + // The partial live-expense date index is maintained by SQL migrations. @@schema("public") } @@ -209,6 +210,7 @@ model ExpenseParticipant { user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@id([expenseId, userId]) + @@index([userId, expenseId, amount]) @@schema("public") } @@ -332,6 +334,21 @@ view BalanceView { @@schema("public") } +/// Directed live expense contributions; filter expenseDate before grouping. +view BalanceEntryView { + expenseId String @db.Uuid + userId Int + friendId Int + groupId Int? + currency String + amount BigInt + expenseDate DateTime + createdAt DateTime + updatedAt DateTime + + @@schema("public") +} + enum SplitType { EQUAL PERCENTAGE diff --git a/prisma/views/public/BalanceEntryView.sql b/prisma/views/public/BalanceEntryView.sql new file mode 100644 index 000000000..970eab7aa --- /dev/null +++ b/prisma/views/public/BalanceEntryView.sql @@ -0,0 +1,11 @@ +SELECT ep."expenseId", ep."userId", e."paidBy" AS "friendId", e."groupId", + e.currency, ep.amount, e."expenseDate", e."createdAt", e."updatedAt" +FROM public."ExpenseParticipant" ep +JOIN public."Expense" e ON e.id = ep."expenseId" +WHERE ep."userId" <> e."paidBy" AND e."deletedAt" IS NULL +UNION ALL +SELECT ep."expenseId", e."paidBy", ep."userId", e."groupId", + e.currency, -ep.amount, e."expenseDate", e."createdAt", e."updatedAt" +FROM public."ExpenseParticipant" ep +JOIN public."Expense" e ON e.id = ep."expenseId" +WHERE ep."userId" <> e."paidBy" AND e."deletedAt" IS NULL; From 35807584ba2cf79d294359da47f63f0f17ca1975 Mon Sep 17 00:00:00 2001 From: krokosik Date: Tue, 20 Oct 2026 21:03:11 +0200 Subject: [PATCH 2/4] Add typed balance queries and seeded regression baselines (#604) --- package.json | 1 + scripts/balance-baseline.ts | 202 ++ scripts/seeded-balance-baseline.json | 2863 +++++++++++++++++++++ src/server/api/services/balanceService.ts | 93 + 4 files changed, 3159 insertions(+) create mode 100644 scripts/balance-baseline.ts create mode 100644 scripts/seeded-balance-baseline.json create mode 100644 src/server/api/services/balanceService.ts diff --git a/package.json b/package.json index 3f1d4dc9e..40cb99d01 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "test:integration:watch": "jest --config jest.integration.config.ts --watch", "test:e2e": "PWTEST_CACHE_DIR=node_modules/.cache/playwright playwright test", "test:docker": "bash scripts/test-production-compose.sh", + "test:balances:baseline": "node --env-file-if-exists=.env --import tsx scripts/balance-baseline.ts", "prepare": "husky" }, "dependencies": { diff --git a/scripts/balance-baseline.ts b/scripts/balance-baseline.ts new file mode 100644 index 000000000..546583c09 --- /dev/null +++ b/scripts/balance-baseline.ts @@ -0,0 +1,202 @@ +import { type BalanceView, type Prisma, PrismaClient } from '@prisma/client'; +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { readFile, writeFile } from 'node:fs/promises'; +import { parseArgs } from 'node:util'; +import { z } from 'zod'; + +import { createDatabaseAdapter } from '../src/server/databaseAdapter'; + +const { values } = parseArgs({ + options: { + capture: { type: 'boolean', default: false }, + entries: { type: 'boolean', default: false }, + snapshot: { type: 'string', default: 'scripts/seeded-balance-baseline.json' }, + }, +}); +assert(!(values.capture && values.entries), 'Capture the legacy implementation before migration'); +const databaseUrl = process.env.DATABASE_URL; +assert( + databaseUrl && ['localhost', '127.0.0.1', '[::1]'].includes(new URL(databaseUrl).hostname), + 'Use the local seeded database', +); +const db = new PrismaClient({ adapter: createDatabaseAdapter(databaseUrl) }); +const timings: { scope: string; ms: number }[] = []; +const snapshotSchema = z.object({ + capturedAt: z.string(), + expenses: z.number(), + participants: z.number(), + queries: z.array( + z.object({ + name: z.string(), + where: z.object({ + userId: z.number().optional(), + friendId: z.number().optional(), + groupId: z.number().optional(), + }), + rows: z.number(), + digest: z.string(), + }), + ), +}); +const normalize = (rows: BalanceView[]) => + rows + .map((row) => ({ + userId: row.userId, + friendId: row.friendId, + groupId: row.groupId, + currency: row.currency, + amount: row.amount.toString(), + createdAt: row.createdAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), + })) + .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))); +const digest = (rows: BalanceView[]) => + createHash('sha256') + .update(JSON.stringify(normalize(rows))) + .digest('hex'); + +try { + const saved = values.capture + ? undefined + : snapshotSchema.parse(JSON.parse(await readFile(values.snapshot, 'utf8'))); + // Load the new service only when explicitly requested, after its migration exists. + const queryEntries = values.entries + ? (await import('../src/server/api/services/balanceService')).getBalances + : undefined; + const snapshot = await db.$transaction( + async (tx) => { + await tx.$executeRaw`SET TRANSACTION READ ONLY`; + const [expenses, participants, users, groups] = await Promise.all([ + tx.expense.count(), + tx.expenseParticipant.count(), + tx.user.findMany({ select: { id: true } }), + tx.group.findMany({ select: { id: true } }), + ]); + const all = await tx.balanceView.findMany(); + assert(all.length, 'Seed the database before capturing balances'); + const pairs = [ + ...new Map( + all.map((row) => [ + `${row.userId}/${row.friendId}`, + { userId: row.userId, friendId: row.friendId }, + ]), + ).values(), + ] + .sort((a, b) => a.userId - b.userId || a.friendId - b.friendId) + .slice(0, 50); + const queries = saved?.queries ?? [ + { name: 'all', where: {}, rows: 0, digest: '' }, + ...users.map(({ id }) => ({ + name: `user/${id}`, + where: { userId: id }, + rows: 0, + digest: '', + })), + ...groups.map(({ id }) => ({ + name: `group/${id}`, + where: { groupId: id }, + rows: 0, + digest: '', + })), + ...pairs.map((where) => ({ + name: `pair/${where.userId}/${where.friendId}`, + where, + rows: 0, + digest: '', + })), + ]; + const results = await queries.reduce>(async (pending, query) => { + const previous = await pending; + const where: Prisma.BalanceViewWhereInput = query.where; + const started = performance.now(); + const rows = queryEntries + ? await queryEntries(tx, { where: query.where }) + : await tx.balanceView.findMany({ where }); + timings.push({ scope: query.name.split('/')[0]!, ms: performance.now() - started }); + const result = { ...query, rows: rows.length, digest: digest(rows) }; + if (saved) { + assert.deepEqual(result, query, `Balance baseline changed: ${query.name}`); + } + return [...previous, result]; + }, Promise.resolve([])); + + // Independent arithmetic oracle: no balance SQL/view is used to construct it. + const records = await tx.expense.findMany({ + where: { deletedAt: null }, + include: { expenseParticipants: true }, + }); + const oracle = new Map(); + records.forEach((expense) => + expense.expenseParticipants.forEach((participant) => { + if (participant.userId === expense.paidBy) { + return; + } + [ + { userId: participant.userId, friendId: expense.paidBy, amount: participant.amount }, + { userId: expense.paidBy, friendId: participant.userId, amount: -participant.amount }, + ].forEach((entry) => { + const key = `${entry.userId}/${entry.friendId}/${expense.groupId}/${expense.currency}`; + const existing = oracle.get(key); + oracle.set(key, { + ...entry, + groupId: expense.groupId, + currency: expense.currency, + amount: (existing?.amount ?? 0n) + entry.amount, + createdAt: + existing && existing.createdAt < expense.createdAt + ? existing.createdAt + : expense.createdAt, + updatedAt: + existing && existing.updatedAt > expense.updatedAt + ? existing.updatedAt + : expense.updatedAt, + }); + }); + }), + ); + assert.deepEqual( + normalize(all), + normalize([...oracle.values()]), + 'Legacy view differs from participant arithmetic', + ); + assert.equal( + all.reduce((sum, row) => sum + row.amount, 0n), + 0n, + 'Mirrored amounts must cancel', + ); + if (saved) { + assert.equal(expenses, saved.expenses, 'Seeded expense count changed'); + assert.equal(participants, saved.participants, 'Seeded participant count changed'); + } + return { + capturedAt: saved?.capturedAt ?? new Date().toISOString(), + expenses, + participants, + queries: results, + }; + }, + { timeout: 120_000, isolationLevel: 'RepeatableRead' }, + ); + if (values.capture) { + await writeFile(values.snapshot, `${JSON.stringify(snapshot, null, 2)}\n`, { flag: 'wx' }); + } + console.info( + `${snapshot.queries.length} seeded balance query baselines passed (${values.entries ? 'entries service' : 'legacy view'}); independent monetary/metadata oracle passed.`, + ); + console.table( + ['all', 'user', 'group', 'pair'].map((scope) => { + const samples = timings + .filter((timing) => timing.scope === scope) + .map((timing) => timing.ms) + .sort((a, b) => a - b); + return { + scope, + queries: samples.length, + medianMs: Number(samples[Math.floor(samples.length / 2)]!.toFixed(3)), + }; + }), + ); +} finally { + await db.$disconnect(); +} diff --git a/scripts/seeded-balance-baseline.json b/scripts/seeded-balance-baseline.json new file mode 100644 index 000000000..6788febbb --- /dev/null +++ b/scripts/seeded-balance-baseline.json @@ -0,0 +1,2863 @@ +{ + "capturedAt": "2026-10-19T21:18:56.679Z", + "expenses": 17830, + "participants": 65914, + "queries": [ + { + "name": "all", + "where": {}, + "rows": 8200, + "digest": "9a4dd000c9280cdb06a201f56cdf0f6cc4f0a235651eeb4e7a3c21d9ff706cd9" + }, + { + "name": "user/0", + "where": { + "userId": 0 + }, + "rows": 67, + "digest": "6912695901e5b34d08dd361e403a9ca506f575af2c194727ea865a29da21d8af" + }, + { + "name": "user/1", + "where": { + "userId": 1 + }, + "rows": 66, + "digest": "2097925aaacde943144174a184634314717fc964fff7a2a2690b0f115d0bec85" + }, + { + "name": "user/2", + "where": { + "userId": 2 + }, + "rows": 11, + "digest": "ad1fcb8e73f15e3f38f6c62d5be65035af5ef02a25b5dceb057a0c22f02a85b6" + }, + { + "name": "user/3", + "where": { + "userId": 3 + }, + "rows": 49, + "digest": "bf1f3ff55385c165c14e63ef0e3c878d6065df306e5a0b968ae4ac4adb413370" + }, + { + "name": "user/4", + "where": { + "userId": 4 + }, + "rows": 47, + "digest": "c94763962deb752e27651ed3efa69f89fc020fbbad2f66f079a9e17bc84c015b" + }, + { + "name": "user/5", + "where": { + "userId": 5 + }, + "rows": 66, + "digest": "8815e6ce65a6fa32bc1a640e81ea989c343c1f134f7aa2a04905363dc9f88e2c" + }, + { + "name": "user/6", + "where": { + "userId": 6 + }, + "rows": 77, + "digest": "88426f9e1c0349585b266cb4eb37974d9f35a6244bf48b0fee57a2346fd9776f" + }, + { + "name": "user/7", + "where": { + "userId": 7 + }, + "rows": 51, + "digest": "84a103ce592f3e028d6d463cf0514274ee22c9739b98129ed9b97e2becd4370c" + }, + { + "name": "user/8", + "where": { + "userId": 8 + }, + "rows": 58, + "digest": "89044ba05210948a0c5d7c35a58502882218de14f84539dd0b75d2ca0ed5793e" + }, + { + "name": "user/9", + "where": { + "userId": 9 + }, + "rows": 27, + "digest": "f6c8c832fa876e1e6469a9d9a8c90ee5507cb809cb1367295dc4fcff4cebab9f" + }, + { + "name": "user/10", + "where": { + "userId": 10 + }, + "rows": 57, + "digest": "7590d8610799d885d045f48cfc7a371e67e00929b7dcb9c6832c103e95936e09" + }, + { + "name": "user/11", + "where": { + "userId": 11 + }, + "rows": 55, + "digest": "11298fd09d4d854d34716b5c5c1132ae0661ad09f9ff1ad8b1c0e67749c4e017" + }, + { + "name": "user/12", + "where": { + "userId": 12 + }, + "rows": 64, + "digest": "3ea705fa3fa5cabfb1ed1ee4a348f35fbb748b455de15081654d2b776daa5b9c" + }, + { + "name": "user/13", + "where": { + "userId": 13 + }, + "rows": 70, + "digest": "3923ba1a1e8094654cc18f55f9bf756f3a7494ae77ec07a2f96c7c73ee879ab4" + }, + { + "name": "user/14", + "where": { + "userId": 14 + }, + "rows": 71, + "digest": "32f0fdab7ec6f19737db5bad6f3e72c041bbcac5e7d1cfcadec7aaa54a46d3fe" + }, + { + "name": "user/15", + "where": { + "userId": 15 + }, + "rows": 51, + "digest": "2077375a95cbbaedadc270915386966b1e963ba61d98c3c1cc9109c2c9d12356" + }, + { + "name": "user/16", + "where": { + "userId": 16 + }, + "rows": 43, + "digest": "f94a0dab85f6c12b09d18ab0d102e8f763091861c9233186fdff2e52376bfe05" + }, + { + "name": "user/17", + "where": { + "userId": 17 + }, + "rows": 52, + "digest": "86848d586ca47f1e41d7acbdf3bf131598b0cbc37d4e3c329e129db95de6c040" + }, + { + "name": "user/18", + "where": { + "userId": 18 + }, + "rows": 59, + "digest": "ad7451568bbb8fc773e6095ab193ecea15ce25749a33f426512cb8d283f79aed" + }, + { + "name": "user/19", + "where": { + "userId": 19 + }, + "rows": 43, + "digest": "75ff326f2dc055059f84e8a65e0b2647109a823520aa78089e6b1b1499294b41" + }, + { + "name": "user/20", + "where": { + "userId": 20 + }, + "rows": 45, + "digest": "d66a831eee726cf1f3959da66e9dc59ef421966378fd9f95f0cae938b8f53f5f" + }, + { + "name": "user/21", + "where": { + "userId": 21 + }, + "rows": 52, + "digest": "ac3c6dc53a517585369d877a3f10e016e491c3e9062d7d2b31ef0e89f6abd9e6" + }, + { + "name": "user/22", + "where": { + "userId": 22 + }, + "rows": 60, + "digest": "1e4ffb3c268f8dff2ed32ce88b4bcc07f7373e3d1a5b4b9be527b0ac7a05f634" + }, + { + "name": "user/23", + "where": { + "userId": 23 + }, + "rows": 69, + "digest": "88e96fb31906f9792ea5a34bedb6047474040dc9fbfd77dd832fa67d862c74e4" + }, + { + "name": "user/24", + "where": { + "userId": 24 + }, + "rows": 38, + "digest": "b75e2a549a08ef010b9d0846bcc796975fee048db480ad3f7de8914ec090216b" + }, + { + "name": "user/25", + "where": { + "userId": 25 + }, + "rows": 47, + "digest": "3a9ac58e7b34eaf13b9e7356dd892dcb5083413c3eeae0731f04f4e9a74a6e6f" + }, + { + "name": "user/26", + "where": { + "userId": 26 + }, + "rows": 45, + "digest": "ac865bc5852f4fe98e7f02930c0d2e79e1dd77f413eb09773417274b4d0dcc64" + }, + { + "name": "user/27", + "where": { + "userId": 27 + }, + "rows": 50, + "digest": "b6f155162a285d8872362ffb89d24aaa096608402f132a607a29119496125936" + }, + { + "name": "user/28", + "where": { + "userId": 28 + }, + "rows": 72, + "digest": "752663153c3aa9eb0062973a8616a05c23c898a08f681354366ad99e22a246a1" + }, + { + "name": "user/29", + "where": { + "userId": 29 + }, + "rows": 57, + "digest": "4c650ddbbfe2d55daa9c681a0b7e5f1a6a9a9d791eda87cc2dfba6e81671c069" + }, + { + "name": "user/30", + "where": { + "userId": 30 + }, + "rows": 77, + "digest": "e7abe66bab1f6d21072a322acf62711eb41b6f9ccf48f246ea4fae96907afb83" + }, + { + "name": "user/31", + "where": { + "userId": 31 + }, + "rows": 44, + "digest": "9095249f26ddbbc8924acb7620a35aea59f1119c616482dada4bc002713d3d49" + }, + { + "name": "user/32", + "where": { + "userId": 32 + }, + "rows": 63, + "digest": "757cb18ae10597a3de98f33e2ea985be270cb12b3f03312d7c6270b200734950" + }, + { + "name": "user/33", + "where": { + "userId": 33 + }, + "rows": 49, + "digest": "52a1cbffaf3efd357093c51f3b238afdd3dc037e4117e4528914037314486d14" + }, + { + "name": "user/34", + "where": { + "userId": 34 + }, + "rows": 45, + "digest": "0e871aa2f7528f0f3de7c29a34e78b5c7790d2f2fca8f46ef3118406b62fdb6f" + }, + { + "name": "user/35", + "where": { + "userId": 35 + }, + "rows": 60, + "digest": "27a6c797a656b785adea2ad778f7bffdee056f22c25379d7ab79bac5aca9ca6d" + }, + { + "name": "user/36", + "where": { + "userId": 36 + }, + "rows": 82, + "digest": "50dec4774dbf2a713a6a00a5980b92d43c02030db368563dac79cfb564ddf86a" + }, + { + "name": "user/37", + "where": { + "userId": 37 + }, + "rows": 49, + "digest": "e8012e447742a0359d06bf11bfa5c50ba1aaa51cde44728e46b83bf176827e23" + }, + { + "name": "user/38", + "where": { + "userId": 38 + }, + "rows": 43, + "digest": "b658807b262e30bd81880bf16f5b82133777677c89e1cd60b5e2794297ddc4d9" + }, + { + "name": "user/39", + "where": { + "userId": 39 + }, + "rows": 51, + "digest": "63b3498d5e098dd7975f759655b8efa7dee064ad4e8625ec80895c1f56b34668" + }, + { + "name": "user/40", + "where": { + "userId": 40 + }, + "rows": 80, + "digest": "f2f04139d24e9493845dc63ce874052b0fc9e7b99559ab8145ca988d3129d0a0" + }, + { + "name": "user/41", + "where": { + "userId": 41 + }, + "rows": 34, + "digest": "ffb17ef5d5858fed427754f60c625579cbc6e26c1e8c45b98273fbaff61e1cf7" + }, + { + "name": "user/42", + "where": { + "userId": 42 + }, + "rows": 46, + "digest": "24e09ff1f232b233961e732deaf4047cf67611915e61ef0e511633addbe077b8" + }, + { + "name": "user/43", + "where": { + "userId": 43 + }, + "rows": 66, + "digest": "5cfc69e12f379dc01556893531bd4c746a637877caa763f66f6681510b8fcafc" + }, + { + "name": "user/44", + "where": { + "userId": 44 + }, + "rows": 70, + "digest": "21b92b2c4fe5c6a0b782c96e730c9429864c4a8cfb4b6cddac548e679e5782d2" + }, + { + "name": "user/45", + "where": { + "userId": 45 + }, + "rows": 65, + "digest": "edc83ad6256ff0cbf05c07cd0e588946a49191d949f98c5ec58d415bed74f7bb" + }, + { + "name": "user/46", + "where": { + "userId": 46 + }, + "rows": 44, + "digest": "6d6a32526e10976b26ae33eab108b983b994d3c54aae3c8565ae67d0b963b77e" + }, + { + "name": "user/47", + "where": { + "userId": 47 + }, + "rows": 47, + "digest": "20f66e6aaf079a9f05b054e75d9cd0724b5ed8192b9cfe2db0e1d8af03b28e02" + }, + { + "name": "user/48", + "where": { + "userId": 48 + }, + "rows": 85, + "digest": "b325d48eb1d37921602cbb925f162d54c531793dd6cecf7405e74eb5053f8357" + }, + { + "name": "user/49", + "where": { + "userId": 49 + }, + "rows": 59, + "digest": "a71b59e141f7777fa1d3cb3e70a30d54473184595746b13e782553dc747d9c7d" + }, + { + "name": "user/50", + "where": { + "userId": 50 + }, + "rows": 56, + "digest": "27f6997c49b0641d8d70a4cb8d1b97803fedc929ffddf01a22aaab8c4f65a568" + }, + { + "name": "user/51", + "where": { + "userId": 51 + }, + "rows": 63, + "digest": "56ddc15aa3fc2e16932267ef9aa135958e9b6c930d856c6790e64ea16f987424" + }, + { + "name": "user/52", + "where": { + "userId": 52 + }, + "rows": 72, + "digest": "e64213a74f662e1059d9c267b84aae4776cc3363a056af1acdfcfa1b9f7dca2d" + }, + { + "name": "user/53", + "where": { + "userId": 53 + }, + "rows": 58, + "digest": "f5f95b328591aefd653351e49c594ce042a4dcf1d4f4e4b51ba68022afa9d758" + }, + { + "name": "user/54", + "where": { + "userId": 54 + }, + "rows": 49, + "digest": "51bdeb3fb009936efcdfc4bffdd52c8359e0a7bed51162d3ae040ab036d06357" + }, + { + "name": "user/55", + "where": { + "userId": 55 + }, + "rows": 42, + "digest": "6362750a10310df1a9313d7d0737a4b554632ae5429631d4beeaef86166f2c76" + }, + { + "name": "user/56", + "where": { + "userId": 56 + }, + "rows": 40, + "digest": "af511f5068380f887a708fbb15252b1f93ca8c561635f55e7be810d424b75bee" + }, + { + "name": "user/57", + "where": { + "userId": 57 + }, + "rows": 48, + "digest": "4e49d2656a2e08c6d0ffe62e9e69d2eaf91534c23e0c580e5b1404564ee3aa0c" + }, + { + "name": "user/58", + "where": { + "userId": 58 + }, + "rows": 21, + "digest": "d8ec4fae73f3acfc2fd3995197ff339609ede094a12319c3ffb95350a3a065f6" + }, + { + "name": "user/59", + "where": { + "userId": 59 + }, + "rows": 36, + "digest": "3135263c0049ff024ae49fcc07bc62325af868f03d0d2089b1dbb4e92e0f1d5a" + }, + { + "name": "user/60", + "where": { + "userId": 60 + }, + "rows": 86, + "digest": "e9eaf60c58e3786e9afc5a6b1e5287a7af301b01e077460f13267b458d23247d" + }, + { + "name": "user/61", + "where": { + "userId": 61 + }, + "rows": 71, + "digest": "13e7573c9a6b8a7e4d7aadc0a29d39958ad1c0e716d15fc18bdf9b937a60b88e" + }, + { + "name": "user/62", + "where": { + "userId": 62 + }, + "rows": 68, + "digest": "9b95838129da07e02178054c8773896e0c5187fba59fa5c4d8ef4976f65ccb48" + }, + { + "name": "user/63", + "where": { + "userId": 63 + }, + "rows": 38, + "digest": "59159d544193b71e7e483f897ab3c16164f485930b551d91babe2769cbab0c72" + }, + { + "name": "user/64", + "where": { + "userId": 64 + }, + "rows": 70, + "digest": "e5bc92d0e3f6318e60c522dd36382f382d3c6a890a740b04db96df283eeb05ca" + }, + { + "name": "user/65", + "where": { + "userId": 65 + }, + "rows": 60, + "digest": "d1a9be70031e2d7a15b45fd266b017a2326d6788ca38c25169405079cc48cd71" + }, + { + "name": "user/66", + "where": { + "userId": 66 + }, + "rows": 63, + "digest": "0a2230d5da8a58fe4093d9aca5518f2e9d04800703dd506d6c24ea6a39881099" + }, + { + "name": "user/67", + "where": { + "userId": 67 + }, + "rows": 70, + "digest": "387a7539d53a559f74b408ea801a8a9183cb34c377539e22e71f0a06651ebdf8" + }, + { + "name": "user/68", + "where": { + "userId": 68 + }, + "rows": 37, + "digest": "0cff290ad7ede910ad4dc0da1ce8f794d07f27b10b8427fc4499b62b7ca88e1d" + }, + { + "name": "user/69", + "where": { + "userId": 69 + }, + "rows": 56, + "digest": "0a845d2b650c7189c305967f2069f1276b4ff189daaf409b12e341924d50aff0" + }, + { + "name": "user/70", + "where": { + "userId": 70 + }, + "rows": 59, + "digest": "4f9bfdf24457d34dd75112ea70d131f974df59907539da03fa002b34b4dcc9a6" + }, + { + "name": "user/71", + "where": { + "userId": 71 + }, + "rows": 69, + "digest": "8da75ee0bc3629ac9d0c07d8dcc9937375e74b8d12cd491c5386d5a2c5f353bc" + }, + { + "name": "user/72", + "where": { + "userId": 72 + }, + "rows": 37, + "digest": "54421052aa33e7d729c24f084a328780530c24d1b13b7ce1daf3f6c0f8bfa0d5" + }, + { + "name": "user/73", + "where": { + "userId": 73 + }, + "rows": 30, + "digest": "a196386754322ee8aebc7f00f497189b89dd142e0732b67455b85798d0b53060" + }, + { + "name": "user/74", + "where": { + "userId": 74 + }, + "rows": 37, + "digest": "c82eae308ba826925b6ababe1a656a99ee6296139dc936e4886358fc36ace771" + }, + { + "name": "user/75", + "where": { + "userId": 75 + }, + "rows": 43, + "digest": "6c5c2c03de260efb0ec0d5aec5a43cc16f09b81cdfde6a725014a1f2e79feb36" + }, + { + "name": "user/76", + "where": { + "userId": 76 + }, + "rows": 52, + "digest": "88c5a5900e38525e72cb7011134b1cb6f7eabc830c4ff1beadc7f959284307f5" + }, + { + "name": "user/77", + "where": { + "userId": 77 + }, + "rows": 80, + "digest": "2e2327be6028844dce0dab98dd3234a8646fe267c35c16822992d14782049315" + }, + { + "name": "user/78", + "where": { + "userId": 78 + }, + "rows": 54, + "digest": "222618a35245add615057d40d4033a541a08e18f4e8ab26a30a4ba1d67a00ef0" + }, + { + "name": "user/79", + "where": { + "userId": 79 + }, + "rows": 78, + "digest": "db0188ed0d3a0b9843825c834bcaf2c3c31666ae180f4c7e69b7346484684dfc" + }, + { + "name": "user/80", + "where": { + "userId": 80 + }, + "rows": 48, + "digest": "f9abbccd03eae2e04cd2c0814c3c3e70622aebd6d5eb7789e02d6ca082d8cb63" + }, + { + "name": "user/81", + "where": { + "userId": 81 + }, + "rows": 67, + "digest": "33defa7475e92870258c4f6bd1b75f60d784df5c2d396a1a38ff8d05c2ee4fe4" + }, + { + "name": "user/82", + "where": { + "userId": 82 + }, + "rows": 36, + "digest": "440f7e2fc23affc51d51e7a2f847fae2ae38dd142ed67d8cc9a1d4f88a69d6e4" + }, + { + "name": "user/83", + "where": { + "userId": 83 + }, + "rows": 51, + "digest": "88cc1aa2a8859805edc9fd5d25d8c7e188ce453209c242217e73e577672d3621" + }, + { + "name": "user/84", + "where": { + "userId": 84 + }, + "rows": 39, + "digest": "051c4e443569e4a028f108691c64fb36a76a2f02bbab7c69b6db600dec30016f" + }, + { + "name": "user/85", + "where": { + "userId": 85 + }, + "rows": 37, + "digest": "fdb45da53f79aff668719fd01613a94331d5a953525e02c4c6fd46d53e2b18f2" + }, + { + "name": "user/86", + "where": { + "userId": 86 + }, + "rows": 55, + "digest": "626956ecbfa19633e2b9bd494a734782bdc25cd96ffc8898a409670557f895db" + }, + { + "name": "user/87", + "where": { + "userId": 87 + }, + "rows": 65, + "digest": "00476bb2b1bcf2152ca8933187a29d337c2f6765107916995320d488fcf13b66" + }, + { + "name": "user/88", + "where": { + "userId": 88 + }, + "rows": 61, + "digest": "9d2d2ae2a601690e80271e9f6e7057a0aab0c856f7c64e319fba5756f1d9d67a" + }, + { + "name": "user/89", + "where": { + "userId": 89 + }, + "rows": 22, + "digest": "c020ce4cac0b60101a1461920466a4d6faf8d46fecac22874749ffb94674d121" + }, + { + "name": "user/90", + "where": { + "userId": 90 + }, + "rows": 41, + "digest": "186a2a269289514f821d6a2855da2a3463dff2649fd0b0452dd77a23b6f60dfe" + }, + { + "name": "user/91", + "where": { + "userId": 91 + }, + "rows": 50, + "digest": "c3d2d6c119c95240319a4943305287df3df235ba3cfe751a3ff9aa7a091128da" + }, + { + "name": "user/92", + "where": { + "userId": 92 + }, + "rows": 66, + "digest": "92eb6f516e872e9f0d02fb5b280f85e9f88924f7dcd0092e4d0c74f35dad9452" + }, + { + "name": "user/93", + "where": { + "userId": 93 + }, + "rows": 64, + "digest": "f8c3760ef5886e6d18354424f9b981e9c5b4f695a5985483882b2c1894d982e2" + }, + { + "name": "user/94", + "where": { + "userId": 94 + }, + "rows": 52, + "digest": "2b7299cba698ab012eea1464e849fefb37cd13726c23073b816fb6d063268580" + }, + { + "name": "user/95", + "where": { + "userId": 95 + }, + "rows": 42, + "digest": "727f4545e85c017f7cc3f025d4d4b13f1e7c53ab5bd1eac04a71604345ee527a" + }, + { + "name": "user/96", + "where": { + "userId": 96 + }, + "rows": 62, + "digest": "2c5d7985782c23a06f3981ece99632ebdddfb271ec99a59e543828c643d3e2cb" + }, + { + "name": "user/97", + "where": { + "userId": 97 + }, + "rows": 46, + "digest": "d053f5c89ec08c5a871489435c1200dc7eff85c1d2f56cd7af31cb14858eaeb6" + }, + { + "name": "user/98", + "where": { + "userId": 98 + }, + "rows": 79, + "digest": "23545600c7f3154a3ace22aa2a79ca49bf8b23e74b1dd2ef495abaf646bf0219" + }, + { + "name": "user/100", + "where": { + "userId": 100 + }, + "rows": 46, + "digest": "3d217f7a6ed9473a39e7f881ead2144c0e4fe781e5b67af0a7cd7d929600d0c5" + }, + { + "name": "user/101", + "where": { + "userId": 101 + }, + "rows": 56, + "digest": "6f2378840c21d74279af18b422505b9ef62467a761444d388373d8cfafbc59bb" + }, + { + "name": "user/102", + "where": { + "userId": 102 + }, + "rows": 56, + "digest": "d53a4eb561bb50219dea775ca41bc5cfba0ecc92af71d161b305f2abfc09a4fa" + }, + { + "name": "user/103", + "where": { + "userId": 103 + }, + "rows": 31, + "digest": "1198ed62afc92808ffab5c612aac6f3d4eaef5e1675f6ea717af3d0bb61f166d" + }, + { + "name": "user/104", + "where": { + "userId": 104 + }, + "rows": 54, + "digest": "923226a50e648294b80e0198260d90fce46cd18a8fbc33b8556af7cbbc16c1ff" + }, + { + "name": "user/105", + "where": { + "userId": 105 + }, + "rows": 38, + "digest": "75d6b581eb76aadac10bffcdd5725a530d67808ded1a186c5601d2df936864f5" + }, + { + "name": "user/106", + "where": { + "userId": 106 + }, + "rows": 62, + "digest": "b434315710732534fb912da1266645287437ffdbab015982f665edb1ec9cc859" + }, + { + "name": "user/107", + "where": { + "userId": 107 + }, + "rows": 43, + "digest": "8241a3e992510231c3e1799486e421b35e5fd2d02b3ef277a2d161098f5da986" + }, + { + "name": "user/108", + "where": { + "userId": 108 + }, + "rows": 43, + "digest": "e425a2a43ff8e5ccfb52812ed20dc8974d28879d1f96ba0bbfbcf2c9c03e8e20" + }, + { + "name": "user/109", + "where": { + "userId": 109 + }, + "rows": 55, + "digest": "4db3c58999bd6112935b7827dc56fbcbd4a18e639d59d4ceca0732d61e317e3a" + }, + { + "name": "user/110", + "where": { + "userId": 110 + }, + "rows": 36, + "digest": "42c89072209dd525a7c8e56cf6d04bccaae195f9735c1e9fb7e354d889a17ea0" + }, + { + "name": "user/111", + "where": { + "userId": 111 + }, + "rows": 70, + "digest": "58d3c5694843b4137643f239a553bd8febbe3c0263025dad17de98e51832ae20" + }, + { + "name": "user/112", + "where": { + "userId": 112 + }, + "rows": 51, + "digest": "ff5670b5f9e6dc37d3551e5d954b8f40bf7c5f9d41648da57c16703810d5776b" + }, + { + "name": "user/113", + "where": { + "userId": 113 + }, + "rows": 52, + "digest": "a6da0e477186609b376d9fe7c3e7db14af746e592b7ea920b445374ae32d7e6f" + }, + { + "name": "user/114", + "where": { + "userId": 114 + }, + "rows": 49, + "digest": "c2e7f61e18e45365a98f9b5d992151b0e1a8081108580a7dcc5c9f4f8a478a61" + }, + { + "name": "user/115", + "where": { + "userId": 115 + }, + "rows": 70, + "digest": "6d52b053d8c5878172f2bb35e03e2e9de847d3806e079838c64575f8d96e417a" + }, + { + "name": "user/116", + "where": { + "userId": 116 + }, + "rows": 30, + "digest": "234681598e45c4743ba6537c5478619eedb065ebdf6787d1cecfc2b02a511692" + }, + { + "name": "user/117", + "where": { + "userId": 117 + }, + "rows": 59, + "digest": "809201ff2a494d342b3590110cf6ea264fb72a71569f64a2ce27ca249e0a6076" + }, + { + "name": "user/118", + "where": { + "userId": 118 + }, + "rows": 42, + "digest": "7329cb484fa3f7961cd939e656c85714eafcde27f197cccd9a008fb89d3ad4f7" + }, + { + "name": "user/119", + "where": { + "userId": 119 + }, + "rows": 38, + "digest": "f16ade28b09fc2f430620f21364e2362a3226626f02ee0f1d865888fef07dbf6" + }, + { + "name": "user/120", + "where": { + "userId": 120 + }, + "rows": 21, + "digest": "9aa546844920daea0b3e7e86bd35d393a64694c7bc31d9cbb756804db681d487" + }, + { + "name": "user/121", + "where": { + "userId": 121 + }, + "rows": 44, + "digest": "2b980a0673eea062c59d716201e08922b88f6c7a6f7b5fd49bfa0e774ccfaa0a" + }, + { + "name": "user/122", + "where": { + "userId": 122 + }, + "rows": 59, + "digest": "c08997737e00347a83abaaa8dde669dc0e1fd55c4f34aaff28adc9595bd29ff0" + }, + { + "name": "user/123", + "where": { + "userId": 123 + }, + "rows": 68, + "digest": "122f812bc7fe60a2ffc9c856413fec4946247b9417c8fdffc80d4cf7983089a2" + }, + { + "name": "user/124", + "where": { + "userId": 124 + }, + "rows": 72, + "digest": "3d4cc784b83cd9083c027b370843d2edd26fb2cbcf6aa3183490f60da89a181e" + }, + { + "name": "user/125", + "where": { + "userId": 125 + }, + "rows": 58, + "digest": "f1e9da801943cb3e850e6ca217af44dc0af673837370be175d777db9481e19c5" + }, + { + "name": "user/126", + "where": { + "userId": 126 + }, + "rows": 62, + "digest": "7a71de92a825a2a4e36a862a9e54b37b482a9825550911af8edbae9cdc127b9a" + }, + { + "name": "user/127", + "where": { + "userId": 127 + }, + "rows": 60, + "digest": "bae0d6d7a3d058f187beefb69d5ed37f8a3df6bff37c9df3cd1e9e832578f876" + }, + { + "name": "user/128", + "where": { + "userId": 128 + }, + "rows": 70, + "digest": "72d380b8474edf916ea15525fd3c857af91f58bded068a8c293ce119c021c4af" + }, + { + "name": "user/129", + "where": { + "userId": 129 + }, + "rows": 47, + "digest": "c9475dd5e7903a8c8422dbe63ad7c3ed839739d50a1401764a3f318dabff009f" + }, + { + "name": "user/130", + "where": { + "userId": 130 + }, + "rows": 62, + "digest": "042aedf0e99b550d3a32f14ecd148951d1eeb2725bb71613c0f6a2f318da44b7" + }, + { + "name": "user/131", + "where": { + "userId": 131 + }, + "rows": 51, + "digest": "ab234f474fc430b1e9ee4005277740c0675bc2001bce8f23df6b58c5829f5c9a" + }, + { + "name": "user/132", + "where": { + "userId": 132 + }, + "rows": 46, + "digest": "a08e488dd1460df0218278b57dbb73b53e862db03493f2041be7d0d7d4ea7f1d" + }, + { + "name": "user/133", + "where": { + "userId": 133 + }, + "rows": 69, + "digest": "82db531862811818f360d124ac273026ebd3407b37b6ab684bd864d52532d393" + }, + { + "name": "user/134", + "where": { + "userId": 134 + }, + "rows": 59, + "digest": "08f4457aa746bdcc99662362c15f0902f5bae112f2a345338db52daefbf15b64" + }, + { + "name": "user/135", + "where": { + "userId": 135 + }, + "rows": 59, + "digest": "eab6c3bc2ada95dcc86034fb3dedb31cf767b90851d8eb8c345990f7c6a9bb62" + }, + { + "name": "user/136", + "where": { + "userId": 136 + }, + "rows": 61, + "digest": "78d5a68593b23e099e0d16f3aa95caea43ff044eb5a6062347c10a36a6a01639" + }, + { + "name": "user/137", + "where": { + "userId": 137 + }, + "rows": 45, + "digest": "493c8ffd4833a07c36d9a710b08ba8d348b0d84564d39e2522ab4e7aa45b7a7e" + }, + { + "name": "user/138", + "where": { + "userId": 138 + }, + "rows": 35, + "digest": "afbf222a84ef32df2906e96993a8e95d57ebd9d73bb2d7b4f449268dc6eee05c" + }, + { + "name": "user/139", + "where": { + "userId": 139 + }, + "rows": 89, + "digest": "456522cce5ca08f29073595c5c963be5162ecc438504dd39431c2a33c6ac1d6b" + }, + { + "name": "user/140", + "where": { + "userId": 140 + }, + "rows": 45, + "digest": "adb48fae7e7ac66a821b1ed4deff96da4d821c415166fcb534a0ae785611834d" + }, + { + "name": "user/141", + "where": { + "userId": 141 + }, + "rows": 50, + "digest": "db1e67ac47d604118c25289d673e0f6b87d187a3fe91e47867be00b22f4581a1" + }, + { + "name": "user/142", + "where": { + "userId": 142 + }, + "rows": 22, + "digest": "9553dd8988f10c26fa96d0194c4348e62c6f47414feb9975d16e37de6ca90867" + }, + { + "name": "user/143", + "where": { + "userId": 143 + }, + "rows": 62, + "digest": "e7e0966c15a954228479ce658f02646815dc4ba07c0d895ddc8e4c06d48b917e" + }, + { + "name": "user/144", + "where": { + "userId": 144 + }, + "rows": 78, + "digest": "6e16f1cbd6b513146cba4a76a6551e893535947142838d1cce2ebcba659543e5" + }, + { + "name": "user/145", + "where": { + "userId": 145 + }, + "rows": 44, + "digest": "63e2e1027136ee3b12c4dd4500556cfec9e832c5714846079f49c63cb4b94908" + }, + { + "name": "user/146", + "where": { + "userId": 146 + }, + "rows": 88, + "digest": "84517631deedc2e13f0193f0450ae092299ce7f4291322479efd1e5cd0027435" + }, + { + "name": "user/147", + "where": { + "userId": 147 + }, + "rows": 67, + "digest": "1b155cff65b466df92a40d83c234cc4d1f756cd6b5abd8fc7f8b727fdea00dd2" + }, + { + "name": "user/148", + "where": { + "userId": 148 + }, + "rows": 75, + "digest": "29b71a2ff6b5908ad8b0b601e72840269cf07dd24447b29d72e8c8490c9761f3" + }, + { + "name": "user/149", + "where": { + "userId": 149 + }, + "rows": 57, + "digest": "88c8b85d7075e31f452f72f25f66b073c6a78ab6788348eca4a99bcc295f1689" + }, + { + "name": "user/99", + "where": { + "userId": 99 + }, + "rows": 90, + "digest": "023663f5b2e013652fe55ae5e9a13989438105b97428c84dd151111919508de2" + }, + { + "name": "group/1", + "where": { + "groupId": 1 + }, + "rows": 42, + "digest": "512779d4050f25f09a9a1a5fdfe2a577db883f4e88d202c5ed75f037d586618c" + }, + { + "name": "group/9", + "where": { + "groupId": 9 + }, + "rows": 12, + "digest": "9ff500f59849d1dbd233149f6cf7855e30b51227fdf50ba23c2141a260de1c7c" + }, + { + "name": "group/18", + "where": { + "groupId": 18 + }, + "rows": 42, + "digest": "58173ec6f82054ee14d51ff62aa49044353df1886988c0e8a0dc8c538740c04f" + }, + { + "name": "group/0", + "where": { + "groupId": 0 + }, + "rows": 56, + "digest": "880483cf97532c6e9a03decabd07b474ef57a25e9378a21e0dda72de13934190" + }, + { + "name": "group/6", + "where": { + "groupId": 6 + }, + "rows": 20, + "digest": "57cad8e6acbc7780cb55459f44cdea773ac559636746d6bcf470cdffeca98131" + }, + { + "name": "group/8", + "where": { + "groupId": 8 + }, + "rows": 14, + "digest": "63f046680b01e66bde44485fd8665d5c94df6d02767e658347e8b354cd505761" + }, + { + "name": "group/10", + "where": { + "groupId": 10 + }, + "rows": 20, + "digest": "ba000985a5cbc0356610d76b17ea00562256f348129fcf4b67813809756ad49e" + }, + { + "name": "group/3", + "where": { + "groupId": 3 + }, + "rows": 62, + "digest": "a30e007b729674ff0c940006f04852bfd0a26399ccdeb467faea7ff9ecb48649" + }, + { + "name": "group/26", + "where": { + "groupId": 26 + }, + "rows": 42, + "digest": "53b463e77301d5e5f749e6c39b0b53b68e89bbf6e5fc94ed30fb6cd805f37c6f" + }, + { + "name": "group/20", + "where": { + "groupId": 20 + }, + "rows": 90, + "digest": "613d53dfc17f126db51dcc482009d9674eb9d3d18259fb8d63b16d6edb4fc58a" + }, + { + "name": "group/4", + "where": { + "groupId": 4 + }, + "rows": 30, + "digest": "06c961742c4ef198016bb414310bce80b15639cf9223b83422d2f85dd69ba8a9" + }, + { + "name": "group/34", + "where": { + "groupId": 34 + }, + "rows": 56, + "digest": "5d030f0617d0bb54546142ac7249b17fe44af7c10eb9019388bb028b9472e8d1" + }, + { + "name": "group/19", + "where": { + "groupId": 19 + }, + "rows": 12, + "digest": "b39b1b0a2e89cbedd83def1a02b1b0d0b096219a0d851a0b728273bc1a4663c6" + }, + { + "name": "group/7", + "where": { + "groupId": 7 + }, + "rows": 110, + "digest": "c8fa4c990c4f2c6c875c6472f5db8d32288227e8c22a550a20bfbf392c73f3a7" + }, + { + "name": "group/14", + "where": { + "groupId": 14 + }, + "rows": 30, + "digest": "e7e3dc37e20439abd0f60e2d3fa9b2c18326bdd796a1d2db40eaf169b786ea56" + }, + { + "name": "group/15", + "where": { + "groupId": 15 + }, + "rows": 30, + "digest": "92732cbb84728a9ea104228c34acd5a3bb68403103f03c3ad2e28a616f76130c" + }, + { + "name": "group/11", + "where": { + "groupId": 11 + }, + "rows": 20, + "digest": "e461d6175a83eacafd9645e27b93f47de96d86b8dd4346b4eab46a5e8fb4e59a" + }, + { + "name": "group/17", + "where": { + "groupId": 17 + }, + "rows": 90, + "digest": "7ca93ce8812a9a53d79a7335d6b22971fd7c16f6d6689dca8fad122276a40e56" + }, + { + "name": "group/23", + "where": { + "groupId": 23 + }, + "rows": 20, + "digest": "12479b9a1c605051047f2a952446e93607e3fbce00d53bcf8ae8947176275361" + }, + { + "name": "group/2", + "where": { + "groupId": 2 + }, + "rows": 30, + "digest": "2f2449816b629d81a38314063c1085dcaae12c5e830b68b66678262f13612fd2" + }, + { + "name": "group/35", + "where": { + "groupId": 35 + }, + "rows": 56, + "digest": "49153ba902aee3222533cc324919524ceb90322628342bb0c5847eee2bf0b2c2" + }, + { + "name": "group/5", + "where": { + "groupId": 5 + }, + "rows": 12, + "digest": "925a95885cc28a1362f08f197a927eceeb7cec026334d606330843031fde10c8" + }, + { + "name": "group/16", + "where": { + "groupId": 16 + }, + "rows": 12, + "digest": "0f38f5fb8d8e9d89d69c4528264d28b61a172c5bf92d97c4c18bf2c0de0b00fe" + }, + { + "name": "group/36", + "where": { + "groupId": 36 + }, + "rows": 72, + "digest": "872fdb6931a8209697d1bc9de223198f4c38fd570203ac1ba794bc4c682a127e" + }, + { + "name": "group/29", + "where": { + "groupId": 29 + }, + "rows": 20, + "digest": "c20b6d3ece40d2a0aff5534765db40522d3afb63718ee66daf5f0b023d3c4eca" + }, + { + "name": "group/40", + "where": { + "groupId": 40 + }, + "rows": 42, + "digest": "ed7614efe8400db127041701d09eb9c481f88f7fbc25ccfdaf28dfe519b1f9da" + }, + { + "name": "group/41", + "where": { + "groupId": 41 + }, + "rows": 20, + "digest": "90a5e8ee8e61881547653995753d84234e89bfc1b75cd5db693abc9b9730eb92" + }, + { + "name": "group/30", + "where": { + "groupId": 30 + }, + "rows": 30, + "digest": "0bfa27de0c082d299f5513b4f4d6d674e57c84ef209016710d1e8731ae864a51" + }, + { + "name": "group/37", + "where": { + "groupId": 37 + }, + "rows": 20, + "digest": "6ac3ab59ca720521959c1e8c8e5b6d23f83b18aa99479e7322d864761317235a" + }, + { + "name": "group/38", + "where": { + "groupId": 38 + }, + "rows": 68, + "digest": "3fcf7446cad20f131b0f9895f152e522708259c149bf798a7787387989b2bc85" + }, + { + "name": "group/42", + "where": { + "groupId": 42 + }, + "rows": 70, + "digest": "da271da22f0724f936a6a1e3f6dafad613a742a3ac1fc0a8b98c58d81ec98fb6" + }, + { + "name": "group/46", + "where": { + "groupId": 46 + }, + "rows": 56, + "digest": "d012e40ee1560dd284a18432dfcd9683247e9745f10648830df6223b19788b95" + }, + { + "name": "group/32", + "where": { + "groupId": 32 + }, + "rows": 28, + "digest": "c3a11ad726c8dd51e556e622aa66bee09054378755ae49e6012fdf822b27565d" + }, + { + "name": "group/39", + "where": { + "groupId": 39 + }, + "rows": 42, + "digest": "f6ebdc191628c8b40baf9096f3d675be9bc9502373775bfadd36ba0f5788823e" + }, + { + "name": "group/43", + "where": { + "groupId": 43 + }, + "rows": 12, + "digest": "6548ff744b134504420bc0c337c32989b37ce82aa2376bc1d5b8aa42eb7db99f" + }, + { + "name": "group/31", + "where": { + "groupId": 31 + }, + "rows": 12, + "digest": "06d3d07ba490c3fe5091ccaf64b246c9ad5775b166209eabf57e4c2dc6a553a9" + }, + { + "name": "group/44", + "where": { + "groupId": 44 + }, + "rows": 30, + "digest": "1caa419042854fa87ee35e3d5d4d1ff1a31b4de23ffd808955ec7cade4470351" + }, + { + "name": "group/33", + "where": { + "groupId": 33 + }, + "rows": 56, + "digest": "acf669b3baf8f746eb0c5e33a78aae4d5ea804cef4ea02dc264f55e9376f0947" + }, + { + "name": "group/47", + "where": { + "groupId": 47 + }, + "rows": 72, + "digest": "a43d28d45a8ce057c0f98eee6606534d6a5a2c1d36d35efdc4ad8d084dc4f57f" + }, + { + "name": "group/45", + "where": { + "groupId": 45 + }, + "rows": 72, + "digest": "8dbfb5f4ced11e20882f4d610fe330ec51eb0dc43156fbd5ffb81e78a8857c90" + }, + { + "name": "group/48", + "where": { + "groupId": 48 + }, + "rows": 30, + "digest": "5135525821c82769fca296ec9a480beeaa0267836666f5212eff30f2429d3f88" + }, + { + "name": "group/53", + "where": { + "groupId": 53 + }, + "rows": 12, + "digest": "202dffa0cef9bbb023cebadf652af1663bdcea4e13141e71bb43b2df87a3ce4a" + }, + { + "name": "group/60", + "where": { + "groupId": 60 + }, + "rows": 30, + "digest": "0b6e0ebecc2a6f669f47d7100788e41bb3bf11bbe9af1ab0b79e3bbd493e85f1" + }, + { + "name": "group/49", + "where": { + "groupId": 49 + }, + "rows": 12, + "digest": "180601ec417cd680ce179b6e2e623811b00c47d3e8c1b7537b6b456037f2c6ef" + }, + { + "name": "group/54", + "where": { + "groupId": 54 + }, + "rows": 12, + "digest": "af58cc14e302a74b6afb092b76f4119a576855c7ed49f6827d09c13abcfd8d91" + }, + { + "name": "group/50", + "where": { + "groupId": 50 + }, + "rows": 20, + "digest": "64e4eedcabdc22c8fa571e666c6bc8615a4948c1fd06347cd6b83195846da31a" + }, + { + "name": "group/52", + "where": { + "groupId": 52 + }, + "rows": 12, + "digest": "cd2778d48f14aa00c53fc9a01332be1c4cd4eb9d196a82c3d7ebe747decdce22" + }, + { + "name": "group/77", + "where": { + "groupId": 77 + }, + "rows": 30, + "digest": "f35530bab13e75f37a3a2198648026240bc7e2a9caee8236a172958fb7f90fbf" + }, + { + "name": "group/51", + "where": { + "groupId": 51 + }, + "rows": 20, + "digest": "35566efabaf9a9a82f09ee2094489f780e79c89ead61d521b7b33552d247bc59" + }, + { + "name": "group/57", + "where": { + "groupId": 57 + }, + "rows": 12, + "digest": "7659db726ce64027b472e087a894013c6b10b4cf0648b3e5cfeded25bf1345d8" + }, + { + "name": "group/66", + "where": { + "groupId": 66 + }, + "rows": 56, + "digest": "4dd4f8885d245dae484cacad712a2e68c5a9b0d9a6fe3ebfef2113f27ba050ed" + }, + { + "name": "group/71", + "where": { + "groupId": 71 + }, + "rows": 12, + "digest": "53f45d44d43702f3f2e9ed1710307d5b60800f6badd4c976217d07bef70588c9" + }, + { + "name": "group/56", + "where": { + "groupId": 56 + }, + "rows": 42, + "digest": "d4081dd1363dd7455f1d543254ecf1a4bc6a115b1caf5271101ea7dc260f7552" + }, + { + "name": "group/55", + "where": { + "groupId": 55 + }, + "rows": 28, + "digest": "8a1397071b8cd90386522ed904e3ceb19acb8afa563cd96d2ac8c9814b9bb0e7" + }, + { + "name": "group/82", + "where": { + "groupId": 82 + }, + "rows": 60, + "digest": "e5054027e0e7be02104ee618a74986e5d8698fd0e3fccdf0d9014bed7cd3fb2c" + }, + { + "name": "group/67", + "where": { + "groupId": 67 + }, + "rows": 12, + "digest": "1054e717c1b6687bc27029a83f047283ec448e0c05201fcd70eaf485e016e090" + }, + { + "name": "group/72", + "where": { + "groupId": 72 + }, + "rows": 30, + "digest": "b79adefe94de70c9bdc35ee182d348d8c2ff30a4ace996308d789036467224ea" + }, + { + "name": "group/58", + "where": { + "groupId": 58 + }, + "rows": 30, + "digest": "4a7fd2667107cd56b58172128050982c7fbef3375c75706873e8333151b25527" + }, + { + "name": "group/68", + "where": { + "groupId": 68 + }, + "rows": 42, + "digest": "2a841dd5ab8ff2664f620a9ea1a40e12cb4e3550833a20b9300389b9bb270287" + }, + { + "name": "group/78", + "where": { + "groupId": 78 + }, + "rows": 12, + "digest": "e295689301f15f9f5eb405a45037f0c2af56dc2dc707b5a4d7d82405ec4b7b23" + }, + { + "name": "group/83", + "where": { + "groupId": 83 + }, + "rows": 20, + "digest": "40fb31c23a852b989124c89d03cc830f50dbd09bf785db4ee1d153dfcadbaf21" + }, + { + "name": "group/73", + "where": { + "groupId": 73 + }, + "rows": 56, + "digest": "b0f425d1414f284bceb882e48a1e56a92680fa6a14c878a610d674b8d0f30852" + }, + { + "name": "group/79", + "where": { + "groupId": 79 + }, + "rows": 12, + "digest": "62e4fbcd2b5cd23d0b4af027db1294943311908b3fc75219c78a4c997413c5ca" + }, + { + "name": "group/84", + "where": { + "groupId": 84 + }, + "rows": 20, + "digest": "656132deaa4e22f7e1862cff36eb37d6f1f8314137f057a0a2a4b9190a1ea660" + }, + { + "name": "group/85", + "where": { + "groupId": 85 + }, + "rows": 10, + "digest": "1b72e303d9acc64d089da0fe97488e06ed3ffd102e0055082880c0e6b085dd02" + }, + { + "name": "group/69", + "where": { + "groupId": 69 + }, + "rows": 30, + "digest": "6ecceae8a5587e1da412357f5dc6a9e90dee69da8563dc8b5a367f6fc8d4bd0a" + }, + { + "name": "group/74", + "where": { + "groupId": 74 + }, + "rows": 12, + "digest": "242938ef69187cbc02cc49cbaa37771e4c445454418c302082ed1fc3b1caf7a9" + }, + { + "name": "group/103", + "where": { + "groupId": 103 + }, + "rows": 90, + "digest": "ec217cc3da0f4f32cfe67a5d0ce0c6f062dc419a5f963def497728ed309aaa71" + }, + { + "name": "group/105", + "where": { + "groupId": 105 + }, + "rows": 20, + "digest": "11ecbd70e989f68e9d03ee62a62113c0beb016bd59ae6514debc435a2092c05a" + }, + { + "name": "group/132", + "where": { + "groupId": 132 + }, + "rows": 12, + "digest": "f6332719e7c4ec364a635583ad453c6c1fceaaeadad6010e80779a90e017c31f" + }, + { + "name": "group/147", + "where": { + "groupId": 147 + }, + "rows": 30, + "digest": "8d64b866c53ee618350388127e1200ebb910c058aef32332afdbed4f81ce04b6" + }, + { + "name": "group/81", + "where": { + "groupId": 81 + }, + "rows": 56, + "digest": "6f1d3289b77d193b1c7adde9bac2fafaf05600d42ccd2a877262e00d8f88cbda" + }, + { + "name": "group/107", + "where": { + "groupId": 107 + }, + "rows": 12, + "digest": "78fed1d29ef064cdb0561bf24d0bde0a52dfcb99a2c49bb2e668944940ee299f" + }, + { + "name": "group/87", + "where": { + "groupId": 87 + }, + "rows": 42, + "digest": "38407062225107b0868ef379ab6733289839dc2a5608a4d47753be4f69c0ae52" + }, + { + "name": "group/90", + "where": { + "groupId": 90 + }, + "rows": 42, + "digest": "0da0cd247538801d1834e0394f4f2176d08ba645fec26cbbd92afbc4987ac343" + }, + { + "name": "group/21", + "where": { + "groupId": 21 + }, + "rows": 72, + "digest": "67d480b8fd8ff2edb515ffdc1c0d31db32bc79c70db637f29263d0d25907c5fa" + }, + { + "name": "group/75", + "where": { + "groupId": 75 + }, + "rows": 30, + "digest": "aa3fe50b9239e12f266888e5e6ff2998ce249682f0303c20080928b3d6edb00d" + }, + { + "name": "group/64", + "where": { + "groupId": 64 + }, + "rows": 42, + "digest": "d0b953b56644e8ac10f684558ecaf980388076707c1aa95e40d570f9bc938ef0" + }, + { + "name": "group/123", + "where": { + "groupId": 123 + }, + "rows": 42, + "digest": "7aa419859ce85d99beaf5a0c1b84e33c36780b429f8415c67eeb1e8885e9521b" + }, + { + "name": "group/140", + "where": { + "groupId": 140 + }, + "rows": 12, + "digest": "a0a0505fad8751213d098bc38217a54ab8d8b1e6e220e31bd33246e0da661025" + }, + { + "name": "group/139", + "where": { + "groupId": 139 + }, + "rows": 42, + "digest": "85e460d560b1f346d29f506496e5cfd18b1c503328e9f5a3802e2aaba1e3f420" + }, + { + "name": "group/91", + "where": { + "groupId": 91 + }, + "rows": 12, + "digest": "2f76cd6a78e4acd0c1431eb0fcdc023db91e6fe88be8e4941de9a0671871f1af" + }, + { + "name": "group/101", + "where": { + "groupId": 101 + }, + "rows": 56, + "digest": "8a756e4b224c935b27969ebb6125ef4be179172106683731e394f184d9783423" + }, + { + "name": "group/126", + "where": { + "groupId": 126 + }, + "rows": 12, + "digest": "edccd01e72e143386ee6290732a8419dc1097b2a36075b1b9a197b99d6f08e3e" + }, + { + "name": "group/136", + "where": { + "groupId": 136 + }, + "rows": 12, + "digest": "936bfc9c33fe1d6072c61ce9f6629a3a6ebc56532a029782b8dabde309dd8d3b" + }, + { + "name": "group/25", + "where": { + "groupId": 25 + }, + "rows": 20, + "digest": "bc13eca380f2ba33d5a15715d7766bf1129ccfe03214661a0a00033e22f4bcc4" + }, + { + "name": "group/80", + "where": { + "groupId": 80 + }, + "rows": 30, + "digest": "6609b03ce0f9f80dee662ee3c8898520daa0ed138d0b2bc13f689fc9a412ecda" + }, + { + "name": "group/97", + "where": { + "groupId": 97 + }, + "rows": 30, + "digest": "c2d0771d81e449cfb96526bbf0e7374599a09917197256d84596443db39628b8" + }, + { + "name": "group/117", + "where": { + "groupId": 117 + }, + "rows": 56, + "digest": "d2f6df4ef026e3b70c22a04d48e7f73aa09ea5a8da1121e7d48bb0123f2a68bd" + }, + { + "name": "group/127", + "where": { + "groupId": 127 + }, + "rows": 30, + "digest": "5db46905ccf9e782d90a95edf2e1a8aa3c817a80587b99518f6c058cb306e526" + }, + { + "name": "group/129", + "where": { + "groupId": 129 + }, + "rows": 12, + "digest": "a7ca11fff97e5c8f0b29ca91eb25d816ce73553553f7844f1ae57ac6029145ab" + }, + { + "name": "group/92", + "where": { + "groupId": 92 + }, + "rows": 12, + "digest": "a8826515e53d7ff6bd5bdf3634fdda532132c4cef9863cf030b3313c7a671c44" + }, + { + "name": "group/94", + "where": { + "groupId": 94 + }, + "rows": 42, + "digest": "5583d6b816da95751075d47f5d3d7edbc19cdce4e5d169332eabcadf64dc694e" + }, + { + "name": "group/128", + "where": { + "groupId": 128 + }, + "rows": 50, + "digest": "a61bf36c8747d0e78b84db6ca3b00b8c433e8a29b4eeb734d20d19494b42f26c" + }, + { + "name": "group/141", + "where": { + "groupId": 141 + }, + "rows": 12, + "digest": "196e81cfe1f2526cd24cfd9854be9bee0d8154fa2a04ac9748e6cb8407e82b98" + }, + { + "name": "group/113", + "where": { + "groupId": 113 + }, + "rows": 90, + "digest": "2f78b293082c2ba0d78624f770c588413871dad94d36ec9570aada8da1b5e781" + }, + { + "name": "group/102", + "where": { + "groupId": 102 + }, + "rows": 56, + "digest": "c684238fcf92057d22c3bc863457b5ccb26fca79a01f7779b65a75bb60d642dc" + }, + { + "name": "group/109", + "where": { + "groupId": 109 + }, + "rows": 20, + "digest": "1f208821d2cdc4264004f095a25a23c9fea4a1b957e6b857c7c5bfb61935888c" + }, + { + "name": "group/121", + "where": { + "groupId": 121 + }, + "rows": 42, + "digest": "8dda88e19811edf918ad0f00240eae5a832c6050828ad1429aa075341e9ace4b" + }, + { + "name": "group/144", + "where": { + "groupId": 144 + }, + "rows": 30, + "digest": "f712fde6906c6919434510be715a7f6a5e8c97c76a108406981edce4159f7673" + }, + { + "name": "group/114", + "where": { + "groupId": 114 + }, + "rows": 12, + "digest": "93466e914ae21caee5e1c1757c5cc22f58019ff8d1c7abfff664e1cd10b248be" + }, + { + "name": "group/99", + "where": { + "groupId": 99 + }, + "rows": 42, + "digest": "bd4147450f8a1b06924a79423eebc02651b6c48b8d64907baa83d9ce626f7d8a" + }, + { + "name": "group/112", + "where": { + "groupId": 112 + }, + "rows": 20, + "digest": "4ebeef0726d703a5c5875d05fe3eb0cee5b09c47220753db84c89494c6efacca" + }, + { + "name": "group/88", + "where": { + "groupId": 88 + }, + "rows": 20, + "digest": "81839e454d7ed1f859471c3d20cb95566ffab92cd3431d16b2d85e443fbffc05" + }, + { + "name": "group/143", + "where": { + "groupId": 143 + }, + "rows": 30, + "digest": "36a16aea44277c5a1a4705f57259b8fcda0e195c88ae32b2ec7f32d09e29bcc2" + }, + { + "name": "group/22", + "where": { + "groupId": 22 + }, + "rows": 56, + "digest": "c98ea1577d9c2827c1d8199276cf9a1ff2766100148d835c8fd18d70e267d0a9" + }, + { + "name": "group/70", + "where": { + "groupId": 70 + }, + "rows": 20, + "digest": "61cbc29928638e468f5e57b13016ea77aed09abfd1d9bc482ddc314d58adea78" + }, + { + "name": "group/93", + "where": { + "groupId": 93 + }, + "rows": 20, + "digest": "881ec1a62e3b055ffd9de4d853dcad66f01f7a5242a81309a36db1d23528459d" + }, + { + "name": "group/116", + "where": { + "groupId": 116 + }, + "rows": 42, + "digest": "3ca2bc7af7d9f921ec101bfd112a2ab7d4bbc10c58d608c2f14bc9e722345a4d" + }, + { + "name": "group/89", + "where": { + "groupId": 89 + }, + "rows": 20, + "digest": "70fc3602751899d70f0810a9dec4ec856fc8dcd6579d42ab5c875a470f788b30" + }, + { + "name": "group/12", + "where": { + "groupId": 12 + }, + "rows": 12, + "digest": "4e6291f01bb147203ec39679e6423aaa0e51eee51e5947eec29eed679c4acb5f" + }, + { + "name": "group/62", + "where": { + "groupId": 62 + }, + "rows": 12, + "digest": "96d88a4ecaa02097e3cf708e24e6ed21786fe94dc29cea8f9618070029262b00" + }, + { + "name": "group/104", + "where": { + "groupId": 104 + }, + "rows": 46, + "digest": "afe1a86074b0dedd76d80475617dc1b17ab8d2d7ff46c99c96dad214ceb365c1" + }, + { + "name": "group/124", + "where": { + "groupId": 124 + }, + "rows": 42, + "digest": "15663d6b1c1a448968acd6b8e147d89cf21e1b1216c9aeeb6e1a8c2c419b8bba" + }, + { + "name": "group/135", + "where": { + "groupId": 135 + }, + "rows": 30, + "digest": "5c2efc9193f7d9be65d7fb15d9e2168c6b341c9a6c88d3f1db93362d3e8da2c9" + }, + { + "name": "group/13", + "where": { + "groupId": 13 + }, + "rows": 42, + "digest": "dc35aac3a7cb42006eefbbb2606b5e69c1d2ec5fb87ea61a2de29862a70a21d0" + }, + { + "name": "group/76", + "where": { + "groupId": 76 + }, + "rows": 90, + "digest": "21fd895fa1e8583739b24e783f6624afb484e0e9f0aca1be749f07fb8d381353" + }, + { + "name": "group/115", + "where": { + "groupId": 115 + }, + "rows": 12, + "digest": "59beebf7d76cbd7ec25afb0e1aff22eddef7810c8367d4a25ea36f9b8818c8ce" + }, + { + "name": "group/130", + "where": { + "groupId": 130 + }, + "rows": 12, + "digest": "eea5040020933fde3dfd31ee0ecf0fe85398e0d5cb9b627aba63461530155172" + }, + { + "name": "group/134", + "where": { + "groupId": 134 + }, + "rows": 12, + "digest": "13ee49f65366f1dcaf92e4b3f82e184abbc46acd8d90e4e68aaccba627c72365" + }, + { + "name": "group/28", + "where": { + "groupId": 28 + }, + "rows": 42, + "digest": "65ec25804af438ff4b254790f089c44b20d10e7cc98f1eff567c223b16c56863" + }, + { + "name": "group/95", + "where": { + "groupId": 95 + }, + "rows": 30, + "digest": "0cfc6c05622cf8a4c35ee1b64c3524187c4a6e47cc0630a4777526e04df2b66d" + }, + { + "name": "group/108", + "where": { + "groupId": 108 + }, + "rows": 30, + "digest": "b671ff898fd16689c12ec2accfd04cd4e4efb47157bfcdd0c949baf9f8fadd38" + }, + { + "name": "group/118", + "where": { + "groupId": 118 + }, + "rows": 42, + "digest": "7498cdfbf8d699b1fd1c31c5092bdc4b732436878c38e076f7f91b65354ff8ab" + }, + { + "name": "group/138", + "where": { + "groupId": 138 + }, + "rows": 12, + "digest": "5e78307499a9269e124ad5d6e81aa35225afd3d440f75084fea5c5c2b86e6130" + }, + { + "name": "group/146", + "where": { + "groupId": 146 + }, + "rows": 90, + "digest": "1f66c818a3fc13787e2dcd6dd0877110f2412dc65785e0ae7a685481b28d9ca3" + }, + { + "name": "group/59", + "where": { + "groupId": 59 + }, + "rows": 30, + "digest": "e4921b6766bf2975e190fef6d07be1579d7af46c68b0caa212009e61d2a2ce62" + }, + { + "name": "group/98", + "where": { + "groupId": 98 + }, + "rows": 70, + "digest": "c9496ee11bdebbf852caa9689e6105c812c0f7d5fa0b1a5ad9ad71b3e5f46db7" + }, + { + "name": "group/111", + "where": { + "groupId": 111 + }, + "rows": 30, + "digest": "3906dea0331c41495c2855ade33ac573945d9790e65b9885861ff4e5709f4287" + }, + { + "name": "group/137", + "where": { + "groupId": 137 + }, + "rows": 12, + "digest": "1163853dcf0235822633a13577f33069e455d8e12cceba9e31cb2c6c108a33a7" + }, + { + "name": "group/24", + "where": { + "groupId": 24 + }, + "rows": 12, + "digest": "66ccb82231ee7db09c878fd84775eb68bd3d75e393e05d9242b4f71c3a5339c3" + }, + { + "name": "group/106", + "where": { + "groupId": 106 + }, + "rows": 12, + "digest": "1a37d4221c8987a2da35b7bb1e30aa0f0a65f021c22e799b639c8bacfbf6df64" + }, + { + "name": "group/120", + "where": { + "groupId": 120 + }, + "rows": 42, + "digest": "e0b739d9a4a94054d8bcef5b6c68e1774e2de2250cebf96cf3869c56648afd3e" + }, + { + "name": "group/133", + "where": { + "groupId": 133 + }, + "rows": 30, + "digest": "5338b28d7dea89dde2b9cb4c7ca2483f0afd8ef87542b9aa8337e62b86d65439" + }, + { + "name": "group/148", + "where": { + "groupId": 148 + }, + "rows": 12, + "digest": "358ecef2b4f82b4aa091bc6357d059c50fadc2754217c11cbf99e7e475b2cbc0" + }, + { + "name": "group/65", + "where": { + "groupId": 65 + }, + "rows": 48, + "digest": "eaed16886921a23a4663dae6791ce80ee2598f355805f013bc0d31ef57cd6975" + }, + { + "name": "group/63", + "where": { + "groupId": 63 + }, + "rows": 30, + "digest": "baf779b6e992d43ada720cc81a6b3a1bd300d6ed6698bd199880cd54501720e0" + }, + { + "name": "group/110", + "where": { + "groupId": 110 + }, + "rows": 56, + "digest": "043a110f271dbd1e186d8ddea8d32dac251d00bbaba817151cfb6464bf179f95" + }, + { + "name": "group/119", + "where": { + "groupId": 119 + }, + "rows": 50, + "digest": "07103fd490b38aef2078333c4033b83524abf946f45649063c6c8a18f793447e" + }, + { + "name": "group/27", + "where": { + "groupId": 27 + }, + "rows": 20, + "digest": "855c55bf95c26de137df4c5cd337575f1f7c58d3defc50fcaef2f559a9a95873" + }, + { + "name": "group/100", + "where": { + "groupId": 100 + }, + "rows": 30, + "digest": "96493a32c22f6f0b6f6094bf098618bc55b858bb75d797ac6b5b0e98cd29e2db" + }, + { + "name": "group/86", + "where": { + "groupId": 86 + }, + "rows": 12, + "digest": "6f86896defafc5890ed95c27aea8dafd7053a188e1015c73af78915ca96b2c72" + }, + { + "name": "group/125", + "where": { + "groupId": 125 + }, + "rows": 30, + "digest": "dc82983386f0ec68e71e80a4ded0f3d89a125504da25e981e53432ca7d678f97" + }, + { + "name": "group/149", + "where": { + "groupId": 149 + }, + "rows": 80, + "digest": "5cb22b8829159223166ee73104c1afaf8d415e97f9e6c46c213688a6993710b7" + }, + { + "name": "group/61", + "where": { + "groupId": 61 + }, + "rows": 12, + "digest": "019106a2de0eaa0cf6a7196bd4d19f3e2075e088ecfba6be801365308d0adfcb" + }, + { + "name": "group/96", + "where": { + "groupId": 96 + }, + "rows": 40, + "digest": "50bbbf9f90d1eed4a25a0446c1745154c1f08ca34a80bd06dd6e4d79064b1c7b" + }, + { + "name": "group/122", + "where": { + "groupId": 122 + }, + "rows": 42, + "digest": "25b19426ab139f7827fd02cfd513125546f7e6ea831efe6aa31bdcd0be56c91a" + }, + { + "name": "group/131", + "where": { + "groupId": 131 + }, + "rows": 20, + "digest": "3f80b6ad20506aec4dd1a47db20c5e0b16466d24a211362a618619626c619afb" + }, + { + "name": "group/145", + "where": { + "groupId": 145 + }, + "rows": 82, + "digest": "f9ba78f76bab6b8c609277ee95dce8d5ec687be2460ab2cc8beabcc17cbe57e1" + }, + { + "name": "group/142", + "where": { + "groupId": 142 + }, + "rows": 30, + "digest": "614d789a464982642b5951eb1e785e06dfcdc5c501a7cca1ad95c0a2cd232048" + }, + { + "name": "pair/0/5", + "where": { + "userId": 0, + "friendId": 5 + }, + "rows": 3, + "digest": "a4c00b80598e0c4090a0c1627d9bb41c41d579f5cc0a7b593b7bfe76479ff7bb" + }, + { + "name": "pair/0/8", + "where": { + "userId": 0, + "friendId": 8 + }, + "rows": 3, + "digest": "8d820368d36207b915a788bd6f98e987f3d8aacd6bfd761b46180af1265f9aee" + }, + { + "name": "pair/0/12", + "where": { + "userId": 0, + "friendId": 12 + }, + "rows": 1, + "digest": "2dfa1352fcc7db3fc4ff051b709bceb26e5e8fa3acadf86f68254c2b12a248d1" + }, + { + "name": "pair/0/13", + "where": { + "userId": 0, + "friendId": 13 + }, + "rows": 3, + "digest": "e8ab2d34df4832d9bd6ce5fe39138c150fda50d94dd4ecc753ffb1bb4fd97fa6" + }, + { + "name": "pair/0/14", + "where": { + "userId": 0, + "friendId": 14 + }, + "rows": 1, + "digest": "96229ec695723cdd13934e6b11fe580b3576376620f8c9ce86f6ecf95e97f35d" + }, + { + "name": "pair/0/20", + "where": { + "userId": 0, + "friendId": 20 + }, + "rows": 1, + "digest": "d0dc9b6bf685e3d1df337f8fb8bc9c3402b32d97ac8d607dc372c62d100c5246" + }, + { + "name": "pair/0/21", + "where": { + "userId": 0, + "friendId": 21 + }, + "rows": 1, + "digest": "ad29043b4bcc4f9e539bc9d1fd842761b6e0160c52771f230ce8aa8bdeaefd05" + }, + { + "name": "pair/0/23", + "where": { + "userId": 0, + "friendId": 23 + }, + "rows": 1, + "digest": "798c60cd0456d66527cce9627b6963c64e607d42822b2d476a6b9e766cf247c3" + }, + { + "name": "pair/0/24", + "where": { + "userId": 0, + "friendId": 24 + }, + "rows": 2, + "digest": "8790f4a098d685440db9de6bf04d61b4968f1baf4cc93826ce8d2e418f92b82e" + }, + { + "name": "pair/0/28", + "where": { + "userId": 0, + "friendId": 28 + }, + "rows": 1, + "digest": "ca64d78e78dab4e578d54d4f11743e107d25403e723441ae058eb4bcde4d75ef" + }, + { + "name": "pair/0/30", + "where": { + "userId": 0, + "friendId": 30 + }, + "rows": 2, + "digest": "a8ff1f08aae9ca94238b6ba71deaed0ad7e0a5e0d494780419c4e427553ad27c" + }, + { + "name": "pair/0/31", + "where": { + "userId": 0, + "friendId": 31 + }, + "rows": 1, + "digest": "b502e7d616aa1cdbf43faa4a6cbb5c8ab95eaf1f69846b9bdebbaed3a1a003a2" + }, + { + "name": "pair/0/32", + "where": { + "userId": 0, + "friendId": 32 + }, + "rows": 3, + "digest": "99fcbb4174f94ddf56ae0d89d0ad283bd1a225f0d37415bd77ea4ea6f3637d5d" + }, + { + "name": "pair/0/34", + "where": { + "userId": 0, + "friendId": 34 + }, + "rows": 1, + "digest": "02cc2c52f07b3e75f4af1b87d9bca28ae4c96a876dc431175ef2f431c0c67de1" + }, + { + "name": "pair/0/36", + "where": { + "userId": 0, + "friendId": 36 + }, + "rows": 1, + "digest": "d873757840efac7974b5dd2fc80a5bad6b485e76147534dcd96723f071689d2d" + }, + { + "name": "pair/0/44", + "where": { + "userId": 0, + "friendId": 44 + }, + "rows": 1, + "digest": "1bd1614b6419bdcca19eb3843ea8d00e5eeef357a2438e789e20bc65dd540dd6" + }, + { + "name": "pair/0/45", + "where": { + "userId": 0, + "friendId": 45 + }, + "rows": 1, + "digest": "43c49490dd6a4383ab18371a2eb683688495c3725f5f5a74acad8d76242a24f8" + }, + { + "name": "pair/0/47", + "where": { + "userId": 0, + "friendId": 47 + }, + "rows": 3, + "digest": "c136db03e19917b9c2212172617d87aceeb7259822abc217f9facedfd1885e08" + }, + { + "name": "pair/0/49", + "where": { + "userId": 0, + "friendId": 49 + }, + "rows": 1, + "digest": "a7f1dcf267eea2d7cd0f66c36b17734a438e47f9082c936490b8d8d6e52f81e1" + }, + { + "name": "pair/0/50", + "where": { + "userId": 0, + "friendId": 50 + }, + "rows": 2, + "digest": "09c535d65c20694451ded615850173783c64afd097591659cb2ccba36ea9bcb9" + }, + { + "name": "pair/0/51", + "where": { + "userId": 0, + "friendId": 51 + }, + "rows": 1, + "digest": "7597962a06ff46a6834b5b2dd6d71cd4e0d64718e1ef637f166384f27922801e" + }, + { + "name": "pair/0/52", + "where": { + "userId": 0, + "friendId": 52 + }, + "rows": 1, + "digest": "d683e17c08d3f151290ec3f36ef22006b7b875abc0be30ddebd4849ec5f7ab92" + }, + { + "name": "pair/0/54", + "where": { + "userId": 0, + "friendId": 54 + }, + "rows": 1, + "digest": "8059e182eeb3dfacbff8939576c760bb49fc43ceac9fcc89bb30d9c6ca8c2c72" + }, + { + "name": "pair/0/55", + "where": { + "userId": 0, + "friendId": 55 + }, + "rows": 2, + "digest": "da83b00dc76bb4af0916c2d6469e6b9c59f32a635cbc5566fd697a486b140558" + }, + { + "name": "pair/0/61", + "where": { + "userId": 0, + "friendId": 61 + }, + "rows": 1, + "digest": "20efd9939f1527a63fe839a743745c60374af170c77ba96b0779dc7022a9ec81" + }, + { + "name": "pair/0/70", + "where": { + "userId": 0, + "friendId": 70 + }, + "rows": 1, + "digest": "e00d44644c48584a41caaca8b478ab28792560d2e43a4b1a73d01f220c8b09d6" + }, + { + "name": "pair/0/71", + "where": { + "userId": 0, + "friendId": 71 + }, + "rows": 1, + "digest": "c03afbe3568941cc259dd6782cf0920b7ed105de16fdf09b2d8f61ab22f9947a" + }, + { + "name": "pair/0/81", + "where": { + "userId": 0, + "friendId": 81 + }, + "rows": 1, + "digest": "70af528a9a15739c753050124d6af5b3097abeaac0de0bedc3c20c326eee6c7b" + }, + { + "name": "pair/0/84", + "where": { + "userId": 0, + "friendId": 84 + }, + "rows": 3, + "digest": "86971d390739513344e19344d789d1e90b5660b07874e6680e6045259957a6b8" + }, + { + "name": "pair/0/85", + "where": { + "userId": 0, + "friendId": 85 + }, + "rows": 2, + "digest": "62335b38158122bc839c20484bce41f2ef188fb52ff736603111f8fe57bfc2b8" + }, + { + "name": "pair/0/86", + "where": { + "userId": 0, + "friendId": 86 + }, + "rows": 1, + "digest": "5da8a424cb2c1bca180b4d1e229d041da1794f5de051e0a200c7e02771f33748" + }, + { + "name": "pair/0/92", + "where": { + "userId": 0, + "friendId": 92 + }, + "rows": 1, + "digest": "aa9a27be61d24b26dc7f1f157db59e5f19c6e793b15bf189f1880403183b3279" + }, + { + "name": "pair/0/93", + "where": { + "userId": 0, + "friendId": 93 + }, + "rows": 1, + "digest": "6658fd8296d063d7ccfdea806de7f331a6ac5bbcf3f3b5eb1ebd4f5ddedfcd90" + }, + { + "name": "pair/0/98", + "where": { + "userId": 0, + "friendId": 98 + }, + "rows": 1, + "digest": "af7101ef097b38cca9f7c171280b1f44f5259828cec7617242ac4d08a48a41aa" + }, + { + "name": "pair/0/104", + "where": { + "userId": 0, + "friendId": 104 + }, + "rows": 1, + "digest": "fae2718e74539f52135858576a6c8d8b6f62a8ca87b5a9101e53133dbbed78fa" + }, + { + "name": "pair/0/106", + "where": { + "userId": 0, + "friendId": 106 + }, + "rows": 2, + "digest": "0c6a625b7e68c3a7936a3715b4f5615a340b68773f145dc27c73fa8d04a49201" + }, + { + "name": "pair/0/111", + "where": { + "userId": 0, + "friendId": 111 + }, + "rows": 1, + "digest": "2d6c03d69c9fa6e934f8d623dae5f392fbce4cefbf6ecb4b6e85c86e6ff5c335" + }, + { + "name": "pair/0/115", + "where": { + "userId": 0, + "friendId": 115 + }, + "rows": 1, + "digest": "7a764d0d971ddb270ed835e0f4038da74dbfedb98f155ea06bc183dcb49f9fbd" + }, + { + "name": "pair/0/120", + "where": { + "userId": 0, + "friendId": 120 + }, + "rows": 1, + "digest": "b1c705dcaab0d770f28f543ed8e30caea2cac0ce53f6a48d67cbcdd3535db122" + }, + { + "name": "pair/0/121", + "where": { + "userId": 0, + "friendId": 121 + }, + "rows": 1, + "digest": "c2b98a2cc4257dceb2067452a6b4ead562686fd4b6d40aa52dc238e676394650" + }, + { + "name": "pair/0/129", + "where": { + "userId": 0, + "friendId": 129 + }, + "rows": 1, + "digest": "b372ce2c8041977888a73bc1ea823f28ab84e059570ec83b083281f6f6cf0c3f" + }, + { + "name": "pair/0/135", + "where": { + "userId": 0, + "friendId": 135 + }, + "rows": 1, + "digest": "75ddda6a210f2e66b3dcbc497ee522b3506f8be74854b46b57bac72d68496b2b" + }, + { + "name": "pair/0/136", + "where": { + "userId": 0, + "friendId": 136 + }, + "rows": 2, + "digest": "0d8e28d05d7fc2110c61b3daa51d3ad5aae4552d32a0b28bd5b408ccc98281ab" + }, + { + "name": "pair/0/139", + "where": { + "userId": 0, + "friendId": 139 + }, + "rows": 1, + "digest": "5ef7c1aeed84bebb844b1c81ea2c107646018235755f0701ba2dcd32ef9c1168" + }, + { + "name": "pair/0/141", + "where": { + "userId": 0, + "friendId": 141 + }, + "rows": 1, + "digest": "4df9dca12707c6ea6d54183184b97138feebfae7fbce3ec79f48a1e328d2d13f" + }, + { + "name": "pair/0/145", + "where": { + "userId": 0, + "friendId": 145 + }, + "rows": 2, + "digest": "5ec2437853a6e377e60a9e634dfca8783753cbd9e84d3e45149d199576d5b6d5" + }, + { + "name": "pair/0/147", + "where": { + "userId": 0, + "friendId": 147 + }, + "rows": 1, + "digest": "7eeb5e849440e8fbe18be6713558f0bb0dd588a2e6694b2ba6a97946cccbfeab" + }, + { + "name": "pair/1/4", + "where": { + "userId": 1, + "friendId": 4 + }, + "rows": 1, + "digest": "a54426259ea4b17dda44b91b75c3a7cb9a31e503bd5ac858235f8bf91e1ffb96" + }, + { + "name": "pair/1/7", + "where": { + "userId": 1, + "friendId": 7 + }, + "rows": 1, + "digest": "19a13e92f02571a3a1c61d5d01eea8c7a8d4f31e4c3a0d9505e015e441250b3d" + }, + { + "name": "pair/1/13", + "where": { + "userId": 1, + "friendId": 13 + }, + "rows": 1, + "digest": "c484c81504dc4f15d614703910942fea1a7acfcbb42f6c2438bed0c42ea18066" + } + ] +} diff --git a/src/server/api/services/balanceService.ts b/src/server/api/services/balanceService.ts new file mode 100644 index 000000000..28e3a811e --- /dev/null +++ b/src/server/api/services/balanceService.ts @@ -0,0 +1,93 @@ +import { type BalanceView, type Prisma } from '@prisma/client'; +import { TRPCError } from '@trpc/server'; +import { z } from 'zod'; + +import { simplifyDebts } from '~/lib/simplify'; + +export const balanceDateRangeSchema = z.object({ + fromDate: z.date().optional(), + toDateExclusive: z.date().optional(), +}); +type BalanceDateRange = z.infer; +type BalanceScope = Pick< + Prisma.BalanceEntryViewWhereInput, + 'userId' | 'friendId' | 'groupId' | 'currency' +>; +type BalanceClient = Pick; + +export const getBalanceDateRange = ( + defaultToDateExclusive: Date, + input?: BalanceDateRange, +): BalanceDateRange => { + const toDateExclusive = input?.toDateExclusive ?? defaultToDateExclusive; + if (input?.fromDate && input.fromDate >= toDateExclusive) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'fromDate must precede toDateExclusive' }); + } + return { fromDate: input?.fromDate, toDateExclusive }; +}; + +/** No date bounds means all history, including future-dated obligations. */ +export const getBalances = async ( + client: Pick, + { where, fromDate, toDateExclusive }: BalanceDateRange & { where?: BalanceScope } = {}, +): Promise => { + const rows = await client.balanceEntryView.groupBy({ + by: ['userId', 'friendId', 'groupId', 'currency'], + where: { + ...where, + expenseDate: { gte: fromDate, lt: toDateExclusive }, + }, + _sum: { amount: true }, + _min: { createdAt: true }, + _max: { updatedAt: true }, + }); + return rows.map(({ userId, friendId, groupId, currency, _sum: sum, _min: min, _max: max }) => ({ + userId, + friendId, + groupId, + currency, + amount: sum.amount ?? 0n, + // Every group has a contribution with non-null timestamps. + createdAt: min.createdAt!, + updatedAt: max.updatedAt!, + })); +}; + +/** Batch full group graphs before simplification, then select the viewer's rows. */ +export const getUserBalances = async ( + client: BalanceClient, + userId: number, + range: BalanceDateRange, +) => { + const raw = await getBalances(client, { ...range, where: { userId } }); + const groupIds = [...new Set(raw.flatMap((row) => (null === row.groupId ? [] : [row.groupId])))]; + const groups = + 0 === groupIds.length + ? [] + : await client.group.findMany({ + where: { id: { in: groupIds } }, + select: { id: true, name: true, simplifyDebts: true }, + }); + const simplifiedIds = groups.filter((group) => group.simplifyDebts).map((group) => group.id); + const graphs = + 0 === simplifiedIds.length + ? [] + : await getBalances(client, { + ...range, + where: { groupId: { in: simplifiedIds } }, + }); + const processed = [ + ...raw.filter((row) => null === row.groupId || !simplifiedIds.includes(row.groupId)), + ...simplifiedIds.flatMap((groupId) => + simplifyDebts(graphs.filter((row) => row.groupId === groupId)).filter( + (row) => row.userId === userId, + ), + ), + ]; + const groupMap = new Map(groups.map((group) => [group.id, group])); + return processed.map((row) => + Object.assign(row, { + group: null === row.groupId ? null : (groupMap.get(row.groupId) ?? null), + }), + ); +}; From 95ed410c93e52cf836692ebc2dd5ac59f9a2995c Mon Sep 17 00:00:00 2001 From: krokosik Date: Tue, 20 Oct 2026 21:03:37 +0200 Subject: [PATCH 3/4] Apply viewer-local date filters to balance APIs (#604) --- src/hooks/useBalanceDateRefresh.ts | 37 +++ src/lib/balanceDate.ts | 46 ++++ src/pages/_app.tsx | 2 + src/server/api/routers/expense.ts | 183 ++++++-------- src/server/api/routers/group.ts | 43 +++- src/server/api/routers/user.ts | 74 ++---- src/server/api/trpc.ts | 8 + src/tests/balanceDate.test.ts | 29 +++ src/tests/balanceDateRefresh.test.tsx | 66 +++++ .../balanceDates.integration.test.ts | 234 ++++++++++++++++++ src/tests/integration/trpc.ts | 8 +- src/utils/api.ts | 4 + 12 files changed, 564 insertions(+), 170 deletions(-) create mode 100644 src/hooks/useBalanceDateRefresh.ts create mode 100644 src/lib/balanceDate.ts create mode 100644 src/tests/balanceDate.test.ts create mode 100644 src/tests/balanceDateRefresh.test.tsx create mode 100644 src/tests/integration/balanceDates.integration.test.ts diff --git a/src/hooks/useBalanceDateRefresh.ts b/src/hooks/useBalanceDateRefresh.ts new file mode 100644 index 000000000..03dc23385 --- /dev/null +++ b/src/hooks/useBalanceDateRefresh.ts @@ -0,0 +1,37 @@ +import { useEffect } from 'react'; + +import { getBalanceDay } from '~/lib/balanceDate'; +import { api } from '~/utils/api'; + +/** Header-derived dates do not change query keys, so invalidate on scope changes. */ +export const useBalanceDateRefresh = () => { + const utils = api.useUtils(); + useEffect(() => { + const current = () => getBalanceDay(Intl.DateTimeFormat().resolvedOptions().timeZone); + let scope = current(); + let timer: ReturnType | undefined = undefined; + const refresh = () => { + const next = current(); + if (scope.day !== next.day || scope.timeZone !== next.timeZone) { + Promise.all([ + utils.expense.getBalances.invalidate(), + utils.expense.getCumulatedBalances.invalidate(), + utils.user.getBalancesWithFriend.invalidate(), + utils.group.getAllGroupsWithBalances.invalidate(), + utils.group.getGroupDetails.invalidate(), + ]).catch(console.error); + } + scope = next; + clearTimeout(timer); + timer = setTimeout(refresh, Math.max(1, scope.toDateExclusive.getTime() - Date.now())); + }; + refresh(); + window.addEventListener('focus', refresh); + document.addEventListener('visibilitychange', refresh); + return () => { + clearTimeout(timer); + window.removeEventListener('focus', refresh); + document.removeEventListener('visibilitychange', refresh); + }; + }, [utils]); +}; diff --git a/src/lib/balanceDate.ts b/src/lib/balanceDate.ts new file mode 100644 index 000000000..360c1f112 --- /dev/null +++ b/src/lib/balanceDate.ts @@ -0,0 +1,46 @@ +export const BALANCE_TIME_ZONE_HEADER = 'x-splitpro-time-zone'; + +export const getBalanceDay = ( + timeZone = 'UTC', + now = new Date(), +): { + timeZone: string; + day: string; + toDateExclusive: Date; +} => { + const formatter = (() => { + try { + return new Intl.DateTimeFormat('en-CA', { + timeZone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + }); + } catch { + return new Intl.DateTimeFormat('en-CA', { + timeZone: 'UTC', + year: 'numeric', + month: '2-digit', + day: '2-digit', + }); + } + })(); + const day = formatter.format(now); + // Search for the next calendar-day boundary rather than adding 24 hours. + // This also handles offset changes at midnight and skipped calendar dates. + const [, end] = Array.from({ length: 28 }).reduce<[number, number]>( + ([start, finish]) => { + if (1 >= finish - start) { + return [start, finish]; + } + const middle = Math.floor((start + finish) / 2); + return formatter.format(new Date(middle)) === day ? [middle, finish] : [start, middle]; + }, + [now.getTime(), now.getTime() + 48 * 60 * 60 * 1000], + ); + return { + timeZone: formatter.resolvedOptions().timeZone, + day, + toDateExclusive: new Date(end), + }; +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 330cc8e9e..e520f301a 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -15,6 +15,7 @@ import { ThemeProvider } from 'next-themes'; import { CurrencyHelpersProvider } from '~/contexts/CurrencyHelpersContext'; import { useAppStore } from '~/store/appStore'; import { api } from '~/utils/api'; +import { useBalanceDateRefresh } from '~/hooks/useBalanceDateRefresh'; import 'react-easy-crop/react-easy-crop.css'; import '~/styles/globals.css'; @@ -27,6 +28,7 @@ type AppPropsWithAuth = AppProps<{ session: Session | null }> & { Component: Pag const MyApp = ({ Component, pageProps: { session, ...pageProps } }: AppPropsWithAuth) => { const { t, ready } = useTranslation(); + useBalanceDateRefresh(); if (!ready) { return ( diff --git a/src/server/api/routers/expense.ts b/src/server/api/routers/expense.ts index ea821f392..558bc0a62 100644 --- a/src/server/api/routers/expense.ts +++ b/src/server/api/routers/expense.ts @@ -1,7 +1,6 @@ import { TRPCError } from '@trpc/server'; import { z } from 'zod'; -import { simplifyDebts } from '~/lib/simplify'; import { createTRPCRouter, groupProcedure, protectedProcedure } from '~/server/api/trpc'; import { db } from '~/server/db'; import { BigMath, currencyConversion } from '~/utils/numbers'; @@ -19,118 +18,98 @@ import { isCurrencyCode } from '~/lib/currency'; import { SplitType } from '@prisma/client'; import { DEFAULT_CATEGORY } from '~/lib/category'; import { getUserMap } from './user'; +import { + balanceDateRangeSchema, + getBalanceDateRange, + getBalances, + getUserBalances, +} from '../services/balanceService'; export const expenseRouter = createTRPCRouter({ - getCumulatedBalances: protectedProcedure.query(async ({ ctx }) => { - const cumulatedBalances = await db.balanceView.groupBy({ - by: ['currency'], - _sum: { amount: true }, - where: { userId: ctx.session.user.id, amount: { not: 0 } }, - orderBy: { _sum: { amount: 'desc' } }, - }); + getCumulatedBalances: protectedProcedure + .input(balanceDateRangeSchema.optional()) + .query(async ({ ctx, input }) => { + const balances = await getBalances(ctx.db, { + ...getBalanceDateRange(ctx.balanceDay.toDateExclusive, input), + where: { userId: ctx.session.user.id }, + }); + const totals = new Map(); + balances.forEach(({ currency, amount }) => + totals.set(currency, (totals.get(currency) ?? 0n) + amount), + ); + const cumulatedBalances = [...totals] + .map(([currency, amount]) => ({ currency, amount })) + .sort((a, b) => (a.amount > b.amount ? -1 : a.amount < b.amount ? 1 : 0)); - // oxlint-disable no-underscore-dangle -- Prisma's generated aggregate API uses the `_sum` field name. - const youOwe = cumulatedBalances - .filter((b) => b._sum.amount && 0 > b._sum.amount) - .map((b) => ({ currency: b.currency, amount: b._sum.amount! })) - .reverse(); - // oxlint-enable no-underscore-dangle + const youOwe = cumulatedBalances.filter((b) => 0n > b.amount).reverse(); + const youGet = cumulatedBalances.filter((b) => 0n < b.amount); - // oxlint-disable no-underscore-dangle -- Prisma's generated aggregate API uses the `_sum` field name. - const youGet = cumulatedBalances - .filter((b) => b._sum.amount && 0 < b._sum.amount) - .map((b) => ({ currency: b.currency, amount: b._sum.amount! })); - // oxlint-enable no-underscore-dangle + return { youOwe, youGet }; + }), - return { youOwe, youGet }; - }), + getBalances: protectedProcedure + .input(balanceDateRangeSchema.optional()) + .query(async ({ ctx, input }) => { + const userBalances = await getUserBalances( + ctx.db, + ctx.session.user.id, + getBalanceDateRange(ctx.balanceDay.toDateExclusive, input), + ); + const processedBalances = userBalances.filter( + (row) => !ctx.session.user.hiddenFriendIds.includes(row.friendId), + ); - getBalances: protectedProcedure.query(async ({ ctx }) => { - const rawBalances = await db.balanceView.findMany({ - where: { - userId: ctx.session.user.id, - friendId: { notIn: ctx.session.user.hiddenFriendIds }, - }, - include: { - group: { - select: { - simplifyDebts: true, + // Group by friendId and fetch user details + const friendIds = [...new Set([...processedBalances].map((b) => b.friendId))]; + const userMap = await getUserMap(friendIds); + + // Aggregate by (friendId, currency) since same pair can appear in multiple groups + const aggregated = processedBalances + .filter((b) => 0n !== b.amount) + .reduce>( + (acc, { friendId, currency, amount }) => { + const key = `${friendId}-${currency}`; + const existing = acc.get(key); + if (existing) { + existing.amount += amount; + } else { + acc.set(key, { friendId, currency, amount }); + } + return acc; }, - }, - }, - }); - - const processedBalances = await Promise.all( - rawBalances.map(async ({ friendId, currency, amount, groupId, group }) => { - if (!group?.simplifyDebts || null === groupId) { - return { friendId, currency, amount }; - } - - const allGroupBalances = await db.balanceView.findMany({ - where: { groupId, currency }, - }); - - const simplified = simplifyDebts(allGroupBalances); - const simplifiedBalance = simplified.find( - (b) => - b.userId === ctx.session.user.id && b.friendId === friendId && b.currency === currency, + new Map(), ); - return { friendId, currency, amount: simplifiedBalance?.amount ?? 0n }; - }), - ); - - // Group by friendId and fetch user details - const friendIds = [...new Set([...processedBalances].map((b) => b.friendId))]; - const userMap = await getUserMap(friendIds); - - // Aggregate by (friendId, currency) since same pair can appear in multiple groups - const aggregated = processedBalances - .filter((b) => 0n !== b.amount) - .reduce>( - (acc, { friendId, currency, amount }) => { - const key = `${friendId}-${currency}`; - const existing = acc.get(key); - if (existing) { - existing.amount += amount; - } else { - acc.set(key, { friendId, currency, amount }); - } - return acc; - }, - new Map(), - ); - - const balancesByFriend = [...aggregated.values()].reduce< - Record - >((acc, { friendId, currency, amount }) => { - if (!acc[friendId]) { - acc[friendId] = []; - } - acc[friendId].push({ currency, amount }); - return acc; - }, {}); + const balancesByFriend = [...aggregated.values()].reduce< + Record + >((acc, { friendId, currency, amount }) => { + if (!acc[friendId]) { + acc[friendId] = []; + } + acc[friendId].push({ currency, amount }); + return acc; + }, {}); - friendIds.forEach((friendId) => { - if (!balancesByFriend[friendId]) { - balancesByFriend[friendId] = []; - } - }); + friendIds.forEach((friendId) => { + if (!balancesByFriend[friendId]) { + balancesByFriend[friendId] = []; + } + }); - const balances = Object.entries(balancesByFriend) - .map(([friendId, currencies]) => ({ - friendId: Number(friendId), - currencies, - friend: userMap[Number(friendId)]!, - maxAmount: currencies.reduce( - (max, curr) => (BigMath.abs(curr.amount) > BigMath.abs(max) ? curr.amount : max), - 0n, - ), - })) - .sort((a, b) => Number(BigMath.abs(b.maxAmount) - BigMath.abs(a.maxAmount))); - - return { balances }; - }), + const balances = Object.entries(balancesByFriend) + .map(([friendId, currencies]) => ({ + friendId: Number(friendId), + currencies, + friend: userMap[Number(friendId)]!, + maxAmount: currencies.reduce( + (max, curr) => (BigMath.abs(curr.amount) > BigMath.abs(max) ? curr.amount : max), + 0n, + ), + })) + .sort((a, b) => Number(BigMath.abs(b.maxAmount) - BigMath.abs(a.maxAmount))); + + return { balances }; + }), addOrEditExpense: protectedProcedure .input(arrayify(createExpenseSchema)) diff --git a/src/server/api/routers/group.ts b/src/server/api/routers/group.ts index 96a664438..009401ae3 100644 --- a/src/server/api/routers/group.ts +++ b/src/server/api/routers/group.ts @@ -12,6 +12,11 @@ import { parseSerializedDefaultSplit, serializeDefaultSplit, } from '~/lib/defaultSplit'; +import { + balanceDateRangeSchema, + getBalanceDateRange, + getBalances, +} from '../services/balanceService'; export const groupRouter = createTRPCRouter({ create: protectedProcedure @@ -68,9 +73,10 @@ export const groupRouter = createTRPCRouter({ }), getAllGroupsWithBalances: protectedProcedure - .input(z.object({ getArchived: z.boolean() }).optional()) + .input(z.object({ getArchived: z.boolean() }).merge(balanceDateRangeSchema).optional()) .query(async ({ ctx, input }) => { - const groups = await ctx.db.groupUser.findMany({ + const range = getBalanceDateRange(ctx.balanceDay.toDateExclusive, input); + const memberships = await ctx.db.groupUser.findMany({ where: { userId: ctx.session.user.id, group: { @@ -80,10 +86,6 @@ export const groupRouter = createTRPCRouter({ include: { group: { include: { - groupBalances: { - where: { userId: ctx.session.user.id }, - }, - // We can sort by group balance view instead expenses: { orderBy: { createdAt: 'desc', @@ -95,6 +97,24 @@ export const groupRouter = createTRPCRouter({ }, }); + const groupBalances = + 0 === memberships.length + ? [] + : await getBalances(ctx.db, { + ...range, + where: { + userId: ctx.session.user.id, + groupId: { in: memberships.map((row) => row.groupId) }, + }, + }); + const groups = memberships.map((membership) => ({ + userId: membership.userId, + groupId: membership.groupId, + group: Object.assign(membership.group, { + groupBalances: groupBalances.filter((row) => row.groupId === membership.groupId), + }), + })); + const sortedGroupsByLatestExpense = groups.sort((a, b) => { const aDate = a.group.expenses[0]?.createdAt ?? new Date(0); const bDate = b.group.expenses[0]?.createdAt ?? new Date(0); @@ -139,7 +159,7 @@ export const groupRouter = createTRPCRouter({ return group; }), - getGroupDetails: groupProcedure.query(async ({ input, ctx }) => { + getGroupDetails: groupProcedure.input(balanceDateRangeSchema).query(async ({ input, ctx }) => { const group = await ctx.db.group.findUnique({ where: { id: input.groupId, @@ -150,7 +170,6 @@ export const groupRouter = createTRPCRouter({ user: true, }, }, - groupBalances: true, groupDefaultSplit: true, }, }); @@ -159,9 +178,10 @@ export const groupRouter = createTRPCRouter({ return group; } - if (group.simplifyDebts) { - group.groupBalances = simplifyDebts(group.groupBalances); - } + const groupBalances = await getBalances(ctx.db, { + ...getBalanceDateRange(ctx.balanceDay.toDateExclusive, input), + where: { groupId: group.id }, + }); const defaultSplit = group.groupDefaultSplit && @@ -172,6 +192,7 @@ export const groupRouter = createTRPCRouter({ return { ...group, + groupBalances: group.simplifyDebts ? simplifyDebts(groupBalances) : groupBalances, defaultSplit, }; }), diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts index ec36a6063..6d284e43f 100644 --- a/src/server/api/routers/user.ts +++ b/src/server/api/routers/user.ts @@ -9,13 +9,17 @@ import { } from '~/lib/defaultSplit'; import { claimInviteCooldown } from '~/lib/inviteCooldown'; import { InviteErrorCode } from '~/lib/error/invite'; -import { simplifyDebts } from '~/lib/simplify'; import { AppError } from '~/server/api/appError'; import { createTRPCRouter, protectedProcedure } from '~/server/api/trpc'; import { db } from '~/server/db'; import { sendFeedbackEmail, sendInviteEmail } from '~/server/mailer'; import { SplitwiseGroupSchema, SplitwiseUserSchema } from '~/types'; +import { + balanceDateRangeSchema, + getBalanceDateRange, + getUserBalances, +} from '../services/balanceService'; import { getSubscriptionEndpoint, sendPushNotificationToUsers, @@ -115,62 +119,22 @@ export const userRouter = createTRPCRouter({ }), getBalancesWithFriend: protectedProcedure - .input(z.object({ friendId: z.number() })) + .input(z.object({ friendId: z.number() }).merge(balanceDateRangeSchema)) .query(async ({ input, ctx }) => { - const rawBalances = await db.balanceView.findMany({ - where: { - userId: ctx.session.user.id, - friendId: input.friendId, - amount: { not: 0 }, - }, - include: { - group: { - select: { - name: true, - simplifyDebts: true, - }, - }, - }, - }); - - const processedBalances = await Promise.all( - rawBalances.map(async ({ groupId, currency, amount, group }) => { - // For non-simplifyDebts groups and non-group balances, use raw balance - if (!group?.simplifyDebts || null === groupId) { - return { - friendId: input.friendId, - currency, - amount, - groupId, - groupName: group?.name ?? null, - }; - } - - // For simplifyDebts groups, fetch all group balances and simplify - const allGroupBalances = await db.balanceView.findMany({ - where: { groupId, currency }, - }); - - const simplified = simplifyDebts(allGroupBalances); - - const simplifiedBalance = simplified.find( - (b) => - b.userId === ctx.session.user.id && - b.friendId === input.friendId && - b.currency === currency, - ); - - return { - friendId: input.friendId, - currency, - amount: simplifiedBalance?.amount ?? 0n, - groupId, - groupName: group.name, - }; - }), + const balances = await getUserBalances( + ctx.db, + ctx.session.user.id, + getBalanceDateRange(ctx.balanceDay.toDateExclusive, input), ); - - return processedBalances.filter((b) => 0n !== b.amount); + return balances + .filter((row) => row.friendId === input.friendId && 0n !== row.amount) + .map(({ friendId, currency, amount, groupId, group }) => ({ + friendId, + currency, + amount, + groupId, + groupName: group?.name ?? null, + })); }), updateUserDetail: protectedProcedure diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index 907849d7b..ec9268767 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -17,6 +17,7 @@ import { ZodError, z } from 'zod'; import { getAppErrorCode } from '~/server/api/appError'; import { getServerAuthSession } from '~/server/auth'; import { db } from '~/server/db'; +import { BALANCE_TIME_ZONE_HEADER, getBalanceDay } from '~/lib/balanceDate'; /** * 1. CONTEXT @@ -29,6 +30,8 @@ import { db } from '~/server/db'; export interface CreateContextOptions { session: Session | null; db?: PrismaClient; + timeZone?: string; + now?: Date; } /** @@ -44,6 +47,7 @@ export interface CreateContextOptions { export const createInnerTRPCContext = (opts: CreateContextOptions) => ({ session: opts.session, db: opts.db ?? db, + balanceDay: getBalanceDay(opts.timeZone, opts.now), }); /** @@ -60,6 +64,10 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => { return createInnerTRPCContext({ session, + timeZone: + 'string' === typeof req.headers[BALANCE_TIME_ZONE_HEADER] + ? req.headers[BALANCE_TIME_ZONE_HEADER] + : undefined, }); }; diff --git a/src/tests/balanceDate.test.ts b/src/tests/balanceDate.test.ts new file mode 100644 index 000000000..e2a5ee99f --- /dev/null +++ b/src/tests/balanceDate.test.ts @@ -0,0 +1,29 @@ +import { getBalanceDay } from '~/lib/balanceDate'; + +describe('getBalanceDay', () => { + it.each([ + ['UTC', '2025-03-09T12:00:00Z', '2025-03-10T00:00:00.000Z'], + ['America/New_York', '2025-03-09T05:00:00Z', '2025-03-10T04:00:00.000Z'], + ['America/New_York', '2025-11-02T04:00:00Z', '2025-11-03T05:00:00.000Z'], + ['Asia/Kathmandu', '2025-01-01T00:00:00Z', '2025-01-01T18:15:00.000Z'], + ['Pacific/Apia', '2011-12-29T12:00:00Z', '2011-12-30T10:00:00.000Z'], + ['America/Santiago', '2025-09-06T12:00:00Z', '2025-09-07T04:00:00.000Z'], + ])('finds the next calendar day in %s at %s', (zone, now, expected) => { + expect(getBalanceDay(zone, new Date(now)).toDateExclusive.toISOString()).toBe(expected); + }); + + it('changes scope at exactly local midnight', () => { + const before = getBalanceDay('America/New_York', new Date('2025-03-10T03:59:59.999Z')); + const after = getBalanceDay('America/New_York', new Date('2025-03-10T04:00:00Z')); + expect(before.day).not.toBe(after.day); + expect(after.toDateExclusive.toISOString()).toBe('2025-03-11T04:00:00.000Z'); + }); + + it('defaults absent and invalid zones to UTC', () => { + const now = new Date('2025-01-01T23:00:00Z'); + expect(getBalanceDay('not-a-zone', now)).toEqual(getBalanceDay(undefined, now)); + expect(getBalanceDay(undefined, now).toDateExclusive.toISOString()).toBe( + '2025-01-02T00:00:00.000Z', + ); + }); +}); diff --git a/src/tests/balanceDateRefresh.test.tsx b/src/tests/balanceDateRefresh.test.tsx new file mode 100644 index 000000000..d862ccad3 --- /dev/null +++ b/src/tests/balanceDateRefresh.test.tsx @@ -0,0 +1,66 @@ +/** @jest-environment jsdom */ +import { act } from 'react'; +import { createRoot } from 'react-dom/client'; + +import { useBalanceDateRefresh } from '~/hooks/useBalanceDateRefresh'; + +const invalidate = jest.fn().mockResolvedValue(undefined); +const utils = { + expense: { getBalances: { invalidate }, getCumulatedBalances: { invalidate } }, + user: { getBalancesWithFriend: { invalidate } }, + group: { getAllGroupsWithBalances: { invalidate }, getGroupDetails: { invalidate } }, +}; +jest.mock('~/utils/api', () => ({ api: { useUtils: () => utils } })); + +const Refresh = () => { + useBalanceDateRefresh(); + return null; +}; + +const mockTimeZone = (getZone: () => string) => { + const DateTimeFormat = Intl.DateTimeFormat; + jest + .spyOn(Intl, 'DateTimeFormat') + .mockImplementation( + (locales, options) => + new DateTimeFormat(locales, { ...options, timeZone: options?.timeZone ?? getZone() }), + ); +}; + +describe('balance date cache refresh', () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2025-01-01T23:59:59Z')); + Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); + }); + afterEach(() => { + jest.restoreAllMocks(); + jest.useRealTimers(); + }); + + it('refreshes balances at local midnight and cleans up timers on unmount', async () => { + mockTimeZone(() => 'UTC'); + const root = createRoot(document.createElement('div')); + await act(async () => root.render()); + expect(jest.getTimerCount()).toBeGreaterThan(0); + expect(invalidate).not.toHaveBeenCalled(); + await act(async () => jest.advanceTimersByTime(1000)); + expect(invalidate).toHaveBeenCalledTimes(5); + await act(async () => root.unmount()); + jest.runOnlyPendingTimers(); + expect(jest.getTimerCount()).toBe(0); + }); + + it('refreshes a suspended tab after a timezone change', async () => { + let timeZone = 'UTC'; + mockTimeZone(() => timeZone); + const root = createRoot(document.createElement('div')); + await act(async () => root.render()); + timeZone = 'Asia/Tokyo'; + await act(async () => window.dispatchEvent(new Event('focus'))); + expect(invalidate).toHaveBeenCalledTimes(5); + await act(async () => window.dispatchEvent(new Event('focus'))); + expect(invalidate).toHaveBeenCalledTimes(5); + await act(async () => root.unmount()); + }); +}); diff --git a/src/tests/integration/balanceDates.integration.test.ts b/src/tests/integration/balanceDates.integration.test.ts new file mode 100644 index 000000000..d0df5064c --- /dev/null +++ b/src/tests/integration/balanceDates.integration.test.ts @@ -0,0 +1,234 @@ +import { getBalances } from '~/server/api/services/balanceService'; + +import { db } from './database'; +import { expenseInput, testScenario } from './factories'; +import { callerFor } from './trpc'; + +const now = new Date('2025-03-09T12:00:00Z'); + +describe('date-aware balances (#604)', () => { + it('keeps Prisma aggregation exact above the JavaScript safe integer range', async () => { + const { owner, member, group } = await testScenario(); + const viewer = callerFor(owner.id, { now }); + await viewer.expense.addOrEditExpense( + expenseInput(owner.id, member.id, { + groupId: group.id, + amount: 18014398509481986n, + expenseDate: now, + }), + ); + expect((await viewer.user.getBalancesWithFriend({ friendId: member.id }))[0]?.amount).toBe( + 9007199254740993n, + ); + }); + + it('dates settlements and both legs of a currency conversion consistently', async () => { + const { owner, member, group, input } = await testScenario(); + const viewer = callerFor(owner.id, { now }); + await viewer.expense.addOrEditExpense({ ...input, expenseDate: now }); + await callerFor(member.id).expense.addOrEditExpense({ + ...expenseInput(member.id, owner.id, { groupId: group.id }), + splitType: SplitType.SETTLEMENT, + expenseDate: new Date('2025-04-01'), + }); + expect((await viewer.user.getBalancesWithFriend({ friendId: member.id }))[0]?.amount).toBe( + 1000n, + ); + expect( + (await getBalances(db, { where: { groupId: group.id, userId: owner.id } }))[0]?.amount, + ).toBe(0n); + await viewer.expense.addOrEditCurrencyConversion({ + amount: 1000n, + rate: 0.9, + from: 'USD', + to: 'EUR', + senderId: owner.id, + receiverId: member.id, + groupId: group.id, + }); + // Align both linked legs to a deterministic future date after the real-time write. + await db.expense.updateMany({ + where: { splitType: SplitType.CURRENCY_CONVERSION }, + data: { expenseDate: new Date('2025-04-01') }, + }); + expect(await viewer.user.getBalancesWithFriend({ friendId: member.id })).toEqual([ + expect.objectContaining({ currency: 'USD', amount: 1000n }), + ]); + const due = callerFor(owner.id, { now: new Date('2025-04-01') }); + expect(await due.user.getBalancesWithFriend({ friendId: member.id })).toEqual( + expect.arrayContaining([ + expect.objectContaining({ currency: 'USD', amount: 1000n }), + expect.objectContaining({ currency: 'EUR', amount: -900n }), + ]), + ); + }); + + it('filters before summing, respects viewer-local days and exposes optional ranges', async () => { + const { owner, member, group } = await testScenario(); + const viewer = callerFor(owner.id, { timeZone: 'America/New_York', now }); + const dates = [ + '2025-03-08T12:00:00Z', + '2025-03-09T05:00:00Z', + '2025-03-10T03:59:59.999Z', + '2025-03-10T04:00:00Z', + ]; + await dates.reduce(async (pending, date) => { + await pending; + await viewer.expense.addOrEditExpense( + expenseInput(owner.id, member.id, { + groupId: group.id, + expenseDate: new Date(date), + }), + ); + }, Promise.resolve()); + expect((await viewer.expense.getBalances()).balances[0]?.currencies).toEqual([ + { currency: 'USD', amount: 3000n }, + ]); + expect(await viewer.expense.getCumulatedBalances()).toEqual({ + youOwe: [], + youGet: [{ currency: 'USD', amount: 3000n }], + }); + expect(await viewer.user.getBalancesWithFriend({ friendId: member.id })).toEqual([ + expect.objectContaining({ amount: 3000n, groupId: group.id }), + ]); + expect((await viewer.group.getAllGroupsWithBalances())[0]?.balances).toEqual({ USD: 3000n }); + expect((await viewer.group.getGroupDetails({ groupId: group.id }))?.groupBalances).toEqual( + expect.arrayContaining([expect.objectContaining({ userId: owner.id, amount: 3000n })]), + ); + const period = { + fromDate: new Date('2025-03-09T05:00:00Z'), + toDateExclusive: new Date('2025-03-10T04:00:00Z'), + }; + expect( + (await viewer.user.getBalancesWithFriend({ friendId: member.id, ...period }))[0]?.amount, + ).toBe(2000n); + const utc = callerFor(owner.id, { timeZone: 'UTC', now }); + expect((await utc.user.getBalancesWithFriend({ friendId: member.id }))[0]?.amount).toBe(2000n); + const tomorrow = callerFor(owner.id, { + timeZone: 'America/New_York', + now: period.toDateExclusive, + }); + expect((await tomorrow.user.getBalancesWithFriend({ friendId: member.id }))[0]?.amount).toBe( + 4000n, + ); + expect( + (await getBalances(db, { where: { groupId: group.id, userId: owner.id } }))[0]?.amount, + ).toBe(4000n); + await expect( + viewer.expense.getBalances({ fromDate: now, toDateExclusive: now }), + ).rejects.toMatchObject({ code: 'BAD_REQUEST' }); + }); + + it('retains future obligations for mutation guards and friend discovery', async () => { + const { owner, member, group, input } = await testScenario(); + const viewer = callerFor(owner.id, { now }); + await viewer.expense.addOrEditExpense({ ...input, expenseDate: new Date('2025-04-01') }); + expect((await viewer.expense.getBalances()).balances).toEqual([]); + expect(await viewer.user.getBalancesWithFriend({ friendId: member.id })).toEqual([]); + expect(await viewer.user.getFriends()).toEqual( + expect.arrayContaining([expect.objectContaining({ id: member.id })]), + ); + await expect(viewer.user.deleteFriend({ friendId: member.id })).rejects.toMatchObject({ + code: 'BAD_REQUEST', + }); + await expect(viewer.group.leaveGroup({ groupId: group.id })).rejects.toMatchObject({ + code: 'BAD_REQUEST', + }); + await expect(viewer.group.toggleArchive({ groupId: group.id })).rejects.toMatchObject({ + code: 'BAD_REQUEST', + }); + await expect(viewer.group.delete({ groupId: group.id })).rejects.toMatchObject({ + code: 'BAD_REQUEST', + }); + }); + + it('keeps a future recurring template out of balances until its expense day', async () => { + const { owner, member, input } = await testScenario(); + const viewer = callerFor(owner.id, { now }); + await viewer.expense.addOrEditExpense({ + ...input, + expenseDate: new Date('2025-04-30T12:00:00Z'), + cronExpression: '0 0 30 * *', + }); + expect(await db.expenseRecurrence.count()).toBe(1); + expect(await viewer.user.getBalancesWithFriend({ friendId: member.id })).toEqual([]); + const due = callerFor(owner.id, { now: new Date('2025-04-30T00:00:00Z') }); + expect((await due.user.getBalancesWithFriend({ friendId: member.id }))[0]?.amount).toBe(1000n); + }); + + it('recomputes edited/deleted contributions and retains a zero net row', async () => { + const { owner, member, input } = await testScenario(); + const viewer = callerFor(owner.id, { now }); + const current = { ...input, expenseDate: new Date('2025-03-09T12:00:00Z') }; + const [expense] = await viewer.expense.addOrEditExpense(current); + await viewer.expense.addOrEditExpense({ + ...current, + expenseId: expense!.id, + expenseDate: new Date('2025-04-01'), + }); + expect(await viewer.user.getBalancesWithFriend({ friendId: member.id })).toEqual([]); + await viewer.expense.addOrEditExpense({ ...current, expenseId: expense!.id }); + await callerFor(member.id).expense.addOrEditExpense( + expenseInput(member.id, owner.id, { + groupId: input.groupId, + expenseDate: current.expenseDate, + }), + ); + expect( + ( + await getBalances(db, { + where: { userId: owner.id }, + toDateExclusive: new Date('2025-03-10'), + }) + )[0]?.amount, + ).toBe(0n); + expect(await viewer.user.getBalancesWithFriend({ friendId: member.id })).toEqual([]); + await viewer.expense.deleteExpense({ expenseId: expense!.id }); + expect((await viewer.user.getBalancesWithFriend({ friendId: member.id }))[0]?.amount).toBe( + -1000n, + ); + }); + + it('simplifies the complete group graph at one cutoff, including newly implied friends', async () => { + const { owner, member, outsider, group } = await testScenario(); + const viewer = callerFor(owner.id, { now }); + await viewer.group.addMembers({ groupId: group.id, userIds: [outsider.id] }); + await viewer.expense.addOrEditExpense( + expenseInput(owner.id, member.id, { + groupId: group.id, + expenseDate: now, + }), + ); + await callerFor(member.id).expense.addOrEditExpense( + expenseInput(member.id, outsider.id, { + groupId: group.id, + expenseDate: now, + }), + ); + await callerFor(outsider.id).expense.addOrEditExpense( + expenseInput(outsider.id, owner.id, { + groupId: group.id, + expenseDate: new Date('2025-04-01'), + }), + ); + await viewer.group.toggleSimplifyDebts({ groupId: group.id }); + const details = await viewer.group.getGroupDetails({ groupId: group.id }); + expect(details?.groupBalances).toEqual( + expect.arrayContaining([ + expect.objectContaining({ userId: owner.id, friendId: outsider.id, amount: 1000n }), + ]), + ); + expect((await viewer.expense.getBalances()).balances).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + friendId: outsider.id, + currencies: [{ currency: 'USD', amount: 1000n }], + }), + ]), + ); + expect((await viewer.user.getBalancesWithFriend({ friendId: outsider.id }))[0]?.amount).toBe( + 1000n, + ); + }); +}); +import { SplitType } from '@prisma/client'; diff --git a/src/tests/integration/trpc.ts b/src/tests/integration/trpc.ts index 1ab394b20..91bf130d3 100644 --- a/src/tests/integration/trpc.ts +++ b/src/tests/integration/trpc.ts @@ -16,7 +16,11 @@ export const sessionFor = (userId: number): Session => ({ expires: '2099-01-01T00:00:00.000Z', }); -export const callerFor = (userId: number | null) => +export const callerFor = (userId: number | null, options: { timeZone?: string; now?: Date } = {}) => appRouter.createCaller( - createInnerTRPCContext({ session: null === userId ? null : sessionFor(userId), db }), + createInnerTRPCContext({ + session: null === userId ? null : sessionFor(userId), + db, + ...options, + }), ); diff --git a/src/utils/api.ts b/src/utils/api.ts index 9e7c9a045..6528331a4 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -10,6 +10,7 @@ import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server'; import superjson from 'superjson'; import { type AppRouter } from '~/server/api/root'; +import { BALANCE_TIME_ZONE_HEADER } from '~/lib/balanceDate'; export const getBaseUrl = () => { if ('undefined' !== typeof window) { @@ -38,6 +39,9 @@ export const api = createTRPCNext({ }), httpBatchLink({ url: `${getBaseUrl()}/api/trpc`, + headers: () => ({ + [BALANCE_TIME_ZONE_HEADER]: Intl.DateTimeFormat().resolvedOptions().timeZone, + }), transformer: superjson, }), ], From e6c32571c90ec26b89f212b41abd1f4f97fb3106 Mon Sep 17 00:00:00 2001 From: krokosik Date: Tue, 20 Oct 2026 21:05:53 +0200 Subject: [PATCH 4/4] Ignore local seeded balance baseline snapshot --- .gitignore | 1 + scripts/seeded-balance-baseline.json | 2863 -------------------------- 2 files changed, 1 insertion(+), 2863 deletions(-) delete mode 100644 scripts/seeded-balance-baseline.json diff --git a/.gitignore b/.gitignore index c73a5bed8..6a9c4e0ba 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,7 @@ certificates .vscode SEED_STATISTICS.md +/scripts/seeded-balance-baseline.json # Agents .worktrees/ diff --git a/scripts/seeded-balance-baseline.json b/scripts/seeded-balance-baseline.json deleted file mode 100644 index 6788febbb..000000000 --- a/scripts/seeded-balance-baseline.json +++ /dev/null @@ -1,2863 +0,0 @@ -{ - "capturedAt": "2026-10-19T21:18:56.679Z", - "expenses": 17830, - "participants": 65914, - "queries": [ - { - "name": "all", - "where": {}, - "rows": 8200, - "digest": "9a4dd000c9280cdb06a201f56cdf0f6cc4f0a235651eeb4e7a3c21d9ff706cd9" - }, - { - "name": "user/0", - "where": { - "userId": 0 - }, - "rows": 67, - "digest": "6912695901e5b34d08dd361e403a9ca506f575af2c194727ea865a29da21d8af" - }, - { - "name": "user/1", - "where": { - "userId": 1 - }, - "rows": 66, - "digest": "2097925aaacde943144174a184634314717fc964fff7a2a2690b0f115d0bec85" - }, - { - "name": "user/2", - "where": { - "userId": 2 - }, - "rows": 11, - "digest": "ad1fcb8e73f15e3f38f6c62d5be65035af5ef02a25b5dceb057a0c22f02a85b6" - }, - { - "name": "user/3", - "where": { - "userId": 3 - }, - "rows": 49, - "digest": "bf1f3ff55385c165c14e63ef0e3c878d6065df306e5a0b968ae4ac4adb413370" - }, - { - "name": "user/4", - "where": { - "userId": 4 - }, - "rows": 47, - "digest": "c94763962deb752e27651ed3efa69f89fc020fbbad2f66f079a9e17bc84c015b" - }, - { - "name": "user/5", - "where": { - "userId": 5 - }, - "rows": 66, - "digest": "8815e6ce65a6fa32bc1a640e81ea989c343c1f134f7aa2a04905363dc9f88e2c" - }, - { - "name": "user/6", - "where": { - "userId": 6 - }, - "rows": 77, - "digest": "88426f9e1c0349585b266cb4eb37974d9f35a6244bf48b0fee57a2346fd9776f" - }, - { - "name": "user/7", - "where": { - "userId": 7 - }, - "rows": 51, - "digest": "84a103ce592f3e028d6d463cf0514274ee22c9739b98129ed9b97e2becd4370c" - }, - { - "name": "user/8", - "where": { - "userId": 8 - }, - "rows": 58, - "digest": "89044ba05210948a0c5d7c35a58502882218de14f84539dd0b75d2ca0ed5793e" - }, - { - "name": "user/9", - "where": { - "userId": 9 - }, - "rows": 27, - "digest": "f6c8c832fa876e1e6469a9d9a8c90ee5507cb809cb1367295dc4fcff4cebab9f" - }, - { - "name": "user/10", - "where": { - "userId": 10 - }, - "rows": 57, - "digest": "7590d8610799d885d045f48cfc7a371e67e00929b7dcb9c6832c103e95936e09" - }, - { - "name": "user/11", - "where": { - "userId": 11 - }, - "rows": 55, - "digest": "11298fd09d4d854d34716b5c5c1132ae0661ad09f9ff1ad8b1c0e67749c4e017" - }, - { - "name": "user/12", - "where": { - "userId": 12 - }, - "rows": 64, - "digest": "3ea705fa3fa5cabfb1ed1ee4a348f35fbb748b455de15081654d2b776daa5b9c" - }, - { - "name": "user/13", - "where": { - "userId": 13 - }, - "rows": 70, - "digest": "3923ba1a1e8094654cc18f55f9bf756f3a7494ae77ec07a2f96c7c73ee879ab4" - }, - { - "name": "user/14", - "where": { - "userId": 14 - }, - "rows": 71, - "digest": "32f0fdab7ec6f19737db5bad6f3e72c041bbcac5e7d1cfcadec7aaa54a46d3fe" - }, - { - "name": "user/15", - "where": { - "userId": 15 - }, - "rows": 51, - "digest": "2077375a95cbbaedadc270915386966b1e963ba61d98c3c1cc9109c2c9d12356" - }, - { - "name": "user/16", - "where": { - "userId": 16 - }, - "rows": 43, - "digest": "f94a0dab85f6c12b09d18ab0d102e8f763091861c9233186fdff2e52376bfe05" - }, - { - "name": "user/17", - "where": { - "userId": 17 - }, - "rows": 52, - "digest": "86848d586ca47f1e41d7acbdf3bf131598b0cbc37d4e3c329e129db95de6c040" - }, - { - "name": "user/18", - "where": { - "userId": 18 - }, - "rows": 59, - "digest": "ad7451568bbb8fc773e6095ab193ecea15ce25749a33f426512cb8d283f79aed" - }, - { - "name": "user/19", - "where": { - "userId": 19 - }, - "rows": 43, - "digest": "75ff326f2dc055059f84e8a65e0b2647109a823520aa78089e6b1b1499294b41" - }, - { - "name": "user/20", - "where": { - "userId": 20 - }, - "rows": 45, - "digest": "d66a831eee726cf1f3959da66e9dc59ef421966378fd9f95f0cae938b8f53f5f" - }, - { - "name": "user/21", - "where": { - "userId": 21 - }, - "rows": 52, - "digest": "ac3c6dc53a517585369d877a3f10e016e491c3e9062d7d2b31ef0e89f6abd9e6" - }, - { - "name": "user/22", - "where": { - "userId": 22 - }, - "rows": 60, - "digest": "1e4ffb3c268f8dff2ed32ce88b4bcc07f7373e3d1a5b4b9be527b0ac7a05f634" - }, - { - "name": "user/23", - "where": { - "userId": 23 - }, - "rows": 69, - "digest": "88e96fb31906f9792ea5a34bedb6047474040dc9fbfd77dd832fa67d862c74e4" - }, - { - "name": "user/24", - "where": { - "userId": 24 - }, - "rows": 38, - "digest": "b75e2a549a08ef010b9d0846bcc796975fee048db480ad3f7de8914ec090216b" - }, - { - "name": "user/25", - "where": { - "userId": 25 - }, - "rows": 47, - "digest": "3a9ac58e7b34eaf13b9e7356dd892dcb5083413c3eeae0731f04f4e9a74a6e6f" - }, - { - "name": "user/26", - "where": { - "userId": 26 - }, - "rows": 45, - "digest": "ac865bc5852f4fe98e7f02930c0d2e79e1dd77f413eb09773417274b4d0dcc64" - }, - { - "name": "user/27", - "where": { - "userId": 27 - }, - "rows": 50, - "digest": "b6f155162a285d8872362ffb89d24aaa096608402f132a607a29119496125936" - }, - { - "name": "user/28", - "where": { - "userId": 28 - }, - "rows": 72, - "digest": "752663153c3aa9eb0062973a8616a05c23c898a08f681354366ad99e22a246a1" - }, - { - "name": "user/29", - "where": { - "userId": 29 - }, - "rows": 57, - "digest": "4c650ddbbfe2d55daa9c681a0b7e5f1a6a9a9d791eda87cc2dfba6e81671c069" - }, - { - "name": "user/30", - "where": { - "userId": 30 - }, - "rows": 77, - "digest": "e7abe66bab1f6d21072a322acf62711eb41b6f9ccf48f246ea4fae96907afb83" - }, - { - "name": "user/31", - "where": { - "userId": 31 - }, - "rows": 44, - "digest": "9095249f26ddbbc8924acb7620a35aea59f1119c616482dada4bc002713d3d49" - }, - { - "name": "user/32", - "where": { - "userId": 32 - }, - "rows": 63, - "digest": "757cb18ae10597a3de98f33e2ea985be270cb12b3f03312d7c6270b200734950" - }, - { - "name": "user/33", - "where": { - "userId": 33 - }, - "rows": 49, - "digest": "52a1cbffaf3efd357093c51f3b238afdd3dc037e4117e4528914037314486d14" - }, - { - "name": "user/34", - "where": { - "userId": 34 - }, - "rows": 45, - "digest": "0e871aa2f7528f0f3de7c29a34e78b5c7790d2f2fca8f46ef3118406b62fdb6f" - }, - { - "name": "user/35", - "where": { - "userId": 35 - }, - "rows": 60, - "digest": "27a6c797a656b785adea2ad778f7bffdee056f22c25379d7ab79bac5aca9ca6d" - }, - { - "name": "user/36", - "where": { - "userId": 36 - }, - "rows": 82, - "digest": "50dec4774dbf2a713a6a00a5980b92d43c02030db368563dac79cfb564ddf86a" - }, - { - "name": "user/37", - "where": { - "userId": 37 - }, - "rows": 49, - "digest": "e8012e447742a0359d06bf11bfa5c50ba1aaa51cde44728e46b83bf176827e23" - }, - { - "name": "user/38", - "where": { - "userId": 38 - }, - "rows": 43, - "digest": "b658807b262e30bd81880bf16f5b82133777677c89e1cd60b5e2794297ddc4d9" - }, - { - "name": "user/39", - "where": { - "userId": 39 - }, - "rows": 51, - "digest": "63b3498d5e098dd7975f759655b8efa7dee064ad4e8625ec80895c1f56b34668" - }, - { - "name": "user/40", - "where": { - "userId": 40 - }, - "rows": 80, - "digest": "f2f04139d24e9493845dc63ce874052b0fc9e7b99559ab8145ca988d3129d0a0" - }, - { - "name": "user/41", - "where": { - "userId": 41 - }, - "rows": 34, - "digest": "ffb17ef5d5858fed427754f60c625579cbc6e26c1e8c45b98273fbaff61e1cf7" - }, - { - "name": "user/42", - "where": { - "userId": 42 - }, - "rows": 46, - "digest": "24e09ff1f232b233961e732deaf4047cf67611915e61ef0e511633addbe077b8" - }, - { - "name": "user/43", - "where": { - "userId": 43 - }, - "rows": 66, - "digest": "5cfc69e12f379dc01556893531bd4c746a637877caa763f66f6681510b8fcafc" - }, - { - "name": "user/44", - "where": { - "userId": 44 - }, - "rows": 70, - "digest": "21b92b2c4fe5c6a0b782c96e730c9429864c4a8cfb4b6cddac548e679e5782d2" - }, - { - "name": "user/45", - "where": { - "userId": 45 - }, - "rows": 65, - "digest": "edc83ad6256ff0cbf05c07cd0e588946a49191d949f98c5ec58d415bed74f7bb" - }, - { - "name": "user/46", - "where": { - "userId": 46 - }, - "rows": 44, - "digest": "6d6a32526e10976b26ae33eab108b983b994d3c54aae3c8565ae67d0b963b77e" - }, - { - "name": "user/47", - "where": { - "userId": 47 - }, - "rows": 47, - "digest": "20f66e6aaf079a9f05b054e75d9cd0724b5ed8192b9cfe2db0e1d8af03b28e02" - }, - { - "name": "user/48", - "where": { - "userId": 48 - }, - "rows": 85, - "digest": "b325d48eb1d37921602cbb925f162d54c531793dd6cecf7405e74eb5053f8357" - }, - { - "name": "user/49", - "where": { - "userId": 49 - }, - "rows": 59, - "digest": "a71b59e141f7777fa1d3cb3e70a30d54473184595746b13e782553dc747d9c7d" - }, - { - "name": "user/50", - "where": { - "userId": 50 - }, - "rows": 56, - "digest": "27f6997c49b0641d8d70a4cb8d1b97803fedc929ffddf01a22aaab8c4f65a568" - }, - { - "name": "user/51", - "where": { - "userId": 51 - }, - "rows": 63, - "digest": "56ddc15aa3fc2e16932267ef9aa135958e9b6c930d856c6790e64ea16f987424" - }, - { - "name": "user/52", - "where": { - "userId": 52 - }, - "rows": 72, - "digest": "e64213a74f662e1059d9c267b84aae4776cc3363a056af1acdfcfa1b9f7dca2d" - }, - { - "name": "user/53", - "where": { - "userId": 53 - }, - "rows": 58, - "digest": "f5f95b328591aefd653351e49c594ce042a4dcf1d4f4e4b51ba68022afa9d758" - }, - { - "name": "user/54", - "where": { - "userId": 54 - }, - "rows": 49, - "digest": "51bdeb3fb009936efcdfc4bffdd52c8359e0a7bed51162d3ae040ab036d06357" - }, - { - "name": "user/55", - "where": { - "userId": 55 - }, - "rows": 42, - "digest": "6362750a10310df1a9313d7d0737a4b554632ae5429631d4beeaef86166f2c76" - }, - { - "name": "user/56", - "where": { - "userId": 56 - }, - "rows": 40, - "digest": "af511f5068380f887a708fbb15252b1f93ca8c561635f55e7be810d424b75bee" - }, - { - "name": "user/57", - "where": { - "userId": 57 - }, - "rows": 48, - "digest": "4e49d2656a2e08c6d0ffe62e9e69d2eaf91534c23e0c580e5b1404564ee3aa0c" - }, - { - "name": "user/58", - "where": { - "userId": 58 - }, - "rows": 21, - "digest": "d8ec4fae73f3acfc2fd3995197ff339609ede094a12319c3ffb95350a3a065f6" - }, - { - "name": "user/59", - "where": { - "userId": 59 - }, - "rows": 36, - "digest": "3135263c0049ff024ae49fcc07bc62325af868f03d0d2089b1dbb4e92e0f1d5a" - }, - { - "name": "user/60", - "where": { - "userId": 60 - }, - "rows": 86, - "digest": "e9eaf60c58e3786e9afc5a6b1e5287a7af301b01e077460f13267b458d23247d" - }, - { - "name": "user/61", - "where": { - "userId": 61 - }, - "rows": 71, - "digest": "13e7573c9a6b8a7e4d7aadc0a29d39958ad1c0e716d15fc18bdf9b937a60b88e" - }, - { - "name": "user/62", - "where": { - "userId": 62 - }, - "rows": 68, - "digest": "9b95838129da07e02178054c8773896e0c5187fba59fa5c4d8ef4976f65ccb48" - }, - { - "name": "user/63", - "where": { - "userId": 63 - }, - "rows": 38, - "digest": "59159d544193b71e7e483f897ab3c16164f485930b551d91babe2769cbab0c72" - }, - { - "name": "user/64", - "where": { - "userId": 64 - }, - "rows": 70, - "digest": "e5bc92d0e3f6318e60c522dd36382f382d3c6a890a740b04db96df283eeb05ca" - }, - { - "name": "user/65", - "where": { - "userId": 65 - }, - "rows": 60, - "digest": "d1a9be70031e2d7a15b45fd266b017a2326d6788ca38c25169405079cc48cd71" - }, - { - "name": "user/66", - "where": { - "userId": 66 - }, - "rows": 63, - "digest": "0a2230d5da8a58fe4093d9aca5518f2e9d04800703dd506d6c24ea6a39881099" - }, - { - "name": "user/67", - "where": { - "userId": 67 - }, - "rows": 70, - "digest": "387a7539d53a559f74b408ea801a8a9183cb34c377539e22e71f0a06651ebdf8" - }, - { - "name": "user/68", - "where": { - "userId": 68 - }, - "rows": 37, - "digest": "0cff290ad7ede910ad4dc0da1ce8f794d07f27b10b8427fc4499b62b7ca88e1d" - }, - { - "name": "user/69", - "where": { - "userId": 69 - }, - "rows": 56, - "digest": "0a845d2b650c7189c305967f2069f1276b4ff189daaf409b12e341924d50aff0" - }, - { - "name": "user/70", - "where": { - "userId": 70 - }, - "rows": 59, - "digest": "4f9bfdf24457d34dd75112ea70d131f974df59907539da03fa002b34b4dcc9a6" - }, - { - "name": "user/71", - "where": { - "userId": 71 - }, - "rows": 69, - "digest": "8da75ee0bc3629ac9d0c07d8dcc9937375e74b8d12cd491c5386d5a2c5f353bc" - }, - { - "name": "user/72", - "where": { - "userId": 72 - }, - "rows": 37, - "digest": "54421052aa33e7d729c24f084a328780530c24d1b13b7ce1daf3f6c0f8bfa0d5" - }, - { - "name": "user/73", - "where": { - "userId": 73 - }, - "rows": 30, - "digest": "a196386754322ee8aebc7f00f497189b89dd142e0732b67455b85798d0b53060" - }, - { - "name": "user/74", - "where": { - "userId": 74 - }, - "rows": 37, - "digest": "c82eae308ba826925b6ababe1a656a99ee6296139dc936e4886358fc36ace771" - }, - { - "name": "user/75", - "where": { - "userId": 75 - }, - "rows": 43, - "digest": "6c5c2c03de260efb0ec0d5aec5a43cc16f09b81cdfde6a725014a1f2e79feb36" - }, - { - "name": "user/76", - "where": { - "userId": 76 - }, - "rows": 52, - "digest": "88c5a5900e38525e72cb7011134b1cb6f7eabc830c4ff1beadc7f959284307f5" - }, - { - "name": "user/77", - "where": { - "userId": 77 - }, - "rows": 80, - "digest": "2e2327be6028844dce0dab98dd3234a8646fe267c35c16822992d14782049315" - }, - { - "name": "user/78", - "where": { - "userId": 78 - }, - "rows": 54, - "digest": "222618a35245add615057d40d4033a541a08e18f4e8ab26a30a4ba1d67a00ef0" - }, - { - "name": "user/79", - "where": { - "userId": 79 - }, - "rows": 78, - "digest": "db0188ed0d3a0b9843825c834bcaf2c3c31666ae180f4c7e69b7346484684dfc" - }, - { - "name": "user/80", - "where": { - "userId": 80 - }, - "rows": 48, - "digest": "f9abbccd03eae2e04cd2c0814c3c3e70622aebd6d5eb7789e02d6ca082d8cb63" - }, - { - "name": "user/81", - "where": { - "userId": 81 - }, - "rows": 67, - "digest": "33defa7475e92870258c4f6bd1b75f60d784df5c2d396a1a38ff8d05c2ee4fe4" - }, - { - "name": "user/82", - "where": { - "userId": 82 - }, - "rows": 36, - "digest": "440f7e2fc23affc51d51e7a2f847fae2ae38dd142ed67d8cc9a1d4f88a69d6e4" - }, - { - "name": "user/83", - "where": { - "userId": 83 - }, - "rows": 51, - "digest": "88cc1aa2a8859805edc9fd5d25d8c7e188ce453209c242217e73e577672d3621" - }, - { - "name": "user/84", - "where": { - "userId": 84 - }, - "rows": 39, - "digest": "051c4e443569e4a028f108691c64fb36a76a2f02bbab7c69b6db600dec30016f" - }, - { - "name": "user/85", - "where": { - "userId": 85 - }, - "rows": 37, - "digest": "fdb45da53f79aff668719fd01613a94331d5a953525e02c4c6fd46d53e2b18f2" - }, - { - "name": "user/86", - "where": { - "userId": 86 - }, - "rows": 55, - "digest": "626956ecbfa19633e2b9bd494a734782bdc25cd96ffc8898a409670557f895db" - }, - { - "name": "user/87", - "where": { - "userId": 87 - }, - "rows": 65, - "digest": "00476bb2b1bcf2152ca8933187a29d337c2f6765107916995320d488fcf13b66" - }, - { - "name": "user/88", - "where": { - "userId": 88 - }, - "rows": 61, - "digest": "9d2d2ae2a601690e80271e9f6e7057a0aab0c856f7c64e319fba5756f1d9d67a" - }, - { - "name": "user/89", - "where": { - "userId": 89 - }, - "rows": 22, - "digest": "c020ce4cac0b60101a1461920466a4d6faf8d46fecac22874749ffb94674d121" - }, - { - "name": "user/90", - "where": { - "userId": 90 - }, - "rows": 41, - "digest": "186a2a269289514f821d6a2855da2a3463dff2649fd0b0452dd77a23b6f60dfe" - }, - { - "name": "user/91", - "where": { - "userId": 91 - }, - "rows": 50, - "digest": "c3d2d6c119c95240319a4943305287df3df235ba3cfe751a3ff9aa7a091128da" - }, - { - "name": "user/92", - "where": { - "userId": 92 - }, - "rows": 66, - "digest": "92eb6f516e872e9f0d02fb5b280f85e9f88924f7dcd0092e4d0c74f35dad9452" - }, - { - "name": "user/93", - "where": { - "userId": 93 - }, - "rows": 64, - "digest": "f8c3760ef5886e6d18354424f9b981e9c5b4f695a5985483882b2c1894d982e2" - }, - { - "name": "user/94", - "where": { - "userId": 94 - }, - "rows": 52, - "digest": "2b7299cba698ab012eea1464e849fefb37cd13726c23073b816fb6d063268580" - }, - { - "name": "user/95", - "where": { - "userId": 95 - }, - "rows": 42, - "digest": "727f4545e85c017f7cc3f025d4d4b13f1e7c53ab5bd1eac04a71604345ee527a" - }, - { - "name": "user/96", - "where": { - "userId": 96 - }, - "rows": 62, - "digest": "2c5d7985782c23a06f3981ece99632ebdddfb271ec99a59e543828c643d3e2cb" - }, - { - "name": "user/97", - "where": { - "userId": 97 - }, - "rows": 46, - "digest": "d053f5c89ec08c5a871489435c1200dc7eff85c1d2f56cd7af31cb14858eaeb6" - }, - { - "name": "user/98", - "where": { - "userId": 98 - }, - "rows": 79, - "digest": "23545600c7f3154a3ace22aa2a79ca49bf8b23e74b1dd2ef495abaf646bf0219" - }, - { - "name": "user/100", - "where": { - "userId": 100 - }, - "rows": 46, - "digest": "3d217f7a6ed9473a39e7f881ead2144c0e4fe781e5b67af0a7cd7d929600d0c5" - }, - { - "name": "user/101", - "where": { - "userId": 101 - }, - "rows": 56, - "digest": "6f2378840c21d74279af18b422505b9ef62467a761444d388373d8cfafbc59bb" - }, - { - "name": "user/102", - "where": { - "userId": 102 - }, - "rows": 56, - "digest": "d53a4eb561bb50219dea775ca41bc5cfba0ecc92af71d161b305f2abfc09a4fa" - }, - { - "name": "user/103", - "where": { - "userId": 103 - }, - "rows": 31, - "digest": "1198ed62afc92808ffab5c612aac6f3d4eaef5e1675f6ea717af3d0bb61f166d" - }, - { - "name": "user/104", - "where": { - "userId": 104 - }, - "rows": 54, - "digest": "923226a50e648294b80e0198260d90fce46cd18a8fbc33b8556af7cbbc16c1ff" - }, - { - "name": "user/105", - "where": { - "userId": 105 - }, - "rows": 38, - "digest": "75d6b581eb76aadac10bffcdd5725a530d67808ded1a186c5601d2df936864f5" - }, - { - "name": "user/106", - "where": { - "userId": 106 - }, - "rows": 62, - "digest": "b434315710732534fb912da1266645287437ffdbab015982f665edb1ec9cc859" - }, - { - "name": "user/107", - "where": { - "userId": 107 - }, - "rows": 43, - "digest": "8241a3e992510231c3e1799486e421b35e5fd2d02b3ef277a2d161098f5da986" - }, - { - "name": "user/108", - "where": { - "userId": 108 - }, - "rows": 43, - "digest": "e425a2a43ff8e5ccfb52812ed20dc8974d28879d1f96ba0bbfbcf2c9c03e8e20" - }, - { - "name": "user/109", - "where": { - "userId": 109 - }, - "rows": 55, - "digest": "4db3c58999bd6112935b7827dc56fbcbd4a18e639d59d4ceca0732d61e317e3a" - }, - { - "name": "user/110", - "where": { - "userId": 110 - }, - "rows": 36, - "digest": "42c89072209dd525a7c8e56cf6d04bccaae195f9735c1e9fb7e354d889a17ea0" - }, - { - "name": "user/111", - "where": { - "userId": 111 - }, - "rows": 70, - "digest": "58d3c5694843b4137643f239a553bd8febbe3c0263025dad17de98e51832ae20" - }, - { - "name": "user/112", - "where": { - "userId": 112 - }, - "rows": 51, - "digest": "ff5670b5f9e6dc37d3551e5d954b8f40bf7c5f9d41648da57c16703810d5776b" - }, - { - "name": "user/113", - "where": { - "userId": 113 - }, - "rows": 52, - "digest": "a6da0e477186609b376d9fe7c3e7db14af746e592b7ea920b445374ae32d7e6f" - }, - { - "name": "user/114", - "where": { - "userId": 114 - }, - "rows": 49, - "digest": "c2e7f61e18e45365a98f9b5d992151b0e1a8081108580a7dcc5c9f4f8a478a61" - }, - { - "name": "user/115", - "where": { - "userId": 115 - }, - "rows": 70, - "digest": "6d52b053d8c5878172f2bb35e03e2e9de847d3806e079838c64575f8d96e417a" - }, - { - "name": "user/116", - "where": { - "userId": 116 - }, - "rows": 30, - "digest": "234681598e45c4743ba6537c5478619eedb065ebdf6787d1cecfc2b02a511692" - }, - { - "name": "user/117", - "where": { - "userId": 117 - }, - "rows": 59, - "digest": "809201ff2a494d342b3590110cf6ea264fb72a71569f64a2ce27ca249e0a6076" - }, - { - "name": "user/118", - "where": { - "userId": 118 - }, - "rows": 42, - "digest": "7329cb484fa3f7961cd939e656c85714eafcde27f197cccd9a008fb89d3ad4f7" - }, - { - "name": "user/119", - "where": { - "userId": 119 - }, - "rows": 38, - "digest": "f16ade28b09fc2f430620f21364e2362a3226626f02ee0f1d865888fef07dbf6" - }, - { - "name": "user/120", - "where": { - "userId": 120 - }, - "rows": 21, - "digest": "9aa546844920daea0b3e7e86bd35d393a64694c7bc31d9cbb756804db681d487" - }, - { - "name": "user/121", - "where": { - "userId": 121 - }, - "rows": 44, - "digest": "2b980a0673eea062c59d716201e08922b88f6c7a6f7b5fd49bfa0e774ccfaa0a" - }, - { - "name": "user/122", - "where": { - "userId": 122 - }, - "rows": 59, - "digest": "c08997737e00347a83abaaa8dde669dc0e1fd55c4f34aaff28adc9595bd29ff0" - }, - { - "name": "user/123", - "where": { - "userId": 123 - }, - "rows": 68, - "digest": "122f812bc7fe60a2ffc9c856413fec4946247b9417c8fdffc80d4cf7983089a2" - }, - { - "name": "user/124", - "where": { - "userId": 124 - }, - "rows": 72, - "digest": "3d4cc784b83cd9083c027b370843d2edd26fb2cbcf6aa3183490f60da89a181e" - }, - { - "name": "user/125", - "where": { - "userId": 125 - }, - "rows": 58, - "digest": "f1e9da801943cb3e850e6ca217af44dc0af673837370be175d777db9481e19c5" - }, - { - "name": "user/126", - "where": { - "userId": 126 - }, - "rows": 62, - "digest": "7a71de92a825a2a4e36a862a9e54b37b482a9825550911af8edbae9cdc127b9a" - }, - { - "name": "user/127", - "where": { - "userId": 127 - }, - "rows": 60, - "digest": "bae0d6d7a3d058f187beefb69d5ed37f8a3df6bff37c9df3cd1e9e832578f876" - }, - { - "name": "user/128", - "where": { - "userId": 128 - }, - "rows": 70, - "digest": "72d380b8474edf916ea15525fd3c857af91f58bded068a8c293ce119c021c4af" - }, - { - "name": "user/129", - "where": { - "userId": 129 - }, - "rows": 47, - "digest": "c9475dd5e7903a8c8422dbe63ad7c3ed839739d50a1401764a3f318dabff009f" - }, - { - "name": "user/130", - "where": { - "userId": 130 - }, - "rows": 62, - "digest": "042aedf0e99b550d3a32f14ecd148951d1eeb2725bb71613c0f6a2f318da44b7" - }, - { - "name": "user/131", - "where": { - "userId": 131 - }, - "rows": 51, - "digest": "ab234f474fc430b1e9ee4005277740c0675bc2001bce8f23df6b58c5829f5c9a" - }, - { - "name": "user/132", - "where": { - "userId": 132 - }, - "rows": 46, - "digest": "a08e488dd1460df0218278b57dbb73b53e862db03493f2041be7d0d7d4ea7f1d" - }, - { - "name": "user/133", - "where": { - "userId": 133 - }, - "rows": 69, - "digest": "82db531862811818f360d124ac273026ebd3407b37b6ab684bd864d52532d393" - }, - { - "name": "user/134", - "where": { - "userId": 134 - }, - "rows": 59, - "digest": "08f4457aa746bdcc99662362c15f0902f5bae112f2a345338db52daefbf15b64" - }, - { - "name": "user/135", - "where": { - "userId": 135 - }, - "rows": 59, - "digest": "eab6c3bc2ada95dcc86034fb3dedb31cf767b90851d8eb8c345990f7c6a9bb62" - }, - { - "name": "user/136", - "where": { - "userId": 136 - }, - "rows": 61, - "digest": "78d5a68593b23e099e0d16f3aa95caea43ff044eb5a6062347c10a36a6a01639" - }, - { - "name": "user/137", - "where": { - "userId": 137 - }, - "rows": 45, - "digest": "493c8ffd4833a07c36d9a710b08ba8d348b0d84564d39e2522ab4e7aa45b7a7e" - }, - { - "name": "user/138", - "where": { - "userId": 138 - }, - "rows": 35, - "digest": "afbf222a84ef32df2906e96993a8e95d57ebd9d73bb2d7b4f449268dc6eee05c" - }, - { - "name": "user/139", - "where": { - "userId": 139 - }, - "rows": 89, - "digest": "456522cce5ca08f29073595c5c963be5162ecc438504dd39431c2a33c6ac1d6b" - }, - { - "name": "user/140", - "where": { - "userId": 140 - }, - "rows": 45, - "digest": "adb48fae7e7ac66a821b1ed4deff96da4d821c415166fcb534a0ae785611834d" - }, - { - "name": "user/141", - "where": { - "userId": 141 - }, - "rows": 50, - "digest": "db1e67ac47d604118c25289d673e0f6b87d187a3fe91e47867be00b22f4581a1" - }, - { - "name": "user/142", - "where": { - "userId": 142 - }, - "rows": 22, - "digest": "9553dd8988f10c26fa96d0194c4348e62c6f47414feb9975d16e37de6ca90867" - }, - { - "name": "user/143", - "where": { - "userId": 143 - }, - "rows": 62, - "digest": "e7e0966c15a954228479ce658f02646815dc4ba07c0d895ddc8e4c06d48b917e" - }, - { - "name": "user/144", - "where": { - "userId": 144 - }, - "rows": 78, - "digest": "6e16f1cbd6b513146cba4a76a6551e893535947142838d1cce2ebcba659543e5" - }, - { - "name": "user/145", - "where": { - "userId": 145 - }, - "rows": 44, - "digest": "63e2e1027136ee3b12c4dd4500556cfec9e832c5714846079f49c63cb4b94908" - }, - { - "name": "user/146", - "where": { - "userId": 146 - }, - "rows": 88, - "digest": "84517631deedc2e13f0193f0450ae092299ce7f4291322479efd1e5cd0027435" - }, - { - "name": "user/147", - "where": { - "userId": 147 - }, - "rows": 67, - "digest": "1b155cff65b466df92a40d83c234cc4d1f756cd6b5abd8fc7f8b727fdea00dd2" - }, - { - "name": "user/148", - "where": { - "userId": 148 - }, - "rows": 75, - "digest": "29b71a2ff6b5908ad8b0b601e72840269cf07dd24447b29d72e8c8490c9761f3" - }, - { - "name": "user/149", - "where": { - "userId": 149 - }, - "rows": 57, - "digest": "88c8b85d7075e31f452f72f25f66b073c6a78ab6788348eca4a99bcc295f1689" - }, - { - "name": "user/99", - "where": { - "userId": 99 - }, - "rows": 90, - "digest": "023663f5b2e013652fe55ae5e9a13989438105b97428c84dd151111919508de2" - }, - { - "name": "group/1", - "where": { - "groupId": 1 - }, - "rows": 42, - "digest": "512779d4050f25f09a9a1a5fdfe2a577db883f4e88d202c5ed75f037d586618c" - }, - { - "name": "group/9", - "where": { - "groupId": 9 - }, - "rows": 12, - "digest": "9ff500f59849d1dbd233149f6cf7855e30b51227fdf50ba23c2141a260de1c7c" - }, - { - "name": "group/18", - "where": { - "groupId": 18 - }, - "rows": 42, - "digest": "58173ec6f82054ee14d51ff62aa49044353df1886988c0e8a0dc8c538740c04f" - }, - { - "name": "group/0", - "where": { - "groupId": 0 - }, - "rows": 56, - "digest": "880483cf97532c6e9a03decabd07b474ef57a25e9378a21e0dda72de13934190" - }, - { - "name": "group/6", - "where": { - "groupId": 6 - }, - "rows": 20, - "digest": "57cad8e6acbc7780cb55459f44cdea773ac559636746d6bcf470cdffeca98131" - }, - { - "name": "group/8", - "where": { - "groupId": 8 - }, - "rows": 14, - "digest": "63f046680b01e66bde44485fd8665d5c94df6d02767e658347e8b354cd505761" - }, - { - "name": "group/10", - "where": { - "groupId": 10 - }, - "rows": 20, - "digest": "ba000985a5cbc0356610d76b17ea00562256f348129fcf4b67813809756ad49e" - }, - { - "name": "group/3", - "where": { - "groupId": 3 - }, - "rows": 62, - "digest": "a30e007b729674ff0c940006f04852bfd0a26399ccdeb467faea7ff9ecb48649" - }, - { - "name": "group/26", - "where": { - "groupId": 26 - }, - "rows": 42, - "digest": "53b463e77301d5e5f749e6c39b0b53b68e89bbf6e5fc94ed30fb6cd805f37c6f" - }, - { - "name": "group/20", - "where": { - "groupId": 20 - }, - "rows": 90, - "digest": "613d53dfc17f126db51dcc482009d9674eb9d3d18259fb8d63b16d6edb4fc58a" - }, - { - "name": "group/4", - "where": { - "groupId": 4 - }, - "rows": 30, - "digest": "06c961742c4ef198016bb414310bce80b15639cf9223b83422d2f85dd69ba8a9" - }, - { - "name": "group/34", - "where": { - "groupId": 34 - }, - "rows": 56, - "digest": "5d030f0617d0bb54546142ac7249b17fe44af7c10eb9019388bb028b9472e8d1" - }, - { - "name": "group/19", - "where": { - "groupId": 19 - }, - "rows": 12, - "digest": "b39b1b0a2e89cbedd83def1a02b1b0d0b096219a0d851a0b728273bc1a4663c6" - }, - { - "name": "group/7", - "where": { - "groupId": 7 - }, - "rows": 110, - "digest": "c8fa4c990c4f2c6c875c6472f5db8d32288227e8c22a550a20bfbf392c73f3a7" - }, - { - "name": "group/14", - "where": { - "groupId": 14 - }, - "rows": 30, - "digest": "e7e3dc37e20439abd0f60e2d3fa9b2c18326bdd796a1d2db40eaf169b786ea56" - }, - { - "name": "group/15", - "where": { - "groupId": 15 - }, - "rows": 30, - "digest": "92732cbb84728a9ea104228c34acd5a3bb68403103f03c3ad2e28a616f76130c" - }, - { - "name": "group/11", - "where": { - "groupId": 11 - }, - "rows": 20, - "digest": "e461d6175a83eacafd9645e27b93f47de96d86b8dd4346b4eab46a5e8fb4e59a" - }, - { - "name": "group/17", - "where": { - "groupId": 17 - }, - "rows": 90, - "digest": "7ca93ce8812a9a53d79a7335d6b22971fd7c16f6d6689dca8fad122276a40e56" - }, - { - "name": "group/23", - "where": { - "groupId": 23 - }, - "rows": 20, - "digest": "12479b9a1c605051047f2a952446e93607e3fbce00d53bcf8ae8947176275361" - }, - { - "name": "group/2", - "where": { - "groupId": 2 - }, - "rows": 30, - "digest": "2f2449816b629d81a38314063c1085dcaae12c5e830b68b66678262f13612fd2" - }, - { - "name": "group/35", - "where": { - "groupId": 35 - }, - "rows": 56, - "digest": "49153ba902aee3222533cc324919524ceb90322628342bb0c5847eee2bf0b2c2" - }, - { - "name": "group/5", - "where": { - "groupId": 5 - }, - "rows": 12, - "digest": "925a95885cc28a1362f08f197a927eceeb7cec026334d606330843031fde10c8" - }, - { - "name": "group/16", - "where": { - "groupId": 16 - }, - "rows": 12, - "digest": "0f38f5fb8d8e9d89d69c4528264d28b61a172c5bf92d97c4c18bf2c0de0b00fe" - }, - { - "name": "group/36", - "where": { - "groupId": 36 - }, - "rows": 72, - "digest": "872fdb6931a8209697d1bc9de223198f4c38fd570203ac1ba794bc4c682a127e" - }, - { - "name": "group/29", - "where": { - "groupId": 29 - }, - "rows": 20, - "digest": "c20b6d3ece40d2a0aff5534765db40522d3afb63718ee66daf5f0b023d3c4eca" - }, - { - "name": "group/40", - "where": { - "groupId": 40 - }, - "rows": 42, - "digest": "ed7614efe8400db127041701d09eb9c481f88f7fbc25ccfdaf28dfe519b1f9da" - }, - { - "name": "group/41", - "where": { - "groupId": 41 - }, - "rows": 20, - "digest": "90a5e8ee8e61881547653995753d84234e89bfc1b75cd5db693abc9b9730eb92" - }, - { - "name": "group/30", - "where": { - "groupId": 30 - }, - "rows": 30, - "digest": "0bfa27de0c082d299f5513b4f4d6d674e57c84ef209016710d1e8731ae864a51" - }, - { - "name": "group/37", - "where": { - "groupId": 37 - }, - "rows": 20, - "digest": "6ac3ab59ca720521959c1e8c8e5b6d23f83b18aa99479e7322d864761317235a" - }, - { - "name": "group/38", - "where": { - "groupId": 38 - }, - "rows": 68, - "digest": "3fcf7446cad20f131b0f9895f152e522708259c149bf798a7787387989b2bc85" - }, - { - "name": "group/42", - "where": { - "groupId": 42 - }, - "rows": 70, - "digest": "da271da22f0724f936a6a1e3f6dafad613a742a3ac1fc0a8b98c58d81ec98fb6" - }, - { - "name": "group/46", - "where": { - "groupId": 46 - }, - "rows": 56, - "digest": "d012e40ee1560dd284a18432dfcd9683247e9745f10648830df6223b19788b95" - }, - { - "name": "group/32", - "where": { - "groupId": 32 - }, - "rows": 28, - "digest": "c3a11ad726c8dd51e556e622aa66bee09054378755ae49e6012fdf822b27565d" - }, - { - "name": "group/39", - "where": { - "groupId": 39 - }, - "rows": 42, - "digest": "f6ebdc191628c8b40baf9096f3d675be9bc9502373775bfadd36ba0f5788823e" - }, - { - "name": "group/43", - "where": { - "groupId": 43 - }, - "rows": 12, - "digest": "6548ff744b134504420bc0c337c32989b37ce82aa2376bc1d5b8aa42eb7db99f" - }, - { - "name": "group/31", - "where": { - "groupId": 31 - }, - "rows": 12, - "digest": "06d3d07ba490c3fe5091ccaf64b246c9ad5775b166209eabf57e4c2dc6a553a9" - }, - { - "name": "group/44", - "where": { - "groupId": 44 - }, - "rows": 30, - "digest": "1caa419042854fa87ee35e3d5d4d1ff1a31b4de23ffd808955ec7cade4470351" - }, - { - "name": "group/33", - "where": { - "groupId": 33 - }, - "rows": 56, - "digest": "acf669b3baf8f746eb0c5e33a78aae4d5ea804cef4ea02dc264f55e9376f0947" - }, - { - "name": "group/47", - "where": { - "groupId": 47 - }, - "rows": 72, - "digest": "a43d28d45a8ce057c0f98eee6606534d6a5a2c1d36d35efdc4ad8d084dc4f57f" - }, - { - "name": "group/45", - "where": { - "groupId": 45 - }, - "rows": 72, - "digest": "8dbfb5f4ced11e20882f4d610fe330ec51eb0dc43156fbd5ffb81e78a8857c90" - }, - { - "name": "group/48", - "where": { - "groupId": 48 - }, - "rows": 30, - "digest": "5135525821c82769fca296ec9a480beeaa0267836666f5212eff30f2429d3f88" - }, - { - "name": "group/53", - "where": { - "groupId": 53 - }, - "rows": 12, - "digest": "202dffa0cef9bbb023cebadf652af1663bdcea4e13141e71bb43b2df87a3ce4a" - }, - { - "name": "group/60", - "where": { - "groupId": 60 - }, - "rows": 30, - "digest": "0b6e0ebecc2a6f669f47d7100788e41bb3bf11bbe9af1ab0b79e3bbd493e85f1" - }, - { - "name": "group/49", - "where": { - "groupId": 49 - }, - "rows": 12, - "digest": "180601ec417cd680ce179b6e2e623811b00c47d3e8c1b7537b6b456037f2c6ef" - }, - { - "name": "group/54", - "where": { - "groupId": 54 - }, - "rows": 12, - "digest": "af58cc14e302a74b6afb092b76f4119a576855c7ed49f6827d09c13abcfd8d91" - }, - { - "name": "group/50", - "where": { - "groupId": 50 - }, - "rows": 20, - "digest": "64e4eedcabdc22c8fa571e666c6bc8615a4948c1fd06347cd6b83195846da31a" - }, - { - "name": "group/52", - "where": { - "groupId": 52 - }, - "rows": 12, - "digest": "cd2778d48f14aa00c53fc9a01332be1c4cd4eb9d196a82c3d7ebe747decdce22" - }, - { - "name": "group/77", - "where": { - "groupId": 77 - }, - "rows": 30, - "digest": "f35530bab13e75f37a3a2198648026240bc7e2a9caee8236a172958fb7f90fbf" - }, - { - "name": "group/51", - "where": { - "groupId": 51 - }, - "rows": 20, - "digest": "35566efabaf9a9a82f09ee2094489f780e79c89ead61d521b7b33552d247bc59" - }, - { - "name": "group/57", - "where": { - "groupId": 57 - }, - "rows": 12, - "digest": "7659db726ce64027b472e087a894013c6b10b4cf0648b3e5cfeded25bf1345d8" - }, - { - "name": "group/66", - "where": { - "groupId": 66 - }, - "rows": 56, - "digest": "4dd4f8885d245dae484cacad712a2e68c5a9b0d9a6fe3ebfef2113f27ba050ed" - }, - { - "name": "group/71", - "where": { - "groupId": 71 - }, - "rows": 12, - "digest": "53f45d44d43702f3f2e9ed1710307d5b60800f6badd4c976217d07bef70588c9" - }, - { - "name": "group/56", - "where": { - "groupId": 56 - }, - "rows": 42, - "digest": "d4081dd1363dd7455f1d543254ecf1a4bc6a115b1caf5271101ea7dc260f7552" - }, - { - "name": "group/55", - "where": { - "groupId": 55 - }, - "rows": 28, - "digest": "8a1397071b8cd90386522ed904e3ceb19acb8afa563cd96d2ac8c9814b9bb0e7" - }, - { - "name": "group/82", - "where": { - "groupId": 82 - }, - "rows": 60, - "digest": "e5054027e0e7be02104ee618a74986e5d8698fd0e3fccdf0d9014bed7cd3fb2c" - }, - { - "name": "group/67", - "where": { - "groupId": 67 - }, - "rows": 12, - "digest": "1054e717c1b6687bc27029a83f047283ec448e0c05201fcd70eaf485e016e090" - }, - { - "name": "group/72", - "where": { - "groupId": 72 - }, - "rows": 30, - "digest": "b79adefe94de70c9bdc35ee182d348d8c2ff30a4ace996308d789036467224ea" - }, - { - "name": "group/58", - "where": { - "groupId": 58 - }, - "rows": 30, - "digest": "4a7fd2667107cd56b58172128050982c7fbef3375c75706873e8333151b25527" - }, - { - "name": "group/68", - "where": { - "groupId": 68 - }, - "rows": 42, - "digest": "2a841dd5ab8ff2664f620a9ea1a40e12cb4e3550833a20b9300389b9bb270287" - }, - { - "name": "group/78", - "where": { - "groupId": 78 - }, - "rows": 12, - "digest": "e295689301f15f9f5eb405a45037f0c2af56dc2dc707b5a4d7d82405ec4b7b23" - }, - { - "name": "group/83", - "where": { - "groupId": 83 - }, - "rows": 20, - "digest": "40fb31c23a852b989124c89d03cc830f50dbd09bf785db4ee1d153dfcadbaf21" - }, - { - "name": "group/73", - "where": { - "groupId": 73 - }, - "rows": 56, - "digest": "b0f425d1414f284bceb882e48a1e56a92680fa6a14c878a610d674b8d0f30852" - }, - { - "name": "group/79", - "where": { - "groupId": 79 - }, - "rows": 12, - "digest": "62e4fbcd2b5cd23d0b4af027db1294943311908b3fc75219c78a4c997413c5ca" - }, - { - "name": "group/84", - "where": { - "groupId": 84 - }, - "rows": 20, - "digest": "656132deaa4e22f7e1862cff36eb37d6f1f8314137f057a0a2a4b9190a1ea660" - }, - { - "name": "group/85", - "where": { - "groupId": 85 - }, - "rows": 10, - "digest": "1b72e303d9acc64d089da0fe97488e06ed3ffd102e0055082880c0e6b085dd02" - }, - { - "name": "group/69", - "where": { - "groupId": 69 - }, - "rows": 30, - "digest": "6ecceae8a5587e1da412357f5dc6a9e90dee69da8563dc8b5a367f6fc8d4bd0a" - }, - { - "name": "group/74", - "where": { - "groupId": 74 - }, - "rows": 12, - "digest": "242938ef69187cbc02cc49cbaa37771e4c445454418c302082ed1fc3b1caf7a9" - }, - { - "name": "group/103", - "where": { - "groupId": 103 - }, - "rows": 90, - "digest": "ec217cc3da0f4f32cfe67a5d0ce0c6f062dc419a5f963def497728ed309aaa71" - }, - { - "name": "group/105", - "where": { - "groupId": 105 - }, - "rows": 20, - "digest": "11ecbd70e989f68e9d03ee62a62113c0beb016bd59ae6514debc435a2092c05a" - }, - { - "name": "group/132", - "where": { - "groupId": 132 - }, - "rows": 12, - "digest": "f6332719e7c4ec364a635583ad453c6c1fceaaeadad6010e80779a90e017c31f" - }, - { - "name": "group/147", - "where": { - "groupId": 147 - }, - "rows": 30, - "digest": "8d64b866c53ee618350388127e1200ebb910c058aef32332afdbed4f81ce04b6" - }, - { - "name": "group/81", - "where": { - "groupId": 81 - }, - "rows": 56, - "digest": "6f1d3289b77d193b1c7adde9bac2fafaf05600d42ccd2a877262e00d8f88cbda" - }, - { - "name": "group/107", - "where": { - "groupId": 107 - }, - "rows": 12, - "digest": "78fed1d29ef064cdb0561bf24d0bde0a52dfcb99a2c49bb2e668944940ee299f" - }, - { - "name": "group/87", - "where": { - "groupId": 87 - }, - "rows": 42, - "digest": "38407062225107b0868ef379ab6733289839dc2a5608a4d47753be4f69c0ae52" - }, - { - "name": "group/90", - "where": { - "groupId": 90 - }, - "rows": 42, - "digest": "0da0cd247538801d1834e0394f4f2176d08ba645fec26cbbd92afbc4987ac343" - }, - { - "name": "group/21", - "where": { - "groupId": 21 - }, - "rows": 72, - "digest": "67d480b8fd8ff2edb515ffdc1c0d31db32bc79c70db637f29263d0d25907c5fa" - }, - { - "name": "group/75", - "where": { - "groupId": 75 - }, - "rows": 30, - "digest": "aa3fe50b9239e12f266888e5e6ff2998ce249682f0303c20080928b3d6edb00d" - }, - { - "name": "group/64", - "where": { - "groupId": 64 - }, - "rows": 42, - "digest": "d0b953b56644e8ac10f684558ecaf980388076707c1aa95e40d570f9bc938ef0" - }, - { - "name": "group/123", - "where": { - "groupId": 123 - }, - "rows": 42, - "digest": "7aa419859ce85d99beaf5a0c1b84e33c36780b429f8415c67eeb1e8885e9521b" - }, - { - "name": "group/140", - "where": { - "groupId": 140 - }, - "rows": 12, - "digest": "a0a0505fad8751213d098bc38217a54ab8d8b1e6e220e31bd33246e0da661025" - }, - { - "name": "group/139", - "where": { - "groupId": 139 - }, - "rows": 42, - "digest": "85e460d560b1f346d29f506496e5cfd18b1c503328e9f5a3802e2aaba1e3f420" - }, - { - "name": "group/91", - "where": { - "groupId": 91 - }, - "rows": 12, - "digest": "2f76cd6a78e4acd0c1431eb0fcdc023db91e6fe88be8e4941de9a0671871f1af" - }, - { - "name": "group/101", - "where": { - "groupId": 101 - }, - "rows": 56, - "digest": "8a756e4b224c935b27969ebb6125ef4be179172106683731e394f184d9783423" - }, - { - "name": "group/126", - "where": { - "groupId": 126 - }, - "rows": 12, - "digest": "edccd01e72e143386ee6290732a8419dc1097b2a36075b1b9a197b99d6f08e3e" - }, - { - "name": "group/136", - "where": { - "groupId": 136 - }, - "rows": 12, - "digest": "936bfc9c33fe1d6072c61ce9f6629a3a6ebc56532a029782b8dabde309dd8d3b" - }, - { - "name": "group/25", - "where": { - "groupId": 25 - }, - "rows": 20, - "digest": "bc13eca380f2ba33d5a15715d7766bf1129ccfe03214661a0a00033e22f4bcc4" - }, - { - "name": "group/80", - "where": { - "groupId": 80 - }, - "rows": 30, - "digest": "6609b03ce0f9f80dee662ee3c8898520daa0ed138d0b2bc13f689fc9a412ecda" - }, - { - "name": "group/97", - "where": { - "groupId": 97 - }, - "rows": 30, - "digest": "c2d0771d81e449cfb96526bbf0e7374599a09917197256d84596443db39628b8" - }, - { - "name": "group/117", - "where": { - "groupId": 117 - }, - "rows": 56, - "digest": "d2f6df4ef026e3b70c22a04d48e7f73aa09ea5a8da1121e7d48bb0123f2a68bd" - }, - { - "name": "group/127", - "where": { - "groupId": 127 - }, - "rows": 30, - "digest": "5db46905ccf9e782d90a95edf2e1a8aa3c817a80587b99518f6c058cb306e526" - }, - { - "name": "group/129", - "where": { - "groupId": 129 - }, - "rows": 12, - "digest": "a7ca11fff97e5c8f0b29ca91eb25d816ce73553553f7844f1ae57ac6029145ab" - }, - { - "name": "group/92", - "where": { - "groupId": 92 - }, - "rows": 12, - "digest": "a8826515e53d7ff6bd5bdf3634fdda532132c4cef9863cf030b3313c7a671c44" - }, - { - "name": "group/94", - "where": { - "groupId": 94 - }, - "rows": 42, - "digest": "5583d6b816da95751075d47f5d3d7edbc19cdce4e5d169332eabcadf64dc694e" - }, - { - "name": "group/128", - "where": { - "groupId": 128 - }, - "rows": 50, - "digest": "a61bf36c8747d0e78b84db6ca3b00b8c433e8a29b4eeb734d20d19494b42f26c" - }, - { - "name": "group/141", - "where": { - "groupId": 141 - }, - "rows": 12, - "digest": "196e81cfe1f2526cd24cfd9854be9bee0d8154fa2a04ac9748e6cb8407e82b98" - }, - { - "name": "group/113", - "where": { - "groupId": 113 - }, - "rows": 90, - "digest": "2f78b293082c2ba0d78624f770c588413871dad94d36ec9570aada8da1b5e781" - }, - { - "name": "group/102", - "where": { - "groupId": 102 - }, - "rows": 56, - "digest": "c684238fcf92057d22c3bc863457b5ccb26fca79a01f7779b65a75bb60d642dc" - }, - { - "name": "group/109", - "where": { - "groupId": 109 - }, - "rows": 20, - "digest": "1f208821d2cdc4264004f095a25a23c9fea4a1b957e6b857c7c5bfb61935888c" - }, - { - "name": "group/121", - "where": { - "groupId": 121 - }, - "rows": 42, - "digest": "8dda88e19811edf918ad0f00240eae5a832c6050828ad1429aa075341e9ace4b" - }, - { - "name": "group/144", - "where": { - "groupId": 144 - }, - "rows": 30, - "digest": "f712fde6906c6919434510be715a7f6a5e8c97c76a108406981edce4159f7673" - }, - { - "name": "group/114", - "where": { - "groupId": 114 - }, - "rows": 12, - "digest": "93466e914ae21caee5e1c1757c5cc22f58019ff8d1c7abfff664e1cd10b248be" - }, - { - "name": "group/99", - "where": { - "groupId": 99 - }, - "rows": 42, - "digest": "bd4147450f8a1b06924a79423eebc02651b6c48b8d64907baa83d9ce626f7d8a" - }, - { - "name": "group/112", - "where": { - "groupId": 112 - }, - "rows": 20, - "digest": "4ebeef0726d703a5c5875d05fe3eb0cee5b09c47220753db84c89494c6efacca" - }, - { - "name": "group/88", - "where": { - "groupId": 88 - }, - "rows": 20, - "digest": "81839e454d7ed1f859471c3d20cb95566ffab92cd3431d16b2d85e443fbffc05" - }, - { - "name": "group/143", - "where": { - "groupId": 143 - }, - "rows": 30, - "digest": "36a16aea44277c5a1a4705f57259b8fcda0e195c88ae32b2ec7f32d09e29bcc2" - }, - { - "name": "group/22", - "where": { - "groupId": 22 - }, - "rows": 56, - "digest": "c98ea1577d9c2827c1d8199276cf9a1ff2766100148d835c8fd18d70e267d0a9" - }, - { - "name": "group/70", - "where": { - "groupId": 70 - }, - "rows": 20, - "digest": "61cbc29928638e468f5e57b13016ea77aed09abfd1d9bc482ddc314d58adea78" - }, - { - "name": "group/93", - "where": { - "groupId": 93 - }, - "rows": 20, - "digest": "881ec1a62e3b055ffd9de4d853dcad66f01f7a5242a81309a36db1d23528459d" - }, - { - "name": "group/116", - "where": { - "groupId": 116 - }, - "rows": 42, - "digest": "3ca2bc7af7d9f921ec101bfd112a2ab7d4bbc10c58d608c2f14bc9e722345a4d" - }, - { - "name": "group/89", - "where": { - "groupId": 89 - }, - "rows": 20, - "digest": "70fc3602751899d70f0810a9dec4ec856fc8dcd6579d42ab5c875a470f788b30" - }, - { - "name": "group/12", - "where": { - "groupId": 12 - }, - "rows": 12, - "digest": "4e6291f01bb147203ec39679e6423aaa0e51eee51e5947eec29eed679c4acb5f" - }, - { - "name": "group/62", - "where": { - "groupId": 62 - }, - "rows": 12, - "digest": "96d88a4ecaa02097e3cf708e24e6ed21786fe94dc29cea8f9618070029262b00" - }, - { - "name": "group/104", - "where": { - "groupId": 104 - }, - "rows": 46, - "digest": "afe1a86074b0dedd76d80475617dc1b17ab8d2d7ff46c99c96dad214ceb365c1" - }, - { - "name": "group/124", - "where": { - "groupId": 124 - }, - "rows": 42, - "digest": "15663d6b1c1a448968acd6b8e147d89cf21e1b1216c9aeeb6e1a8c2c419b8bba" - }, - { - "name": "group/135", - "where": { - "groupId": 135 - }, - "rows": 30, - "digest": "5c2efc9193f7d9be65d7fb15d9e2168c6b341c9a6c88d3f1db93362d3e8da2c9" - }, - { - "name": "group/13", - "where": { - "groupId": 13 - }, - "rows": 42, - "digest": "dc35aac3a7cb42006eefbbb2606b5e69c1d2ec5fb87ea61a2de29862a70a21d0" - }, - { - "name": "group/76", - "where": { - "groupId": 76 - }, - "rows": 90, - "digest": "21fd895fa1e8583739b24e783f6624afb484e0e9f0aca1be749f07fb8d381353" - }, - { - "name": "group/115", - "where": { - "groupId": 115 - }, - "rows": 12, - "digest": "59beebf7d76cbd7ec25afb0e1aff22eddef7810c8367d4a25ea36f9b8818c8ce" - }, - { - "name": "group/130", - "where": { - "groupId": 130 - }, - "rows": 12, - "digest": "eea5040020933fde3dfd31ee0ecf0fe85398e0d5cb9b627aba63461530155172" - }, - { - "name": "group/134", - "where": { - "groupId": 134 - }, - "rows": 12, - "digest": "13ee49f65366f1dcaf92e4b3f82e184abbc46acd8d90e4e68aaccba627c72365" - }, - { - "name": "group/28", - "where": { - "groupId": 28 - }, - "rows": 42, - "digest": "65ec25804af438ff4b254790f089c44b20d10e7cc98f1eff567c223b16c56863" - }, - { - "name": "group/95", - "where": { - "groupId": 95 - }, - "rows": 30, - "digest": "0cfc6c05622cf8a4c35ee1b64c3524187c4a6e47cc0630a4777526e04df2b66d" - }, - { - "name": "group/108", - "where": { - "groupId": 108 - }, - "rows": 30, - "digest": "b671ff898fd16689c12ec2accfd04cd4e4efb47157bfcdd0c949baf9f8fadd38" - }, - { - "name": "group/118", - "where": { - "groupId": 118 - }, - "rows": 42, - "digest": "7498cdfbf8d699b1fd1c31c5092bdc4b732436878c38e076f7f91b65354ff8ab" - }, - { - "name": "group/138", - "where": { - "groupId": 138 - }, - "rows": 12, - "digest": "5e78307499a9269e124ad5d6e81aa35225afd3d440f75084fea5c5c2b86e6130" - }, - { - "name": "group/146", - "where": { - "groupId": 146 - }, - "rows": 90, - "digest": "1f66c818a3fc13787e2dcd6dd0877110f2412dc65785e0ae7a685481b28d9ca3" - }, - { - "name": "group/59", - "where": { - "groupId": 59 - }, - "rows": 30, - "digest": "e4921b6766bf2975e190fef6d07be1579d7af46c68b0caa212009e61d2a2ce62" - }, - { - "name": "group/98", - "where": { - "groupId": 98 - }, - "rows": 70, - "digest": "c9496ee11bdebbf852caa9689e6105c812c0f7d5fa0b1a5ad9ad71b3e5f46db7" - }, - { - "name": "group/111", - "where": { - "groupId": 111 - }, - "rows": 30, - "digest": "3906dea0331c41495c2855ade33ac573945d9790e65b9885861ff4e5709f4287" - }, - { - "name": "group/137", - "where": { - "groupId": 137 - }, - "rows": 12, - "digest": "1163853dcf0235822633a13577f33069e455d8e12cceba9e31cb2c6c108a33a7" - }, - { - "name": "group/24", - "where": { - "groupId": 24 - }, - "rows": 12, - "digest": "66ccb82231ee7db09c878fd84775eb68bd3d75e393e05d9242b4f71c3a5339c3" - }, - { - "name": "group/106", - "where": { - "groupId": 106 - }, - "rows": 12, - "digest": "1a37d4221c8987a2da35b7bb1e30aa0f0a65f021c22e799b639c8bacfbf6df64" - }, - { - "name": "group/120", - "where": { - "groupId": 120 - }, - "rows": 42, - "digest": "e0b739d9a4a94054d8bcef5b6c68e1774e2de2250cebf96cf3869c56648afd3e" - }, - { - "name": "group/133", - "where": { - "groupId": 133 - }, - "rows": 30, - "digest": "5338b28d7dea89dde2b9cb4c7ca2483f0afd8ef87542b9aa8337e62b86d65439" - }, - { - "name": "group/148", - "where": { - "groupId": 148 - }, - "rows": 12, - "digest": "358ecef2b4f82b4aa091bc6357d059c50fadc2754217c11cbf99e7e475b2cbc0" - }, - { - "name": "group/65", - "where": { - "groupId": 65 - }, - "rows": 48, - "digest": "eaed16886921a23a4663dae6791ce80ee2598f355805f013bc0d31ef57cd6975" - }, - { - "name": "group/63", - "where": { - "groupId": 63 - }, - "rows": 30, - "digest": "baf779b6e992d43ada720cc81a6b3a1bd300d6ed6698bd199880cd54501720e0" - }, - { - "name": "group/110", - "where": { - "groupId": 110 - }, - "rows": 56, - "digest": "043a110f271dbd1e186d8ddea8d32dac251d00bbaba817151cfb6464bf179f95" - }, - { - "name": "group/119", - "where": { - "groupId": 119 - }, - "rows": 50, - "digest": "07103fd490b38aef2078333c4033b83524abf946f45649063c6c8a18f793447e" - }, - { - "name": "group/27", - "where": { - "groupId": 27 - }, - "rows": 20, - "digest": "855c55bf95c26de137df4c5cd337575f1f7c58d3defc50fcaef2f559a9a95873" - }, - { - "name": "group/100", - "where": { - "groupId": 100 - }, - "rows": 30, - "digest": "96493a32c22f6f0b6f6094bf098618bc55b858bb75d797ac6b5b0e98cd29e2db" - }, - { - "name": "group/86", - "where": { - "groupId": 86 - }, - "rows": 12, - "digest": "6f86896defafc5890ed95c27aea8dafd7053a188e1015c73af78915ca96b2c72" - }, - { - "name": "group/125", - "where": { - "groupId": 125 - }, - "rows": 30, - "digest": "dc82983386f0ec68e71e80a4ded0f3d89a125504da25e981e53432ca7d678f97" - }, - { - "name": "group/149", - "where": { - "groupId": 149 - }, - "rows": 80, - "digest": "5cb22b8829159223166ee73104c1afaf8d415e97f9e6c46c213688a6993710b7" - }, - { - "name": "group/61", - "where": { - "groupId": 61 - }, - "rows": 12, - "digest": "019106a2de0eaa0cf6a7196bd4d19f3e2075e088ecfba6be801365308d0adfcb" - }, - { - "name": "group/96", - "where": { - "groupId": 96 - }, - "rows": 40, - "digest": "50bbbf9f90d1eed4a25a0446c1745154c1f08ca34a80bd06dd6e4d79064b1c7b" - }, - { - "name": "group/122", - "where": { - "groupId": 122 - }, - "rows": 42, - "digest": "25b19426ab139f7827fd02cfd513125546f7e6ea831efe6aa31bdcd0be56c91a" - }, - { - "name": "group/131", - "where": { - "groupId": 131 - }, - "rows": 20, - "digest": "3f80b6ad20506aec4dd1a47db20c5e0b16466d24a211362a618619626c619afb" - }, - { - "name": "group/145", - "where": { - "groupId": 145 - }, - "rows": 82, - "digest": "f9ba78f76bab6b8c609277ee95dce8d5ec687be2460ab2cc8beabcc17cbe57e1" - }, - { - "name": "group/142", - "where": { - "groupId": 142 - }, - "rows": 30, - "digest": "614d789a464982642b5951eb1e785e06dfcdc5c501a7cca1ad95c0a2cd232048" - }, - { - "name": "pair/0/5", - "where": { - "userId": 0, - "friendId": 5 - }, - "rows": 3, - "digest": "a4c00b80598e0c4090a0c1627d9bb41c41d579f5cc0a7b593b7bfe76479ff7bb" - }, - { - "name": "pair/0/8", - "where": { - "userId": 0, - "friendId": 8 - }, - "rows": 3, - "digest": "8d820368d36207b915a788bd6f98e987f3d8aacd6bfd761b46180af1265f9aee" - }, - { - "name": "pair/0/12", - "where": { - "userId": 0, - "friendId": 12 - }, - "rows": 1, - "digest": "2dfa1352fcc7db3fc4ff051b709bceb26e5e8fa3acadf86f68254c2b12a248d1" - }, - { - "name": "pair/0/13", - "where": { - "userId": 0, - "friendId": 13 - }, - "rows": 3, - "digest": "e8ab2d34df4832d9bd6ce5fe39138c150fda50d94dd4ecc753ffb1bb4fd97fa6" - }, - { - "name": "pair/0/14", - "where": { - "userId": 0, - "friendId": 14 - }, - "rows": 1, - "digest": "96229ec695723cdd13934e6b11fe580b3576376620f8c9ce86f6ecf95e97f35d" - }, - { - "name": "pair/0/20", - "where": { - "userId": 0, - "friendId": 20 - }, - "rows": 1, - "digest": "d0dc9b6bf685e3d1df337f8fb8bc9c3402b32d97ac8d607dc372c62d100c5246" - }, - { - "name": "pair/0/21", - "where": { - "userId": 0, - "friendId": 21 - }, - "rows": 1, - "digest": "ad29043b4bcc4f9e539bc9d1fd842761b6e0160c52771f230ce8aa8bdeaefd05" - }, - { - "name": "pair/0/23", - "where": { - "userId": 0, - "friendId": 23 - }, - "rows": 1, - "digest": "798c60cd0456d66527cce9627b6963c64e607d42822b2d476a6b9e766cf247c3" - }, - { - "name": "pair/0/24", - "where": { - "userId": 0, - "friendId": 24 - }, - "rows": 2, - "digest": "8790f4a098d685440db9de6bf04d61b4968f1baf4cc93826ce8d2e418f92b82e" - }, - { - "name": "pair/0/28", - "where": { - "userId": 0, - "friendId": 28 - }, - "rows": 1, - "digest": "ca64d78e78dab4e578d54d4f11743e107d25403e723441ae058eb4bcde4d75ef" - }, - { - "name": "pair/0/30", - "where": { - "userId": 0, - "friendId": 30 - }, - "rows": 2, - "digest": "a8ff1f08aae9ca94238b6ba71deaed0ad7e0a5e0d494780419c4e427553ad27c" - }, - { - "name": "pair/0/31", - "where": { - "userId": 0, - "friendId": 31 - }, - "rows": 1, - "digest": "b502e7d616aa1cdbf43faa4a6cbb5c8ab95eaf1f69846b9bdebbaed3a1a003a2" - }, - { - "name": "pair/0/32", - "where": { - "userId": 0, - "friendId": 32 - }, - "rows": 3, - "digest": "99fcbb4174f94ddf56ae0d89d0ad283bd1a225f0d37415bd77ea4ea6f3637d5d" - }, - { - "name": "pair/0/34", - "where": { - "userId": 0, - "friendId": 34 - }, - "rows": 1, - "digest": "02cc2c52f07b3e75f4af1b87d9bca28ae4c96a876dc431175ef2f431c0c67de1" - }, - { - "name": "pair/0/36", - "where": { - "userId": 0, - "friendId": 36 - }, - "rows": 1, - "digest": "d873757840efac7974b5dd2fc80a5bad6b485e76147534dcd96723f071689d2d" - }, - { - "name": "pair/0/44", - "where": { - "userId": 0, - "friendId": 44 - }, - "rows": 1, - "digest": "1bd1614b6419bdcca19eb3843ea8d00e5eeef357a2438e789e20bc65dd540dd6" - }, - { - "name": "pair/0/45", - "where": { - "userId": 0, - "friendId": 45 - }, - "rows": 1, - "digest": "43c49490dd6a4383ab18371a2eb683688495c3725f5f5a74acad8d76242a24f8" - }, - { - "name": "pair/0/47", - "where": { - "userId": 0, - "friendId": 47 - }, - "rows": 3, - "digest": "c136db03e19917b9c2212172617d87aceeb7259822abc217f9facedfd1885e08" - }, - { - "name": "pair/0/49", - "where": { - "userId": 0, - "friendId": 49 - }, - "rows": 1, - "digest": "a7f1dcf267eea2d7cd0f66c36b17734a438e47f9082c936490b8d8d6e52f81e1" - }, - { - "name": "pair/0/50", - "where": { - "userId": 0, - "friendId": 50 - }, - "rows": 2, - "digest": "09c535d65c20694451ded615850173783c64afd097591659cb2ccba36ea9bcb9" - }, - { - "name": "pair/0/51", - "where": { - "userId": 0, - "friendId": 51 - }, - "rows": 1, - "digest": "7597962a06ff46a6834b5b2dd6d71cd4e0d64718e1ef637f166384f27922801e" - }, - { - "name": "pair/0/52", - "where": { - "userId": 0, - "friendId": 52 - }, - "rows": 1, - "digest": "d683e17c08d3f151290ec3f36ef22006b7b875abc0be30ddebd4849ec5f7ab92" - }, - { - "name": "pair/0/54", - "where": { - "userId": 0, - "friendId": 54 - }, - "rows": 1, - "digest": "8059e182eeb3dfacbff8939576c760bb49fc43ceac9fcc89bb30d9c6ca8c2c72" - }, - { - "name": "pair/0/55", - "where": { - "userId": 0, - "friendId": 55 - }, - "rows": 2, - "digest": "da83b00dc76bb4af0916c2d6469e6b9c59f32a635cbc5566fd697a486b140558" - }, - { - "name": "pair/0/61", - "where": { - "userId": 0, - "friendId": 61 - }, - "rows": 1, - "digest": "20efd9939f1527a63fe839a743745c60374af170c77ba96b0779dc7022a9ec81" - }, - { - "name": "pair/0/70", - "where": { - "userId": 0, - "friendId": 70 - }, - "rows": 1, - "digest": "e00d44644c48584a41caaca8b478ab28792560d2e43a4b1a73d01f220c8b09d6" - }, - { - "name": "pair/0/71", - "where": { - "userId": 0, - "friendId": 71 - }, - "rows": 1, - "digest": "c03afbe3568941cc259dd6782cf0920b7ed105de16fdf09b2d8f61ab22f9947a" - }, - { - "name": "pair/0/81", - "where": { - "userId": 0, - "friendId": 81 - }, - "rows": 1, - "digest": "70af528a9a15739c753050124d6af5b3097abeaac0de0bedc3c20c326eee6c7b" - }, - { - "name": "pair/0/84", - "where": { - "userId": 0, - "friendId": 84 - }, - "rows": 3, - "digest": "86971d390739513344e19344d789d1e90b5660b07874e6680e6045259957a6b8" - }, - { - "name": "pair/0/85", - "where": { - "userId": 0, - "friendId": 85 - }, - "rows": 2, - "digest": "62335b38158122bc839c20484bce41f2ef188fb52ff736603111f8fe57bfc2b8" - }, - { - "name": "pair/0/86", - "where": { - "userId": 0, - "friendId": 86 - }, - "rows": 1, - "digest": "5da8a424cb2c1bca180b4d1e229d041da1794f5de051e0a200c7e02771f33748" - }, - { - "name": "pair/0/92", - "where": { - "userId": 0, - "friendId": 92 - }, - "rows": 1, - "digest": "aa9a27be61d24b26dc7f1f157db59e5f19c6e793b15bf189f1880403183b3279" - }, - { - "name": "pair/0/93", - "where": { - "userId": 0, - "friendId": 93 - }, - "rows": 1, - "digest": "6658fd8296d063d7ccfdea806de7f331a6ac5bbcf3f3b5eb1ebd4f5ddedfcd90" - }, - { - "name": "pair/0/98", - "where": { - "userId": 0, - "friendId": 98 - }, - "rows": 1, - "digest": "af7101ef097b38cca9f7c171280b1f44f5259828cec7617242ac4d08a48a41aa" - }, - { - "name": "pair/0/104", - "where": { - "userId": 0, - "friendId": 104 - }, - "rows": 1, - "digest": "fae2718e74539f52135858576a6c8d8b6f62a8ca87b5a9101e53133dbbed78fa" - }, - { - "name": "pair/0/106", - "where": { - "userId": 0, - "friendId": 106 - }, - "rows": 2, - "digest": "0c6a625b7e68c3a7936a3715b4f5615a340b68773f145dc27c73fa8d04a49201" - }, - { - "name": "pair/0/111", - "where": { - "userId": 0, - "friendId": 111 - }, - "rows": 1, - "digest": "2d6c03d69c9fa6e934f8d623dae5f392fbce4cefbf6ecb4b6e85c86e6ff5c335" - }, - { - "name": "pair/0/115", - "where": { - "userId": 0, - "friendId": 115 - }, - "rows": 1, - "digest": "7a764d0d971ddb270ed835e0f4038da74dbfedb98f155ea06bc183dcb49f9fbd" - }, - { - "name": "pair/0/120", - "where": { - "userId": 0, - "friendId": 120 - }, - "rows": 1, - "digest": "b1c705dcaab0d770f28f543ed8e30caea2cac0ce53f6a48d67cbcdd3535db122" - }, - { - "name": "pair/0/121", - "where": { - "userId": 0, - "friendId": 121 - }, - "rows": 1, - "digest": "c2b98a2cc4257dceb2067452a6b4ead562686fd4b6d40aa52dc238e676394650" - }, - { - "name": "pair/0/129", - "where": { - "userId": 0, - "friendId": 129 - }, - "rows": 1, - "digest": "b372ce2c8041977888a73bc1ea823f28ab84e059570ec83b083281f6f6cf0c3f" - }, - { - "name": "pair/0/135", - "where": { - "userId": 0, - "friendId": 135 - }, - "rows": 1, - "digest": "75ddda6a210f2e66b3dcbc497ee522b3506f8be74854b46b57bac72d68496b2b" - }, - { - "name": "pair/0/136", - "where": { - "userId": 0, - "friendId": 136 - }, - "rows": 2, - "digest": "0d8e28d05d7fc2110c61b3daa51d3ad5aae4552d32a0b28bd5b408ccc98281ab" - }, - { - "name": "pair/0/139", - "where": { - "userId": 0, - "friendId": 139 - }, - "rows": 1, - "digest": "5ef7c1aeed84bebb844b1c81ea2c107646018235755f0701ba2dcd32ef9c1168" - }, - { - "name": "pair/0/141", - "where": { - "userId": 0, - "friendId": 141 - }, - "rows": 1, - "digest": "4df9dca12707c6ea6d54183184b97138feebfae7fbce3ec79f48a1e328d2d13f" - }, - { - "name": "pair/0/145", - "where": { - "userId": 0, - "friendId": 145 - }, - "rows": 2, - "digest": "5ec2437853a6e377e60a9e634dfca8783753cbd9e84d3e45149d199576d5b6d5" - }, - { - "name": "pair/0/147", - "where": { - "userId": 0, - "friendId": 147 - }, - "rows": 1, - "digest": "7eeb5e849440e8fbe18be6713558f0bb0dd588a2e6694b2ba6a97946cccbfeab" - }, - { - "name": "pair/1/4", - "where": { - "userId": 1, - "friendId": 4 - }, - "rows": 1, - "digest": "a54426259ea4b17dda44b91b75c3a7cb9a31e503bd5ac858235f8bf91e1ffb96" - }, - { - "name": "pair/1/7", - "where": { - "userId": 1, - "friendId": 7 - }, - "rows": 1, - "digest": "19a13e92f02571a3a1c61d5d01eea8c7a8d4f31e4c3a0d9505e015e441250b3d" - }, - { - "name": "pair/1/13", - "where": { - "userId": 1, - "friendId": 13 - }, - "rows": 1, - "digest": "c484c81504dc4f15d614703910942fea1a7acfcbb42f6c2438bed0c42ea18066" - } - ] -}