src/Products/NotificationsBundle/Subscriber/OneRoster/OneRosterTidySubscriber.php line 241

Open in your IDE?
  1. <?php
  2. namespace Products\NotificationsBundle\Subscriber\OneRoster;
  3. use App\Entity\System\School;
  4. use Cms\CoreBundle\Entity\AbstractOneRosterEntity;
  5. use Cms\CoreBundle\Entity\OneRoster\OneRosterOrg;
  6. use Cms\CoreBundle\Entity\OneRoster\OneRosterUser;
  7. use Cms\CoreBundle\Entity\OneRosterSync;
  8. use Cms\CoreBundle\Events\OneRosterEvents;
  9. use Cms\CoreBundle\Util\DateTimeUtils;
  10. use Platform\QueueBundle\Event\AsyncEvent;
  11. use Products\NotificationsBundle\Entity\Lists\SchoolList;
  12. use Products\NotificationsBundle\Entity\Profile;
  13. use Products\NotificationsBundle\Entity\ProfileContact;
  14. use Products\NotificationsBundle\Entity\Recipients\AppRecipient;
  15. use Products\NotificationsBundle\Entity\Recipients\EmailRecipient;
  16. use Products\NotificationsBundle\Entity\Recipients\PhoneRecipient;
  17. use Products\NotificationsBundle\Util\Preferences;
  18. use Products\NotificationsBundle\Util\Reachability;
  19. /**
  20.  * Class OneRosterPrepareSubscriber
  21.  * @package Products\NotificationsBundle\Subscriber\OneRoster
  22.  */
  23. final class OneRosterTidySubscriber extends AbstractNotificationsOneRosterSubscriber
  24. {
  25.     const TESTER__ALIAS_PREFIX 'app.notifications.testers.default';
  26.     const TESTER__LAST_NAME 'Δ SchoolNow';
  27.     const TESTER__EMAIL__PREFIX 'sn';
  28.     const TESTER__EMAIL__DOMAIN 'campussuite.com';
  29.     const TESTER__PHONES = [
  30.         OneRosterUser::TYPES__STAFF => '+15136205313',
  31.         OneRosterUser::TYPES__FAMILY => '+15136205313',
  32.         OneRosterUser::TYPES__STUDENT => '+15136205313',
  33.         OneRosterUser::TYPES__COMMUNITY => '+15136205313',
  34.     ];
  35.     /**
  36.      * {@inheritdoc}
  37.      */
  38.     public static function getSubscribedEvents(): array
  39.     {
  40.         return [
  41.             OneRosterEvents::EVENT__TIDY => [
  42.                 ['syncTesters'0],
  43.                 ['discardProfileContacts'0],
  44.                 ['cleanup'0],
  45.             ],
  46.         ];
  47.     }
  48.     /**
  49.      * @param AsyncEvent $event
  50.      */
  51.     public function syncTesters(AsyncEvent $event): void
  52.     {
  53.         // get the job
  54.         $job $this->loadJob($event);
  55.         // DEBUGGING
  56.         $event->getOutput()->writeln(sprintf(
  57.             'Sync #%s loaded',
  58.             $job->getIdentifier()
  59.         ));
  60.         // ensure we are meant to process this
  61.         if ( ! $this->checkTypes($job, [
  62.             OneRosterSync::STRATEGIES__NOTIFICATIONS__STAFF,
  63.             OneRosterSync::STRATEGIES__NOTIFICATIONS__FAMILY,
  64.             OneRosterSync::STRATEGIES__NOTIFICATIONS__STUDENTS,
  65.             OneRosterSync::STRATEGIES__NOTIFICATIONS__COMMUNITY,
  66.         ])) {
  67.             return;
  68.         }
  69.         // grab all schools
  70.         $schools $this->em->getRepository(School::class)->findAll();
  71.         // assemble data for testing users
  72.         $roles array_values(array_filter([
  73.             $this->checkTypes($job, [OneRosterSync::STRATEGIES__NOTIFICATIONS__STAFF]) ? AbstractOneRosterEntity::ENUMS__ROLE_TYPE__TEACHER null,
  74.             $this->checkTypes($job, [OneRosterSync::STRATEGIES__NOTIFICATIONS__FAMILY]) ? AbstractOneRosterEntity::ENUMS__ROLE_TYPE__PARENT null,
  75.             $this->checkTypes($job, [OneRosterSync::STRATEGIES__NOTIFICATIONS__STUDENTS]) ? AbstractOneRosterEntity::ENUMS__ROLE_TYPE__STUDENT null,
  76.             $this->checkTypes($job, [OneRosterSync::STRATEGIES__NOTIFICATIONS__COMMUNITY]) ? AbstractOneRosterEntity::ENUMS__ROLE_TYPE__COMMUNITY null,
  77.         ]));
  78.         // loop over each tester
  79.         foreach ($roles as $role) {
  80.             // determine the type
  81.             $type OneRosterUser::ROLES_MAPPING[$role];
  82.             // generate the full alias
  83.             $alias sprintf(
  84.                 '%s.%s',
  85.                 self::TESTER__ALIAS_PREFIX,
  86.                 $type
  87.             );
  88.             // try to find the main profile
  89.             $profile $this->em->getRepository(Profile::class)->findOneBy([
  90.                 'alias' => $alias,
  91.             ]);
  92.             // make one if not found
  93.             if ( ! $profile) {
  94.                 $profile = new Profile();
  95.             }
  96.             // set things on it
  97.             $profile
  98.                 ->setAlias($alias)
  99.                 ->markFlag(Profile::FLAGS__FIXED)
  100.                 ->setRole($role)
  101.                 ->setFirstName(sprintf(
  102.                     'Δ Test %s',
  103.                     strtoupper($role)
  104.                 ))
  105.                 ->setLastName(self::TESTER__LAST_NAME)
  106.                 ->setOneRosterId($job->getSync()->getDistrictId())
  107.                 ->setOneRosterArchived(false)
  108.                 ->setStanding(Reachability::STANDINGS__GOOD)
  109.                 ->setDiscardedAt(null)
  110.             ;
  111.             // basic metadata
  112.             $profile->setMetadata(array_merge(
  113.                 $profile->getMetadata(),
  114.                 [
  115.                     '_role' => $role,
  116.                     '_role_type' => OneRosterUser::TYPES_LOOKUP[
  117.                         OneRosterUser::ROLES_MAPPING[$role]
  118.                     ],
  119.                     '_district' => $job->getSync()->getDistrictId(),
  120.                     '_orgs' => array_map(
  121.                         static function (School $school) {
  122.                             return $school->getOneRosterOrg();
  123.                         },
  124.                         $schools
  125.                     ),
  126.                     '_schools' => array_values(array_filter(array_map(
  127.                         static function (School $school) {
  128.                             if ($school->isTypeDistrict()) {
  129.                                 return null;
  130.                             }
  131.                             return $school->getOneRosterOrg();
  132.                         },
  133.                         $schools
  134.                     ))),
  135.                     '_grades' => AbstractOneRosterEntity::CEDS__GRADES,
  136.                 ]
  137.             ));
  138.             // save it
  139.             $this->em->save($profile);
  140.             // generate email address
  141.             $emailAddress sprintf(
  142.                 '%s+%s+%s@%s',
  143.                 self::TESTER__EMAIL__PREFIX,
  144.                 $job->getTenant()->getSlug(),
  145.                 OneRosterUser::TYPES_LOOKUP[$type],
  146.                 self::TESTER__EMAIL__DOMAIN
  147.             );
  148.             // find or make the email contact for this profile
  149.             $this->em->save(
  150.                 $email = ($this->em->getRepository(EmailRecipient::class)->findOneBy([
  151.                     'contact' => $emailAddress,
  152.                 ]) ?: new EmailRecipient())
  153.                     ->setContact($emailAddress)
  154.                     ->setStanding(Reachability::STANDINGS__GOOD)
  155.             );
  156.             // attach email
  157.             $this->em->save(
  158.                 ($this->em->getRepository(ProfileContact::class)->findOneBy([
  159.                     'profile' => $profile,
  160.                     'recipient' => $email,
  161.                 ]) ?: new ProfileContact())
  162.                     ->setProfile($profile)
  163.                     ->setRecipient($email)
  164.                     ->setPrimaryPreferences(Preferences::PREFERENCES__EMAIL)
  165.                     ->setSecondaryPreferences(Preferences::PREFERENCES__EMAIL)
  166.                     ->setOneRosterId($profile->getOneRosterId())
  167.                     ->setOneRosterArchived($profile->isOneRosterArchived())
  168.                     ->setDiscardedAt(null)
  169.             );
  170.             // find the phone contact for this profile
  171.             $this->em->save(
  172.                 $phone = ($this->em->getRepository(PhoneRecipient::class)->findOneBy([
  173.                     'contact' => self::TESTER__PHONES[$type],
  174.                 ]) ?: new PhoneRecipient())
  175.                     ->setContact(self::TESTER__PHONES[$type])
  176.                     ->setMethod(PhoneRecipient::METHODS__HYBRID)
  177.                     ->setStanding(Reachability::STANDINGS__GOOD)
  178.             );
  179.             // attach phone
  180.             $this->em->save(
  181.                 ($this->em->getRepository(ProfileContact::class)->findOneBy([
  182.                     'profile' => $profile,
  183.                     'recipient' => $phone,
  184.                 ]) ?: new ProfileContact())
  185.                     ->setProfile($profile)
  186.                     ->setRecipient($phone)
  187.                     ->setPrimaryPreferences(Preferences::PREFERENCES__SMS Preferences::PREFERENCES__VOICE)
  188.                     ->setSecondaryPreferences(Preferences::PREFERENCES__SMS)
  189.                     ->setOneRosterId($profile->getOneRosterId())
  190.                     ->setOneRosterArchived($profile->isOneRosterArchived())
  191.                     ->setDiscardedAt(null)
  192.             );
  193.             // get push contacts
  194.             $pushes $this->em->getRepository(ProfileContact::class)->createQueryBuilder('contacts')
  195.                 ->leftJoin('contacts.recipient''recipients')
  196.                 ->andWhere('recipients INSTANCE OF ' AppRecipient::class)
  197.                 ->getQuery()
  198.                 ->getResult();
  199.             // delete any extra contacts for this test user
  200.             $this->em->createQueryBuilder()
  201.                 ->delete(ProfileContact::class, 'contacts')
  202.                 ->andWhere('contacts.tenant = :tenant')
  203.                 ->setParameter('tenant'$job->getTenant())
  204.                 ->andWhere('contacts.profile = :profile')
  205.                 ->setParameter('profile'$profile)
  206.                 ->andWhere('contacts.recipient NOT IN (:recipients)')
  207.                 ->setParameter('recipients', [$email$phone, ...$pushes])
  208.                 ->getQuery()
  209.                 ->execute();
  210.         }
  211.     }
  212.     /**
  213.      * @param AsyncEvent $event
  214.      * @return void
  215.      */
  216.     public function discardProfileContacts(AsyncEvent $event): void
  217.     {
  218.         // data should be an array with an id of a sync
  219.         $job $this->loadJob($event);
  220.         // run bulk query
  221.         // this should discard every profile that has an invalid/missing oneroster id
  222.         $discards $this->em->createQueryBuilder()
  223.             ->update(ProfileContact::class, 'contacts')
  224.             ->set('contacts.discarded'':state')
  225.             ->setParameter('state'true)
  226.             ->set('contacts.discardedAt'':timestamp')
  227.             ->setParameter('timestamp'DateTimeUtils::now())
  228.             ->andWhere('contacts.tenant = :tenant')// filter by tenant
  229.             ->setParameter('tenant'$job->getTenant()->getId())
  230.             ->andWhere('contacts.discarded != :state')// for performance, only update ones not already discarded
  231.             ->andWhere($this->em->getExpressionBuilder()->in(
  232.                 'contacts.profile',
  233.                 $this->em->createQueryBuilder()
  234.                     ->select('profiles.id')
  235.                     ->from(Profile::class, 'profiles')
  236.                     ->andWhere('profiles.tenant = :tenant')// we are making a string subquery, so the tenant var in the other qb will be used here eventually
  237.                     ->andWhere('profiles.discarded = :discarded')// filter things already discarded
  238.                     ->getDQL()
  239.             ))// only match objects that are missing from stashed objects
  240.             ->setParameter('discarded'true)// have to set the parameter here for the subquery
  241.             ->getQuery()
  242.             ->execute();
  243.         // DEBUGGING
  244.         $event->getOutput()->writeln(sprintf(
  245.             'Discarded %s profile contacts for sync #%s',
  246.             $discards,
  247.             $job->getIdentifier()
  248.         ));
  249.     }
  250.     /**
  251.      * @param AsyncEvent $event
  252.      */
  253.     public function cleanup(AsyncEvent $event): void
  254.     {
  255.         // get the job we are working with
  256.         $job $this->loadJob($event);
  257.         // DEBUGGING
  258.         $event->getOutput()->writeln(sprintf(
  259.             'Sync #%s loaded',
  260.             $job->getIdentifier()
  261.         ));
  262.         // ensure we are meant to process this
  263.         if ( ! $this->checkTypes($job, [
  264.             OneRosterSync::STRATEGIES__NOTIFICATIONS__STAFF,
  265.             OneRosterSync::STRATEGIES__NOTIFICATIONS__FAMILY,
  266.             OneRosterSync::STRATEGIES__NOTIFICATIONS__STUDENTS,
  267.             OneRosterSync::STRATEGIES__NOTIFICATIONS__COMMUNITY,
  268.         ])) {
  269.             return;
  270.         }
  271.         // remove school lists no longer needed
  272.         $removals $this->em->createQueryBuilder()
  273.             ->delete(SchoolList::class, 'lists')
  274.             ->andWhere('lists.tenant = :tenant')// filter by tenant
  275.             ->setParameter('tenant'$job->getTenant()->getId())
  276.             ->andWhere($this->em->getExpressionBuilder()->notIn(
  277.                 'lists.onerosterId',
  278.                 $this->em->createQueryBuilder()
  279.                     ->select('orgs.sourcedId')
  280.                     ->from(OneRosterOrg::class, 'orgs')
  281.                     ->andWhere('orgs.tenant = :tenant')// we are making a string subquery, so the tenant var in the other qb will be used here eventually
  282.                     ->getDQL()
  283.             ))// only match objects that are missing from stashed objects
  284.             ->getQuery()
  285.             ->execute();
  286.         // DEBUGGING
  287.         $event->getOutput()->writeln(sprintf(
  288.             'Removed %s org lists for sync #%s',
  289.             $removals,
  290.             $job->getIdentifier()
  291.         ));
  292.         // TODO: topics may be tied to existing dynamic lists made custom...
  293. //        // remove school topics no longer needed
  294. //        $removals = $this->em->createQueryBuilder()
  295. //            ->delete(Topic::class, 'topics')
  296. //            ->andWhere('topics.tenant = :tenant')// filter by tenant
  297. //            ->setParameter('tenant', $job->getTenant()->getId())
  298. //            ->andWhere($this->em->getExpressionBuilder()->notIn(
  299. //                'topics.onerosterId',
  300. //                $this->em->createQueryBuilder()
  301. //                    ->select('orgs.sourcedId')
  302. //                    ->from(OneRosterOrg::class, 'orgs')
  303. //                    ->andWhere('orgs.tenant = :tenant')// we are making a string subquery, so the tenant var in the other qb will be used here eventually
  304. //                    ->getDQL()
  305. //            ))// only match objects that are missing from stashed objects
  306. //            ->getQuery()
  307. //            ->execute();
  308. //
  309. //        // DEBUGGING
  310. //        $event->getOutput()->writeln(sprintf(
  311. //            'Removed %s org topics for sync #%s',
  312. //            $removals,
  313. //            $job->getIdentifier()
  314. //        ));
  315.     }
  316. }