Kaynağa Gözat

Create upcoming openings: metadata, mappings and tests

Leszek Wiesner 3 yıl önce
ebeveyn
işleme
d325544e8d

+ 148 - 0
metadata-protobuf/compiled/proto/WorkingGroups_pb.d.ts

@@ -90,6 +90,46 @@ export namespace OpeningMetadata {
   }
 }
 
+export class UpcomingOpeningMetadata extends jspb.Message {
+  hasExpectedStart(): boolean;
+  clearExpectedStart(): void;
+  getExpectedStart(): number | undefined;
+  setExpectedStart(value: number): void;
+
+  hasRewardPerBlock(): boolean;
+  clearRewardPerBlock(): void;
+  getRewardPerBlock(): number | undefined;
+  setRewardPerBlock(value: number): void;
+
+  hasMinApplicationStake(): boolean;
+  clearMinApplicationStake(): void;
+  getMinApplicationStake(): number | undefined;
+  setMinApplicationStake(value: number): void;
+
+  hasMetadata(): boolean;
+  clearMetadata(): void;
+  getMetadata(): OpeningMetadata;
+  setMetadata(value?: OpeningMetadata): void;
+
+  serializeBinary(): Uint8Array;
+  toObject(includeInstance?: boolean): UpcomingOpeningMetadata.AsObject;
+  static toObject(includeInstance: boolean, msg: UpcomingOpeningMetadata): UpcomingOpeningMetadata.AsObject;
+  static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
+  static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
+  static serializeBinaryToWriter(message: UpcomingOpeningMetadata, writer: jspb.BinaryWriter): void;
+  static deserializeBinary(bytes: Uint8Array): UpcomingOpeningMetadata;
+  static deserializeBinaryFromReader(message: UpcomingOpeningMetadata, reader: jspb.BinaryReader): UpcomingOpeningMetadata;
+}
+
+export namespace UpcomingOpeningMetadata {
+  export type AsObject = {
+    expectedStart?: number,
+    rewardPerBlock?: number,
+    minApplicationStake?: number,
+    metadata: OpeningMetadata.AsObject,
+  }
+}
+
 export class ApplicationMetadata extends jspb.Message {
   clearAnswersList(): void;
   getAnswersList(): Array<string>;
@@ -152,3 +192,111 @@ export namespace WorkingGroupStatusMetadata {
   }
 }
 
+export class SetGroupMetadata extends jspb.Message {
+  hasNewmetadata(): boolean;
+  clearNewmetadata(): void;
+  getNewmetadata(): WorkingGroupStatusMetadata;
+  setNewmetadata(value?: WorkingGroupStatusMetadata): void;
+
+  serializeBinary(): Uint8Array;
+  toObject(includeInstance?: boolean): SetGroupMetadata.AsObject;
+  static toObject(includeInstance: boolean, msg: SetGroupMetadata): SetGroupMetadata.AsObject;
+  static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
+  static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
+  static serializeBinaryToWriter(message: SetGroupMetadata, writer: jspb.BinaryWriter): void;
+  static deserializeBinary(bytes: Uint8Array): SetGroupMetadata;
+  static deserializeBinaryFromReader(message: SetGroupMetadata, reader: jspb.BinaryReader): SetGroupMetadata;
+}
+
+export namespace SetGroupMetadata {
+  export type AsObject = {
+    newmetadata: WorkingGroupStatusMetadata.AsObject,
+  }
+}
+
+export class AddUpcomingOpening extends jspb.Message {
+  hasMetadata(): boolean;
+  clearMetadata(): void;
+  getMetadata(): UpcomingOpeningMetadata;
+  setMetadata(value?: UpcomingOpeningMetadata): void;
+
+  serializeBinary(): Uint8Array;
+  toObject(includeInstance?: boolean): AddUpcomingOpening.AsObject;
+  static toObject(includeInstance: boolean, msg: AddUpcomingOpening): AddUpcomingOpening.AsObject;
+  static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
+  static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
+  static serializeBinaryToWriter(message: AddUpcomingOpening, writer: jspb.BinaryWriter): void;
+  static deserializeBinary(bytes: Uint8Array): AddUpcomingOpening;
+  static deserializeBinaryFromReader(message: AddUpcomingOpening, reader: jspb.BinaryReader): AddUpcomingOpening;
+}
+
+export namespace AddUpcomingOpening {
+  export type AsObject = {
+    metadata: UpcomingOpeningMetadata.AsObject,
+  }
+}
+
+export class RemoveUpcomingOpening extends jspb.Message {
+  hasId(): boolean;
+  clearId(): void;
+  getId(): string | undefined;
+  setId(value: string): void;
+
+  serializeBinary(): Uint8Array;
+  toObject(includeInstance?: boolean): RemoveUpcomingOpening.AsObject;
+  static toObject(includeInstance: boolean, msg: RemoveUpcomingOpening): RemoveUpcomingOpening.AsObject;
+  static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
+  static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
+  static serializeBinaryToWriter(message: RemoveUpcomingOpening, writer: jspb.BinaryWriter): void;
+  static deserializeBinary(bytes: Uint8Array): RemoveUpcomingOpening;
+  static deserializeBinaryFromReader(message: RemoveUpcomingOpening, reader: jspb.BinaryReader): RemoveUpcomingOpening;
+}
+
+export namespace RemoveUpcomingOpening {
+  export type AsObject = {
+    id?: string,
+  }
+}
+
+export class WorkingGroupMetadataAction extends jspb.Message {
+  hasSetgroupmetadata(): boolean;
+  clearSetgroupmetadata(): void;
+  getSetgroupmetadata(): SetGroupMetadata | undefined;
+  setSetgroupmetadata(value?: SetGroupMetadata): void;
+
+  hasAddupcomingopening(): boolean;
+  clearAddupcomingopening(): void;
+  getAddupcomingopening(): AddUpcomingOpening | undefined;
+  setAddupcomingopening(value?: AddUpcomingOpening): void;
+
+  hasRemoveupcomingopening(): boolean;
+  clearRemoveupcomingopening(): void;
+  getRemoveupcomingopening(): RemoveUpcomingOpening | undefined;
+  setRemoveupcomingopening(value?: RemoveUpcomingOpening): void;
+
+  getActionCase(): WorkingGroupMetadataAction.ActionCase;
+  serializeBinary(): Uint8Array;
+  toObject(includeInstance?: boolean): WorkingGroupMetadataAction.AsObject;
+  static toObject(includeInstance: boolean, msg: WorkingGroupMetadataAction): WorkingGroupMetadataAction.AsObject;
+  static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
+  static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
+  static serializeBinaryToWriter(message: WorkingGroupMetadataAction, writer: jspb.BinaryWriter): void;
+  static deserializeBinary(bytes: Uint8Array): WorkingGroupMetadataAction;
+  static deserializeBinaryFromReader(message: WorkingGroupMetadataAction, reader: jspb.BinaryReader): WorkingGroupMetadataAction;
+}
+
+export namespace WorkingGroupMetadataAction {
+  export type AsObject = {
+    setgroupmetadata?: SetGroupMetadata.AsObject,
+    addupcomingopening?: AddUpcomingOpening.AsObject,
+    removeupcomingopening?: RemoveUpcomingOpening.AsObject,
+  }
+
+  export enum ActionCase {
+    ACTION_NOT_SET = 0,
+    SETGROUPMETADATA = 1,
+    ADDUPCOMINGOPENING = 2,
+    REMOVEUPCOMINGOPENING = 3,
+  }
+}
+

Dosya farkı çok büyük olduğundan ihmal edildi
+ 1109 - 124
metadata-protobuf/compiled/proto/WorkingGroups_pb.js


+ 29 - 0
metadata-protobuf/proto/WorkingGroups.proto

@@ -17,13 +17,42 @@ message OpeningMetadata {
   repeated ApplicationFormQuestion application_form_questions = 6; // List of questions that should be answered during application
 }
 
+message UpcomingOpeningMetadata {
+  required uint64 expected_start = 1; // Expected opening start (timestamp)
+  required uint64 reward_per_block = 2; // Expected reward per block
+  required uint64 min_application_stake = 3; // Expected min. application stake
+  required OpeningMetadata metadata = 4; // Opening metadata
+}
+
 message ApplicationMetadata {
   repeated string answers = 1; // List of answers to opening application form questions
 }
 
+// set_status_text extrinsic messages:
+
 message WorkingGroupStatusMetadata {
   optional string description = 1; // Full status description (md-formatted)
   optional string about = 2; // Status about text (md-formatted)
   optional string status = 3; // The status itself (expected to be 1-3 words)
   optional string status_message = 4; // Short status message
 }
+
+message SetGroupMetadata {
+  required WorkingGroupStatusMetadata newMetadata = 1; // New working group metadata to set (can be a partial update)
+}
+
+message AddUpcomingOpening {
+  required UpcomingOpeningMetadata metadata = 1; // Upcoming opening metadata
+}
+
+message RemoveUpcomingOpening {
+  required string id = 1; // Upcoming opening query-node id
+}
+
+message WorkingGroupMetadataAction {
+  oneof action {
+    SetGroupMetadata setGroupMetadata = 1;
+    AddUpcomingOpening addUpcomingOpening = 2;
+    RemoveUpcomingOpening removeUpcomingOpening = 3;
+  }
+}

+ 6 - 1
query-node/build.sh

@@ -26,5 +26,10 @@ yarn format
 # and are inline with root workspace resolutions
 yarn
 
-yarn workspace query-node build:dev
+# FIXME: Temporary workaround for Hydra bug. After it's fixed this can be just: "yarn workspace query-node build:dev"
+yarn workspace query-node config:dev
+yarn workspace query-node codegen
+sed -i 's/get bytes(): Option/get optBytes(): Option/' ./mappings/generated/types/storage-working-group.ts
+yarn workspace query-node compile
+
 yarn workspace query-node-mappings build

+ 1 - 1
query-node/manifest.yml

@@ -44,7 +44,7 @@ typegen:
     - storageWorkingGroup.BudgetSet
     - storageWorkingGroup.WorkerRewardAccountUpdated
     - storageWorkingGroup.WorkerRewardAmountUpdated
-    # - storageWorkingGroup.StatusTextChanged FIXME: Hydra bug
+    - storageWorkingGroup.StatusTextChanged
     - storageWorkingGroup.BudgetSpending
   calls:
     - members.updateProfile

+ 104 - 16
query-node/mappings/workingGroups.ts

@@ -4,7 +4,12 @@ eslint-disable @typescript-eslint/naming-convention
 import { SubstrateEvent } from '@dzlzv/hydra-common'
 import { DatabaseManager } from '@dzlzv/hydra-db-utils'
 import { StorageWorkingGroup as WorkingGroups } from './generated/types'
-import { ApplicationMetadata, OpeningMetadata } from '@joystream/metadata-protobuf'
+import {
+  AddUpcomingOpening,
+  ApplicationMetadata,
+  OpeningMetadata,
+  WorkingGroupMetadataAction,
+} from '@joystream/metadata-protobuf'
 import { Bytes } from '@polkadot/types'
 import { createEvent, deserializeMetadata, getOrCreateBlock } from './common'
 import BN from 'bn.js'
@@ -35,6 +40,8 @@ import {
   ApplicationStatusCancelled,
   ApplicationWithdrawnEvent,
   ApplicationStatusWithdrawn,
+  UpcomingWorkingGroupOpening,
+  StatusTextChangedEvent,
 } from 'query-node/dist/model'
 import { createType } from '@joystream/types'
 import _ from 'lodash'
@@ -99,16 +106,14 @@ function parseQuestionInputType(type: InputTypeMap[keyof InputTypeMap]) {
   return ApplicationFormQuestionType.TEXT
 }
 
-function getDefaultOpeningMetadata(opening: WorkingGroupOpening): OpeningMetadata {
+function getDefaultOpeningMetadata(group: WorkingGroup, openingType: WorkingGroupOpeningType): OpeningMetadata {
   const metadata = new OpeningMetadata()
   metadata.setShortDescription(
-    `${_.startCase(opening.group.name)} ${
-      opening.type === WorkingGroupOpeningType.REGULAR ? 'worker' : 'leader'
-    } opening`
+    `${_.startCase(group.name)} ${openingType === WorkingGroupOpeningType.REGULAR ? 'worker' : 'leader'} opening`
   )
   metadata.setDescription(
-    `Apply to this opening in order to be considered for ${_.startCase(opening.group.name)} ${
-      opening.type === WorkingGroupOpeningType.REGULAR ? 'worker' : 'leader'
+    `Apply to this opening in order to be considered for ${_.startCase(group.name)} ${
+      openingType === WorkingGroupOpeningType.REGULAR ? 'worker' : 'leader'
     } role!`
   )
   metadata.setApplicationDetails(`- Fill the application form`)
@@ -123,12 +128,20 @@ function getDefaultOpeningMetadata(opening: WorkingGroupOpening): OpeningMetadat
 async function createOpeningMeta(
   db: DatabaseManager,
   event_: SubstrateEvent,
-  opening: WorkingGroupOpening,
-  metadataBytes: Bytes
+  group: WorkingGroup,
+  openingType: WorkingGroupOpeningType,
+  originalMeta: Bytes | OpeningMetadata
 ): Promise<WorkingGroupOpeningMetadata> {
-  const deserializedMetadata = await deserializeMetadata(OpeningMetadata, metadataBytes)
-  const metadata = deserializedMetadata || (await getDefaultOpeningMetadata(opening))
-  const originallyValid = !!deserializedMetadata
+  let originallyValid: boolean
+  let metadata: OpeningMetadata
+  if (originalMeta instanceof Bytes) {
+    const deserializedMetadata = await deserializeMetadata(OpeningMetadata, originalMeta)
+    metadata = deserializedMetadata || (await getDefaultOpeningMetadata(group, openingType))
+    originallyValid = !!deserializedMetadata
+  } else {
+    metadata = originalMeta
+    originallyValid = true
+  }
   const eventTime = new Date(event_.blockTimestamp.toNumber())
 
   const {
@@ -199,6 +212,58 @@ async function createApplicationQuestionAnswers(
   )
 }
 
+async function handleAddUpcomingOpeningAction(
+  db: DatabaseManager,
+  event_: SubstrateEvent,
+  statusChangedEvent: StatusTextChangedEvent,
+  action: AddUpcomingOpening
+) {
+  const upcomingOpeningMeta = action.getMetadata().toObject()
+  const group = await getWorkingGroup(db, event_)
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+  const openingMeta = await createOpeningMeta(
+    db,
+    event_,
+    group,
+    WorkingGroupOpeningType.REGULAR,
+    action.getMetadata().getMetadata()
+  )
+  const upcomingOpening = new UpcomingWorkingGroupOpening({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    metadata: openingMeta,
+    group,
+    rewardPerBlock: new BN(upcomingOpeningMeta.rewardPerBlock!),
+    expectedStart: new Date(upcomingOpeningMeta.expectedStart!),
+    stakeAmount: new BN(upcomingOpeningMeta.minApplicationStake!),
+    createdInEvent: statusChangedEvent,
+    createdAtBlock: await getOrCreateBlock(db, event_),
+  })
+  await db.save<UpcomingWorkingGroupOpening>(upcomingOpening)
+}
+
+async function handleWorkingGroupMetadataAction(
+  db: DatabaseManager,
+  event_: SubstrateEvent,
+  statusChangedEvent: StatusTextChangedEvent,
+  action: WorkingGroupMetadataAction
+) {
+  switch (action.getActionCase()) {
+    case WorkingGroupMetadataAction.ActionCase.ADDUPCOMINGOPENING: {
+      await handleAddUpcomingOpeningAction(db, event_, statusChangedEvent, action.getAddupcomingopening()!)
+      break
+    }
+    case WorkingGroupMetadataAction.ActionCase.REMOVEUPCOMINGOPENING: {
+      // TODO
+      break
+    }
+    case WorkingGroupMetadataAction.ActionCase.SETGROUPMETADATA: {
+      // TODO:
+      break
+    }
+  }
+}
+
 // Mapping functions
 export async function workingGroups_OpeningAdded(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
@@ -227,7 +292,7 @@ export async function workingGroups_OpeningAdded(db: DatabaseManager, event_: Su
     type: openingType.isLeader ? WorkingGroupOpeningType.LEADER : WorkingGroupOpeningType.REGULAR,
   })
 
-  const metadata = await createOpeningMeta(db, event_, opening, metadataBytes)
+  const metadata = await createOpeningMeta(db, event_, group, opening.type, metadataBytes)
   opening.metadata = metadata
 
   await db.save<WorkingGroupOpening>(opening)
@@ -484,6 +549,32 @@ export async function workingGroups_ApplicationWithdrawn(db: DatabaseManager, ev
   await db.save<WorkingGroupApplication>(application)
 }
 
+export async function workingGroups_StatusTextChanged(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const { optBytes } = new WorkingGroups.StatusTextChangedEvent(event_).data
+  const group = await getWorkingGroup(db, event_)
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+
+  const statusTextChangedEvent = new StatusTextChangedEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    group,
+    event: await createEvent(db, event_, EventType.StatusTextChanged),
+    metadata: optBytes.isSome ? optBytes.unwrap().toString() : undefined,
+  })
+
+  await db.save<StatusTextChangedEvent>(statusTextChangedEvent)
+
+  if (optBytes.isSome) {
+    const metadata = deserializeMetadata(WorkingGroupMetadataAction, optBytes.unwrap())
+    if (metadata) {
+      handleWorkingGroupMetadataAction(db, event_, statusTextChangedEvent, metadata)
+    }
+  } else {
+    console.warn('StatusTextChanged event: no metadata provided')
+  }
+}
+
 export async function workingGroups_WorkerRoleAccountUpdated(
   db: DatabaseManager,
   event_: SubstrateEvent
@@ -526,9 +617,6 @@ export async function workingGroups_WorkerRewardAmountUpdated(
 ): Promise<void> {
   // TBD
 }
-export async function workingGroups_StatusTextChanged(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TBD
-}
 export async function workingGroups_BudgetSpending(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   // TBD
 }

+ 24 - 1
query-node/schemas/workingGroups.graphql

@@ -181,7 +181,7 @@ type WorkingGroupOpening @entity {
   "Opening metadata"
   metadata: WorkingGroupOpeningMetadata!
 
-  "Role stake amount"
+  "Min. application/role stake amount"
   stakeAmount: BigInt!
 
   "Role stake unstaking period in blocks"
@@ -197,6 +197,29 @@ type WorkingGroupOpening @entity {
   createdAt: DateTime!
 }
 
+type UpcomingWorkingGroupOpening @entity {
+  "Event the upcoming opening was created in"
+  createdInEvent: StatusTextChangedEvent! @unique
+
+  "Block the upcoming opening was added at"
+  createdAtBlock: Block!
+
+  "Related working group"
+  group: WorkingGroup!
+
+  "Expected opening start time"
+  expectedStart: DateTime!
+
+  "Expected min. application/role stake amount"
+  stakeAmount: BigInt!
+
+  "Expected reward per block"
+  rewardPerBlock: BigInt!
+
+  "Opening metadata"
+  metadata: WorkingGroupOpeningMetadata!
+}
+
 type ApplicationStatusPending @variant {
   # No additional information needed
   _phantom: Int

+ 2 - 2
query-node/schemas/workingGroupsEvents.graphql

@@ -250,8 +250,8 @@ type StatusTextChangedEvent @entity {
   "Related group"
   group: WorkingGroup!
 
-  "New working group metadata"
-  metadata: WorkingGroupMetadata!
+  "Original action metadata as hex string"
+  metadata: String
 }
 
 type BudgetSpendingEvent @entity {

+ 71 - 0
tests/integration-tests/src/QueryNodeApi.ts

@@ -12,6 +12,8 @@ import {
   OpeningFilledEvent,
   Query,
   ReferralCutUpdatedEvent,
+  StatusTextChangedEvent,
+  UpcomingWorkingGroupOpening,
 } from './QueryNodeApiSchema.generated'
 import Debugger from 'debug'
 import { ApplicationId, OpeningId } from '@joystream/types/working-group'
@@ -782,4 +784,73 @@ export class QueryNodeApi {
       })
     ).data.openingCanceledEvents[0]
   }
+
+  public async getStatusTextChangedEvent(
+    blockNumber: number,
+    indexInBlock: number
+  ): Promise<StatusTextChangedEvent | undefined> {
+    const STATUS_TEXT_CHANGED_BY_ID = gql`
+      query($eventId: ID!) {
+        statusTextChangedEvents(where: { eventId_eq: $eventId }) {
+          ${EVENT_GENERIC_FIELDS}
+          group {
+            name
+          }
+          metadata
+        }
+      }
+    `
+
+    const eventId = `${blockNumber}-${indexInBlock}`
+    this.queryDebug(`Executing getStatusTextChangedEvent(${eventId})`)
+
+    return (
+      await this.queryNodeProvider.query<Pick<Query, 'statusTextChangedEvents'>>({
+        query: STATUS_TEXT_CHANGED_BY_ID,
+        variables: { eventId },
+      })
+    ).data.statusTextChangedEvents[0]
+  }
+
+  public async getUpcomingOpeningByCreatedInEventId(eventId: string): Promise<UpcomingWorkingGroupOpening | undefined> {
+    const UPCOMING_OPENING_BY_ID = gql`
+      query($eventId: ID!) {
+        upcomingWorkingGroupOpenings(where: { createdInEventId_eq: $eventId }) {
+          group {
+            name
+          }
+          metadata {
+            shortDescription
+            description
+            hiringLimit
+            expectedEnding
+            applicationDetails
+            applicationFormQuestions {
+              question
+              type
+              index
+            }
+          }
+          expectedStart
+          stakeAmount
+          rewardPerBlock
+          createdAtBlock {
+            number
+            timestamp
+            network
+          }
+          createdAt
+        }
+      }
+    `
+
+    this.queryDebug(`Executing getUpcomingOpeningByCreatedInEventId(${eventId})`)
+
+    return (
+      await this.queryNodeProvider.query<Pick<Query, 'upcomingWorkingGroupOpenings'>>({
+        query: UPCOMING_OPENING_BY_ID,
+        variables: { eventId },
+      })
+    ).data.upcomingWorkingGroupOpenings[0]
+  }
 }

+ 177 - 10
tests/integration-tests/src/QueryNodeApiSchema.generated.ts

@@ -753,6 +753,7 @@ export type Block = BaseGraphQlObject & {
   eventinBlock?: Maybe<Array<Event>>
   membershipregisteredAtBlock?: Maybe<Array<Membership>>
   membershipsystemsnapshotsnapshotBlock?: Maybe<Array<MembershipSystemSnapshot>>
+  upcomingworkinggroupopeningcreatedAtBlock?: Maybe<Array<UpcomingWorkingGroupOpening>>
   workerhiredAtBlock?: Maybe<Array<Worker>>
   workinggroupapplicationcreatedAtBlock?: Maybe<Array<WorkingGroupApplication>>
   workinggroupmetadatasetAtBlock?: Maybe<Array<WorkingGroupMetadata>>
@@ -3977,6 +3978,9 @@ export type Query = {
   terminatedWorkerEvents: Array<TerminatedWorkerEvent>
   terminatedWorkerEventByUniqueInput?: Maybe<TerminatedWorkerEvent>
   terminatedWorkerEventsConnection: TerminatedWorkerEventConnection
+  upcomingWorkingGroupOpenings: Array<UpcomingWorkingGroupOpening>
+  upcomingWorkingGroupOpeningByUniqueInput?: Maybe<UpcomingWorkingGroupOpening>
+  upcomingWorkingGroupOpeningsConnection: UpcomingWorkingGroupOpeningConnection
   workerExitedEvents: Array<WorkerExitedEvent>
   workerExitedEventByUniqueInput?: Maybe<WorkerExitedEvent>
   workerExitedEventsConnection: WorkerExitedEventConnection
@@ -4779,6 +4783,26 @@ export type QueryTerminatedWorkerEventsConnectionArgs = {
   orderBy?: Maybe<TerminatedWorkerEventOrderByInput>
 }
 
+export type QueryUpcomingWorkingGroupOpeningsArgs = {
+  offset?: Maybe<Scalars['Int']>
+  limit?: Maybe<Scalars['Int']>
+  where?: Maybe<UpcomingWorkingGroupOpeningWhereInput>
+  orderBy?: Maybe<UpcomingWorkingGroupOpeningOrderByInput>
+}
+
+export type QueryUpcomingWorkingGroupOpeningByUniqueInputArgs = {
+  where: UpcomingWorkingGroupOpeningWhereUniqueInput
+}
+
+export type QueryUpcomingWorkingGroupOpeningsConnectionArgs = {
+  first?: Maybe<Scalars['Int']>
+  after?: Maybe<Scalars['String']>
+  last?: Maybe<Scalars['Int']>
+  before?: Maybe<Scalars['String']>
+  where?: Maybe<UpcomingWorkingGroupOpeningWhereInput>
+  orderBy?: Maybe<UpcomingWorkingGroupOpeningOrderByInput>
+}
+
 export type QueryWorkerExitedEventsArgs = {
   offset?: Maybe<Scalars['Int']>
   limit?: Maybe<Scalars['Int']>
@@ -5865,8 +5889,9 @@ export type StatusTextChangedEvent = BaseGraphQlObject & {
   eventId: Scalars['String']
   group: WorkingGroup
   groupId: Scalars['String']
-  metadata: WorkingGroupMetadata
-  metadataId: Scalars['String']
+  /** Original action metadata as hex string */
+  metadata?: Maybe<Scalars['String']>
+  upcomingworkinggroupopeningcreatedInEvent?: Maybe<Array<UpcomingWorkingGroupOpening>>
 }
 
 export type StatusTextChangedEventConnection = {
@@ -5879,7 +5904,7 @@ export type StatusTextChangedEventConnection = {
 export type StatusTextChangedEventCreateInput = {
   eventId: Scalars['ID']
   groupId: Scalars['ID']
-  metadataId: Scalars['ID']
+  metadata?: Maybe<Scalars['String']>
 }
 
 export type StatusTextChangedEventEdge = {
@@ -5899,14 +5924,14 @@ export enum StatusTextChangedEventOrderByInput {
   EventIdDesc = 'eventId_DESC',
   GroupIdAsc = 'groupId_ASC',
   GroupIdDesc = 'groupId_DESC',
-  MetadataIdAsc = 'metadataId_ASC',
-  MetadataIdDesc = 'metadataId_DESC',
+  MetadataAsc = 'metadata_ASC',
+  MetadataDesc = 'metadata_DESC',
 }
 
 export type StatusTextChangedEventUpdateInput = {
   eventId?: Maybe<Scalars['ID']>
   groupId?: Maybe<Scalars['ID']>
-  metadataId?: Maybe<Scalars['ID']>
+  metadata?: Maybe<Scalars['String']>
 }
 
 export type StatusTextChangedEventWhereInput = {
@@ -5938,8 +5963,11 @@ export type StatusTextChangedEventWhereInput = {
   eventId_in?: Maybe<Array<Scalars['ID']>>
   groupId_eq?: Maybe<Scalars['ID']>
   groupId_in?: Maybe<Array<Scalars['ID']>>
-  metadataId_eq?: Maybe<Scalars['ID']>
-  metadataId_in?: Maybe<Array<Scalars['ID']>>
+  metadata_eq?: Maybe<Scalars['String']>
+  metadata_contains?: Maybe<Scalars['String']>
+  metadata_startsWith?: Maybe<Scalars['String']>
+  metadata_endsWith?: Maybe<Scalars['String']>
+  metadata_in?: Maybe<Array<Scalars['String']>>
 }
 
 export type StatusTextChangedEventWhereUniqueInput = {
@@ -6187,6 +6215,144 @@ export type TerminatedWorkerEventWhereUniqueInput = {
   id: Scalars['ID']
 }
 
+export type UpcomingWorkingGroupOpening = BaseGraphQlObject & {
+  __typename?: 'UpcomingWorkingGroupOpening'
+  id: Scalars['ID']
+  createdAt: Scalars['DateTime']
+  createdById: Scalars['String']
+  updatedAt?: Maybe<Scalars['DateTime']>
+  updatedById?: Maybe<Scalars['String']>
+  deletedAt?: Maybe<Scalars['DateTime']>
+  deletedById?: Maybe<Scalars['String']>
+  version: Scalars['Int']
+  createdInEvent: StatusTextChangedEvent
+  createdInEventId: Scalars['String']
+  group: WorkingGroup
+  groupId: Scalars['String']
+  /** Expected opening start time */
+  expectedStart: Scalars['DateTime']
+  /** Expected min. application/role stake amount */
+  stakeAmount: Scalars['BigInt']
+  /** Expected reward per block */
+  rewardPerBlock: Scalars['BigInt']
+  createdAtBlock: Block
+  createdAtBlockId: Scalars['String']
+  metadata: WorkingGroupOpeningMetadata
+  metadataId: Scalars['String']
+}
+
+export type UpcomingWorkingGroupOpeningConnection = {
+  __typename?: 'UpcomingWorkingGroupOpeningConnection'
+  totalCount: Scalars['Int']
+  edges: Array<UpcomingWorkingGroupOpeningEdge>
+  pageInfo: PageInfo
+}
+
+export type UpcomingWorkingGroupOpeningCreateInput = {
+  createdInEventId: Scalars['ID']
+  groupId: Scalars['ID']
+  expectedStart: Scalars['DateTime']
+  stakeAmount: Scalars['BigInt']
+  rewardPerBlock: Scalars['BigInt']
+  createdAtBlockId: Scalars['ID']
+  metadataId: Scalars['ID']
+}
+
+export type UpcomingWorkingGroupOpeningEdge = {
+  __typename?: 'UpcomingWorkingGroupOpeningEdge'
+  node: UpcomingWorkingGroupOpening
+  cursor: Scalars['String']
+}
+
+export enum UpcomingWorkingGroupOpeningOrderByInput {
+  CreatedAtAsc = 'createdAt_ASC',
+  CreatedAtDesc = 'createdAt_DESC',
+  UpdatedAtAsc = 'updatedAt_ASC',
+  UpdatedAtDesc = 'updatedAt_DESC',
+  DeletedAtAsc = 'deletedAt_ASC',
+  DeletedAtDesc = 'deletedAt_DESC',
+  CreatedInEventIdAsc = 'createdInEventId_ASC',
+  CreatedInEventIdDesc = 'createdInEventId_DESC',
+  GroupIdAsc = 'groupId_ASC',
+  GroupIdDesc = 'groupId_DESC',
+  ExpectedStartAsc = 'expectedStart_ASC',
+  ExpectedStartDesc = 'expectedStart_DESC',
+  StakeAmountAsc = 'stakeAmount_ASC',
+  StakeAmountDesc = 'stakeAmount_DESC',
+  RewardPerBlockAsc = 'rewardPerBlock_ASC',
+  RewardPerBlockDesc = 'rewardPerBlock_DESC',
+  CreatedAtBlockIdAsc = 'createdAtBlockId_ASC',
+  CreatedAtBlockIdDesc = 'createdAtBlockId_DESC',
+  MetadataIdAsc = 'metadataId_ASC',
+  MetadataIdDesc = 'metadataId_DESC',
+}
+
+export type UpcomingWorkingGroupOpeningUpdateInput = {
+  createdInEventId?: Maybe<Scalars['ID']>
+  groupId?: Maybe<Scalars['ID']>
+  expectedStart?: Maybe<Scalars['DateTime']>
+  stakeAmount?: Maybe<Scalars['BigInt']>
+  rewardPerBlock?: Maybe<Scalars['BigInt']>
+  createdAtBlockId?: Maybe<Scalars['ID']>
+  metadataId?: Maybe<Scalars['ID']>
+}
+
+export type UpcomingWorkingGroupOpeningWhereInput = {
+  id_eq?: Maybe<Scalars['ID']>
+  id_in?: Maybe<Array<Scalars['ID']>>
+  createdAt_eq?: Maybe<Scalars['DateTime']>
+  createdAt_lt?: Maybe<Scalars['DateTime']>
+  createdAt_lte?: Maybe<Scalars['DateTime']>
+  createdAt_gt?: Maybe<Scalars['DateTime']>
+  createdAt_gte?: Maybe<Scalars['DateTime']>
+  createdById_eq?: Maybe<Scalars['ID']>
+  createdById_in?: Maybe<Array<Scalars['ID']>>
+  updatedAt_eq?: Maybe<Scalars['DateTime']>
+  updatedAt_lt?: Maybe<Scalars['DateTime']>
+  updatedAt_lte?: Maybe<Scalars['DateTime']>
+  updatedAt_gt?: Maybe<Scalars['DateTime']>
+  updatedAt_gte?: Maybe<Scalars['DateTime']>
+  updatedById_eq?: Maybe<Scalars['ID']>
+  updatedById_in?: Maybe<Array<Scalars['ID']>>
+  deletedAt_all?: Maybe<Scalars['Boolean']>
+  deletedAt_eq?: Maybe<Scalars['DateTime']>
+  deletedAt_lt?: Maybe<Scalars['DateTime']>
+  deletedAt_lte?: Maybe<Scalars['DateTime']>
+  deletedAt_gt?: Maybe<Scalars['DateTime']>
+  deletedAt_gte?: Maybe<Scalars['DateTime']>
+  deletedById_eq?: Maybe<Scalars['ID']>
+  deletedById_in?: Maybe<Array<Scalars['ID']>>
+  createdInEventId_eq?: Maybe<Scalars['ID']>
+  createdInEventId_in?: Maybe<Array<Scalars['ID']>>
+  groupId_eq?: Maybe<Scalars['ID']>
+  groupId_in?: Maybe<Array<Scalars['ID']>>
+  expectedStart_eq?: Maybe<Scalars['DateTime']>
+  expectedStart_lt?: Maybe<Scalars['DateTime']>
+  expectedStart_lte?: Maybe<Scalars['DateTime']>
+  expectedStart_gt?: Maybe<Scalars['DateTime']>
+  expectedStart_gte?: Maybe<Scalars['DateTime']>
+  stakeAmount_eq?: Maybe<Scalars['BigInt']>
+  stakeAmount_gt?: Maybe<Scalars['BigInt']>
+  stakeAmount_gte?: Maybe<Scalars['BigInt']>
+  stakeAmount_lt?: Maybe<Scalars['BigInt']>
+  stakeAmount_lte?: Maybe<Scalars['BigInt']>
+  stakeAmount_in?: Maybe<Array<Scalars['BigInt']>>
+  rewardPerBlock_eq?: Maybe<Scalars['BigInt']>
+  rewardPerBlock_gt?: Maybe<Scalars['BigInt']>
+  rewardPerBlock_gte?: Maybe<Scalars['BigInt']>
+  rewardPerBlock_lt?: Maybe<Scalars['BigInt']>
+  rewardPerBlock_lte?: Maybe<Scalars['BigInt']>
+  rewardPerBlock_in?: Maybe<Array<Scalars['BigInt']>>
+  createdAtBlockId_eq?: Maybe<Scalars['ID']>
+  createdAtBlockId_in?: Maybe<Array<Scalars['ID']>>
+  metadataId_eq?: Maybe<Scalars['ID']>
+  metadataId_in?: Maybe<Array<Scalars['ID']>>
+}
+
+export type UpcomingWorkingGroupOpeningWhereUniqueInput = {
+  id: Scalars['ID']
+}
+
 export type Worker = BaseGraphQlObject & {
   __typename?: 'Worker'
   id: Scalars['ID']
@@ -7126,6 +7292,7 @@ export type WorkingGroup = BaseGraphQlObject & {
   statustextchangedeventgroup?: Maybe<Array<StatusTextChangedEvent>>
   terminatedleadereventgroup?: Maybe<Array<TerminatedLeaderEvent>>
   terminatedworkereventgroup?: Maybe<Array<TerminatedWorkerEvent>>
+  upcomingworkinggroupopeninggroup?: Maybe<Array<UpcomingWorkingGroupOpening>>
   workerexitedeventgroup?: Maybe<Array<WorkerExitedEvent>>
   workerrewardaccountupdatedeventgroup?: Maybe<Array<WorkerRewardAccountUpdatedEvent>>
   workerrewardamountupdatedeventgroup?: Maybe<Array<WorkerRewardAmountUpdatedEvent>>
@@ -7346,7 +7513,6 @@ export type WorkingGroupMetadata = BaseGraphQlObject & {
   setAtBlockId: Scalars['String']
   /** The time at which status was set */
   setAtTime: Scalars['DateTime']
-  statustextchangedeventmetadata?: Maybe<Array<StatusTextChangedEvent>>
   workinggroupstatus?: Maybe<Array<WorkingGroup>>
 }
 
@@ -7482,7 +7648,7 @@ export type WorkingGroupOpening = BaseGraphQlObject & {
   status: WorkingGroupOpeningStatus
   metadata: WorkingGroupOpeningMetadata
   metadataId: Scalars['String']
-  /** Role stake amount */
+  /** Min. application/role stake amount */
   stakeAmount: Scalars['BigInt']
   /** Role stake unstaking period in blocks */
   unstakingPeriod: Scalars['Int']
@@ -7545,6 +7711,7 @@ export type WorkingGroupOpeningMetadata = BaseGraphQlObject & {
   /** Md-formatted text explaining the application process */
   applicationDetails: Scalars['String']
   applicationFormQuestions: Array<ApplicationFormQuestion>
+  upcomingworkinggroupopeningmetadata?: Maybe<Array<UpcomingWorkingGroupOpening>>
   workinggroupopeningmetadata?: Maybe<Array<WorkingGroupOpening>>
 }
 

+ 148 - 41
tests/integration-tests/src/fixtures/workingGroupsModule.ts

@@ -16,8 +16,17 @@ import {
   Worker,
   ApplicationWithdrawnEvent,
   OpeningCanceledEvent,
+  StatusTextChangedEvent,
+  UpcomingWorkingGroupOpening,
+  WorkingGroupOpeningMetadata,
 } from '../QueryNodeApiSchema.generated'
-import { ApplicationMetadata, OpeningMetadata } from '@joystream/metadata-protobuf'
+import {
+  AddUpcomingOpening,
+  ApplicationMetadata,
+  OpeningMetadata,
+  UpcomingOpeningMetadata,
+  WorkingGroupMetadataAction,
+} from '@joystream/metadata-protobuf'
 import {
   WorkingGroupModuleName,
   MemberContext,
@@ -54,15 +63,22 @@ const queryNodeQuestionTypeToMetadataQuestionType = (type: ApplicationFormQuesti
   return OpeningMetadata.ApplicationFormQuestion.InputType.TEXTAREA
 }
 
-export class CreateOpeningFixture extends BaseFixture {
-  private query: QueryNodeApi
-  private group: WorkingGroupModuleName
-  private debug: Debugger.Debugger
-  private openingParams: OpeningParams
-  private asSudo: boolean
+abstract class BaseCreateOpeningFixture extends BaseFixture {
+  protected query: QueryNodeApi
+  protected group: WorkingGroupModuleName
+  protected openingParams: OpeningParams
 
-  private event?: OpeningAddedEventDetails
-  private tx?: SubmittableExtrinsic<'promise'>
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    group: WorkingGroupModuleName,
+    openingParams?: Partial<OpeningParams>
+  ) {
+    super(api)
+    this.query = query
+    this.group = group
+    this.openingParams = _.merge(this.defaultOpeningParams, openingParams)
+  }
 
   private defaultOpeningParams: OpeningParams = {
     stake: MIN_APPLICATION_STAKE,
@@ -85,7 +101,7 @@ export class CreateOpeningFixture extends BaseFixture {
     return this.defaultOpeningParams
   }
 
-  private getMetadata(): OpeningMetadata {
+  protected getMetadata(): OpeningMetadata {
     const metadataObj = this.openingParams.metadata as Required<OpeningMetadata.AsObject>
     const metadata = new OpeningMetadata()
     metadata.setShortDescription(metadataObj.shortDescription)
@@ -103,13 +119,32 @@ export class CreateOpeningFixture extends BaseFixture {
     return metadata
   }
 
-  public getCreatedOpeningId(): OpeningId {
-    if (!this.event) {
-      throw new Error('Trying to get created opening id before it was created!')
-    }
-    return this.event.openingId
+  protected assertQueriedOpeningMetadataIsValid(qOpeningMeta: WorkingGroupOpeningMetadata) {
+    assert.equal(qOpeningMeta.shortDescription, this.openingParams.metadata.shortDescription)
+    assert.equal(qOpeningMeta.description, this.openingParams.metadata.description)
+    assert.equal(new Date(qOpeningMeta.expectedEnding).getTime(), this.openingParams.metadata.expectedEndingTimestamp)
+    assert.equal(qOpeningMeta.hiringLimit, this.openingParams.metadata.hiringLimit)
+    assert.equal(qOpeningMeta.applicationDetails, this.openingParams.metadata.applicationDetails)
+    assert.deepEqual(
+      qOpeningMeta.applicationFormQuestions
+        .sort((a, b) => a.index - b.index)
+        .map(({ question, type }) => ({
+          question,
+          type: queryNodeQuestionTypeToMetadataQuestionType(type),
+        })),
+      this.openingParams.metadata.applicationFormQuestionsList
+    )
   }
 
+  abstract execute(): Promise<void>
+}
+export class CreateOpeningFixture extends BaseCreateOpeningFixture {
+  private debug: Debugger.Debugger
+  private asSudo: boolean
+
+  private event?: OpeningAddedEventDetails
+  private tx?: SubmittableExtrinsic<'promise'>
+
   public constructor(
     api: Api,
     query: QueryNodeApi,
@@ -117,14 +152,18 @@ export class CreateOpeningFixture extends BaseFixture {
     openingParams?: Partial<OpeningParams>,
     asSudo = false
   ) {
-    super(api)
-    this.query = query
-    this.debug = Debugger('fixture:CreateOpeningFixture')
-    this.group = group
-    this.openingParams = _.merge(this.defaultOpeningParams, openingParams)
+    super(api, query, group, openingParams)
+    this.debug = Debugger(`fixture:CreateOpeningFixture:${group}`)
     this.asSudo = asSudo
   }
 
+  public getCreatedOpeningId(): OpeningId {
+    if (!this.event) {
+      throw new Error('Trying to get created opening id before it was created!')
+    }
+    return this.event.openingId
+  }
+
   private assertOpeningMatchQueriedResult(
     eventDetails: OpeningAddedEventDetails,
     qOpening?: WorkingGroupOpening | null
@@ -141,23 +180,7 @@ export class CreateOpeningFixture extends BaseFixture {
     assert.equal(qOpening.stakeAmount, this.openingParams.stake.toString())
     assert.equal(qOpening.unstakingPeriod, this.openingParams.unstakingPeriod)
     // Metadata
-    assert.equal(qOpening.metadata.shortDescription, this.openingParams.metadata.shortDescription)
-    assert.equal(qOpening.metadata.description, this.openingParams.metadata.description)
-    assert.equal(
-      new Date(qOpening.metadata.expectedEnding).getTime(),
-      this.openingParams.metadata.expectedEndingTimestamp
-    )
-    assert.equal(qOpening.metadata.hiringLimit, this.openingParams.metadata.hiringLimit)
-    assert.equal(qOpening.metadata.applicationDetails, this.openingParams.metadata.applicationDetails)
-    assert.deepEqual(
-      qOpening.metadata.applicationFormQuestions
-        .sort((a, b) => a.index - b.index)
-        .map(({ question, type }) => ({
-          question,
-          type: queryNodeQuestionTypeToMetadataQuestionType(type),
-        })),
-      this.openingParams.metadata.applicationFormQuestionsList
-    )
+    this.assertQueriedOpeningMetadataIsValid(qOpening.metadata)
   }
 
   private assertQueriedOpeningAddedEventIsValid(
@@ -237,7 +260,7 @@ export class ApplyOnOpeningHappyCaseFixture extends BaseFixture {
   ) {
     super(api)
     this.query = query
-    this.debug = Debugger('fixture:ApplyOnOpeningHappyCaseFixture')
+    this.debug = Debugger(`fixture:ApplyOnOpeningHappyCaseFixture:${group}`)
     this.group = group
     this.applicant = applicant
     this.stakingAccount = stakingAccount
@@ -366,7 +389,7 @@ export class SudoFillLeadOpeningFixture extends BaseFixture {
   ) {
     super(api)
     this.query = query
-    this.debug = Debugger('fixture:SudoFillLeadOpeningFixture')
+    this.debug = Debugger(`fixture:SudoFillLeadOpeningFixture:${group}`)
     this.group = group
     this.openingId = openingId
     this.acceptedApplicationIds = acceptedApplicationIds
@@ -518,7 +541,7 @@ export class WithdrawApplicationsFixture extends BaseFixture {
   ) {
     super(api)
     this.query = query
-    this.debug = Debugger('fixture:WithdrawApplicationsFixture')
+    this.debug = Debugger(`fixture:WithdrawApplicationsFixture:${group}`)
     this.group = group
     this.accounts = accounts
     this.applicationIds = applicationIds
@@ -603,7 +626,7 @@ export class CancelOpeningFixture extends BaseFixture {
   public constructor(api: Api, query: QueryNodeApi, group: WorkingGroupModuleName, openingId: OpeningId) {
     super(api)
     this.query = query
-    this.debug = Debugger('fixture:CancelOpeningFixture')
+    this.debug = Debugger(`fixture:CancelOpeningFixture:${group}`)
     this.group = group
     this.openingId = openingId
   }
@@ -660,3 +683,87 @@ export class CancelOpeningFixture extends BaseFixture {
     this.assertQueriedOpeningIsValid(qEvent, qOpening)
   }
 }
+export class CreateUpcomingOpeningFixture extends BaseCreateOpeningFixture {
+  private debug: Debugger.Debugger
+  private expectedStartTs: number
+
+  private tx?: SubmittableExtrinsic<'promise'>
+  private event?: EventDetails
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    group: WorkingGroupModuleName,
+    openingParams?: OpeningParams,
+    expectedStartTs?: number
+  ) {
+    super(api, query, group, openingParams)
+    this.debug = Debugger(`fixture:CreateUpcomingOpening:${group}`)
+    this.expectedStartTs = expectedStartTs || Date.now() + 3600
+  }
+
+  private getActionMetadata(): WorkingGroupMetadataAction {
+    const actionMeta = new WorkingGroupMetadataAction()
+    const addUpcomingOpeningMeta = new AddUpcomingOpening()
+
+    const upcomingOpeningMeta = new UpcomingOpeningMetadata()
+    const openingMeta = this.getMetadata()
+    upcomingOpeningMeta.setMetadata(openingMeta)
+    upcomingOpeningMeta.setExpectedStart(this.expectedStartTs)
+    upcomingOpeningMeta.setMinApplicationStake(this.openingParams.stake.toNumber())
+    upcomingOpeningMeta.setRewardPerBlock(this.openingParams.reward.toNumber())
+
+    addUpcomingOpeningMeta.setMetadata(upcomingOpeningMeta)
+    actionMeta.setAddupcomingopening(addUpcomingOpeningMeta)
+
+    return actionMeta
+  }
+
+  async execute() {
+    const account = await this.api.getLeadRoleKey(this.group)
+    this.tx = this.api.tx[this.group].setStatusText(Utils.metadataToBytes(this.getActionMetadata()))
+    const txFee = await this.api.estimateTxFee(this.tx, account)
+    await this.api.treasuryTransferBalance(account, txFee)
+    const result = await this.api.signAndSend(this.tx, account)
+    this.event = await this.api.retrieveWorkingGroupsEventDetails(result, this.group, 'StatusTextChanged')
+  }
+
+  private assertQueriedUpcomingOpeningIsValid(
+    eventDetails: EventDetails,
+    qUpcomingOpening?: UpcomingWorkingGroupOpening | null
+  ) {
+    if (!qUpcomingOpening) {
+      throw new Error('Query node: Upcoming opening not found!')
+    }
+    assert.equal(new Date(qUpcomingOpening.expectedStart).getTime(), this.expectedStartTs)
+    assert.equal(qUpcomingOpening.group.name, this.group)
+    assert.equal(qUpcomingOpening.rewardPerBlock, this.openingParams.reward.toString())
+    assert.equal(qUpcomingOpening.stakeAmount, this.openingParams.stake.toString())
+    assert.equal(qUpcomingOpening.createdAtBlock.number, eventDetails.blockNumber)
+    this.assertQueriedOpeningMetadataIsValid(qUpcomingOpening.metadata)
+  }
+
+  private assertQueriedStatusTextChangedEventIsValid(txHash: string, qEvent?: StatusTextChangedEvent) {
+    if (!qEvent) {
+      throw new Error('Query node: StatusTextChangedEvent not found!')
+    }
+    assert.equal(qEvent.event.inExtrinsic, txHash)
+    assert.equal(qEvent.event.type, EventType.StatusTextChanged)
+    assert.equal(qEvent.group.name, this.group)
+    assert.equal(qEvent.metadata, Utils.metadataToBytes(this.getActionMetadata()).toString())
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    const tx = this.tx!
+    const event = this.event!
+    // Query the event
+    const qEvent = (await this.query.tryQueryWithTimeout(
+      () => this.query.getStatusTextChangedEvent(event.blockNumber, event.indexInBlock),
+      (qEvent) => this.assertQueriedStatusTextChangedEventIsValid(tx.hash.toString(), qEvent)
+    )) as OpeningCanceledEvent
+    // Query the opening
+    const qUpcomingOpening = await this.query.getUpcomingOpeningByCreatedInEventId(qEvent.id)
+    this.assertQueriedUpcomingOpeningIsValid(event, qUpcomingOpening)
+  }
+}

+ 21 - 0
tests/integration-tests/src/flows/working-groups/upcomingOpenings.ts

@@ -0,0 +1,21 @@
+import { FlowProps } from '../../Flow'
+import { CreateUpcomingOpeningFixture } from '../../fixtures/workingGroupsModule'
+
+import Debugger from 'debug'
+import { FixtureRunner } from '../../Fixture'
+import { workingGroups } from '../../types'
+
+export default async function upcomingOpenings({ api, query, env }: FlowProps): Promise<void> {
+  await Promise.all(
+    workingGroups.map(async (group) => {
+      const debug = Debugger(`flow:upcoming-openings:${group}`)
+      debug('Started')
+      api.enableDebugTxLogs()
+
+      const createUpcomingOpeningFixture = new CreateUpcomingOpeningFixture(api, query, group)
+      await new FixtureRunner(createUpcomingOpeningFixture).runWithQueryNodeChecks()
+
+      debug('Done')
+    })
+  )
+}

+ 2 - 0
tests/integration-tests/src/scenarios/workingGroups.ts

@@ -1,8 +1,10 @@
 import leadOpening from '../flows/working-groups/leadOpening'
 import openingAndApplicationStatus from '../flows/working-groups/openingAndApplicationStatus'
+import upcomingOpenings from '../flows/working-groups/upcomingOpenings'
 import { scenario } from '../Scenario'
 
 scenario(async ({ job }) => {
   const sudoHireLead = job('sudo lead opening', leadOpening)
   job('opening and application status', openingAndApplicationStatus).requires(sudoHireLead)
+  job('upcoming openings', upcomingOpenings).requires(sudoHireLead)
 })

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor