diff --git a/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitPushNotificationValidationRulesFactory.php b/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitPushNotificationValidationRulesFactory.php new file mode 100644 index 00000000..b573097b --- /dev/null +++ b/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitPushNotificationValidationRulesFactory.php @@ -0,0 +1,31 @@ + 'required|string', + 'platform' => 'required|in:MOBILE,WEB', + 'channel' => 'required_if:platform,MOBILE|in:EVERYONE,SPEAKERS,ATTENDEES,MEMBERS,SUMMIT,EVENT,GROUP', + 'event_id' => 'required_if:channel,EVENT|integer', + 'group_id' => 'required_if:channel,GROUP|integer', + 'recipient_ids' => 'required_if:channel,MEMBERS|int_array', + ]; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitTicketTypeValidationRulesFactory.php b/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitTicketTypeValidationRulesFactory.php index 8397119b..a5427a9a 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitTicketTypeValidationRulesFactory.php +++ b/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitTicketTypeValidationRulesFactory.php @@ -38,5 +38,4 @@ final class SummitTicketTypeValidationRulesFactory ]; } - } \ No newline at end of file diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitNotificationsApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitNotificationsApiController.php index b16a08da..656d9d73 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitNotificationsApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitNotificationsApiController.php @@ -12,17 +12,18 @@ * limitations under the License. **/ use App\Http\Utils\PagingConstants; +use App\Services\Model\ISummitPushNotificationService; use models\exceptions\EntityNotFoundException; use models\exceptions\ValidationException; +use models\main\IMemberRepository; use models\oauth2\IResourceServerContext; use models\summit\ISummitNotificationRepository; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Input; use models\summit\ISummitRepository; -use models\summit\SummitPushNotificationChannel; +use ModelSerializers\SerializerRegistry; use utils\Filter; use utils\FilterParser; -use utils\FilterParserException; use utils\OrderParser; use utils\PagingInfo; use Illuminate\Support\Facades\Validator; @@ -39,21 +40,30 @@ class OAuth2SummitNotificationsApiController extends OAuth2ProtectedController private $summit_repository; /** - * OAuth2SummitNotificationsApiController constructor. - * @param ISummitRepository $summit_repository - * @param ISummitNotificationRepository $notification_repository - * @param IResourceServerContext $resource_server_context + * @var ISummitPushNotificationService */ + private $push_notification_service; + + /** + * @var IMemberRepository + */ + private $member_repository; + public function __construct ( ISummitRepository $summit_repository, ISummitNotificationRepository $notification_repository, + IMemberRepository $member_repository, + ISummitPushNotificationService $push_notification_service, IResourceServerContext $resource_server_context ) { parent::__construct($resource_server_context); - $this->repository = $notification_repository; - $this->summit_repository = $summit_repository; + + $this->repository = $notification_repository; + $this->push_notification_service = $push_notification_service; + $this->member_repository = $member_repository; + $this->summit_repository = $summit_repository; } /** @@ -148,4 +158,54 @@ class OAuth2SummitNotificationsApiController extends OAuth2ProtectedController return $this->error500($ex); } } + + /** + * @param $summit_id + * @return mixed + */ + public function addPushNotification($summit_id){ + try { + + if(!Request::isJson()) return $this->error400(); + $data = Input::json(); + + $summit = SummitFinderStrategyFactory::build($this->summit_repository, $this->resource_server_context)->find($summit_id); + if (is_null($summit)) return $this->error404(); + + $rules = SummitPushNotificationValidationRulesFactory::build($data->all()); + // Creates a Validator instance and validates the data. + $validation = Validator::make($data->all(), $rules); + + if ($validation->fails()) { + $messages = $validation->messages()->toArray(); + + return $this->error412 + ( + $messages + ); + } + + $current_member = null; + if(!is_null($this->resource_server_context->getCurrentUserExternalId())){ + $current_member = $this->member_repository->getById($this->resource_server_context->getCurrentUserExternalId()); + } + + $notification = $this->push_notification_service->addPushNotification($summit, $current_member, $data->all()); + + return $this->created(SerializerRegistry::getInstance()->getSerializer($notification)->serialize()); + } + catch (ValidationException $ex1) { + Log::warning($ex1); + return $this->error412([$ex1->getMessage()]); + } + catch(EntityNotFoundException $ex2) + { + Log::warning($ex2); + return $this->error404(['message'=> $ex2->getMessage()]); + } + catch (\Exception $ex) { + Log::error($ex); + return $this->error500($ex); + } + } } \ No newline at end of file diff --git a/app/Http/routes.php b/app/Http/routes.php index 8cd6f272..0b91ccdb 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -239,6 +239,7 @@ Route::group([ // notifications Route::group(['prefix' => 'notifications'], function () { Route::get('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitNotificationsApiController@getAll']); + Route::post('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitNotificationsApiController@addPushNotification']); }); // speakers diff --git a/app/ModelSerializers/PushNotificationMessageSerializer.php b/app/ModelSerializers/PushNotificationMessageSerializer.php index 28a0ccfc..d1194a48 100644 --- a/app/ModelSerializers/PushNotificationMessageSerializer.php +++ b/app/ModelSerializers/PushNotificationMessageSerializer.php @@ -21,7 +21,6 @@ use ModelSerializers\SilverStripeSerializer; class PushNotificationMessageSerializer extends SilverStripeSerializer { protected static $array_mappings = [ - 'Message' => 'message:json_string', 'Priority' => 'priority:json_string', 'Created' => 'created:datetime_epoch', diff --git a/app/Models/Foundation/Main/Member.php b/app/Models/Foundation/Main/Member.php index 69baab85..b231c28f 100644 --- a/app/Models/Foundation/Main/Member.php +++ b/app/Models/Foundation/Main/Member.php @@ -1,5 +1,4 @@ is_sent = false; + $this->is_sent = false; + $this->approved = false; + $this->priority = self::PriorityNormal; } /** @@ -92,7 +97,7 @@ class PushNotificationMessage extends SilverstripeBaseModel */ public function getOwnerId(){ try{ - return $this->owner->getId(); + return is_null($this->owner) ? 0 : $this->owner->getId(); } catch (\Exception $ex){ return 0; @@ -104,7 +109,7 @@ class PushNotificationMessage extends SilverstripeBaseModel */ public function getApprovedById(){ try{ - return $this->approved_by->getId(); + return is_null($this->approved_by) ? 0 : $this->approved_by->getId(); } catch (\Exception $ex){ return 0; diff --git a/app/Models/Foundation/Summit/Factories/SummitPushNotificationFactory.php b/app/Models/Foundation/Summit/Factories/SummitPushNotificationFactory.php new file mode 100644 index 00000000..6b178995 --- /dev/null +++ b/app/Models/Foundation/Summit/Factories/SummitPushNotificationFactory.php @@ -0,0 +1,61 @@ +setMessage(trim($data['message'])); + + if(isset($data['channel'])) + $notification->setChannel(trim($data['channel'])); + + if(isset($data['platform'])) + $notification->setPlatform(trim($data['platform'])); + + if(isset($params['event'])) + $notification->setSummitEvent($params['event']); + + if(isset($params['group'])) + $notification->setGroup($params['group']); + + if(isset($params['owner'])) + $notification->setOwner($params['owner']); + + if(isset($params['recipients'])) + { + foreach($params['recipients'] as $recipient) + $notification->addRecipient($recipient); + } + + $notification->setSummit($summit); + + return $notification; + } +} \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Summit.php b/app/Models/Foundation/Summit/Summit.php index a82ce2e0..2eb8c24e 100644 --- a/app/Models/Foundation/Summit/Summit.php +++ b/app/Models/Foundation/Summit/Summit.php @@ -544,6 +544,7 @@ class Summit extends SilverstripeBaseModel $this->excluded_categories_for_upload_slide_decks = new ArrayCollection; $this->rsvp_templates = new ArrayCollection; $this->track_tag_groups = new ArrayCollection; + $this->notifications = new ArrayCollection; } /** @@ -1002,6 +1003,12 @@ class Summit extends SilverstripeBaseModel */ private $entity_events; + /** + * @ORM\OneToMany(targetEntity="models\summit\SummitPushNotification", mappedBy="summit", cascade={"persist"}, orphanRemoval=true) + * @var SummitPushNotification[] + */ + private $notifications; + /** * @param SummitEvent $summit_event * @return bool @@ -1937,4 +1944,13 @@ SQL; $this->max_submission_allowed_per_user = $max_submission_allowed_per_user; } + /** + * @param SummitPushNotification $notification + * @return $this + */ + public function addNotification(SummitPushNotification $notification){ + $this->notifications->add($notification); + $notification->setSummit($this); + return $this; + } } \ No newline at end of file diff --git a/app/Models/Foundation/Summit/SummitPushNotification.php b/app/Models/Foundation/Summit/SummitPushNotification.php index d190f78b..5ca100f4 100644 --- a/app/Models/Foundation/Summit/SummitPushNotification.php +++ b/app/Models/Foundation/Summit/SummitPushNotification.php @@ -14,6 +14,7 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use models\main\Group; +use models\main\Member; use models\main\PushNotificationMessage; /** * Class SummitPushNotificationChannel @@ -140,20 +141,21 @@ class SummitPushNotification extends PushNotificationMessage return $this->recipients; } - /** - * @param ArrayCollection $recipients - */ - public function setRecipients($recipients) - { - $this->recipients = $recipients; - } - /** * SummitPushNotification constructor. */ public function __construct() { parent::__construct(); - $this->recipients = new ArrayCollection(); + $this->recipients = new ArrayCollection; + } + + /** + * @param Member $member + * @return $this + */ + public function addRecipient(Member $member){ + $this->recipients->add($member); + return $this; } } \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 2aaa0b9c..1d72553a 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -185,6 +185,19 @@ class AppServiceProvider extends ServiceProvider return true; }); + Validator::extend('int_array', function($attribute, $value, $parameters, $validator) + { + $validator->addReplacer('int_array', function($message, $attribute, $rule, $parameters) use ($validator) { + return sprintf("%s should be an array of int", $attribute); + }); + if(!is_array($value)) return false; + foreach($value as $element) + { + if(!is_int($element)) return false; + } + return true; + }); + Validator::extend('team_permission', function($attribute, $value, $parameters, $validator) { $validator->addReplacer('team_permission', function($message, $attribute, $rule, $parameters) use ($validator) { diff --git a/app/Security/SummitScopes.php b/app/Security/SummitScopes.php index fa13354a..ab40b52b 100644 --- a/app/Security/SummitScopes.php +++ b/app/Security/SummitScopes.php @@ -21,6 +21,7 @@ final class SummitScopes const ReadSummitData = '%s/summits/read'; const ReadAllSummitData = '%s/summits/read/all'; const ReadNotifications = '%s/summits/read-notifications'; + const WriteNotifications = '%s/summits/write-notifications'; const WriteSummitData = '%s/summits/write'; const WriteSpeakersData = '%s/speakers/write'; diff --git a/app/Services/Model/ISummitPushNotificationService.php b/app/Services/Model/ISummitPushNotificationService.php new file mode 100644 index 00000000..bfc971c4 --- /dev/null +++ b/app/Services/Model/ISummitPushNotificationService.php @@ -0,0 +1,34 @@ +group_repository = $group_repository; + $this->member_repository = $member_repository; + } + + /** + * @param Summit $summit + * @param Member|null $current_member + * @param array $data + * @return SummitPushNotification + * @throws ValidationException + * @throws EntityNotFoundException + */ + public function addPushNotification(Summit $summit, Member $current_member, array $data) + { + return $this->tx_service->transaction(function () use ($summit, $current_member, $data) { + $params = []; + if (!is_null($current_member)) + $params['owner'] = $current_member; + + if (isset($data['event_id'])) { + $event = $summit->getScheduleEvent(intval($data['event_id'])); + if (is_null($event)) { + throw new EntityNotFoundException + ( + trans + ( + 'not_found_errors.SummitPushNotificationService.addPushNotification.EventNotFound', + [ + 'summit_id' => $summit->getId(), + 'event_id' => $data['event_id'] + ] + ) + ); + } + $params['event'] = $event; + } + + if (isset($data['group_id'])) { + $group = $this->group_repository->getById(intval($data['group_id'])); + if (is_null($group)) { + throw new EntityNotFoundException + ( + trans + ( + 'not_found_errors.SummitPushNotificationService.addPushNotification.GroupNotFound', + [ + 'summit_id' => $summit->getId(), + 'group_id' => $data['group_id'] + ] + ) + ); + } + $params['group'] = $group; + } + + if (isset($data['recipient_ids'])) { + + $recipients = []; + foreach ($data['recipient_ids'] as $recipient_id) { + $recipient = $this->member_repository->getById(intval($recipient_id)); + + if (is_null($recipient)) { + throw new EntityNotFoundException + ( + 'not_found_errors.SummitPushNotificationService.addPushNotification.MemberNotFound', + [ + 'summit_id' => $summit->getId(), + 'member_id' => $recipient_id + ] + ); + } + + if(!$recipient->isActive()){ + throw new ValidationException + ( + trans + ( + 'validation_errors.SummitPushNotificationService.addPushNotification.MemberNotActive', + [ + 'summit_id' => $summit->getId(), + 'member_id' => $recipient_id + ] + ) + ); + } + $recipients[] = $recipient; + } + + $params['recipients'] = $recipients; + + } + + $notification = SummitPushNotificationFactory::build($summit, $data, $params); + + if($notification->getChannel() == SummitPushNotificationChannel::Members){ + // auto approve for members + $notification->setApproved(true); + if(!is_null($current_member)) + $notification->setApprovedBy($current_member); + } + + $summit->addNotification($notification); + + return $notification; + + }); + } +} \ No newline at end of file diff --git a/app/Services/ServicesProvider.php b/app/Services/ServicesProvider.php index 26be7fb5..56366205 100644 --- a/app/Services/ServicesProvider.php +++ b/app/Services/ServicesProvider.php @@ -22,6 +22,7 @@ use App\Services\Model\IMemberService; use App\Services\Model\IPresentationCategoryGroupService; use App\Services\Model\IRSVPTemplateService; use App\Services\Model\ISummitEventTypeService; +use App\Services\Model\ISummitPushNotificationService; use App\Services\Model\ISummitTicketTypeService; use App\Services\Model\ISummitTrackService; use App\Services\Model\PresentationCategoryGroupService; @@ -29,6 +30,7 @@ use App\Services\Model\SummitLocationService; use App\Services\Model\MemberService; use App\Services\Model\RSVPTemplateService; use App\Services\Model\SummitPromoCodeService; +use App\Services\Model\SummitPushNotificationService; use App\Services\Model\SummitTicketTypeService; use App\Services\Model\SummitTrackService; use App\Services\SummitEventTypeService; @@ -204,6 +206,11 @@ final class ServicesProvider extends ServiceProvider PresentationCategoryGroupService::class ); + App::singleton( + ISummitPushNotificationService::class, + SummitPushNotificationService::class + ); + App::singleton(IGeoCodingAPI::class, function(){ return new GoogleGeoCodingAPI ( diff --git a/database/seeds/ApiEndpointsSeeder.php b/database/seeds/ApiEndpointsSeeder.php index cfa0a8a1..68136dfc 100644 --- a/database/seeds/ApiEndpointsSeeder.php +++ b/database/seeds/ApiEndpointsSeeder.php @@ -1409,6 +1409,13 @@ class ApiEndpointsSeeder extends Seeder sprintf(SummitScopes::ReadAllSummitData, $current_realm), sprintf(SummitScopes::ReadNotifications, $current_realm) ], + 'name' => 'add-notifications', + 'route' => '/api/v1/summits/{id}/notifications', + 'http_method' => 'POST', + 'scopes' => [ + sprintf(SummitScopes::WriteSummitData, $current_realm), + sprintf(SummitScopes::WriteNotifications, $current_realm) + ], ], // promo codes [ diff --git a/resources/lang/en/not_found_errors.php b/resources/lang/en/not_found_errors.php index 53b12f47..3b5b842d 100644 --- a/resources/lang/en/not_found_errors.php +++ b/resources/lang/en/not_found_errors.php @@ -70,4 +70,8 @@ return [ 'PresentationCategoryGroupService.disassociateAllowedGroup2TrackGroup.GroupNotFound' => 'group :group_id does not exists.', 'SummitService.updateSummit.SummitNotFound' => 'summit :summit_id not found', 'SummitService.deleteSummit.SummitNotFound' => 'summit :summit_id not found', + // SummitPushNotificationService + 'SummitPushNotificationService.addPushNotification.EventNotFound' => 'event :event_id does not belongs to summit :summit_id schedule', + 'SummitPushNotificationService.addPushNotification.GroupNotFound' => 'group :group_id not found', + 'SummitPushNotificationService.addPushNotification.MemberNotFound' => 'member :member_id not found', ]; \ No newline at end of file diff --git a/resources/lang/en/validation_errors.php b/resources/lang/en/validation_errors.php index 5305646e..bf6d8b24 100644 --- a/resources/lang/en/validation_errors.php +++ b/resources/lang/en/validation_errors.php @@ -70,4 +70,6 @@ return [ 'SummitService.updateSummit.NameAlreadyExists'=> 'name :name its already being assigned to another summit', 'SummitService.updateSummit.SummitAlreadyActive' => 'summit :active_summit_id is already activated please deactivate it to set current summit as active', 'SummitTrackService.copyTracks.SameSummit' => 'from summit is equal a to summit.', + // SummitPushNotificationService + 'SummitPushNotificationService.addPushNotification.MemberNotActive' => 'member :member_id is not active', ]; \ No newline at end of file diff --git a/tests/OAuth2SummitNotificationsApiControllerTest.php b/tests/OAuth2SummitNotificationsApiControllerTest.php index d6f55a3c..00749775 100644 --- a/tests/OAuth2SummitNotificationsApiControllerTest.php +++ b/tests/OAuth2SummitNotificationsApiControllerTest.php @@ -59,4 +59,125 @@ final class OAuth2SummitNotificationsApiControllerTest extends ProtectedApiTest return $notifications; } + + /** + * @param int $summit_id + * @return mixed + */ + public function testAddPushNotificationEveryone($summit_id = 24){ + + $params = [ + 'id' => $summit_id, + ]; + + $message = str_random(16).'_message'; + + $data = [ + 'message' => $message, + 'channel' => \models\summit\SummitPushNotificationChannel::Everyone, + 'platform' => \models\summit\SummitPushNotification::PlatformMobile, + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "POST", + "OAuth2SummitNotificationsApiController@addPushNotification", + $params, + [], + [], + [], + $headers, + json_encode($data) + ); + + $content = $response->getContent(); + $this->assertResponseStatus(201); + $notification = json_decode($content); + $this->assertTrue(!is_null($notification)); + return $notification; + } + + /** + * @param int $summit_id + * @return mixed + */ + public function testAddPushNotificationMembersFail($summit_id = 24){ + + $params = [ + 'id' => $summit_id, + ]; + + $message = str_random(16).'_message'; + + $data = [ + 'message' => $message, + 'channel' => \models\summit\SummitPushNotificationChannel::Members, + 'platform' => \models\summit\SummitPushNotification::PlatformMobile, + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "POST", + "OAuth2SummitNotificationsApiController@addPushNotification", + $params, + [], + [], + [], + $headers, + json_encode($data) + ); + + $content = $response->getContent(); + $this->assertResponseStatus(412); + } + + /** + * @param int $summit_id + * @return mixed + */ + public function testAddPushNotificationMembers($summit_id = 24){ + + $params = [ + 'id' => $summit_id, + ]; + + $message = str_random(16).'_message'; + + $data = [ + 'message' => $message, + 'channel' => \models\summit\SummitPushNotificationChannel::Members, + 'platform' => \models\summit\SummitPushNotification::PlatformMobile, + 'recipient_ids' => [13867] + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "POST", + "OAuth2SummitNotificationsApiController@addPushNotification", + $params, + [], + [], + [], + $headers, + json_encode($data) + ); + + $content = $response->getContent(); + $this->assertResponseStatus(201); + $notification = json_decode($content); + $this->assertTrue(!is_null($notification)); + return $notification; + } } \ No newline at end of file