<?php
namespace Products\NotificationsBundle\Subscriber\OneRoster;
use App\Entity\System\School;
use Cms\CoreBundle\Entity\AbstractOneRosterEntity;
use Cms\CoreBundle\Entity\OneRoster\OneRosterOrg;
use Cms\CoreBundle\Entity\OneRoster\OneRosterUser;
use Cms\CoreBundle\Entity\OneRosterSync;
use Cms\CoreBundle\Events\OneRosterEvents;
use Cms\CoreBundle\Util\DateTimeUtils;
use Platform\QueueBundle\Event\AsyncEvent;
use Products\NotificationsBundle\Entity\Lists\SchoolList;
use Products\NotificationsBundle\Entity\Profile;
use Products\NotificationsBundle\Entity\ProfileContact;
use Products\NotificationsBundle\Entity\Recipients\AppRecipient;
use Products\NotificationsBundle\Entity\Recipients\EmailRecipient;
use Products\NotificationsBundle\Entity\Recipients\PhoneRecipient;
use Products\NotificationsBundle\Util\Preferences;
use Products\NotificationsBundle\Util\Reachability;
/**
* Class OneRosterPrepareSubscriber
* @package Products\NotificationsBundle\Subscriber\OneRoster
*/
final class OneRosterTidySubscriber extends AbstractNotificationsOneRosterSubscriber
{
const TESTER__ALIAS_PREFIX = 'app.notifications.testers.default';
const TESTER__LAST_NAME = 'Δ SchoolNow';
const TESTER__EMAIL__PREFIX = 'sn';
const TESTER__EMAIL__DOMAIN = 'campussuite.com';
const TESTER__PHONES = [
OneRosterUser::TYPES__STAFF => '+15136205313',
OneRosterUser::TYPES__FAMILY => '+15136205313',
OneRosterUser::TYPES__STUDENT => '+15136205313',
OneRosterUser::TYPES__COMMUNITY => '+15136205313',
];
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array
{
return [
OneRosterEvents::EVENT__TIDY => [
['syncTesters', 0],
['discardProfileContacts', 0],
['cleanup', 0],
],
];
}
/**
* @param AsyncEvent $event
*/
public function syncTesters(AsyncEvent $event): void
{
// get the job
$job = $this->loadJob($event);
// DEBUGGING
$event->getOutput()->writeln(sprintf(
'Sync #%s loaded',
$job->getIdentifier()
));
// ensure we are meant to process this
if ( ! $this->checkTypes($job, [
OneRosterSync::STRATEGIES__NOTIFICATIONS__STAFF,
OneRosterSync::STRATEGIES__NOTIFICATIONS__FAMILY,
OneRosterSync::STRATEGIES__NOTIFICATIONS__STUDENTS,
OneRosterSync::STRATEGIES__NOTIFICATIONS__COMMUNITY,
])) {
return;
}
// grab all schools
$schools = $this->em->getRepository(School::class)->findAll();
// assemble data for testing users
$roles = array_values(array_filter([
$this->checkTypes($job, [OneRosterSync::STRATEGIES__NOTIFICATIONS__STAFF]) ? AbstractOneRosterEntity::ENUMS__ROLE_TYPE__TEACHER : null,
$this->checkTypes($job, [OneRosterSync::STRATEGIES__NOTIFICATIONS__FAMILY]) ? AbstractOneRosterEntity::ENUMS__ROLE_TYPE__PARENT : null,
$this->checkTypes($job, [OneRosterSync::STRATEGIES__NOTIFICATIONS__STUDENTS]) ? AbstractOneRosterEntity::ENUMS__ROLE_TYPE__STUDENT : null,
$this->checkTypes($job, [OneRosterSync::STRATEGIES__NOTIFICATIONS__COMMUNITY]) ? AbstractOneRosterEntity::ENUMS__ROLE_TYPE__COMMUNITY : null,
]));
// loop over each tester
foreach ($roles as $role) {
// determine the type
$type = OneRosterUser::ROLES_MAPPING[$role];
// generate the full alias
$alias = sprintf(
'%s.%s',
self::TESTER__ALIAS_PREFIX,
$type
);
// try to find the main profile
$profile = $this->em->getRepository(Profile::class)->findOneBy([
'alias' => $alias,
]);
// make one if not found
if ( ! $profile) {
$profile = new Profile();
}
// set things on it
$profile
->setAlias($alias)
->markFlag(Profile::FLAGS__FIXED)
->setRole($role)
->setFirstName(sprintf(
'Δ Test %s',
strtoupper($role)
))
->setLastName(self::TESTER__LAST_NAME)
->setOneRosterId($job->getSync()->getDistrictId())
->setOneRosterArchived(false)
->setStanding(Reachability::STANDINGS__GOOD)
->setDiscardedAt(null)
;
// basic metadata
$profile->setMetadata(array_merge(
$profile->getMetadata(),
[
'_role' => $role,
'_role_type' => OneRosterUser::TYPES_LOOKUP[
OneRosterUser::ROLES_MAPPING[$role]
],
'_district' => $job->getSync()->getDistrictId(),
'_orgs' => array_map(
static function (School $school) {
return $school->getOneRosterOrg();
},
$schools
),
'_schools' => array_values(array_filter(array_map(
static function (School $school) {
if ($school->isTypeDistrict()) {
return null;
}
return $school->getOneRosterOrg();
},
$schools
))),
'_grades' => AbstractOneRosterEntity::CEDS__GRADES,
]
));
// save it
$this->em->save($profile);
// generate email address
$emailAddress = sprintf(
'%s+%s+%s@%s',
self::TESTER__EMAIL__PREFIX,
$job->getTenant()->getSlug(),
OneRosterUser::TYPES_LOOKUP[$type],
self::TESTER__EMAIL__DOMAIN
);
// find or make the email contact for this profile
$this->em->save(
$email = ($this->em->getRepository(EmailRecipient::class)->findOneBy([
'contact' => $emailAddress,
]) ?: new EmailRecipient())
->setContact($emailAddress)
->setStanding(Reachability::STANDINGS__GOOD)
);
// attach email
$this->em->save(
($this->em->getRepository(ProfileContact::class)->findOneBy([
'profile' => $profile,
'recipient' => $email,
]) ?: new ProfileContact())
->setProfile($profile)
->setRecipient($email)
->setPrimaryPreferences(Preferences::PREFERENCES__EMAIL)
->setSecondaryPreferences(Preferences::PREFERENCES__EMAIL)
->setOneRosterId($profile->getOneRosterId())
->setOneRosterArchived($profile->isOneRosterArchived())
->setDiscardedAt(null)
);
// find the phone contact for this profile
$this->em->save(
$phone = ($this->em->getRepository(PhoneRecipient::class)->findOneBy([
'contact' => self::TESTER__PHONES[$type],
]) ?: new PhoneRecipient())
->setContact(self::TESTER__PHONES[$type])
->setMethod(PhoneRecipient::METHODS__HYBRID)
->setStanding(Reachability::STANDINGS__GOOD)
);
// attach phone
$this->em->save(
($this->em->getRepository(ProfileContact::class)->findOneBy([
'profile' => $profile,
'recipient' => $phone,
]) ?: new ProfileContact())
->setProfile($profile)
->setRecipient($phone)
->setPrimaryPreferences(Preferences::PREFERENCES__SMS | Preferences::PREFERENCES__VOICE)
->setSecondaryPreferences(Preferences::PREFERENCES__SMS)
->setOneRosterId($profile->getOneRosterId())
->setOneRosterArchived($profile->isOneRosterArchived())
->setDiscardedAt(null)
);
// get push contacts
$pushes = $this->em->getRepository(ProfileContact::class)->createQueryBuilder('contacts')
->leftJoin('contacts.recipient', 'recipients')
->andWhere('recipients INSTANCE OF ' . AppRecipient::class)
->getQuery()
->getResult();
// delete any extra contacts for this test user
$this->em->createQueryBuilder()
->delete(ProfileContact::class, 'contacts')
->andWhere('contacts.tenant = :tenant')
->setParameter('tenant', $job->getTenant())
->andWhere('contacts.profile = :profile')
->setParameter('profile', $profile)
->andWhere('contacts.recipient NOT IN (:recipients)')
->setParameter('recipients', [$email, $phone, ...$pushes])
->getQuery()
->execute();
}
}
/**
* @param AsyncEvent $event
* @return void
*/
public function discardProfileContacts(AsyncEvent $event): void
{
// data should be an array with an id of a sync
$job = $this->loadJob($event);
// run bulk query
// this should discard every profile that has an invalid/missing oneroster id
$discards = $this->em->createQueryBuilder()
->update(ProfileContact::class, 'contacts')
->set('contacts.discarded', ':state')
->setParameter('state', true)
->set('contacts.discardedAt', ':timestamp')
->setParameter('timestamp', DateTimeUtils::now())
->andWhere('contacts.tenant = :tenant')// filter by tenant
->setParameter('tenant', $job->getTenant()->getId())
->andWhere('contacts.discarded != :state')// for performance, only update ones not already discarded
->andWhere($this->em->getExpressionBuilder()->in(
'contacts.profile',
$this->em->createQueryBuilder()
->select('profiles.id')
->from(Profile::class, 'profiles')
->andWhere('profiles.tenant = :tenant')// we are making a string subquery, so the tenant var in the other qb will be used here eventually
->andWhere('profiles.discarded = :discarded')// filter things already discarded
->getDQL()
))// only match objects that are missing from stashed objects
->setParameter('discarded', true)// have to set the parameter here for the subquery
->getQuery()
->execute();
// DEBUGGING
$event->getOutput()->writeln(sprintf(
'Discarded %s profile contacts for sync #%s',
$discards,
$job->getIdentifier()
));
}
/**
* @param AsyncEvent $event
*/
public function cleanup(AsyncEvent $event): void
{
// get the job we are working with
$job = $this->loadJob($event);
// DEBUGGING
$event->getOutput()->writeln(sprintf(
'Sync #%s loaded',
$job->getIdentifier()
));
// ensure we are meant to process this
if ( ! $this->checkTypes($job, [
OneRosterSync::STRATEGIES__NOTIFICATIONS__STAFF,
OneRosterSync::STRATEGIES__NOTIFICATIONS__FAMILY,
OneRosterSync::STRATEGIES__NOTIFICATIONS__STUDENTS,
OneRosterSync::STRATEGIES__NOTIFICATIONS__COMMUNITY,
])) {
return;
}
// remove school lists no longer needed
$removals = $this->em->createQueryBuilder()
->delete(SchoolList::class, 'lists')
->andWhere('lists.tenant = :tenant')// filter by tenant
->setParameter('tenant', $job->getTenant()->getId())
->andWhere($this->em->getExpressionBuilder()->notIn(
'lists.onerosterId',
$this->em->createQueryBuilder()
->select('orgs.sourcedId')
->from(OneRosterOrg::class, 'orgs')
->andWhere('orgs.tenant = :tenant')// we are making a string subquery, so the tenant var in the other qb will be used here eventually
->getDQL()
))// only match objects that are missing from stashed objects
->getQuery()
->execute();
// DEBUGGING
$event->getOutput()->writeln(sprintf(
'Removed %s org lists for sync #%s',
$removals,
$job->getIdentifier()
));
// TODO: topics may be tied to existing dynamic lists made custom...
// // remove school topics no longer needed
// $removals = $this->em->createQueryBuilder()
// ->delete(Topic::class, 'topics')
// ->andWhere('topics.tenant = :tenant')// filter by tenant
// ->setParameter('tenant', $job->getTenant()->getId())
// ->andWhere($this->em->getExpressionBuilder()->notIn(
// 'topics.onerosterId',
// $this->em->createQueryBuilder()
// ->select('orgs.sourcedId')
// ->from(OneRosterOrg::class, 'orgs')
// ->andWhere('orgs.tenant = :tenant')// we are making a string subquery, so the tenant var in the other qb will be used here eventually
// ->getDQL()
// ))// only match objects that are missing from stashed objects
// ->getQuery()
// ->execute();
//
// // DEBUGGING
// $event->getOutput()->writeln(sprintf(
// 'Removed %s org topics for sync #%s',
// $removals,
// $job->getIdentifier()
// ));
}
}