courts_model.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. const pull = require('../server/node_modules/pull-stream');
  2. const moment = require('../server/node_modules/moment');
  3. const { getConfig } = require('../configs/config-manager.js');
  4. const logLimit = getConfig().ssbLogStream?.limit || 1000;
  5. const CASE_ANSWER_DAYS = 7;
  6. const CASE_EVIDENCE_DAYS = 14;
  7. const CASE_DECISION_DAYS = 21;
  8. const POPULAR_DAYS = 14;
  9. const FEED_ID_RE = /^@.+\.ed25519$/;
  10. module.exports = ({ cooler, services = {} }) => {
  11. let ssb;
  12. let userId;
  13. const openSsb = async () => {
  14. if (!ssb) {
  15. ssb = await cooler.open();
  16. userId = ssb.id;
  17. }
  18. return ssb;
  19. };
  20. const nowISO = () => new Date().toISOString();
  21. const ensureArray = (x) => (Array.isArray(x) ? x : x ? [x] : []);
  22. async function readLog() {
  23. const ssbClient = await openSsb();
  24. return new Promise((resolve, reject) => {
  25. pull(
  26. ssbClient.createLogStream({ limit: logLimit }),
  27. pull.collect((err, arr) => (err ? reject(err) : resolve(arr)))
  28. );
  29. });
  30. }
  31. async function listByType(type) {
  32. const msgs = await readLog();
  33. const tomb = new Set();
  34. const rep = new Map();
  35. const map = new Map();
  36. for (const m of msgs) {
  37. const k = m.key || m.id;
  38. const c = m.value?.content || m.content;
  39. if (!c) continue;
  40. if (c.type === 'tombstone' && c.target) tomb.add(c.target);
  41. if (c.type === type) {
  42. if (c.replaces) rep.set(c.replaces, k);
  43. map.set(k, { id: k, ...c });
  44. }
  45. }
  46. for (const oldId of rep.keys()) map.delete(oldId);
  47. for (const tId of tomb) map.delete(tId);
  48. return [...map.values()];
  49. }
  50. async function getCurrentUserId() {
  51. await openSsb();
  52. return userId;
  53. }
  54. async function resolveRespondent(candidateInput) {
  55. const s = String(candidateInput || '').trim();
  56. if (!s) return null;
  57. if (FEED_ID_RE.test(s)) {
  58. return { type: 'inhabitant', id: s };
  59. }
  60. if (services.tribes && services.tribes.getTribeById) {
  61. try {
  62. const t = await services.tribes.getTribeById(s);
  63. if (t && t.id) return { type: 'tribe', id: t.id };
  64. } catch {}
  65. }
  66. return null;
  67. }
  68. function computeDeadlines(openedAt) {
  69. const answerBy = moment(openedAt).add(CASE_ANSWER_DAYS, 'days').toISOString();
  70. const evidenceBy = moment(openedAt).add(CASE_EVIDENCE_DAYS, 'days').toISOString();
  71. const decisionBy = moment(openedAt).add(CASE_DECISION_DAYS, 'days').toISOString();
  72. return { answerBy, evidenceBy, decisionBy };
  73. }
  74. async function openCase({ titleBase, respondentInput, method }) {
  75. const ssbClient = await openSsb();
  76. const rawTitle = String(titleBase || '').trim();
  77. if (!rawTitle) throw new Error('Title is required.');
  78. const resp = await resolveRespondent(respondentInput);
  79. if (!resp) throw new Error('Accused / Respondent not found.');
  80. const m = String(method || '').trim().toUpperCase();
  81. const ALLOWED = new Set(['JUDGE', 'DICTATOR', 'POPULAR', 'MEDIATION', 'KARMATOCRACY']);
  82. if (!ALLOWED.has(m)) throw new Error('Invalid resolution method.');
  83. if (m === 'DICTATOR' && services.parliament && services.parliament.getGovernmentCard) {
  84. try {
  85. const gov = await services.parliament.getGovernmentCard();
  86. const gm = String(gov && gov.method ? gov.method : '').toUpperCase();
  87. if (gm !== 'DICTATORSHIP') throw new Error('DICTATOR method requires DICTATORSHIP government.');
  88. } catch (e) {
  89. throw new Error('Unable to verify government method for DICTATOR.');
  90. }
  91. }
  92. const openedAt = nowISO();
  93. const prefix = moment(openedAt).format('MM/YYYY') + '_';
  94. const title = prefix + rawTitle;
  95. const { answerBy, evidenceBy, decisionBy } = computeDeadlines(openedAt);
  96. const content = {
  97. type: 'courtsCase',
  98. title,
  99. accuser: userId,
  100. respondentType: resp.type,
  101. respondentId: resp.id,
  102. method: m,
  103. status: 'OPEN',
  104. openedAt,
  105. answerBy,
  106. evidenceBy,
  107. decisionBy,
  108. mediatorsAccuser: [],
  109. mediatorsRespondent: [],
  110. createdAt: openedAt
  111. };
  112. return await new Promise((resolve, reject) =>
  113. ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
  114. );
  115. }
  116. async function listCases(filter = 'open') {
  117. const all = await listByType('courtsCase');
  118. const sorted = all.sort((a, b) => {
  119. const ta = new Date(a.openedAt || a.createdAt || 0).getTime();
  120. const tb = new Date(b.openedAt || b.createdAt || 0).getTime();
  121. return tb - ta;
  122. });
  123. if (filter === 'open') {
  124. return sorted.filter((c) => {
  125. const s = String(c.status || '').toUpperCase();
  126. return s !== 'DECIDED' && s !== 'CLOSED' && s !== 'SOLVED' && s !== 'UNSOLVED' && s !== 'DISCARDED';
  127. });
  128. }
  129. if (filter === 'history') {
  130. return sorted.filter((c) => {
  131. const s = String(c.status || '').toUpperCase();
  132. return s === 'DECIDED' || s === 'CLOSED' || s === 'SOLVED' || s === 'UNSOLVED' || s === 'DISCARDED';
  133. });
  134. }
  135. return sorted;
  136. }
  137. async function listCasesForUser(uid) {
  138. const all = await listByType('courtsCase');
  139. const id = String(uid || userId || '');
  140. const rows = [];
  141. for (const c of all) {
  142. const isAccuser = String(c.accuser || '') === id;
  143. const isRespondent = String(c.respondentId || '') === id;
  144. const ma = ensureArray(c.mediatorsAccuser || []);
  145. const mr = ensureArray(c.mediatorsRespondent || []);
  146. const isMediator = ma.includes(id) || mr.includes(id);
  147. const isJudge = String(c.judgeId || '') === id;
  148. const isDictator = false;
  149. const mine = isAccuser || isRespondent || isMediator || isJudge || isDictator;
  150. if (!mine) continue;
  151. let myPublicPreference = null;
  152. if (isAccuser && typeof c.publicPrefAccuser === 'boolean') {
  153. myPublicPreference = c.publicPrefAccuser;
  154. } else if (isRespondent && typeof c.publicPrefRespondent === 'boolean') {
  155. myPublicPreference = c.publicPrefRespondent;
  156. }
  157. rows.push({
  158. ...c,
  159. respondent: c.respondentId || c.respondent,
  160. isAccuser,
  161. isRespondent,
  162. isMediator,
  163. isJudge,
  164. isDictator,
  165. mine,
  166. myPublicPreference
  167. });
  168. }
  169. rows.sort((a, b) => {
  170. const ta = new Date(a.openedAt || a.createdAt || 0).getTime();
  171. const tb = new Date(b.openedAt || b.createdAt || 0).getTime();
  172. return tb - ta;
  173. });
  174. return rows;
  175. }
  176. async function getCaseById(caseId) {
  177. const id = String(caseId || '').trim();
  178. if (!id) return null;
  179. const all = await listByType('courtsCase');
  180. return all.find((c) => c.id === id) || null;
  181. }
  182. async function upsertCase(obj) {
  183. const ssbClient = await openSsb();
  184. const { id, ...rest } = obj;
  185. const updated = {
  186. ...rest,
  187. type: 'courtsCase',
  188. replaces: id,
  189. updatedAt: nowISO()
  190. };
  191. return await new Promise((resolve, reject) =>
  192. ssbClient.publish(updated, (err, msg) => (err ? reject(err) : resolve(msg)))
  193. );
  194. }
  195. function getCaseRole(caseObj, uid) {
  196. const id = String(uid || '');
  197. if (!id) return 'OTHER';
  198. if (String(caseObj.accuser || '') === id) return 'ACCUSER';
  199. if (String(caseObj.respondentId || '') === id) return 'DEFENCE';
  200. const ma = ensureArray(caseObj.mediatorsAccuser || []);
  201. const mr = ensureArray(caseObj.mediatorsRespondent || []);
  202. if (ma.includes(id) || mr.includes(id)) return 'MEDIATOR';
  203. if (String(caseObj.judgeId || '') === id) return 'JUDGE';
  204. return 'OTHER';
  205. }
  206. async function setMediators({ caseId, side, mediators }) {
  207. const c = await getCaseById(caseId);
  208. if (!c) throw new Error('Case not found.');
  209. const role = side === 'accuser' ? 'ACCUSER' : side === 'respondent' ? 'DEFENCE' : null;
  210. if (!role) throw new Error('Invalid side.');
  211. const myRole = getCaseRole(c, userId);
  212. if (role === 'ACCUSER' && myRole !== 'ACCUSER') throw new Error('Only accuser can set these mediators.');
  213. if (role === 'DEFENCE' && myRole !== 'DEFENCE') throw new Error('Only defence can set these mediators.');
  214. const list = Array.from(
  215. new Set(
  216. ensureArray(mediators || [])
  217. .map((x) => String(x || '').trim())
  218. .filter(Boolean)
  219. )
  220. );
  221. const clean = list.filter((id) => id !== c.accuser && id !== c.respondentId);
  222. if (side === 'accuser') c.mediatorsAccuser = clean;
  223. else c.mediatorsRespondent = clean;
  224. await upsertCase(c);
  225. return c;
  226. }
  227. async function assignJudge({ caseId, judgeId }) {
  228. const c = await getCaseById(caseId);
  229. if (!c) throw new Error('Case not found.');
  230. const m = String(c.method || '').toUpperCase();
  231. if (m !== 'JUDGE') throw new Error('This case does not use a judge.');
  232. const myRole = getCaseRole(c, userId);
  233. if (myRole !== 'ACCUSER' && myRole !== 'DEFENCE') throw new Error('Only parties can assign a judge.');
  234. const id = String(judgeId || '').trim();
  235. if (!id) throw new Error('Judge ID is required.');
  236. if (!FEED_ID_RE.test(id)) throw new Error('Invalid judge ID.');
  237. if (id === String(c.accuser || '') || id === String(c.respondentId || '')) {
  238. throw new Error('Judge cannot be a party of the case.');
  239. }
  240. c.judgeId = id;
  241. await upsertCase(c);
  242. return c;
  243. }
  244. async function addEvidence({ caseId, text, link, imageMarkdown }) {
  245. const c = await getCaseById(caseId);
  246. if (!c) throw new Error('Case not found.');
  247. const role = getCaseRole(c, userId);
  248. if (role === 'OTHER') throw new Error('You are not involved in this case.');
  249. const t = String(text || '').trim();
  250. const l = String(link || '').trim();
  251. let imageUrl = null;
  252. if (imageMarkdown) {
  253. const match = imageMarkdown.match(/\(([^)]+)\)/);
  254. imageUrl = match ? match[1] : imageMarkdown;
  255. }
  256. if (!t && !l && !imageUrl) throw new Error('Text, link or image is required.');
  257. const ssbClient = await openSsb();
  258. const content = {
  259. type: 'courtsEvidence',
  260. caseId: c.id,
  261. author: userId,
  262. role,
  263. text: t,
  264. link: l,
  265. imageUrl,
  266. createdAt: nowISO()
  267. };
  268. return await new Promise((resolve, reject) =>
  269. ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
  270. );
  271. }
  272. async function answerCase({ caseId, stance, text }) {
  273. const c = await getCaseById(caseId);
  274. if (!c) throw new Error('Case not found.');
  275. if (String(c.respondentId || '') !== String(userId || '')) throw new Error('Only the respondent can answer.');
  276. const s = String(stance || '').trim().toUpperCase();
  277. const ALLOWED = new Set(['DENY', 'ADMIT', 'PARTIAL']);
  278. if (!ALLOWED.has(s)) throw new Error('Invalid stance.');
  279. const t = String(text || '').trim();
  280. if (!t) throw new Error('Response text is required.');
  281. const ssbClient = await openSsb();
  282. const content = {
  283. type: 'courtsAnswer',
  284. caseId: c.id,
  285. respondent: userId,
  286. stance: s,
  287. text: t,
  288. createdAt: nowISO()
  289. };
  290. await new Promise((resolve, reject) =>
  291. ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
  292. );
  293. c.status = 'IN_PROGRESS';
  294. c.answeredAt = nowISO();
  295. await upsertCase(c);
  296. return c;
  297. }
  298. async function issueVerdict({ caseId, result, orders }) {
  299. const c = await getCaseById(caseId);
  300. if (!c) throw new Error('Case not found.');
  301. const involved =
  302. String(c.accuser || '') === String(userId || '') ||
  303. String(c.respondentId || '') === String(userId || '') ||
  304. ensureArray(c.mediatorsAccuser || []).includes(userId) ||
  305. ensureArray(c.mediatorsRespondent || []).includes(userId);
  306. if (involved) throw new Error('You cannot be judge and party in the same case.');
  307. const r = String(result || '').trim();
  308. if (!r) throw new Error('Result is required.');
  309. const o = String(orders || '').trim();
  310. const ssbClient = await openSsb();
  311. const content = {
  312. type: 'courtsVerdict',
  313. caseId: c.id,
  314. judgeId: userId,
  315. result: r,
  316. orders: o,
  317. createdAt: nowISO()
  318. };
  319. await new Promise((resolve, reject) =>
  320. ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
  321. );
  322. c.status = 'DECIDED';
  323. c.verdictAt = nowISO();
  324. c.judgeId = userId;
  325. await upsertCase(c);
  326. return c;
  327. }
  328. async function proposeSettlement({ caseId, terms }) {
  329. const c = await getCaseById(caseId);
  330. if (!c) throw new Error('Case not found.');
  331. const role = getCaseRole(c, userId);
  332. if (role === 'OTHER') throw new Error('You are not involved in this case.');
  333. const t = String(terms || '').trim();
  334. if (!t) throw new Error('Terms are required.');
  335. const ssbClient = await openSsb();
  336. const content = {
  337. type: 'courtsSettlementProposal',
  338. caseId: c.id,
  339. proposer: userId,
  340. terms: t,
  341. createdAt: nowISO()
  342. };
  343. return await new Promise((resolve, reject) =>
  344. ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
  345. );
  346. }
  347. async function acceptSettlement({ caseId }) {
  348. const c = await getCaseById(caseId);
  349. if (!c) throw new Error('Case not found.');
  350. const role = getCaseRole(c, userId);
  351. if (role !== 'ACCUSER' && role !== 'DEFENCE') throw new Error('Only parties can accept a settlement.');
  352. const ssbClient = await openSsb();
  353. const content = {
  354. type: 'courtsSettlementAccepted',
  355. caseId: c.id,
  356. by: userId,
  357. createdAt: nowISO()
  358. };
  359. await new Promise((resolve, reject) =>
  360. ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
  361. );
  362. c.status = 'CLOSED';
  363. c.closedAt = nowISO();
  364. await upsertCase(c);
  365. return c;
  366. }
  367. async function setPublicPreference({ caseId, preference }) {
  368. const c = await getCaseById(caseId);
  369. if (!c) throw new Error('Case not found.');
  370. const id = String(userId || '');
  371. const pref = !!preference;
  372. if (String(c.accuser || '') === id) {
  373. c.publicPrefAccuser = pref;
  374. } else if (String(c.respondentId || '') === id) {
  375. c.publicPrefRespondent = pref;
  376. } else {
  377. throw new Error('Only parties can set visibility preference.');
  378. }
  379. await upsertCase(c);
  380. return c;
  381. }
  382. async function openPopularVote({ caseId }) {
  383. if (!services.votes || !services.votes.createVote) throw new Error('Votes service not available.');
  384. const c = await getCaseById(caseId);
  385. if (!c) throw new Error('Case not found.');
  386. const m = String(c.method || '').toUpperCase();
  387. if (m !== 'POPULAR' && m !== 'KARMATOCRACY') throw new Error('This case does not use public voting.');
  388. if (c.voteId) throw new Error('Vote already opened.');
  389. const question = c.title || `Case ${caseId}`;
  390. const deadline = moment().add(POPULAR_DAYS, 'days').toISOString();
  391. const voteMsg = await services.votes.createVote(
  392. question,
  393. deadline,
  394. ['YES', 'NO', 'ABSTENTION'],
  395. [`courtsCase:${caseId}`, `courtsMethod:${m}`]
  396. );
  397. c.voteId = voteMsg.key || voteMsg.id;
  398. await upsertCase(c);
  399. return c;
  400. }
  401. async function getInhabitantKarma(feedId) {
  402. if (services.banking && services.banking.getUserEngagementScore) {
  403. try {
  404. const v = await services.banking.getUserEngagementScore(feedId);
  405. return Number(v || 0) || 0;
  406. } catch {
  407. return 0;
  408. }
  409. }
  410. return 0;
  411. }
  412. async function getFirstUserTimestamp(feedId) {
  413. const ssbClient = await openSsb();
  414. return new Promise((resolve) => {
  415. pull(
  416. ssbClient.createUserStream({ id: feedId, reverse: false }),
  417. pull.filter((m) => m && m.value && m.value.content && m.value.content.type !== 'tombstone'),
  418. pull.take(1),
  419. pull.collect((err, arr) => {
  420. if (err || !arr || !arr.length) return resolve(Date.now());
  421. const m = arr[0];
  422. const ts = (m.value && m.value.timestamp) || m.timestamp || Date.now();
  423. resolve(ts < 1e12 ? ts * 1000 : ts);
  424. })
  425. );
  426. });
  427. }
  428. async function nominateJudge({ judgeId }) {
  429. const id = String(judgeId || '').trim();
  430. if (!id) throw new Error('Judge ID is required.');
  431. if (!FEED_ID_RE.test(id)) throw new Error('Invalid judge ID.');
  432. const ssbClient = await openSsb();
  433. const content = {
  434. type: 'courtsNomination',
  435. judgeId: id,
  436. createdAt: nowISO()
  437. };
  438. return await new Promise((resolve, reject) =>
  439. ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
  440. );
  441. }
  442. async function voteNomination(nominationId) {
  443. const id = String(nominationId || '').trim();
  444. if (!id) throw new Error('Nomination not found.');
  445. const nominations = await listByType('courtsNomination');
  446. const nomination = nominations.find((n) => n.id === id);
  447. if (!nomination) throw new Error('Nomination not found.');
  448. if (String(nomination.judgeId || '') === String(userId || '')) {
  449. throw new Error('You cannot vote for yourself.');
  450. }
  451. const votes = await listByType('courtsNominationVote');
  452. const already = votes.find(
  453. (v) =>
  454. String(v.nominationId || '') === id &&
  455. String(v.voter || '') === String(userId || '')
  456. );
  457. if (already) throw new Error('You have already voted.');
  458. const ssbClient = await openSsb();
  459. const content = {
  460. type: 'courtsNominationVote',
  461. nominationId: id,
  462. voter: userId,
  463. createdAt: nowISO()
  464. };
  465. return await new Promise((resolve, reject) =>
  466. ssbClient.publish(content, (err, msg) => (err ? reject(err) : resolve(msg)))
  467. );
  468. }
  469. async function listNominations() {
  470. const nominations = await listByType('courtsNomination');
  471. const votes = await listByType('courtsNominationVote');
  472. const byId = new Map();
  473. for (const n of nominations) {
  474. byId.set(n.id, { ...n, supports: 0, karma: 0, profileSince: 0 });
  475. }
  476. for (const v of votes) {
  477. const rec = byId.get(v.nominationId);
  478. if (rec) rec.supports = (rec.supports || 0) + 1;
  479. }
  480. const rows = [];
  481. for (const rec of byId.values()) {
  482. const karma = await getInhabitantKarma(rec.judgeId);
  483. const since = await getFirstUserTimestamp(rec.judgeId);
  484. rows.push({ ...rec, karma, profileSince: since });
  485. }
  486. rows.sort((a, b) => {
  487. if ((b.supports || 0) !== (a.supports || 0)) return (b.supports || 0) - (a.supports || 0);
  488. if ((b.karma || 0) !== (a.karma || 0)) return (b.karma || 0) - (a.karma || 0);
  489. if ((a.profileSince || 0) !== (b.profileSince || 0)) return (a.profileSince || 0) - (b.profileSince || 0);
  490. const ta = new Date(a.createdAt || 0).getTime();
  491. const tb = new Date(b.createdAt || 0).getTime();
  492. if (ta !== tb) return ta - tb;
  493. return String(a.judgeId || '').localeCompare(String(b.judgeId || ''));
  494. });
  495. return rows;
  496. }
  497. async function getCaseDetails({ caseId }) {
  498. const id = String(caseId || '').trim();
  499. if (!id) return null;
  500. const base = await getCaseById(id);
  501. if (!base) return null;
  502. const currentUser = await getCurrentUserId();
  503. const me = String(currentUser || '');
  504. const accuserId = String(base.accuser || '');
  505. const respondentId = String(base.respondentId || '');
  506. const ma = ensureArray(base.mediatorsAccuser || []);
  507. const mr = ensureArray(base.mediatorsRespondent || []);
  508. const judgeId = String(base.judgeId || '');
  509. const dictatorId = String(base.dictatorId || '');
  510. const isAccuser = accuserId === me;
  511. const isRespondent = respondentId === me;
  512. const isMediator = ma.includes(me) || mr.includes(me);
  513. const isJudge = judgeId === me;
  514. const isDictator = dictatorId === me;
  515. const mine = isAccuser || isRespondent || isMediator || isJudge || isDictator;
  516. let myPublicPreference = null;
  517. if (isAccuser && typeof base.publicPrefAccuser === 'boolean') {
  518. myPublicPreference = base.publicPrefAccuser;
  519. } else if (isRespondent && typeof base.publicPrefRespondent === 'boolean') {
  520. myPublicPreference = base.publicPrefRespondent;
  521. }
  522. const publicDetails = base.publicPrefAccuser === true && base.publicPrefRespondent === true;
  523. const evidencesAll = await listByType('courtsEvidence');
  524. const answersAll = await listByType('courtsAnswer');
  525. const settlementsAll = await listByType('courtsSettlementProposal');
  526. const verdictsAll = await listByType('courtsVerdict');
  527. const acceptedAll = await listByType('courtsSettlementAccepted');
  528. const evidences = evidencesAll
  529. .filter((e) => String(e.caseId || '') === id)
  530. .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
  531. const answers = answersAll
  532. .filter((a) => String(a.caseId || '') === id)
  533. .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
  534. const settlements = settlementsAll
  535. .filter((s) => String(s.caseId || '') === id)
  536. .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
  537. const verdicts = verdictsAll
  538. .filter((v) => String(v.caseId || '') === id)
  539. .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
  540. const verdict = verdicts.length ? verdicts[verdicts.length - 1] : null;
  541. const acceptedSettlements = acceptedAll
  542. .filter((s) => String(s.caseId || '') === id)
  543. .sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0));
  544. const decidedAt =
  545. base.verdictAt ||
  546. base.closedAt ||
  547. (verdict && verdict.createdAt) ||
  548. base.decidedAt;
  549. const hasVerdict = !!verdict;
  550. const supportCount = typeof base.supportCount !== 'undefined' ? base.supportCount : 0;
  551. return {
  552. ...base,
  553. id,
  554. respondent: base.respondentId || base.respondent,
  555. evidences,
  556. answers,
  557. settlements,
  558. acceptedSettlements,
  559. verdict,
  560. decidedAt,
  561. isAccuser,
  562. isRespondent,
  563. isMediator,
  564. isJudge,
  565. isDictator,
  566. mine,
  567. publicDetails,
  568. myPublicPreference,
  569. supportCount,
  570. hasVerdict
  571. };
  572. }
  573. return {
  574. getCurrentUserId,
  575. openCase,
  576. listCases,
  577. listCasesForUser,
  578. getCaseById,
  579. setMediators,
  580. assignJudge,
  581. addEvidence,
  582. answerCase,
  583. issueVerdict,
  584. proposeSettlement,
  585. acceptSettlement,
  586. setPublicPreference,
  587. openPopularVote,
  588. nominateJudge,
  589. voteNomination,
  590. listNominations,
  591. getCaseDetails
  592. };
  593. };