-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathCampaignFundingRepository.php
More file actions
176 lines (149 loc) · 6.77 KB
/
CampaignFundingRepository.php
File metadata and controls
176 lines (149 loc) · 6.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
<?php
declare(strict_types=1);
namespace MatchBot\Domain;
use Assert\Assertion;
use Doctrine\ORM\EntityRepository;
/**
* @extends EntityRepository<CampaignFunding>
*/
class CampaignFundingRepository extends EntityRepository
{
/**
* Get available-for-allocation `CampaignFunding`s, without a lock.
*
* Ordering is well-defined as far being champion funds first (currently given allocationOrder=100) then pledges
* (given allocationOrder=200). The more specific ordering is arbitrary, determined by the order funds were first
* read from the Salesforce implementation's API. This doesn't matter in effect because the allocations can't
* mirror the reality of what happens after a campaign if not all pledges are used, which varies per charity. In
* the case of pro-rata'ing the amount from each pledger, MatchBot's allocations cannot accurately reflect the
* amount due at the end. It would not be feasible to track these proportional amounts during the allocation phase
* because we would have to split amounts up constantly and it would break the decimal strings,
* no-floating-point-maths approach we've taken to ensure accuracy.
*
* @param Campaign $campaign
* @return CampaignFunding[] Sorted in the order funds should be allocated
*/
public function getAvailableFundings(Campaign $campaign, bool $forceNotBigGive = false): array
{
// Temporary option to allow for fixing wrong BG allocations.
// Salesforce Champion Funding a09WS00000BDhATYA1 has Fund.id 31740.
$extraCondition = $forceNotBigGive ? " AND cf.fund != 31740 " : '';
$query = $this->getEntityManager()->createQuery('
SELECT cf FROM MatchBot\Domain\CampaignFunding cf JOIN cf.fund fund
WHERE :campaign MEMBER OF cf.campaigns ' . $extraCondition . '
AND cf.amountAvailable > 0
ORDER BY fund.allocationOrder, cf.id
');
$query->setParameter('campaign', $campaign->getId());
/** @var CampaignFunding[] $result */
$result = $query->getResult();
return $result;
}
/**
* Returns all CampaignFunding associated with campaign, including those that have zero
* funds available at this time.
*
* Order is just by CF ID, whereas {@see self::getAvailableFundings()} gives an ordered-by-allocation-order list.
*
* @return CampaignFunding[]
*/
public function getAllFundingsForCampaign(Campaign $campaign): array
{
$query = $this->getEntityManager()->createQuery('
SELECT cf FROM MatchBot\Domain\CampaignFunding cf
WHERE :campaign MEMBER OF cf.campaigns
ORDER BY cf.id
');
$query->setParameter('campaign', $campaign->getId());
/** @var CampaignFunding[] $result */
$result = $query->getResult();
return $result;
}
/**
* Get the total amount available for a meta-campaign, ensuring that shared fundings
* are only counted once even if they're linked to multiple campaigns.
*
* @return array{totalAmount: Money, totalAmountAvailable: Money}
*/
public function getAmountsForMetaCampaign(MetaCampaign $metaCampaign): array
{
// First, get all unique CampaignFunding IDs for the meta-campaign – campaigns we expect to go ahead only
$idQuery = $this->getEntityManager()->createQuery(dql: <<<'DQL'
SELECT DISTINCT cf.id FROM MatchBot\Domain\CampaignFunding cf
LEFT JOIN cf.campaigns campaign
WHERE campaign.metaCampaignSlug = :slug
AND campaign.relatedApplicationStatus = :appApproved
AND campaign.relatedApplicationCharityResponseToOffer = :appOfferAccepted
DQL
);
$idQuery->setParameter('slug', $metaCampaign->getSlug()->slug);
$idQuery->setParameter('appApproved', ApplicationStatus::Approved->value);
$idQuery->setParameter('appOfferAccepted', CharityResponseToOffer::Accepted->value);
/** @var list<array{id: int}> $ids */
$ids = $idQuery->getResult();
if (empty($ids)) {
return self::metaCampaignZeroes(Currency::GBP);
}
// Extract just the IDs into a flat array
$fundingIds = array_map(fn($row) => $row['id'], $ids);
// Then, sum the amounts for these unique IDs
$sumQuery = $this->getEntityManager()->createQuery(dql: <<<'DQL'
SELECT
COALESCE(SUM(cf.amount), 0) as totalAmount,
COALESCE(SUM(cf.amountAvailable), 0) as totalAmountAvailable,
cf.currencyCode
FROM MatchBot\Domain\CampaignFunding cf
WHERE cf.id IN (:ids)
GROUP BY cf.currencyCode
DQL
);
$sumQuery->setParameter('ids', $fundingIds);
/** @var list<array{totalAmount: numeric-string, totalAmountAvailable: numeric-string, currencyCode: string}> $result */
$result = $sumQuery->getResult();
Assertion::maxCount($result, 1, 'Campaign Fundings in multiple currencies found for same metacampaign');
if ($result === []) {
return self::metaCampaignZeroes(Currency::GBP);
}
$currency = Currency::fromIsoCode($result[0]['currencyCode']);
return [
'totalAmount' => Money::fromNumericString($result[0]['totalAmount'], $currency),
'totalAmountAvailable' => Money::fromNumericString($result[0]['totalAmountAvailable'], $currency),
];
}
public function getFunding(Fund $fund): ?CampaignFunding
{
$query = $this->getEntityManager()->createQuery('
SELECT cf FROM MatchBot\Domain\CampaignFunding cf
WHERE cf.fund = :fund
')->setMaxResults(1);
$query->setParameter('fund', $fund->getId());
$query->execute();
/** @var ?CampaignFunding $result */
$result = $query->getOneOrNullResult();
return $result;
}
public function getFundingForCampaign(Campaign $campaign, Fund $fund): ?CampaignFunding
{
$query = $this->getEntityManager()->createQuery('
SELECT cf FROM MatchBot\Domain\CampaignFunding cf
WHERE :campaign MEMBER OF cf.campaigns
AND cf.fund = :fund
')->setMaxResults(1);
$query->setParameter('campaign', $campaign->getId());
$query->setParameter('fund', $fund->getId());
$query->execute();
/** @var ?CampaignFunding $result */
$result = $query->getOneOrNullResult();
return $result;
}
/**
* @return array{totalAmount: Money, totalAmountAvailable: Money}
*/
private static function metaCampaignZeroes(Currency $currency): array
{
return [
'totalAmount' => Money::zero($currency),
'totalAmountAvailable' => Money::zero($currency),
];
}
}