Skip to content

Commit 7e11d50

Browse files
committed
feat: per-sponsor member permission tracking with JSON column on Sponsor_User
Signed-off-by: romanetar <roman_ag@hotmail.com>
1 parent 31b61e7 commit 7e11d50

14 files changed

Lines changed: 845 additions & 63 deletions

app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitBadgeScanApiController.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ function getAddValidationRules(array $payload): array
9595
return [
9696
'qr_code' => 'required_without:attendee_email|string',
9797
'attendee_email' => 'required_without:qr_code|email',
98+
'qr_code' => 'required|string',
99+
'sponsor_id' => 'sometimes|integer',
98100
'scan_date' => 'required|date_format:U|epoch_seconds',
99101
'notes' => 'sometimes|string|max:1024',
100102
'extra_questions' => 'sometimes|extra_question_dto_array',
@@ -115,6 +117,9 @@ function getCheckInValidationRules(): array
115117
* @param Summit $summit
116118
* @param array $payload
117119
* @return IEntity
120+
* @throws EntityNotFoundException
121+
* @throws HTTP403ForbiddenException
122+
* @throws ValidationException
118123
*/
119124
protected function addChild(Summit $summit, array $payload): IEntity
120125
{
@@ -381,7 +386,7 @@ function($filter) use($summit, $current_member){
381386
if (!is_null($current_member)){
382387
if ($current_member->isAuthzFor($summit)) return $filter;
383388
// add filter for sponsor user
384-
if ($current_member->isSponsorUser()) {
389+
if ($current_member->isSponsorUser() || $current_member->isExternalSponsorUser()) {
385390
$sponsor_ids = $current_member->getSponsorMembershipIds($summit);
386391
// is allowed sponsors are empty, add dummy value
387392
if (!count($sponsor_ids)) $sponsor_ids[] = 0;

app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ protected function applyExtraFilters(Filter $filter):Filter {
187187
// check AUTHZ for sponsors
188188
if($current_member->isAuthzFor($summit)) return $filter;
189189
// add filter for sponsor user
190-
if ($current_member->isSponsorUser()) {
190+
if ($current_member->isSponsorUser() || $current_member->isExternalSponsorUser()) {
191191
$sponsor_ids = $current_member->getSponsorMembershipIds($summit);
192192
// is allowed sponsors are empty, add dummy value
193193
if (!count($sponsor_ids)) $sponsor_ids[] = 0;

app/Jobs/SponsorServices/UpdateSponsorMemberGroupsMQJob.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,13 @@ public function handle(SponsorServicesMQJob $job): void
6262
$data = $payload['data'];
6363
$user_external_id = intval($data['user_external_id']);
6464
$group_slug = $data['group_slug'];
65+
$sponsor_id = intval($data['sponsor_id']);
66+
$summit_id = intval($data['summit_id']);
6567

6668
if ($event_type === EventTypes::AUTH_USER_ADDED_TO_GROUP) {
67-
$this->service->addSponsorUserToGroup($user_external_id, $group_slug);
69+
$this->service->addSponsorUserToGroup($user_external_id, $group_slug, $sponsor_id, $summit_id);
6870
} else if ($event_type === EventTypes::AUTH_USER_REMOVED_FROM_GROUP) {
69-
$this->service->removeSponsorUserFromGroup($user_external_id, $group_slug);
71+
$this->service->removeSponsorUserFromGroup($user_external_id, $group_slug, $sponsor_id, $summit_id);
7072
}
7173
$job->delete();
7274
} catch (\Exception $ex) {

app/Models/Foundation/Main/Member.php

Lines changed: 131 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use App\Models\Foundation\Summit\Events\RSVP\RSVPInvitation;
2222
use Doctrine\ORM\Query\ResultSetMappingBuilder;
2323
use Illuminate\Support\Facades\Config;
24+
use Doctrine\DBAL\ParameterType;
2425
use LaravelDoctrine\ORM\Facades\EntityManager;
2526
use models\summit\Presentation;
2627
use models\summit\SummitMetric;
@@ -1832,21 +1833,44 @@ public function getLastNSponsorMemberships($last_n = 2)
18321833
*/
18331834
public function getActiveSummitsSponsorMemberships()
18341835
{
1835-
$dql = <<<DQL
1836-
SELECT sp
1837-
FROM models\summit\Sponsor sp
1838-
JOIN sp.members m
1839-
JOIN sp.summit s
1840-
WHERE m.id = :member_id
1841-
AND s.end_date >= :now
1842-
ORDER BY s.begin_date ASC
1843-
DQL;
1844-
1845-
$query = $this->createQuery($dql);
1846-
return $query
1847-
->setParameter('member_id', $this->getId())
1848-
->setParameter('now', new \DateTime('now', new \DateTimeZone('UTC')))
1849-
->getResult();
1836+
// Step 1 — use native SQL (needed for JSON_CONTAINS) to collect IDs only.
1837+
$idSql = <<<SQL
1838+
SELECT sp.ID
1839+
FROM Sponsor sp
1840+
INNER JOIN Sponsor_Users su ON su.SponsorID = sp.ID
1841+
INNER JOIN Summit s ON s.ID = sp.SummitID
1842+
WHERE su.MemberID = :member_id
1843+
AND s.SummitEndDate >= :now
1844+
AND (
1845+
JSON_CONTAINS(COALESCE(su.Permissions, '[]'), JSON_QUOTE(:slug_sponsors))
1846+
OR JSON_CONTAINS(COALESCE(su.Permissions, '[]'), JSON_QUOTE(:slug_external))
1847+
)
1848+
ORDER BY s.SummitBeginDate ASC
1849+
SQL;
1850+
1851+
$stmt = $this->prepareRawSQL($idSql, [
1852+
'member_id' => $this->getId(),
1853+
'now' => (new \DateTime('now', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s'),
1854+
'slug_sponsors' => IGroup::Sponsors,
1855+
'slug_external' => IGroup::SponsorExternalUsers,
1856+
]);
1857+
$ids = $stmt->executeQuery()->fetchFirstColumn();
1858+
1859+
if (empty($ids)) {
1860+
return [];
1861+
}
1862+
1863+
// Step 2 — load each Sponsor by PK. find() uses a different code path that avoids
1864+
// the ORM 3 assertion failure triggered by the OneToOne inverse associations on Sponsor
1865+
// (lead_report_setting, sponsorservices_statistics) when using DQL/native query hydration.
1866+
$sponsors = [];
1867+
foreach ($ids as $id) {
1868+
$sponsor = $this->getEM()->find(\models\summit\Sponsor::class, $id);
1869+
if ($sponsor !== null) {
1870+
$sponsors[] = $sponsor;
1871+
}
1872+
}
1873+
return $sponsors;
18501874
}
18511875

18521876
/**
@@ -1859,11 +1883,17 @@ public function getSponsorMembershipIds(Summit $summit): array
18591883
FROM Sponsor_Users
18601884
INNER JOIN Sponsor ON Sponsor.ID = Sponsor_Users.SponsorID
18611885
WHERE MemberID = :member_id AND Sponsor.SummitID = :summit_id
1886+
AND (
1887+
JSON_CONTAINS(COALESCE(Sponsor_Users.Permissions, '[]'), JSON_QUOTE(:slug_sponsors))
1888+
OR JSON_CONTAINS(COALESCE(Sponsor_Users.Permissions, '[]'), JSON_QUOTE(:slug_external))
1889+
)
18621890
SQL;
18631891

1864-
$stmt = $this->prepareRawSQL($sql, [
1865-
'member_id' => $this->getId(),
1866-
'summit_id' => $summit->getId(),
1892+
$stmt = $this->prepareRawSQL($sql, [
1893+
'member_id' => $this->getId(),
1894+
'summit_id' => $summit->getId(),
1895+
'slug_sponsors' => IGroup::Sponsors,
1896+
'slug_external' => IGroup::SponsorExternalUsers,
18671897
]);
18681898
$res = $stmt->executeQuery();
18691899
return $res->fetchFirstColumn();
@@ -1881,11 +1911,17 @@ public function hasSponsorMembershipsFor(Summit $summit, Sponsor $sponsor = null
18811911
WHERE
18821912
MemberID = :member_id
18831913
AND Sponsor.SummitID = :summit_id
1914+
AND (
1915+
JSON_CONTAINS(COALESCE(Sponsor_Users.Permissions, '[]'), JSON_QUOTE(:slug_sponsors))
1916+
OR JSON_CONTAINS(COALESCE(Sponsor_Users.Permissions, '[]'), JSON_QUOTE(:slug_external))
1917+
)
18841918
SQL;
18851919

1886-
$params = [
1887-
'member_id' => $this->getId(),
1888-
'summit_id' => $summit->getId(),
1920+
$params = [
1921+
'member_id' => $this->getId(),
1922+
'summit_id' => $summit->getId(),
1923+
'slug_sponsors' => IGroup::Sponsors,
1924+
'slug_external' => IGroup::SponsorExternalUsers,
18891925
];
18901926

18911927
if(!is_null($sponsor)) {
@@ -1956,12 +1992,26 @@ public function addSummitRegistrationOrder(SummitOrder $summit_order)
19561992

19571993
/**
19581994
* @param Summit $summit
1995+
* @return ArrayCollection
1996+
*/
1997+
public function getSponsorsBySummit(Summit $summit): ArrayCollection
1998+
{
1999+
return new ArrayCollection(
2000+
$this->sponsor_memberships->filter(function ($entity) use ($summit) {
2001+
return $entity->getSummitId() == $summit->getId();
2002+
})->toArray()
2003+
);
2004+
}
2005+
2006+
/**
2007+
* @param Summit $summit
2008+
* @param int $sponsor_id
19592009
* @return Sponsor|null
19602010
*/
1961-
public function getSponsorBySummit(Summit $summit): ?Sponsor
2011+
public function getSponsorBySummitAndId(Summit $summit, int $sponsor_id): ?Sponsor
19622012
{
1963-
$sponsor = $this->sponsor_memberships->filter(function ($entity) use ($summit) {
1964-
return $entity->getSummitId() == $summit->getId();
2013+
$sponsor = $this->sponsor_memberships->filter(function ($entity) use ($summit, $sponsor_id) {
2014+
return $entity->getSummitId() == $summit->getId() && $entity->getId() == $sponsor_id;
19652015
})->first();
19662016

19672017
return $sponsor === false ? null : $sponsor;
@@ -3412,6 +3462,63 @@ public function getIndividualMemberJoinDate(): ?\DateTime
34123462
return $this->individual_member_join_date;
34133463
}
34143464

3465+
/**
3466+
* Appends $group_slug to the Permissions JSON array on the Sponsor_Users row
3467+
* for this member and the given sponsor. Idempotent: the slug is only added
3468+
* when it is not already present.
3469+
*/
3470+
public function addSponsorPermission(int $sponsor_id, string $group_slug): void
3471+
{
3472+
$sql = <<<SQL
3473+
UPDATE Sponsor_Users
3474+
SET Permissions = IF(
3475+
JSON_CONTAINS(COALESCE(Permissions, '[]'), JSON_QUOTE(:group_slug)),
3476+
Permissions,
3477+
JSON_ARRAY_APPEND(COALESCE(Permissions, '[]'), '$', :group_slug)
3478+
)
3479+
WHERE SponsorID = :sponsor_id AND MemberID = :member_id
3480+
SQL;
3481+
$stmt = $this->prepareRawSQL($sql, [
3482+
'group_slug' => $group_slug,
3483+
'sponsor_id' => $sponsor_id,
3484+
'member_id' => $this->getId(),
3485+
]);
3486+
$stmt->executeStatement();
3487+
}
3488+
3489+
/**
3490+
* Removes $group_slug from the Permissions JSON array on the Sponsor_Users row
3491+
* for this member and the given sponsor, then returns how many other Sponsor_Users
3492+
* rows for this member still carry that slug. The caller uses the count to decide
3493+
* whether to also revoke the global group membership.
3494+
*/
3495+
public function removeSponsorPermission(int $sponsor_id, string $group_slug): int
3496+
{
3497+
$removeSQL = <<<SQL
3498+
UPDATE Sponsor_Users
3499+
SET Permissions = JSON_REMOVE(Permissions, JSON_UNQUOTE(JSON_SEARCH(Permissions, 'one', :group_slug)))
3500+
WHERE SponsorID = :sponsor_id AND MemberID = :member_id
3501+
AND JSON_CONTAINS(COALESCE(Permissions, '[]'), JSON_QUOTE(:group_slug))
3502+
SQL;
3503+
$stmt = $this->prepareRawSQL($removeSQL, [
3504+
'group_slug' => $group_slug,
3505+
'sponsor_id' => $sponsor_id,
3506+
'member_id' => $this->getId(),
3507+
]);
3508+
$stmt->executeStatement();
3509+
3510+
$countSQL = <<<SQL
3511+
SELECT COUNT(*) FROM Sponsor_Users
3512+
WHERE MemberID = :member_id
3513+
AND JSON_CONTAINS(COALESCE(Permissions, '[]'), JSON_QUOTE(:group_slug))
3514+
SQL;
3515+
$stmt = $this->prepareRawSQL($countSQL, [
3516+
'member_id' => $this->getId(),
3517+
'group_slug' => $group_slug,
3518+
]);
3519+
return intval($stmt->executeQuery()->fetchOne());
3520+
}
3521+
34153522
public function addSponsorMembership(Sponsor $sponsor):void
34163523
{
34173524
if($this->sponsor_memberships->contains($sponsor)) return;

app/Models/Foundation/Main/Strategies/SponsorMemberSummitStrategy.php

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* See the License for the specific language governing permissions and
1212
* limitations under the License.
1313
**/
14+
use App\Models\Foundation\Main\IGroup;
1415
use LaravelDoctrine\ORM\Facades\Registry;
1516
use Libs\Utils\Doctrine\DoctrineStatementValueBinder;
1617
use models\summit\Summit;
@@ -41,13 +42,19 @@ public function getAllAllowedSummitIds(): array
4142
$sql = <<<SQL
4243
SELECT DISTINCT(Sponsor.SummitID)
4344
FROM Sponsor_Users INNER JOIN Sponsor ON Sponsor_Users.SponsorID = Sponsor.ID
44-
WHERE Sponsor_Users.MemberID = :member_id;
45+
WHERE Sponsor_Users.MemberID = :member_id
46+
AND (
47+
JSON_CONTAINS(COALESCE(Sponsor_Users.Permissions, '[]'), JSON_QUOTE(:slug_sponsors))
48+
OR JSON_CONTAINS(COALESCE(Sponsor_Users.Permissions, '[]'), JSON_QUOTE(:slug_external))
49+
);
4550
SQL;
4651

4752
$stmt = DoctrineStatementValueBinder::bind(
4853
$em->getConnection()->prepare($sql),
4954
[
50-
'member_id' => $this->member_id,
55+
'member_id' => $this->member_id,
56+
'slug_sponsors' => IGroup::Sponsors,
57+
'slug_external' => IGroup::SponsorExternalUsers,
5158
]
5259
);
5360
$res = $stmt->executeQuery();
@@ -67,14 +74,20 @@ public function isSummitAllowed(Summit $summit): bool
6774
SELECT COUNT(Sponsor.SummitID)
6875
FROM Sponsor_Users INNER JOIN Sponsor ON Sponsor_Users.SponsorID = Sponsor.ID
6976
WHERE Sponsor_Users.MemberID = :member_id
70-
AND Sponsor.SummitID = :summit_id
77+
AND Sponsor.SummitID = :summit_id
78+
AND (
79+
JSON_CONTAINS(COALESCE(Sponsor_Users.Permissions, '[]'), JSON_QUOTE(:slug_sponsors))
80+
OR JSON_CONTAINS(COALESCE(Sponsor_Users.Permissions, '[]'), JSON_QUOTE(:slug_external))
81+
)
7182
SQL;
7283

7384
$stmt = DoctrineStatementValueBinder::bind(
7485
$em->getConnection()->prepare($sql),
7586
[
76-
'member_id' => $this->member_id,
77-
'summit_id' => $summit->getId(),
87+
'member_id' => $this->member_id,
88+
'summit_id' => $summit->getId(),
89+
'slug_sponsors' => IGroup::Sponsors,
90+
'slug_external' => IGroup::SponsorExternalUsers,
7891
]
7992
);
8093
$res = $stmt->executeQuery();

app/Models/Foundation/Summit/Sponsor.php

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -392,16 +392,6 @@ public function addUser(Member $user)
392392
);
393393
}
394394

395-
if($user->hasSponsorMembershipsFor($this->getSummit()))
396-
throw new ValidationException
397-
(
398-
sprintf
399-
(
400-
"Member %s already belongs to an sponsor for summit %s",
401-
$user->getId(),
402-
$this->getSummit()->getId()
403-
)
404-
);
405395
// see https://www.doctrine-project.org/projects/doctrine-orm/en/3.5/reference/working-with-associations.html#synchronizing-bidirectional-collections
406396
$this->members->add($user);
407397
$user->addSponsorMembership($this);

app/Services/Model/ISponsorUserSyncService.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,20 +47,24 @@ public function removeSponsorUser(int $summit_id, int $user_id, ?int $sponsor_id
4747
/**
4848
* @param int $user_id
4949
* @param string $group_slug
50+
* @param int $sponsor_id
51+
* @param int $summit_id
5052
* @return void
5153
* @throws EntityNotFoundException
5254
* @throws ValidationException
5355
* @throws Exception
5456
*/
55-
public function addSponsorUserToGroup(int $user_id, string $group_slug): void;
57+
public function addSponsorUserToGroup(int $user_id, string $group_slug, int $sponsor_id, int $summit_id): void;
5658

5759
/**
5860
* @param int $user_id
5961
* @param string $group_slug
62+
* @param int $sponsor_id
63+
* @param int $summit_id
6064
* @return void
6165
* @throws EntityNotFoundException
6266
* @throws ValidationException
6367
* @throws Exception
6468
*/
65-
public function removeSponsorUserFromGroup(int $user_id, string $group_slug): void;
69+
public function removeSponsorUserFromGroup(int $user_id, string $group_slug, int $sponsor_id, int $summit_id): void;
6670
}

app/Services/Model/Imp/SponsorUserInfoGrantService.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ public function addBadgeScan(Summit $summit, Member $current_member, array $data
167167

168168
/*
169169
if(!($scan_date >= $begin_date && $scan_date <= $end_date))
170-
throw new ValidationException("scan_date is does not belong to summit period.");
170+
throw new ValidationException("scan_date does not belong to summit period.");
171171
*/
172172
if(empty($ticket_number)){
173173
throw new ValidationException("Ticket not found.");
@@ -178,10 +178,20 @@ public function addBadgeScan(Summit $summit, Member $current_member, array $data
178178
if(is_null($badge))
179179
throw new EntityNotFoundException("badge not found.");
180180

181-
$sponsor = $current_member->getSponsorBySummit($summit);
181+
$member_sponsors = $current_member->getSponsorsBySummit($summit);
182182

183-
if(is_null($sponsor))
184-
throw new ValidationException("Current member does not belongs to any summit sponsor.");
183+
if($member_sponsors->isEmpty())
184+
throw new ValidationException("Current member does not belong to any sponsor of this summit.");
185+
186+
if($member_sponsors->count() === 1) {
187+
$sponsor = $member_sponsors->first();
188+
} else {
189+
if(empty($data['sponsor_id']))
190+
throw new ValidationException("sponsor_id is required when the member belongs to multiple sponsors.");
191+
$sponsor = $current_member->getSponsorBySummitAndId($summit, intval($data['sponsor_id']));
192+
if(is_null($sponsor))
193+
throw new ValidationException("Current member does not belong to the selected summit sponsor.");
194+
}
185195

186196
$scan = new SponsorBadgeScan();
187197
$scan->setScanDate($scan_date);

0 commit comments

Comments
 (0)