workingGroups.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. /*
  2. eslint-disable @typescript-eslint/naming-convention
  3. */
  4. import { SubstrateEvent } from '@dzlzv/hydra-common'
  5. import { DatabaseManager } from '@dzlzv/hydra-db-utils'
  6. import { StorageWorkingGroup as WorkingGroups } from './generated/types'
  7. import { ApplicationMetadata, OpeningMetadata } from '@joystream/metadata-protobuf'
  8. import { Bytes } from '@polkadot/types'
  9. import { createEvent, deserializeMetadata } from './common'
  10. import BN from 'bn.js'
  11. import {
  12. WorkingGroupOpening,
  13. OpeningAddedEvent,
  14. WorkingGroup,
  15. WorkingGroupOpeningMetadata,
  16. ApplicationFormQuestion,
  17. ApplicationFormQuestionType,
  18. OpeningStatusOpen,
  19. WorkingGroupOpeningType,
  20. EventType,
  21. Event,
  22. WorkingGroupApplication,
  23. ApplicationFormQuestionAnswer,
  24. AppliedOnOpeningEvent,
  25. Membership,
  26. ApplicationStatusPending,
  27. ApplicationStatusAccepted,
  28. ApplicationStatusRejected,
  29. Worker,
  30. WorkerStatusActive,
  31. OpeningFilledEvent,
  32. OpeningStatusFilled,
  33. // LeaderSetEvent,
  34. OpeningCanceledEvent,
  35. OpeningStatusCancelled,
  36. ApplicationStatusCancelled,
  37. ApplicationWithdrawnEvent,
  38. ApplicationStatusWithdrawn,
  39. } from 'query-node/dist/model'
  40. import { createType } from '@joystream/types'
  41. import _ from 'lodash'
  42. // Shortcuts
  43. type InputTypeMap = OpeningMetadata.ApplicationFormQuestion.InputTypeMap
  44. const InputType = OpeningMetadata.ApplicationFormQuestion.InputType
  45. // Reusable functions
  46. async function getWorkingGroup(db: DatabaseManager, event_: SubstrateEvent): Promise<WorkingGroup> {
  47. const [groupName] = event_.name.split('.')
  48. const group = await db.get(WorkingGroup, { where: { name: groupName } })
  49. if (!group) {
  50. throw new Error(`Working group ${groupName} not found!`)
  51. }
  52. return group
  53. }
  54. async function getOpening(
  55. db: DatabaseManager,
  56. openingDbId: string,
  57. relations: string[] = []
  58. ): Promise<WorkingGroupOpening> {
  59. const opening = await db.get(WorkingGroupOpening, { where: { id: openingDbId }, relations })
  60. if (!opening) {
  61. throw new Error(`Opening not found by id ${openingDbId}`)
  62. }
  63. return opening
  64. }
  65. async function getApplication(db: DatabaseManager, applicationDbId: string): Promise<WorkingGroupApplication> {
  66. const application = await db.get(WorkingGroupApplication, { where: { id: applicationDbId } })
  67. if (!application) {
  68. throw new Error(`Application not found by id ${applicationDbId}`)
  69. }
  70. return application
  71. }
  72. async function getApplicationFormQuestions(
  73. db: DatabaseManager,
  74. openingDbId: string
  75. ): Promise<ApplicationFormQuestion[]> {
  76. const openingWithQuestions = await getOpening(db, openingDbId, ['metadata', 'metadata.applicationFormQuestions'])
  77. if (!openingWithQuestions) {
  78. throw new Error(`Opening not found by id: ${openingDbId}`)
  79. }
  80. if (!openingWithQuestions.metadata.applicationFormQuestions) {
  81. throw new Error(`Application form questions not found for opening: ${openingDbId}`)
  82. }
  83. return openingWithQuestions.metadata.applicationFormQuestions
  84. }
  85. function parseQuestionInputType(type: InputTypeMap[keyof InputTypeMap]) {
  86. if (type === InputType.TEXTAREA) {
  87. return ApplicationFormQuestionType.TEXTAREA
  88. }
  89. return ApplicationFormQuestionType.TEXT
  90. }
  91. function getDefaultOpeningMetadata(opening: WorkingGroupOpening): OpeningMetadata {
  92. const metadata = new OpeningMetadata()
  93. metadata.setShortDescription(
  94. `${_.startCase(opening.group.name)} ${
  95. opening.type === WorkingGroupOpeningType.REGULAR ? 'worker' : 'leader'
  96. } opening`
  97. )
  98. metadata.setDescription(
  99. `Apply to this opening in order to be considered for ${_.startCase(opening.group.name)} ${
  100. opening.type === WorkingGroupOpeningType.REGULAR ? 'worker' : 'leader'
  101. } role!`
  102. )
  103. metadata.setApplicationDetails(`- Fill the application form`)
  104. const applicationFormQuestion = new OpeningMetadata.ApplicationFormQuestion()
  105. applicationFormQuestion.setQuestion('What makes you a good candidate?')
  106. applicationFormQuestion.setType(OpeningMetadata.ApplicationFormQuestion.InputType.TEXTAREA)
  107. metadata.addApplicationFormQuestions(applicationFormQuestion)
  108. return metadata
  109. }
  110. async function createOpeningMeta(
  111. db: DatabaseManager,
  112. event_: SubstrateEvent,
  113. opening: WorkingGroupOpening,
  114. metadataBytes: Bytes
  115. ): Promise<WorkingGroupOpeningMetadata> {
  116. const deserializedMetadata = await deserializeMetadata(OpeningMetadata, metadataBytes)
  117. const metadata = deserializedMetadata || (await getDefaultOpeningMetadata(opening))
  118. const originallyValid = !!deserializedMetadata
  119. const eventTime = new Date(event_.blockTimestamp.toNumber())
  120. const {
  121. applicationFormQuestionsList,
  122. applicationDetails,
  123. description,
  124. expectedEndingTimestamp,
  125. hiringLimit,
  126. shortDescription,
  127. } = metadata.toObject()
  128. const openingMetadata = new WorkingGroupOpeningMetadata({
  129. createdAt: eventTime,
  130. updatedAt: eventTime,
  131. originallyValid,
  132. applicationDetails,
  133. description,
  134. shortDescription,
  135. hiringLimit,
  136. expectedEnding: new Date(expectedEndingTimestamp!),
  137. applicationFormQuestions: [],
  138. })
  139. await db.save<WorkingGroupOpeningMetadata>(openingMetadata)
  140. await Promise.all(
  141. applicationFormQuestionsList.map(async ({ question, type }, index) => {
  142. const applicationFormQuestion = new ApplicationFormQuestion({
  143. createdAt: eventTime,
  144. updatedAt: eventTime,
  145. question,
  146. type: parseQuestionInputType(type!),
  147. index,
  148. openingMetadata,
  149. })
  150. await db.save<ApplicationFormQuestion>(applicationFormQuestion)
  151. return applicationFormQuestion
  152. })
  153. )
  154. return openingMetadata
  155. }
  156. async function createApplicationQuestionAnswers(
  157. db: DatabaseManager,
  158. application: WorkingGroupApplication,
  159. metadataBytes: Bytes
  160. ) {
  161. const metadata = deserializeMetadata(ApplicationMetadata, metadataBytes)
  162. if (!metadata) {
  163. return
  164. }
  165. const questions = await getApplicationFormQuestions(db, application.opening.id)
  166. const { answersList } = metadata.toObject()
  167. await Promise.all(
  168. answersList.slice(0, questions.length).map(async (answer, index) => {
  169. const applicationFormQuestionAnswer = new ApplicationFormQuestionAnswer({
  170. createdAt: application.createdAt,
  171. updatedAt: application.updatedAt,
  172. application,
  173. question: questions[index],
  174. answer,
  175. })
  176. await db.save<ApplicationFormQuestionAnswer>(applicationFormQuestionAnswer)
  177. return applicationFormQuestionAnswer
  178. })
  179. )
  180. }
  181. // Mapping functions
  182. export async function workingGroups_OpeningAdded(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  183. event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
  184. const {
  185. balance: rewardPerBlock,
  186. bytes: metadataBytes,
  187. openingId: openingRuntimeId,
  188. openingType,
  189. stakePolicy,
  190. } = new WorkingGroups.OpeningAddedEvent(event_).data
  191. const group = await getWorkingGroup(db, event_)
  192. const eventTime = new Date(event_.blockTimestamp.toNumber())
  193. const opening = new WorkingGroupOpening({
  194. createdAt: eventTime,
  195. updatedAt: eventTime,
  196. createdAtBlock: event_.blockNumber,
  197. id: `${group.name}-${openingRuntimeId.toString()}`,
  198. runtimeId: openingRuntimeId.toNumber(),
  199. applications: [],
  200. group,
  201. rewardPerBlock: rewardPerBlock.unwrapOr(new BN(0)),
  202. stakeAmount: stakePolicy.stake_amount,
  203. unstakingPeriod: stakePolicy.leaving_unstaking_period.toNumber(),
  204. status: new OpeningStatusOpen(),
  205. type: openingType.isLeader ? WorkingGroupOpeningType.LEADER : WorkingGroupOpeningType.REGULAR,
  206. })
  207. const metadata = await createOpeningMeta(db, event_, opening, metadataBytes)
  208. opening.metadata = metadata
  209. await db.save<WorkingGroupOpening>(opening)
  210. const event = await createEvent(event_, EventType.OpeningAdded)
  211. const openingAddedEvent = new OpeningAddedEvent({
  212. createdAt: eventTime,
  213. updatedAt: eventTime,
  214. event,
  215. group,
  216. opening,
  217. })
  218. await db.save<Event>(event)
  219. await db.save<OpeningAddedEvent>(openingAddedEvent)
  220. }
  221. export async function workingGroups_AppliedOnOpening(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  222. event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
  223. const eventTime = new Date(event_.blockTimestamp.toNumber())
  224. const {
  225. applicationId: applicationRuntimeId,
  226. applyOnOpeningParameters: {
  227. opening_id: openingRuntimeId,
  228. description: metadataBytes,
  229. member_id: memberId,
  230. reward_account_id: rewardAccount,
  231. role_account_id: roleAccout,
  232. stake_parameters: { stake, staking_account_id: stakingAccount },
  233. },
  234. } = new WorkingGroups.AppliedOnOpeningEvent(event_).data
  235. const group = await getWorkingGroup(db, event_)
  236. const openingDbId = `${group.name}-${openingRuntimeId.toString()}`
  237. const application = new WorkingGroupApplication({
  238. createdAt: eventTime,
  239. updatedAt: eventTime,
  240. createdAtBlock: event_.blockNumber,
  241. id: `${group.name}-${applicationRuntimeId.toString()}`,
  242. runtimeId: applicationRuntimeId.toNumber(),
  243. opening: new WorkingGroupOpening({ id: openingDbId }),
  244. applicant: new Membership({ id: memberId.toString() }),
  245. rewardAccount: rewardAccount.toString(),
  246. roleAccount: roleAccout.toString(),
  247. stakingAccount: stakingAccount.toString(),
  248. status: new ApplicationStatusPending(),
  249. answers: [],
  250. stake,
  251. })
  252. await db.save<WorkingGroupApplication>(application)
  253. await createApplicationQuestionAnswers(db, application, metadataBytes)
  254. const event = await createEvent(event_, EventType.AppliedOnOpening)
  255. const appliedOnOpeningEvent = new AppliedOnOpeningEvent({
  256. createdAt: eventTime,
  257. updatedAt: eventTime,
  258. event,
  259. group,
  260. opening: new WorkingGroupOpening({ id: openingDbId }),
  261. application,
  262. })
  263. await db.save<Event>(event)
  264. await db.save<AppliedOnOpeningEvent>(appliedOnOpeningEvent)
  265. }
  266. export async function workingGroups_OpeningFilled(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  267. event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
  268. const eventTime = new Date(event_.blockTimestamp.toNumber())
  269. const {
  270. openingId: openingRuntimeId,
  271. applicationId: applicationIdsSet,
  272. applicationIdToWorkerIdMap,
  273. } = new WorkingGroups.OpeningFilledEvent(event_).data
  274. const group = await getWorkingGroup(db, event_)
  275. const opening = await getOpening(db, `${group.name}-${openingRuntimeId.toString()}`, [
  276. 'applications',
  277. 'applications.applicant',
  278. ])
  279. const acceptedApplicationIds = createType('Vec<ApplicationId>', applicationIdsSet.toHex() as any)
  280. // Save the event
  281. const event = await createEvent(event_, EventType.OpeningFilled)
  282. const openingFilledEvent = new OpeningFilledEvent({
  283. createdAt: eventTime,
  284. updatedAt: eventTime,
  285. event,
  286. group,
  287. opening,
  288. })
  289. await db.save<Event>(event)
  290. await db.save<OpeningFilledEvent>(openingFilledEvent)
  291. const hiredWorkers: Worker[] = []
  292. // Update applications and create new workers
  293. await Promise.all(
  294. (opening.applications || [])
  295. // Skip withdrawn applications
  296. .filter((application) => application.status.isTypeOf !== 'ApplicationStatusWithdrawn')
  297. .map(async (application) => {
  298. const isAccepted = acceptedApplicationIds.some((runtimeId) => runtimeId.toNumber() === application.runtimeId)
  299. const applicationStatus = isAccepted ? new ApplicationStatusAccepted() : new ApplicationStatusRejected()
  300. applicationStatus.openingFilledEventId = openingFilledEvent.id
  301. application.status = applicationStatus
  302. application.updatedAt = eventTime
  303. if (isAccepted) {
  304. // Cannot use "applicationIdToWorkerIdMap.get" here,
  305. // it only works if the passed instance is identical to BTreeMap key instance (=== instead of .eq)
  306. const [, workerRuntimeId] =
  307. Array.from(applicationIdToWorkerIdMap.entries()).find(
  308. ([applicationRuntimeId]) => applicationRuntimeId.toNumber() === application.runtimeId
  309. ) || []
  310. if (!workerRuntimeId) {
  311. throw new Error(
  312. `Fatal: No worker id found by accepted application id ${application.id} when handling OpeningFilled event!`
  313. )
  314. }
  315. const worker = new Worker({
  316. createdAt: eventTime,
  317. updatedAt: eventTime,
  318. id: `${group.name}-${workerRuntimeId.toString()}`,
  319. runtimeId: workerRuntimeId.toNumber(),
  320. hiredAtBlock: event_.blockNumber,
  321. hiredAtTime: new Date(event_.blockTimestamp.toNumber()),
  322. application,
  323. group,
  324. isLead: opening.type === WorkingGroupOpeningType.LEADER,
  325. membership: application.applicant,
  326. stake: application.stake,
  327. roleAccount: application.roleAccount,
  328. rewardAccount: application.rewardAccount,
  329. stakeAccount: application.stakingAccount,
  330. payouts: [],
  331. status: new WorkerStatusActive(),
  332. entry: openingFilledEvent,
  333. })
  334. await db.save<Worker>(worker)
  335. hiredWorkers.push(worker)
  336. }
  337. await db.save<WorkingGroupApplication>(application)
  338. })
  339. )
  340. // Set opening status
  341. const openingFilled = new OpeningStatusFilled()
  342. openingFilled.openingFilledEventId = openingFilledEvent.id
  343. opening.status = openingFilled
  344. opening.updatedAt = eventTime
  345. await db.save<WorkingGroupOpening>(opening)
  346. // Update working group if necessary
  347. if (opening.type === WorkingGroupOpeningType.LEADER && hiredWorkers.length) {
  348. group.leader = hiredWorkers[0]
  349. group.updatedAt = eventTime
  350. await db.save<WorkingGroup>(group)
  351. }
  352. }
  353. // FIXME: Currently this event cannot be handled directly, because the worker does not yet exist at the time when it is emitted
  354. // export async function workingGroups_LeaderSet(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  355. // event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
  356. // const { workerId: workerRuntimeId } = new WorkingGroups.LeaderSetEvent(event_).data
  357. // const group = await getWorkingGroup(db, event_)
  358. // const workerDbId = `${group.name}-${workerRuntimeId.toString()}`
  359. // const worker = new Worker({ id: workerDbId })
  360. // const eventTime = new Date(event_.blockTimestamp.toNumber())
  361. // // Create and save event
  362. // const event = createEvent(event_, EventType.LeaderSet)
  363. // const leaderSetEvent = new LeaderSetEvent({
  364. // createdAt: eventTime,
  365. // updatedAt: eventTime,
  366. // event,
  367. // group,
  368. // worker,
  369. // })
  370. // await db.save<Event>(event)
  371. // await db.save<LeaderSetEvent>(leaderSetEvent)
  372. // }
  373. export async function workingGroups_OpeningCanceled(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  374. event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
  375. const { openingId: openingRuntimeId } = new WorkingGroups.OpeningCanceledEvent(event_).data
  376. const group = await getWorkingGroup(db, event_)
  377. const opening = await getOpening(db, `${group.name}-${openingRuntimeId.toString()}`, ['applications'])
  378. const eventTime = new Date(event_.blockTimestamp.toNumber())
  379. // Create and save event
  380. const event = createEvent(event_, EventType.OpeningCanceled)
  381. const openingCanceledEvent = new OpeningCanceledEvent({
  382. createdAt: eventTime,
  383. updatedAt: eventTime,
  384. event,
  385. group,
  386. opening,
  387. })
  388. await db.save<Event>(event)
  389. await db.save<OpeningCanceledEvent>(openingCanceledEvent)
  390. // Set opening status
  391. const openingCancelled = new OpeningStatusCancelled()
  392. openingCancelled.openingCancelledEventId = openingCanceledEvent.id
  393. opening.status = openingCancelled
  394. opening.updatedAt = eventTime
  395. await db.save<WorkingGroupOpening>(opening)
  396. // Set applications status
  397. const applicationCancelled = new ApplicationStatusCancelled()
  398. applicationCancelled.openingCancelledEventId = openingCanceledEvent.id
  399. await Promise.all(
  400. (opening.applications || [])
  401. // Skip withdrawn applications
  402. .filter((application) => application.status.isTypeOf !== 'ApplicationStatusWithdrawn')
  403. .map(async (application) => {
  404. application.status = applicationCancelled
  405. application.updatedAt = eventTime
  406. await db.save<WorkingGroupApplication>(application)
  407. })
  408. )
  409. }
  410. export async function workingGroups_ApplicationWithdrawn(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  411. event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
  412. const { applicationId: applicationRuntimeId } = new WorkingGroups.ApplicationWithdrawnEvent(event_).data
  413. const group = await getWorkingGroup(db, event_)
  414. const application = await getApplication(db, `${group.name}-${applicationRuntimeId.toString()}`)
  415. const eventTime = new Date(event_.blockTimestamp.toNumber())
  416. // Create and save event
  417. const event = createEvent(event_, EventType.ApplicationWithdrawn)
  418. const applicationWithdrawnEvent = new ApplicationWithdrawnEvent({
  419. createdAt: eventTime,
  420. updatedAt: eventTime,
  421. event,
  422. group,
  423. application,
  424. })
  425. await db.save<Event>(event)
  426. await db.save<ApplicationWithdrawnEvent>(applicationWithdrawnEvent)
  427. // Set application status
  428. const statusWithdrawn = new ApplicationStatusWithdrawn()
  429. statusWithdrawn.applicationWithdrawnEventId = applicationWithdrawnEvent.id
  430. application.status = statusWithdrawn
  431. application.updatedAt = eventTime
  432. await db.save<WorkingGroupApplication>(application)
  433. }
  434. export async function workingGroups_WorkerRoleAccountUpdated(
  435. db: DatabaseManager,
  436. event_: SubstrateEvent
  437. ): Promise<void> {
  438. // TBD
  439. }
  440. export async function workingGroups_LeaderUnset(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  441. // TBD
  442. }
  443. export async function workingGroups_WorkerExited(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  444. // TBD
  445. }
  446. export async function workingGroups_TerminatedWorker(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  447. // TBD
  448. }
  449. export async function workingGroups_TerminatedLeader(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  450. // TBD
  451. }
  452. export async function workingGroups_StakeSlashed(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  453. // TBD
  454. }
  455. export async function workingGroups_StakeDecreased(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  456. // TBD
  457. }
  458. export async function workingGroups_StakeIncreased(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  459. // TBD
  460. }
  461. export async function workingGroups_BudgetSet(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  462. // TBD
  463. }
  464. export async function workingGroups_WorkerRewardAccountUpdated(
  465. db: DatabaseManager,
  466. event_: SubstrateEvent
  467. ): Promise<void> {
  468. // TBD
  469. }
  470. export async function workingGroups_WorkerRewardAmountUpdated(
  471. db: DatabaseManager,
  472. event_: SubstrateEvent
  473. ): Promise<void> {
  474. // TBD
  475. }
  476. export async function workingGroups_StatusTextChanged(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  477. // TBD
  478. }
  479. export async function workingGroups_BudgetSpending(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
  480. // TBD
  481. }