value, AttendanceStatus::LATE->value ); $course = SqlPart::of( '( SELECT s.course_id FROM %i o JOIN %i s ON s.id = o.session_id WHERE o.id = %d )', Db::table( 'live_occurrences' ), Db::table( 'live_sessions' ), $occurrence_id ); $changed = 0; foreach ( array_chunk( $users, self::CHUNK ) as $chunk ) { $where = SqlPart::of( 'FROM %i i JOIN %i v ON v.id = i.invitation_id WHERE v.occurrence_id = %d AND v.course_id > 0 AND v.course_id = ', Db::table( 'invitees' ), Db::table( 'invitations' ), $occurrence_id )->add( $course )->add( self::sent_sql() )->add( SqlPart::and_in( 'i.user_id', $chunk ) )->add( SqlPart::of( ' AND ( ( i.outcome = %d AND ', InviteeOutcome::NONE->value ) )->add( $here )->add( SqlPart::of( ' ) OR ( i.outcome = %d AND NOT ', InviteeOutcome::ATTENDED->value ) )->add( $here )->add( ' ) )' ); $changed += self::write( $where, $where, SqlPart::of( '( CASE WHEN ' )->add( $here )->add( ' THEN ' . InviteeOutcome::ATTENDED->value . ' ELSE ' . InviteeOutcome::NONE->value . ' END )' ) ); } return $changed; } public static function enrolled( $enrolment_id, $user_id = 0, $course_id = 0, $org_id = 0, $source = 0 ): int { unset( $enrolment_id, $org_id, $source ); if ( (int) $user_id <= 0 || (int) $course_id <= 0 || ! InvitationService::used() ) { return 0; } global $wpdb; $where = SqlPart::of( 'FROM %i i JOIN %i v ON v.id = i.invitation_id WHERE i.user_id = %d AND i.answer = %d AND i.outcome IN (%d, %d) AND v.course_id = %d AND v.yes_action <> %d', Db::table( 'invitees' ), Db::table( 'invitations' ), (int) $user_id, RsvpAnswer::YES->value, InviteeOutcome::NONE->value, InviteeOutcome::ATTENDED->value, (int) $course_id, YesAction::JOIN_COHORT->value )->add( self::sent_sql() ); return self::write( $where, $where, SqlPart::of( (string) InviteeOutcome::ENROLLED->value ) ); } public static function cohort( $cohort_id, $user_ids = array(), $joined = true ): int { $users = self::ids( (array) $user_ids ); if ( true !== (bool) $joined || (int) $cohort_id <= 0 || array() === $users || ! InvitationService::used() ) { return 0; } global $wpdb; $changed = 0; foreach ( array_chunk( $users, self::CHUNK ) as $chunk ) { $where = SqlPart::of( 'FROM %i v JOIN %i i ON i.invitation_id = v.id WHERE v.target_cohort_id = %d AND i.answer = %d AND i.outcome IN (%d, %d)', Db::table( 'invitations' ), Db::table( 'invitees' ), (int) $cohort_id, RsvpAnswer::YES->value, InviteeOutcome::NONE->value, InviteeOutcome::ATTENDED->value )->add( self::sent_sql() )->add( SqlPart::and_in( 'i.user_id', $chunk ) ); $changed += self::write( $where, $where, SqlPart::of( (string) InviteeOutcome::JOINED->value ) ); } return $changed; } public static function daily(): int { if ( ! InvitationService::used() ) { return 0; } global $wpdb; $now = Clock::now(); $since = gmdate( 'Y-m-d H:i:s', Clock::timestamp() - self::NET_DAYS * DAY_IN_SECONDS ); $start = gmdate( 'Y-m-d H:i:s', Clock::timestamp() - ( self::NET_DAYS + self::NET_LESSON_DAYS ) * DAY_IN_SECONDS ); $read = SqlPart::of( 'FROM %i o JOIN %i s ON s.id = o.session_id JOIN %i v ON v.course_id = s.course_id AND v.occurrence_id = o.id JOIN %i i ON i.invitation_id = v.id JOIN %i a ON a.occurrence_id = o.id AND a.user_id = i.user_id WHERE o.state = 1 AND o.starts_at >= %s AND o.starts_at <= %s AND o.ends_at >= %s AND o.ends_at <= %s AND s.course_id > 0 AND i.outcome = %d AND a.status IN (%d, %d)', Db::table( 'live_occurrences' ), Db::table( 'live_sessions' ), Db::table( 'invitations' ), Db::table( 'invitees' ), Db::table( 'attendance' ), $start, $now, $since, $now, InviteeOutcome::NONE->value, AttendanceStatus::PRESENT->value, AttendanceStatus::LATE->value )->add( self::sent_sql() ); $lock = SqlPart::of( 'FROM %i i JOIN %i v ON v.id = i.invitation_id WHERE i.outcome = %d AND EXISTS ( SELECT 1 FROM %i a WHERE a.occurrence_id = v.occurrence_id AND a.user_id = i.user_id AND a.status IN (%d, %d) )', Db::table( 'invitees' ), Db::table( 'invitations' ), InviteeOutcome::NONE->value, Db::table( 'attendance' ), AttendanceStatus::PRESENT->value, AttendanceStatus::LATE->value )->add( self::sent_sql() ); return self::write( $read, $lock, SqlPart::of( (string) InviteeOutcome::ATTENDED->value ) ); } private static function sent_sql(): SqlPart { global $wpdb; return SqlPart::of( ' AND v.kind = %d AND v.state <> %d AND i.user_id > 0 AND i.sent_version > 0', InvitationKind::RSVP->value, InvitationState::CANCELLED->value ); } private static function write( SqlPart $read, SqlPart $lock, SqlPart $outcome ): int { $ids = array_map( 'intval', Db::col( SqlPart::of( 'SELECT /* outside-tx */ i.id ' )->add( $read )->add( ' ORDER BY i.id' ) ) ); $changed = 0; foreach ( array_chunk( $ids, self::CHUNK ) as $chunk ) { $changed += self::write_chunk( $lock, $outcome, $chunk ); } return $changed; } private static function write_chunk( SqlPart $lock, SqlPart $outcome, array $ids ): int { $rows = Transaction::run( static function () use ( $lock, $outcome, $ids ): array { global $wpdb; $rows = Db::rows( SqlPart::of( 'SELECT i.id, i.invitation_id, i.user_id, i.email_hash, i.lang, v.course_id, v.unit_id, v.occurrence_id, v.target_cohort_id, ' )->add( $outcome )->add( ' AS rl_new ' )->add( $lock )->add( SqlPart::of( ' AND i.id IN (' . implode( ',', array_fill( 0, count( $ids ), '%d' ) ) . ') ORDER BY i.id', array_map( 'intval', $ids ) ) )->add( ' FOR UPDATE' ) ); if ( array() === $rows ) { return array(); } $now = Transaction::now(); $case = SqlPart::of( '' ); $at = SqlPart::of( '' ); foreach ( $rows as $r ) { $case = $case->add( SqlPart::of( ' WHEN %d THEN %d', (int) $r['id'], (int) $r['rl_new'] ) ); $at = $at->add( InviteeOutcome::NONE->value === (int) $r['rl_new'] ? SqlPart::of( ' WHEN %d THEN NULL', (int) $r['id'] ) : SqlPart::of( ' WHEN %d THEN %s', (int) $r['id'], $now ) ); } $in = implode( ',', array_map( static fn( array $r ): int => (int) $r['id'], $rows ) ); Db::query( SqlPart::of( 'UPDATE %i SET ', Db::table( 'invitees' ) )->add( SqlPart::of( 'outcome = CASE id' )->add( $case )->add( ' ELSE outcome END, outcome_at = CASE id' )->add( $at )->add( ' ELSE outcome_at END WHERE id IN (' )->add( $in )->add( ')' ) ) ); $events = array(); foreach ( $rows as $r ) { $events[] = new Event( Verb::INVITATION_OUTCOME, Actor::system(), (int) $r['user_id'], (int) $r['course_id'], (int) $r['unit_id'], 0, 0, (float) (int) $r['rl_new'], (int) $r['id'] ); } ( new EventWriter() )->record( ...$events ); return $rows; } ); foreach ( $rows as $r ) { $set = InviteeOutcome::tryFrom( (int) $r['rl_new'] ) ?? InviteeOutcome::NONE; if ( InviteeOutcome::NONE === $set ) { continue; } Events::fire( 'invitation_' . $set->slug(), array( 'user_id' => (int) $r['user_id'], 'email_hash' => (string) $r['email_hash'], 'lang' => (string) $r['lang'], 'course_id' => (int) $r['course_id'], 'unit_id' => (int) $r['unit_id'], 'occurrence_id' => (int) $r['occurrence_id'], 'invitation_id' => (int) $r['invitation_id'], 'invitee_id' => (int) $r['id'], 'cohort_id' => (int) $r['target_cohort_id'], 'outcome' => $set->slug(), ) ); } if ( array() !== $rows ) { InvitationRepository::forget(); } return count( $rows ); } private static function ids( array $ids ): array { $out = array(); foreach ( $ids as $id ) { $id = (int) $id; if ( $id > 0 ) { $out[ $id ] = $id; } } return array_values( $out ); } }