Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
72.89% |
2767 / 3796 |
|
57.45% |
54 / 94 |
CRAP | |
0.00% |
0 / 1 |
ProjectService | |
72.89% |
2767 / 3796 |
|
57.45% |
54 / 94 |
12962.95 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
105 / 105 |
|
100.00% |
1 / 1 |
1 | |||
userCanAccessProject | |
79.03% |
49 / 62 |
|
0.00% |
0 / 1 |
13.33 | |||
getUserMaxAccessLevel | |
78.12% |
50 / 64 |
|
0.00% |
0 / 1 |
16.05 | |||
getGuestAccessLevel | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getShareAccessLevel | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
2 | |||
createProject | |
100.00% |
65 / 65 |
|
100.00% |
1 / 1 |
11 | |||
deleteProject | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
4 | |||
getProjectInfo | |
100.00% |
64 / 64 |
|
100.00% |
1 / 1 |
5 | |||
getSmallStats | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
3 | |||
getProjectStatistics | |
97.66% |
250 / 256 |
|
0.00% |
0 / 1 |
87 | |||
addBill | |
98.68% |
75 / 76 |
|
0.00% |
0 / 1 |
31 | |||
deleteBill | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
4 | |||
getMemberById | |
100.00% |
39 / 39 |
|
100.00% |
1 / 1 |
3 | |||
getProjectById | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
2 | |||
getBill | |
100.00% |
57 / 57 |
|
100.00% |
1 / 1 |
3 | |||
deleteBillOwersOfBill | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
autoSettlement | |
88.00% |
22 / 25 |
|
0.00% |
0 / 1 |
6.06 | |||
getProjectSettlement | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
centeredSettle | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
5 | |||
settle | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
orderBalance | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
reduceBalance | |
80.77% |
21 / 26 |
|
0.00% |
0 / 1 |
7.35 | |||
sortCreditersDebiters | |
84.62% |
11 / 13 |
|
0.00% |
0 / 1 |
8.23 | |||
editMember | |
100.00% |
54 / 54 |
|
100.00% |
1 / 1 |
24 | |||
editProject | |
100.00% |
41 / 41 |
|
100.00% |
1 / 1 |
22 | |||
addMember | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
14 | |||
getNbBills | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
5 | |||
getBillsWithLimit | |
55.96% |
61 / 109 |
|
0.00% |
0 / 1 |
113.07 | |||
getBillFromRow | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
1 | |||
getBills | |
69.91% |
79 / 113 |
|
0.00% |
0 / 1 |
26.83 | |||
getAllBillIds | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
2 | |||
getMembers | |
100.00% |
47 / 47 |
|
100.00% |
1 / 1 |
6 | |||
getBalance | |
92.86% |
26 / 28 |
|
0.00% |
0 / 1 |
7.02 | |||
isUserInCircle | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
72 | |||
getProjectNames | |
68.09% |
64 / 94 |
|
0.00% |
0 / 1 |
26.39 | |||
getProjects | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getCategoriesOrPaymentModes | |
100.00% |
113 / 113 |
|
100.00% |
1 / 1 |
19 | |||
getCurrencies | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
2 | |||
getUserShares | |
89.74% |
35 / 39 |
|
0.00% |
0 / 1 |
5.03 | |||
getPublicShares | |
90.62% |
29 / 32 |
|
0.00% |
0 / 1 |
3.01 | |||
getProjectInfoFromShareToken | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
2 | |||
getGroupShares | |
88.89% |
32 / 36 |
|
0.00% |
0 / 1 |
5.03 | |||
getCircleShares | |
11.76% |
4 / 34 |
|
0.00% |
0 / 1 |
22.17 | |||
deleteMember | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
4 | |||
getBillsOfMember | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
2 | |||
getMemberByName | |
100.00% |
39 / 39 |
|
100.00% |
1 / 1 |
3 | |||
getMemberByUserid | |
100.00% |
40 / 40 |
|
100.00% |
1 / 1 |
4 | |||
editBill | |
98.85% |
86 / 87 |
|
0.00% |
0 / 1 |
35 | |||
cronRepeatBills | |
98.41% |
62 / 63 |
|
0.00% |
0 / 1 |
11 | |||
getProjectTimeZone | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
4.18 | |||
copyBillPaymentModeOver | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
3 | |||
copyBillCategoryOver | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
3 | |||
moveBill | |
97.22% |
35 / 36 |
|
0.00% |
0 / 1 |
4 | |||
repeatBill | |
100.00% |
43 / 43 |
|
100.00% |
1 / 1 |
9 | |||
getNextRepetitionDate | |
83.72% |
36 / 43 |
|
0.00% |
0 / 1 |
15.97 | |||
addPaymentMode | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
4 | |||
getPaymentMode | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
2 | |||
deletePaymentMode | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
2 | |||
savePaymentModeOrder | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
editPaymentMode | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
6 | |||
addCategory | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
4 | |||
getCategory | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
2 | |||
deleteCategory | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
2 | |||
saveCategoryOrder | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
editCategory | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
6 | |||
addCurrency | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
getCurrency | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
2 | |||
deleteCurrency | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
2 | |||
editCurrency | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
4 | |||
addUserShare | |
100.00% |
68 / 68 |
|
100.00% |
1 / 1 |
8 | |||
addPublicShare | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
1 | |||
editShareAccessLevel | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
3 | |||
editShareAccess | |
94.44% |
34 / 36 |
|
0.00% |
0 / 1 |
9.01 | |||
editGuestAccessLevel | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
deleteUserShare | |
98.36% |
60 / 61 |
|
0.00% |
0 / 1 |
4 | |||
deletePublicShare | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
3 | |||
addGroupShare | |
100.00% |
43 / 43 |
|
100.00% |
1 / 1 |
4 | |||
deleteGroupShare | |
100.00% |
40 / 40 |
|
100.00% |
1 / 1 |
3 | |||
addCircleShare | |
0.00% |
0 / 58 |
|
0.00% |
0 / 1 |
72 | |||
deleteCircleShare | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
12 | |||
exportCsvSettlement | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
30 | |||
createAndCheckExportDirectory | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
exportCsvStatistics | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
20 | |||
exportCsvProject | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
42 | |||
getJsonProject | |
0.00% |
0 / 73 |
|
0.00% |
0 / 1 |
110 | |||
importCsvProjectAtomicWrapper | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
importCsvProject | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
importCsvProjectStream | |
0.00% |
0 / 237 |
|
0.00% |
0 / 1 |
6806 | |||
importSWProject | |
0.00% |
0 / 137 |
|
0.00% |
0 / 1 |
1980 | |||
cronAutoExport | |
0.00% |
0 / 67 |
|
0.00% |
0 / 1 |
90 | |||
searchBills | |
100.00% |
45 / 45 |
|
100.00% |
1 / 1 |
2 | |||
applyBillSearchTermCondition | |
52.17% |
12 / 23 |
|
0.00% |
0 / 1 |
2.44 | |||
getBillActivity | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
30 | |||
updateProjectLastChanged | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | /** |
4 | * Nextcloud - cospend |
5 | * |
6 | * This file is licensed under the Affero General Public License version 3 or |
7 | * later. See the COPYING file. |
8 | * |
9 | * @author Julien Veyssier |
10 | * @copyright Julien Veyssier 2019 |
11 | */ |
12 | |
13 | namespace OCA\Cospend\Service; |
14 | |
15 | use DateTimeZone; |
16 | use Exception; |
17 | use Generator; |
18 | use OCP\Files\FileInfo; |
19 | use OCP\IL10N; |
20 | use OCP\IConfig; |
21 | use OCP\DB\QueryBuilder\IQueryBuilder; |
22 | |
23 | use OCP\App\IAppManager; |
24 | use OCP\IGroupManager; |
25 | use OCP\IAvatarManager; |
26 | use OCP\Notification\IManager as INotificationManager; |
27 | |
28 | use OCP\IUserManager; |
29 | use OCP\IDBConnection; |
30 | use OCP\IDateTimeZone; |
31 | use OCP\Files\Folder; |
32 | use OCP\Files\IRootFolder; |
33 | |
34 | use DateTimeImmutable; |
35 | use DateInterval; |
36 | use DateTime; |
37 | |
38 | use OCA\Cospend\Utils; |
39 | use OCA\Cospend\AppInfo\Application; |
40 | use OCA\Cospend\Activity\ActivityManager; |
41 | use OCA\Cospend\Db\ProjectMapper; |
42 | use OCA\Cospend\Db\BillMapper; |
43 | use Throwable; |
44 | use function str_replace; |
45 | |
46 | class ProjectService { |
47 | |
48 | /** |
49 | * @var IL10N |
50 | */ |
51 | private $trans; |
52 | /** |
53 | * @var IConfig |
54 | */ |
55 | private $config; |
56 | /** |
57 | * @var ProjectMapper |
58 | */ |
59 | private $projectMapper; |
60 | /** |
61 | * @var BillMapper |
62 | */ |
63 | private $billMapper; |
64 | /** |
65 | * @var ActivityManager |
66 | */ |
67 | private $activityManager; |
68 | /** |
69 | * @var IAvatarManager |
70 | */ |
71 | private $avatarManager; |
72 | /** |
73 | * @var IUserManager |
74 | */ |
75 | private $userManager; |
76 | /** |
77 | * @var IAppManager |
78 | */ |
79 | private $appManager; |
80 | /** |
81 | * @var IGroupManager |
82 | */ |
83 | private $groupManager; |
84 | /** |
85 | * @var IDateTimeZone |
86 | */ |
87 | private $dateTimeZone; |
88 | /** |
89 | * @var IRootFolder |
90 | */ |
91 | private $root; |
92 | /** |
93 | * @var INotificationManager |
94 | */ |
95 | private $notificationManager; |
96 | /** |
97 | * @var IDBConnection |
98 | */ |
99 | private $db; |
100 | /** |
101 | * @var array |
102 | */ |
103 | private $hardCodedCategoryNames; |
104 | /** |
105 | * @var array[] |
106 | */ |
107 | public $defaultCategories; |
108 | /** |
109 | * @var array[] |
110 | */ |
111 | public $defaultPaymentModes; |
112 | |
113 | public function __construct (IL10N $trans, |
114 | IConfig $config, |
115 | ProjectMapper $projectMapper, |
116 | BillMapper $billMapper, |
117 | ActivityManager $activityManager, |
118 | IAvatarManager $avatarManager, |
119 | IUserManager $userManager, |
120 | IAppManager $appManager, |
121 | IGroupManager $groupManager, |
122 | IDateTimeZone $dateTimeZone, |
123 | IRootFolder $root, |
124 | INotificationManager $notificationManager, |
125 | IDBConnection $db) { |
126 | $this->trans = $trans; |
127 | $this->config = $config; |
128 | $this->projectMapper = $projectMapper; |
129 | $this->billMapper = $billMapper; |
130 | $this->activityManager = $activityManager; |
131 | $this->avatarManager = $avatarManager; |
132 | $this->userManager = $userManager; |
133 | $this->appManager = $appManager; |
134 | $this->groupManager = $groupManager; |
135 | $this->dateTimeZone = $dateTimeZone; |
136 | $this->root = $root; |
137 | $this->notificationManager = $notificationManager; |
138 | $this->db = $db; |
139 | |
140 | $this->defaultCategories = [ |
141 | [ |
142 | 'name' => $this->trans->t('Grocery'), |
143 | 'icon' => '🛒', |
144 | 'color' => '#ffaa00', |
145 | ], |
146 | [ |
147 | 'name' => $this->trans->t('Bar/Party'), |
148 | 'icon' => '🎉', |
149 | 'color' => '#aa55ff', |
150 | ], |
151 | [ |
152 | 'name' => $this->trans->t('Rent'), |
153 | 'icon' => '🏠', |
154 | 'color' => '#da8733', |
155 | ], |
156 | [ |
157 | 'name' => $this->trans->t('Bill'), |
158 | 'icon' => '🌩', |
159 | 'color' => '#4aa6b0', |
160 | ], |
161 | [ |
162 | 'name' => $this->trans->t('Excursion/Culture'), |
163 | 'icon' => '🚸', |
164 | 'color' => '#0055ff', |
165 | ], |
166 | [ |
167 | 'name' => $this->trans->t('Health'), |
168 | 'icon' => '💚', |
169 | 'color' => '#bf090c', |
170 | ], |
171 | [ |
172 | 'name' => $this->trans->t('Shopping'), |
173 | 'icon' => '🛍', |
174 | 'color' => '#e167d1', |
175 | ], |
176 | [ |
177 | 'name' => $this->trans->t('Restaurant'), |
178 | 'icon' => '🍴', |
179 | 'color' => '#d0d5e1', |
180 | ], |
181 | [ |
182 | 'name' => $this->trans->t('Accommodation'), |
183 | 'icon' => '🛌', |
184 | 'color' => '#5de1a3', |
185 | ], |
186 | [ |
187 | 'name' => $this->trans->t('Transport'), |
188 | 'icon' => '🚌', |
189 | 'color' => '#6f2ee1', |
190 | ], |
191 | [ |
192 | 'name' => $this->trans->t('Sport'), |
193 | 'icon' => '🎾', |
194 | 'color' => '#69e177', |
195 | ], |
196 | ]; |
197 | |
198 | $this->defaultPaymentModes = [ |
199 | [ |
200 | 'name' => $this->trans->t('Credit card'), |
201 | 'icon' => '💳', |
202 | 'color' => '#FF7F50', |
203 | 'old_id' => 'c', |
204 | ], |
205 | [ |
206 | 'name' => $this->trans->t('Cash'), |
207 | 'icon' => '💵', |
208 | 'color' => '#556B2F', |
209 | 'old_id' => 'b', |
210 | ], |
211 | [ |
212 | 'name' => $this->trans->t('Check'), |
213 | 'icon' => '🎫', |
214 | 'color' => '#A9A9A9', |
215 | 'old_id' => 'f', |
216 | ], |
217 | [ |
218 | 'name' => $this->trans->t('Transfer'), |
219 | 'icon' => '⇄', |
220 | 'color' => '#00CED1', |
221 | 'old_id' => 't', |
222 | ], |
223 | [ |
224 | 'name' => $this->trans->t('Online service'), |
225 | 'icon' => '🌎', |
226 | 'color' => '#9932CC', |
227 | 'old_id' => 'o', |
228 | ], |
229 | ]; |
230 | |
231 | $this->hardCodedCategoryNames = [ |
232 | '-11' => $this->trans->t('Reimbursement'), |
233 | ]; |
234 | } |
235 | |
236 | /** |
237 | * check if user owns the project |
238 | * or if the project is shared with the user |
239 | * |
240 | * @param string $userid |
241 | * @param string $projectid |
242 | * @return bool |
243 | */ |
244 | public function userCanAccessProject(string $userid, string $projectid): bool { |
245 | $projectInfo = $this->getProjectInfo($projectid); |
246 | if ($projectInfo !== null) { |
247 | // does the user own the project ? |
248 | if ($projectInfo['userid'] === $userid) { |
249 | return true; |
250 | } else { |
251 | $qb = $this->db->getQueryBuilder(); |
252 | // is the project shared with the user ? |
253 | $qb->select('userid', 'projectid') |
254 | ->from('cospend_shares', 's') |
255 | ->where( |
256 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['user'], IQueryBuilder::PARAM_STR)) |
257 | ) |
258 | ->andWhere( |
259 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
260 | ) |
261 | ->andWhere( |
262 | $qb->expr()->eq('userid', $qb->createNamedParameter($userid, IQueryBuilder::PARAM_STR)) |
263 | ); |
264 | $req = $qb->executeQuery(); |
265 | $dbProjectId = null; |
266 | while ($row = $req->fetch()) { |
267 | $dbProjectId = $row['projectid']; |
268 | break; |
269 | } |
270 | $req->closeCursor(); |
271 | $qb = $qb->resetQueryParts(); |
272 | |
273 | if ($dbProjectId !== null) { |
274 | return true; |
275 | } else { |
276 | // if not, is the project shared with a group containing the user? |
277 | $userO = $this->userManager->get($userid); |
278 | $accessWithGroup = null; |
279 | |
280 | $qb->select('userid') |
281 | ->from('cospend_shares', 's') |
282 | ->where( |
283 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['group'], IQueryBuilder::PARAM_STR)) |
284 | ) |
285 | ->andWhere( |
286 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
287 | ); |
288 | $req = $qb->executeQuery(); |
289 | while ($row = $req->fetch()) { |
290 | $groupId = $row['userid']; |
291 | if ($this->groupManager->groupExists($groupId) && $this->groupManager->get($groupId)->inGroup($userO)) { |
292 | $accessWithGroup = $groupId; |
293 | break; |
294 | } |
295 | } |
296 | $req->closeCursor(); |
297 | $qb = $qb->resetQueryParts(); |
298 | |
299 | if ($accessWithGroup !== null) { |
300 | return true; |
301 | } else { |
302 | // if not, are circles enabled and is the project shared with a circle containing the user? |
303 | $circlesEnabled = $this->appManager->isEnabledForUser('circles'); |
304 | if ($circlesEnabled) { |
305 | $qb->select('userid') |
306 | ->from('cospend_shares', 's') |
307 | ->where( |
308 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['circle'], IQueryBuilder::PARAM_STR)) |
309 | ) |
310 | ->andWhere( |
311 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
312 | ); |
313 | $req = $qb->executeQuery(); |
314 | while ($row = $req->fetch()) { |
315 | $circleId = $row['userid']; |
316 | if ($this->isUserInCircle($userid, $circleId)) { |
317 | return true; |
318 | } |
319 | } |
320 | } |
321 | return false; |
322 | } |
323 | |
324 | } |
325 | } |
326 | } else { |
327 | return false; |
328 | } |
329 | } |
330 | |
331 | /** |
332 | * Get max access level of a given user for a given project |
333 | * |
334 | * @param string $userid |
335 | * @param string $projectid |
336 | * @return int |
337 | */ |
338 | public function getUserMaxAccessLevel(string $userid, string $projectid): int { |
339 | $result = 0; |
340 | $projectInfo = $this->getProjectInfo($projectid); |
341 | if ($projectInfo !== null) { |
342 | // does the user own the project ? |
343 | if ($projectInfo['userid'] === $userid) { |
344 | return Application::ACCESS_LEVELS['admin']; |
345 | } else { |
346 | $qb = $this->db->getQueryBuilder(); |
347 | // is the project shared with the user ? |
348 | $qb->select('userid', 'projectid', 'accesslevel') |
349 | ->from('cospend_shares') |
350 | ->where( |
351 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['user'], IQueryBuilder::PARAM_STR)) |
352 | ) |
353 | ->andWhere( |
354 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
355 | ) |
356 | ->andWhere( |
357 | $qb->expr()->eq('userid', $qb->createNamedParameter($userid, IQueryBuilder::PARAM_STR)) |
358 | ); |
359 | $req = $qb->executeQuery(); |
360 | $dbProjectId = null; |
361 | $dbAccessLevel = null; |
362 | while ($row = $req->fetch()) { |
363 | $dbProjectId = $row['projectid']; |
364 | $dbAccessLevel = (int) $row['accesslevel']; |
365 | break; |
366 | } |
367 | $req->closeCursor(); |
368 | $qb = $qb->resetQueryParts(); |
369 | |
370 | if ($dbProjectId !== null && $dbAccessLevel > $result) { |
371 | $result = $dbAccessLevel; |
372 | } |
373 | |
374 | // is the project shared with a group containing the user? |
375 | $userO = $this->userManager->get($userid); |
376 | |
377 | $qb->select('userid', 'accesslevel') |
378 | ->from('cospend_shares') |
379 | ->where( |
380 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['group'], IQueryBuilder::PARAM_STR)) |
381 | ) |
382 | ->andWhere( |
383 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
384 | ); |
385 | $req = $qb->executeQuery(); |
386 | while ($row = $req->fetch()){ |
387 | $groupId = $row['userid']; |
388 | $dbAccessLevel = (int) $row['accesslevel']; |
389 | if ($this->groupManager->groupExists($groupId) |
390 | && $this->groupManager->get($groupId)->inGroup($userO) |
391 | && $dbAccessLevel > $result |
392 | ) { |
393 | $result = $dbAccessLevel; |
394 | } |
395 | } |
396 | $req->closeCursor(); |
397 | $qb = $qb->resetQueryParts(); |
398 | |
399 | // are circles enabled and is the project shared with a circle containing the user |
400 | $circlesEnabled = $this->appManager->isEnabledForUser('circles'); |
401 | if ($circlesEnabled) { |
402 | $qb->select('userid', 'accesslevel') |
403 | ->from('cospend_shares') |
404 | ->where( |
405 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['circle'], IQueryBuilder::PARAM_STR)) |
406 | ) |
407 | ->andWhere( |
408 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
409 | ); |
410 | $req = $qb->executeQuery(); |
411 | while ($row = $req->fetch()) { |
412 | $circleId = $row['userid']; |
413 | $dbAccessLevel = (int) $row['accesslevel']; |
414 | if ($this->isUserInCircle($userid, $circleId) && $dbAccessLevel > $result) { |
415 | $result = $dbAccessLevel; |
416 | } |
417 | } |
418 | } |
419 | } |
420 | } |
421 | |
422 | return $result; |
423 | } |
424 | |
425 | /** |
426 | * Get guest access level of a given project |
427 | * |
428 | * @param string $projectid |
429 | * @return int |
430 | */ |
431 | public function getGuestAccessLevel(string $projectid): int { |
432 | $projectInfo = $this->getProjectInfo($projectid); |
433 | if ($projectInfo !== null) { |
434 | return (int) $projectInfo['guestaccesslevel']; |
435 | } else { |
436 | return Application::ACCESS_LEVELS['none']; |
437 | } |
438 | } |
439 | |
440 | /** |
441 | * Get access level of a shared access |
442 | * |
443 | * @param string $projectid |
444 | * @param int $shid |
445 | * @return int |
446 | */ |
447 | public function getShareAccessLevel(string $projectid, int $shid): int { |
448 | $result = 0; |
449 | $qb = $this->db->getQueryBuilder(); |
450 | $qb->select('accesslevel') |
451 | ->from('cospend_shares') |
452 | ->where( |
453 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
454 | ) |
455 | ->andWhere( |
456 | $qb->expr()->eq('id', $qb->createNamedParameter($shid, IQueryBuilder::PARAM_INT)) |
457 | ); |
458 | $req = $qb->executeQuery(); |
459 | while ($row = $req->fetch()){ |
460 | $result = (int) $row['accesslevel']; |
461 | break; |
462 | } |
463 | $req->closeCursor(); |
464 | $qb->resetQueryParts(); |
465 | |
466 | return $result; |
467 | } |
468 | |
469 | /** |
470 | * Create a project |
471 | * |
472 | * @param string $name |
473 | * @param string $id |
474 | * @param string|null $password |
475 | * @param string|null $contact_email |
476 | * @param string $userid |
477 | * @param bool $createDefaultCategories |
478 | * @return array |
479 | * @throws \OCP\DB\Exception |
480 | */ |
481 | public function createProject(string $name, string $id, ?string $password, ?string $contact_email, string $userid = '', |
482 | bool $createDefaultCategories = true, bool $createDefaultPaymentModes = true): array { |
483 | $qb = $this->db->getQueryBuilder(); |
484 | |
485 | $qb->select('id') |
486 | ->from('cospend_projects', 'p') |
487 | ->where( |
488 | $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_STR)) |
489 | ); |
490 | $req = $qb->executeQuery(); |
491 | |
492 | $dbid = null; |
493 | while ($row = $req->fetch()){ |
494 | $dbid = $row['id']; |
495 | break; |
496 | } |
497 | $req->closeCursor(); |
498 | $qb = $qb->resetQueryParts(); |
499 | if ($dbid === null) { |
500 | // check if id is valid |
501 | if (strpos($id, '/') !== false) { |
502 | return ['message' => $this->trans->t('Invalid project id')]; |
503 | } |
504 | $dbPassword = ''; |
505 | if ($password !== null && $password !== '') { |
506 | $dbPassword = password_hash($password, PASSWORD_DEFAULT); |
507 | } |
508 | if ($contact_email === null) { |
509 | $contact_email = ''; |
510 | } |
511 | $ts = (new DateTime())->getTimestamp(); |
512 | $qb->insert('cospend_projects') |
513 | ->values([ |
514 | 'userid' => $qb->createNamedParameter($userid, IQueryBuilder::PARAM_STR), |
515 | 'id' => $qb->createNamedParameter($id, IQueryBuilder::PARAM_STR), |
516 | 'name' => $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR), |
517 | 'password' => $qb->createNamedParameter($dbPassword, IQueryBuilder::PARAM_STR), |
518 | 'email' => $qb->createNamedParameter($contact_email, IQueryBuilder::PARAM_STR), |
519 | 'lastchanged' => $qb->createNamedParameter($ts, IQueryBuilder::PARAM_INT) |
520 | ]); |
521 | $qb->executeStatement(); |
522 | $qb = $qb->resetQueryParts(); |
523 | |
524 | // create default categories |
525 | if ($createDefaultCategories) { |
526 | foreach ($this->defaultCategories as $category) { |
527 | $icon = urlencode($category['icon']); |
528 | $color = $category['color']; |
529 | $name = $category['name']; |
530 | $qb->insert('cospend_categories') |
531 | ->values([ |
532 | 'projectid' => $qb->createNamedParameter($id, IQueryBuilder::PARAM_STR), |
533 | 'encoded_icon' => $qb->createNamedParameter($icon, IQueryBuilder::PARAM_STR), |
534 | 'color' => $qb->createNamedParameter($color, IQueryBuilder::PARAM_STR), |
535 | 'name' => $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR), |
536 | ]); |
537 | $qb->executeStatement(); |
538 | $qb = $qb->resetQueryParts(); |
539 | } |
540 | } |
541 | |
542 | // create default payment modes |
543 | if ($createDefaultPaymentModes) { |
544 | foreach ($this->defaultPaymentModes as $pm) { |
545 | $icon = urlencode($pm['icon']); |
546 | $color = $pm['color']; |
547 | $name = $pm['name']; |
548 | $oldId = $pm['old_id']; |
549 | $qb->insert('cospend_paymentmodes') |
550 | ->values([ |
551 | 'projectid' => $qb->createNamedParameter($id, IQueryBuilder::PARAM_STR), |
552 | 'encoded_icon' => $qb->createNamedParameter($icon, IQueryBuilder::PARAM_STR), |
553 | 'color' => $qb->createNamedParameter($color, IQueryBuilder::PARAM_STR), |
554 | 'name' => $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR), |
555 | 'old_id' => $qb->createNamedParameter($oldId, IQueryBuilder::PARAM_STR), |
556 | ]); |
557 | $qb->executeStatement(); |
558 | $qb = $qb->resetQueryParts(); |
559 | } |
560 | } |
561 | |
562 | return ['id' => $id]; |
563 | } else { |
564 | return ['message' => $this->trans->t('A project with id "%1$s" already exists', [$id])]; |
565 | } |
566 | } |
567 | |
568 | /** |
569 | * Delete a project and all associated data |
570 | * |
571 | * @param string $projectid |
572 | * @return array |
573 | */ |
574 | public function deleteProject(string $projectid): array { |
575 | $projectToDelete = $this->getProjectById($projectid); |
576 | if ($projectToDelete !== null) { |
577 | $qb = $this->db->getQueryBuilder(); |
578 | |
579 | // TODO do that with one request |
580 | // delete project bills |
581 | $bills = $this->getBills($projectid); |
582 | foreach ($bills as $bill) { |
583 | $this->deleteBillOwersOfBill($bill['id']); |
584 | } |
585 | |
586 | $associatedTableNames = [ |
587 | 'cospend_bills', |
588 | 'cospend_members', |
589 | 'cospend_shares', |
590 | 'cospend_currencies', |
591 | 'cospend_categories', |
592 | 'cospend_paymentmodes' |
593 | ]; |
594 | |
595 | foreach ($associatedTableNames as $tableName) { |
596 | $qb->delete($tableName) |
597 | ->where( |
598 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
599 | ); |
600 | $qb->executeStatement(); |
601 | $qb = $qb->resetQueryParts(); |
602 | } |
603 | |
604 | // delete project |
605 | $qb->delete('cospend_projects') |
606 | ->where( |
607 | $qb->expr()->eq('id', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
608 | ); |
609 | $qb->executeStatement(); |
610 | $qb->resetQueryParts(); |
611 | |
612 | return ['message' => 'DELETED']; |
613 | } else { |
614 | return ['error' => $this->trans->t('Not Found')]; |
615 | } |
616 | } |
617 | |
618 | /** |
619 | * Get all project data |
620 | * |
621 | * @param string $projectid |
622 | * @return array |
623 | */ |
624 | public function getProjectInfo(string $projectid): ?array { |
625 | $projectInfo = null; |
626 | |
627 | $qb = $this->db->getQueryBuilder(); |
628 | |
629 | $qb->select('id', 'password', 'name', 'email', 'userid', 'lastchanged', 'guestaccesslevel', |
630 | 'autoexport', 'currencyname', 'deletiondisabled', 'categorysort', 'paymentmodesort') |
631 | ->from('cospend_projects') |
632 | ->where( |
633 | $qb->expr()->eq('id', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
634 | ); |
635 | $req = $qb->executeQuery(); |
636 | |
637 | $dbProjectId = null; |
638 | while ($row = $req->fetch()){ |
639 | $dbProjectId = $row['id']; |
640 | $dbName = $row['name']; |
641 | $dbEmail= $row['email']; |
642 | $dbUserId = $row['userid']; |
643 | $dbGuestAccessLevel = (int) $row['guestaccesslevel']; |
644 | $dbLastchanged = (int) $row['lastchanged']; |
645 | $dbAutoexport= $row['autoexport']; |
646 | $dbCurrencyName = $row['currencyname']; |
647 | $dbDeletionDisabled = ((int) $row['deletiondisabled']) === 1; |
648 | $dbCategorySort = $row['categorysort']; |
649 | $dbPaymentModeSort = $row['paymentmodesort']; |
650 | break; |
651 | } |
652 | $req->closeCursor(); |
653 | $qb->resetQueryParts(); |
654 | if ($dbProjectId !== null) { |
655 | $smallStats = $this->getSmallStats($dbProjectId); |
656 | $members = $this->getMembers($dbProjectId, 'lowername'); |
657 | $activeMembers = []; |
658 | foreach ($members as $member) { |
659 | if ($member['activated']) { |
660 | $activeMembers[] = $member; |
661 | } |
662 | } |
663 | $balance = $this->getBalance($dbProjectId); |
664 | $currencies = $this->getCurrencies($dbProjectId); |
665 | $categories = $this->getCategoriesOrPaymentModes($dbProjectId); |
666 | $paymentModes = $this->getCategoriesOrPaymentModes($dbProjectId, false); |
667 | // get all shares |
668 | $userShares = $this->getUserShares($dbProjectId); |
669 | $groupShares = $this->getGroupShares($dbProjectId); |
670 | $circleShares = $this->getCircleShares($dbProjectId); |
671 | $publicShares = $this->getPublicShares($dbProjectId); |
672 | $shares = array_merge($userShares, $groupShares, $circleShares, $publicShares); |
673 | |
674 | $projectInfo = [ |
675 | 'userid' => $dbUserId, |
676 | 'name' => $dbName, |
677 | 'contact_email' => $dbEmail, |
678 | 'id' => $dbProjectId, |
679 | 'guestaccesslevel' => $dbGuestAccessLevel, |
680 | 'autoexport' => $dbAutoexport, |
681 | 'currencyname' => $dbCurrencyName, |
682 | 'lastchanged' => $dbLastchanged, |
683 | 'active_members' => $activeMembers, |
684 | 'members' => $members, |
685 | 'balance' => $balance, |
686 | 'nb_bills' => $smallStats['nb_bills'], |
687 | 'total_spent' => $smallStats['total_spent'], |
688 | 'shares' => $shares, |
689 | 'currencies' => $currencies, |
690 | 'categories' => $categories, |
691 | 'paymentmodes' => $paymentModes, |
692 | 'deletion_disabled' => $dbDeletionDisabled, |
693 | 'categorysort' => $dbCategorySort, |
694 | 'paymentmodesort' => $dbPaymentModeSort, |
695 | ]; |
696 | } |
697 | |
698 | return $projectInfo; |
699 | } |
700 | |
701 | /** |
702 | * Get number of bills and total spent amount for a givne project |
703 | * |
704 | * @param string $projectId |
705 | * @return array |
706 | */ |
707 | private function getSmallStats(string $projectId): array { |
708 | $nbBills = 0; |
709 | $qb = $this->db->getQueryBuilder(); |
710 | $qb->selectAlias($qb->createFunction('COUNT(*)'), 'count_bills') |
711 | ->from('cospend_bills') |
712 | ->where( |
713 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
714 | ); |
715 | $req = $qb->executeQuery(); |
716 | while ($row = $req->fetch()) { |
717 | $nbBills = (int) $row['count_bills']; |
718 | } |
719 | $qb = $qb->resetQueryParts(); |
720 | |
721 | $totalSpent = 0; |
722 | $qb->selectAlias($qb->createFunction('SUM(amount)'), 'sum_amount') |
723 | ->from('cospend_bills') |
724 | ->where( |
725 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
726 | ); |
727 | $req = $qb->executeQuery(); |
728 | while ($row = $req->fetch()) { |
729 | $totalSpent = (int) $row['sum_amount']; |
730 | } |
731 | $qb->resetQueryParts(); |
732 | return [ |
733 | 'nb_bills' => $nbBills, |
734 | 'total_spent' => $totalSpent, |
735 | ]; |
736 | } |
737 | |
738 | /** |
739 | * Get project statistics |
740 | * |
741 | * @param string $projectId |
742 | * @param string|null $memberOrder |
743 | * @param int|null $tsMin |
744 | * @param int|null $tsMax |
745 | * @param int|null $paymentModeId |
746 | * @param int|null $categoryId |
747 | * @param float|null $amountMin |
748 | * @param float|null $amountMax |
749 | * @param bool|null $showDisabled |
750 | * @param int|null $currencyId |
751 | * @param int|null $payerId |
752 | * @return array |
753 | * @throws \OCP\DB\Exception |
754 | */ |
755 | public function getProjectStatistics(string $projectId, ?string $memberOrder = null, ?int $tsMin = null, ?int $tsMax = null, |
756 | ?int $paymentModeId = null, ?int $categoryId = null, ?float $amountMin = null, ?float $amountMax = null, |
757 | bool $showDisabled = true, ?int $currencyId = null, ?int $payerId = null): array { |
758 | $timeZone = $this->dateTimeZone->getTimeZone(); |
759 | $membersWeight = []; |
760 | $membersNbBills = []; |
761 | $membersBalance = []; |
762 | $membersFilteredBalance = []; |
763 | $membersPaid = [ |
764 | 'total' => [], |
765 | ]; |
766 | $membersSpent = []; |
767 | $membersPaidFor = []; |
768 | |
769 | $currency = null; |
770 | if ($currencyId !== null && $currencyId !== 0) { |
771 | $currency = $this->getCurrency($projectId, $currencyId); |
772 | } |
773 | |
774 | $projectCategories = $this->getCategoriesOrPaymentModes($projectId); |
775 | $projectPaymentModes = $this->getCategoriesOrPaymentModes($projectId, false); |
776 | |
777 | // get the real global balances with no filters |
778 | $balances = $this->getBalance($projectId); |
779 | |
780 | $members = $this->getMembers($projectId, $memberOrder); |
781 | foreach ($members as $member) { |
782 | $memberId = $member['id']; |
783 | $memberWeight = $member['weight']; |
784 | $membersWeight[$memberId] = $memberWeight; |
785 | $membersNbBills[$memberId] = 0; |
786 | $membersBalance[$memberId] = $balances[$memberId]; |
787 | $membersFilteredBalance[$memberId] = 0.0; |
788 | $membersPaid[$memberId] = 0.0; |
789 | $membersSpent[$memberId] = 0.0; |
790 | $membersPaidFor[$memberId] = []; |
791 | foreach ($members as $m) { |
792 | $membersPaidFor[$memberId][$m['id']] = 0.0; |
793 | } |
794 | $membersPaidFor['total'][$memberId] = 0.0; |
795 | } |
796 | |
797 | // build list of members to display |
798 | $membersToDisplay = []; |
799 | $allMembersIds = []; |
800 | foreach ($members as $member) { |
801 | $memberId = $member['id']; |
802 | $allMembersIds[] = $memberId; |
803 | // only take enabled members or those with non-zero balance |
804 | $mBalance = (float) $membersBalance[$memberId]; |
805 | if ($showDisabled || $member['activated'] || $mBalance >= 0.01 || $mBalance <= -0.01) { |
806 | $membersToDisplay[$memberId] = $member; |
807 | } |
808 | } |
809 | |
810 | // compute stats |
811 | $bills = $this->getBills( |
812 | $projectId, $tsMin, $tsMax, null, $paymentModeId, $categoryId, |
813 | $amountMin, $amountMax, null, null, false, $payerId |
814 | ); |
815 | |
816 | /* |
817 | $firstBillTs = $bills[0]['timestamp']; |
818 | $firstBillDate = DateTime::createFromFormat('U', $firstBillTs); |
819 | $firstBillDate->setTimezone($timeZone); |
820 | $firstBillDate->modify('first day of'); |
821 | $firstBillDate->setTime(0, 0); |
822 | $year1 = (int) $firstBillDate->format('Y'); |
823 | $month1 = (int) $firstBillDate->format('m'); |
824 | |
825 | $lastBillTs = $bills[count($bills) - 1]['timestamp']; |
826 | $lastBillDate = DateTime::createFromFormat('U', $lastBillTs); |
827 | $lastBillDate->setTimezone($timeZone); |
828 | $lastBillDate->modify('first day of'); |
829 | $lastBillDate->setTime(0, 0); |
830 | $year2 = (int) $lastBillDate->format('Y'); |
831 | $month2 = (int) $lastBillDate->format('m'); |
832 | |
833 | $fullMonthNumber = (($year2 - $year1) * 12) + ($month2 - $month1 + 1); |
834 | */ |
835 | |
836 | // compute classic stats |
837 | foreach ($bills as $bill) { |
838 | $payerId = $bill['payer_id']; |
839 | $amount = $bill['amount']; |
840 | $owers = $bill['owers']; |
841 | |
842 | $membersNbBills[$payerId]++; |
843 | $membersFilteredBalance[$payerId] += $amount; |
844 | $membersPaid[$payerId] += $amount; |
845 | |
846 | $nbOwerShares = 0.0; |
847 | foreach ($owers as $ower) { |
848 | $owerWeight = $ower['weight']; |
849 | if ($owerWeight === 0.0) { |
850 | $owerWeight = 1.0; |
851 | } |
852 | $nbOwerShares += $owerWeight; |
853 | } |
854 | foreach ($owers as $ower) { |
855 | $owerWeight = $ower['weight']; |
856 | if ($owerWeight === 0.0) { |
857 | $owerWeight = 1.0; |
858 | } |
859 | $owerId = $ower['id']; |
860 | $spent = $amount / $nbOwerShares * $owerWeight; |
861 | $membersFilteredBalance[$owerId] -= $spent; |
862 | $membersSpent[$owerId] += $spent; |
863 | // membersPaidFor |
864 | $membersPaidFor[$payerId][$owerId] += $spent; |
865 | $membersPaidFor['total'][$owerId] += $spent; |
866 | } |
867 | } |
868 | foreach ($members as $member) { |
869 | $memberId = $member['id']; |
870 | $membersPaidFor[$memberId]['total'] = $membersPaid[$memberId]; |
871 | } |
872 | |
873 | // build global stats data |
874 | $statistics = []; |
875 | if ($currency === null) { |
876 | foreach ($membersToDisplay as $memberId => $member) { |
877 | $statistic = [ |
878 | 'balance' => $membersBalance[$memberId], |
879 | 'filtered_balance' => $membersFilteredBalance[$memberId], |
880 | 'paid' => $membersPaid[$memberId], |
881 | 'spent' => $membersSpent[$memberId], |
882 | 'member' => $member |
883 | ]; |
884 | $statistics[] = $statistic; |
885 | } |
886 | } |
887 | else { |
888 | foreach ($membersToDisplay as $memberId => $member) { |
889 | $statistic = [ |
890 | 'balance' => ($membersBalance[$memberId] === 0.0) ? 0 : $membersBalance[$memberId] / $currency['exchange_rate'], |
891 | 'filtered_balance' => ($membersFilteredBalance[$memberId] === 0.0) ? 0 : $membersFilteredBalance[$memberId] / $currency['exchange_rate'], |
892 | 'paid' => ($membersPaid[$memberId] === 0.0) ? 0 : $membersPaid[$memberId] / $currency['exchange_rate'], |
893 | 'spent' => ($membersSpent[$memberId] === 0.0) ? 0 : $membersSpent[$memberId] / $currency['exchange_rate'], |
894 | 'member' => $member |
895 | ]; |
896 | $statistics[] = $statistic; |
897 | } |
898 | } |
899 | |
900 | // compute monthly member stats |
901 | $memberMonthlyPaidStats = []; |
902 | $memberMonthlySpentStats = []; |
903 | $allMembersKey = 0; |
904 | foreach ($bills as $bill) { |
905 | $payerId = $bill['payer_id']; |
906 | $amount = $bill['amount']; |
907 | $owers = $bill['owers']; |
908 | $date = DateTime::createFromFormat('U', $bill['timestamp']); |
909 | $date->setTimezone($timeZone); |
910 | $month = $date->format('Y-m'); |
911 | //////////////// PAID |
912 | // initialize this month |
913 | if (!array_key_exists($month, $memberMonthlyPaidStats)) { |
914 | $memberMonthlyPaidStats[$month] = []; |
915 | foreach ($membersToDisplay as $memberId => $member) { |
916 | $memberMonthlyPaidStats[$month][$memberId] = 0; |
917 | } |
918 | $memberMonthlyPaidStats[$month][$allMembersKey] = 0; |
919 | } |
920 | |
921 | // add paid amount |
922 | if (array_key_exists($payerId, $membersToDisplay)) { |
923 | $memberMonthlyPaidStats[$month][$payerId] += $amount; |
924 | $memberMonthlyPaidStats[$month][$allMembersKey] += $amount; |
925 | } |
926 | //////////////// SPENT |
927 | // initialize this month |
928 | if (!array_key_exists($month, $memberMonthlySpentStats)) { |
929 | $memberMonthlySpentStats[$month] = []; |
930 | foreach ($membersToDisplay as $memberId => $member) { |
931 | $memberMonthlySpentStats[$month][$memberId] = 0; |
932 | } |
933 | $memberMonthlySpentStats[$month][$allMembersKey] = 0; |
934 | } |
935 | // spent value for all members is the bill amount (like the paid value) |
936 | $memberMonthlySpentStats[$month][$allMembersKey] += $amount; |
937 | // compute number of shares |
938 | $nbOwerShares = 0.0; |
939 | foreach ($owers as $ower) { |
940 | $owerWeight = $ower['weight']; |
941 | if ($owerWeight === 0.0) { |
942 | $owerWeight = 1.0; |
943 | } |
944 | $nbOwerShares += $owerWeight; |
945 | } |
946 | // compute how much each ower has spent |
947 | foreach ($owers as $ower) { |
948 | $owerWeight = $ower['weight']; |
949 | if ($owerWeight === 0.0) { |
950 | $owerWeight = 1.0; |
951 | } |
952 | $owerId = $ower['id']; |
953 | $spent = $amount / $nbOwerShares * $owerWeight; |
954 | if (array_key_exists($owerId, $membersToDisplay)) { |
955 | $memberMonthlySpentStats[$month][$owerId] += $spent; |
956 | } |
957 | } |
958 | } |
959 | // monthly paid and spent average |
960 | $averageKey = $this->trans->t('Average per month'); |
961 | // number of months with actual bills |
962 | $nbMonth = count(array_keys($memberMonthlyPaidStats)); |
963 | $realMonths = array_keys($memberMonthlyPaidStats); |
964 | if ($nbMonth > 0) { |
965 | ////////////////////// PAID |
966 | $averagePaidStats = []; |
967 | foreach ($membersToDisplay as $memberId => $member) { |
968 | $sum = 0; |
969 | foreach ($memberMonthlyPaidStats as $month => $mStat) { |
970 | $sum += $memberMonthlyPaidStats[$month][$memberId]; |
971 | } |
972 | $averagePaidStats[$memberId] = $sum / $nbMonth; |
973 | } |
974 | // average for all members |
975 | $sum = 0; |
976 | foreach ($memberMonthlyPaidStats as $month => $mStat) { |
977 | $sum += $memberMonthlyPaidStats[$month][$allMembersKey]; |
978 | } |
979 | $averagePaidStats[$allMembersKey] = $sum / $nbMonth; |
980 | |
981 | $memberMonthlyPaidStats[$averageKey] = $averagePaidStats; |
982 | ////////////////////// SPENT |
983 | $averageSpentStats = []; |
984 | foreach ($membersToDisplay as $memberId => $member) { |
985 | $sum = 0; |
986 | foreach ($memberMonthlySpentStats as $month => $mStat) { |
987 | $sum += $memberMonthlySpentStats[$month][$memberId]; |
988 | } |
989 | $averageSpentStats[$memberId] = $sum / $nbMonth; |
990 | } |
991 | // average for all members |
992 | $sum = 0; |
993 | foreach ($memberMonthlySpentStats as $month => $mStat) { |
994 | $sum += $memberMonthlySpentStats[$month][$allMembersKey]; |
995 | } |
996 | $averageSpentStats[$allMembersKey] = $sum / $nbMonth; |
997 | |
998 | $memberMonthlySpentStats[$averageKey] = $averageSpentStats; |
999 | } |
1000 | // convert if necessary |
1001 | if ($currency !== null) { |
1002 | foreach ($memberMonthlyPaidStats as $month => $mStat) { |
1003 | foreach ($mStat as $mid => $val) { |
1004 | $memberMonthlyPaidStats[$month][$mid] = ($memberMonthlyPaidStats[$month][$mid] === 0.0) |
1005 | ? 0 |
1006 | : $memberMonthlyPaidStats[$month][$mid] / $currency['exchange_rate']; |
1007 | } |
1008 | } |
1009 | foreach ($memberMonthlySpentStats as $month => $mStat) { |
1010 | foreach ($mStat as $mid => $val) { |
1011 | $memberMonthlySpentStats[$month][$mid] = ($memberMonthlySpentStats[$month][$mid] === 0.0) |
1012 | ? 0 |
1013 | : $memberMonthlySpentStats[$month][$mid] / $currency['exchange_rate']; |
1014 | } |
1015 | } |
1016 | } |
1017 | // compute category and payment mode stats |
1018 | $categoryStats = []; |
1019 | $paymentModeStats = []; |
1020 | foreach ($bills as $bill) { |
1021 | // category |
1022 | $billCategoryId = $bill['categoryid']; |
1023 | if (!array_key_exists(strval($billCategoryId), $this->hardCodedCategoryNames) && |
1024 | !array_key_exists(strval($billCategoryId), $projectCategories) |
1025 | ) { |
1026 | $billCategoryId = 0; |
1027 | } |
1028 | $amount = $bill['amount']; |
1029 | if (!array_key_exists($billCategoryId, $categoryStats)) { |
1030 | $categoryStats[$billCategoryId] = 0; |
1031 | } |
1032 | $categoryStats[$billCategoryId] += $amount; |
1033 | |
1034 | // payment mode |
1035 | $paymentModeId = $bill['paymentmodeid']; |
1036 | if (!array_key_exists(strval($paymentModeId), $projectPaymentModes)) { |
1037 | $paymentModeId = 0; |
1038 | } |
1039 | $amount = $bill['amount']; |
1040 | if (!array_key_exists($paymentModeId, $paymentModeStats)) { |
1041 | $paymentModeStats[$paymentModeId] = 0; |
1042 | } |
1043 | $paymentModeStats[$paymentModeId] += $amount; |
1044 | } |
1045 | // convert if necessary |
1046 | if ($currency !== null) { |
1047 | foreach ($categoryStats as $catId => $val) { |
1048 | $categoryStats[$catId] = ($val === 0.0) ? 0 : $val / $currency['exchange_rate']; |
1049 | } |
1050 | foreach ($paymentModeStats as $pmId => $val) { |
1051 | $paymentModeStats[$pmId] = ($val === 0.0) ? 0 : $val / $currency['exchange_rate']; |
1052 | } |
1053 | } |
1054 | // compute category per member stats |
1055 | $categoryMemberStats = []; |
1056 | foreach ($bills as $bill) { |
1057 | $payerId = $bill['payer_id']; |
1058 | $billCategoryId = $bill['categoryid']; |
1059 | if (!array_key_exists(strval($billCategoryId), $this->hardCodedCategoryNames) && |
1060 | !array_key_exists(strval($billCategoryId), $projectCategories) |
1061 | ) { |
1062 | $billCategoryId = 0; |
1063 | } |
1064 | $amount = $bill['amount']; |
1065 | if (!array_key_exists($billCategoryId, $categoryMemberStats)) { |
1066 | $categoryMemberStats[$billCategoryId] = []; |
1067 | foreach ($membersToDisplay as $memberId => $member) { |
1068 | $categoryMemberStats[$billCategoryId][$memberId] = 0; |
1069 | } |
1070 | } |
1071 | if (array_key_exists($payerId, $membersToDisplay)) { |
1072 | $categoryMemberStats[$billCategoryId][$payerId] += $amount; |
1073 | } |
1074 | } |
1075 | // convert if necessary |
1076 | if ($currency !== null) { |
1077 | foreach ($categoryMemberStats as $catId => $mStat) { |
1078 | foreach ($mStat as $mid => $val) { |
1079 | $categoryMemberStats[$catId][$mid] = ($val === 0.0) ? 0 : $val / $currency['exchange_rate']; |
1080 | } |
1081 | } |
1082 | } |
1083 | // compute category/payment mode per month stats |
1084 | $categoryMonthlyStats = []; |
1085 | $paymentModeMonthlyStats = []; |
1086 | foreach ($bills as $bill) { |
1087 | $amount = $bill['amount']; |
1088 | $date = DateTime::createFromFormat('U', $bill['timestamp']); |
1089 | $date->setTimezone($timeZone); |
1090 | $month = $date->format('Y-m'); |
1091 | |
1092 | // category |
1093 | $billCategoryId = $bill['categoryid']; |
1094 | if (!array_key_exists($billCategoryId, $categoryMonthlyStats)) { |
1095 | $categoryMonthlyStats[$billCategoryId] = []; |
1096 | } |
1097 | if (!array_key_exists($month, $categoryMonthlyStats[$billCategoryId])) { |
1098 | $categoryMonthlyStats[$billCategoryId][$month] = 0; |
1099 | } |
1100 | $categoryMonthlyStats[$billCategoryId][$month] += $amount; |
1101 | |
1102 | // payment mode |
1103 | $paymentModeId = $bill['paymentmodeid']; |
1104 | if (!array_key_exists($paymentModeId, $paymentModeMonthlyStats)) { |
1105 | $paymentModeMonthlyStats[$paymentModeId] = []; |
1106 | } |
1107 | if (!array_key_exists($month, $paymentModeMonthlyStats[$paymentModeId])) { |
1108 | $paymentModeMonthlyStats[$paymentModeId][$month] = 0; |
1109 | } |
1110 | $paymentModeMonthlyStats[$paymentModeId][$month] += $amount; |
1111 | } |
1112 | // average per month |
1113 | foreach ($categoryMonthlyStats as $catId => $monthValues) { |
1114 | $sum = 0; |
1115 | foreach ($monthValues as $month => $value) { |
1116 | $sum += $value; |
1117 | } |
1118 | $avg = $sum / $nbMonth; |
1119 | $categoryMonthlyStats[$catId][$averageKey] = $avg; |
1120 | } |
1121 | foreach ($paymentModeMonthlyStats as $pmId => $monthValues) { |
1122 | $sum = 0; |
1123 | foreach ($monthValues as $month => $value) { |
1124 | $sum += $value; |
1125 | } |
1126 | $avg = $sum / $nbMonth; |
1127 | $paymentModeMonthlyStats[$pmId][$averageKey] = $avg; |
1128 | } |
1129 | // convert if necessary |
1130 | if ($currency !== null) { |
1131 | foreach ($categoryMonthlyStats as $catId => $cStat) { |
1132 | foreach ($cStat as $month => $val) { |
1133 | $categoryMonthlyStats[$catId][$month] = ($val === 0.0) ? 0 : $val / $currency['exchange_rate']; |
1134 | } |
1135 | } |
1136 | foreach ($paymentModeMonthlyStats as $pmId => $pmStat) { |
1137 | foreach ($pmStat as $month => $val) { |
1138 | $paymentModeMonthlyStats[$pmId][$month] = ($val === 0.0) ? 0 : $val / $currency['exchange_rate']; |
1139 | } |
1140 | } |
1141 | } |
1142 | |
1143 | return [ |
1144 | 'stats' => $statistics, |
1145 | 'memberMonthlyPaidStats' => count($memberMonthlyPaidStats) > 0 ? $memberMonthlyPaidStats : null, |
1146 | 'memberMonthlySpentStats' => count($memberMonthlySpentStats) > 0 ? $memberMonthlySpentStats : null, |
1147 | 'categoryStats' => $categoryStats, |
1148 | 'categoryMonthlyStats' => $categoryMonthlyStats, |
1149 | 'paymentModeStats' => $paymentModeStats, |
1150 | 'paymentModeMonthlyStats' => $paymentModeMonthlyStats, |
1151 | 'categoryMemberStats' => $categoryMemberStats, |
1152 | 'memberIds' => array_keys($membersToDisplay), |
1153 | 'allMemberIds' => $allMembersIds, |
1154 | 'membersPaidFor' => $membersPaidFor, |
1155 | 'realMonths' => $realMonths, |
1156 | ]; |
1157 | } |
1158 | |
1159 | /** |
1160 | * Add a bill in a given project |
1161 | * |
1162 | * @param string $projectid |
1163 | * @param string|null $date |
1164 | * @param string|null $what |
1165 | * @param int|null $payer |
1166 | * @param string|null $payed_for |
1167 | * @param float|null $amount |
1168 | * @param string|null $repeat |
1169 | * @param string|null $paymentmode |
1170 | * @param int|null $paymentmodeid |
1171 | * @param int|null $categoryid |
1172 | * @param int|null $repeatallactive |
1173 | * @param string|null $repeatuntil |
1174 | * @param int|null $timestamp |
1175 | * @param string|null $comment |
1176 | * @param int|null $repeatfreq |
1177 | * @param array|null $paymentModes |
1178 | * @return array |
1179 | * @throws \OCP\DB\Exception |
1180 | */ |
1181 | public function addBill(string $projectid, ?string $date, ?string $what, ?int $payer, ?string $payed_for, |
1182 | ?float $amount, ?string $repeat, ?string $paymentmode = null, ?int $paymentmodeid = null, |
1183 | ?int $categoryid = null, int $repeatallactive = 0, ?string $repeatuntil = null, |
1184 | ?int $timestamp = null, ?string $comment = null, ?int $repeatfreq = null, |
1185 | ?array $paymentModes = null): array { |
1186 | // if we don't have the payment modes, get them now |
1187 | if (is_null($paymentModes)) { |
1188 | $paymentModes = $this->getCategoriesOrPaymentModes($projectid, false); |
1189 | } |
1190 | |
1191 | if ($repeat === null || $repeat === '' || strlen($repeat) !== 1) { |
1192 | return ['repeat' => $this->trans->t('Invalid value')]; |
1193 | } elseif (!in_array($repeat, array_values(Application::FREQUENCIES))) { |
1194 | return ['repeat' => $this->trans->t('Invalid frequency')]; |
1195 | } |
1196 | if ($repeatuntil !== null && $repeatuntil === '') { |
1197 | $repeatuntil = null; |
1198 | } |
1199 | // priority to timestamp (moneybuster might send both for a moment) |
1200 | if ($timestamp === null || !is_numeric($timestamp)) { |
1201 | if ($date === null || $date === '') { |
1202 | return ['message' => $this->trans->t('Timestamp (or date) field is required')]; |
1203 | } else { |
1204 | $datetime = DateTime::createFromFormat('Y-m-d', $date); |
1205 | if ($datetime === false) { |
1206 | return ['date' => $this->trans->t('Invalid date')]; |
1207 | } |
1208 | $dateTs = $datetime->getTimestamp(); |
1209 | } |
1210 | } else { |
1211 | $dateTs = (int) $timestamp; |
1212 | } |
1213 | if ($what === null) { |
1214 | $what = ''; |
1215 | } |
1216 | if ($amount === null) { |
1217 | return ['amount' => $this->trans->t('This field is required')]; |
1218 | } |
1219 | if ($payer === null) { |
1220 | return ['payer' => $this->trans->t('This field is required')]; |
1221 | } |
1222 | if ($this->getMemberById($projectid, $payer) === null) { |
1223 | return ['payer' => $this->trans->t('Not a valid choice')]; |
1224 | } |
1225 | // check owers |
1226 | $owerIds = explode(',', $payed_for); |
1227 | if ($payed_for === null || $payed_for === '' || count($owerIds) === 0) { |
1228 | return ['payed_for' => $this->trans->t('Invalid value')]; |
1229 | } |
1230 | foreach ($owerIds as $owerId) { |
1231 | if (!is_numeric($owerId)) { |
1232 | return ['payed_for' => $this->trans->t('Invalid value')]; |
1233 | } |
1234 | if ($this->getMemberById($projectid, $owerId) === null) { |
1235 | return ['payed_for' => $this->trans->t('Not a valid choice')]; |
1236 | } |
1237 | } |
1238 | // payment mode |
1239 | if (!is_null($paymentmodeid)) { |
1240 | // is the old_id set for this payment mode? if yes, use it for old 'paymentmode' column |
1241 | $paymentmode = 'n'; |
1242 | if (isset($paymentModes[$paymentmodeid], $paymentModes[$paymentmodeid]['old_id']) |
1243 | && $paymentModes[$paymentmodeid]['old_id'] !== null |
1244 | && $paymentModes[$paymentmodeid]['old_id'] !== '' |
1245 | ) { |
1246 | $paymentmode = $paymentModes[$paymentmodeid]['old_id']; |
1247 | } |
1248 | } elseif (!is_null($paymentmode)) { |
1249 | // is there a pm with this old id? if yes, use it for new id |
1250 | $paymentmodeid = 0; |
1251 | foreach ($paymentModes as $id => $pm) { |
1252 | if ($pm['old_id'] === $paymentmode) { |
1253 | $paymentmodeid = $id; |
1254 | break; |
1255 | } |
1256 | } |
1257 | } |
1258 | |
1259 | // last modification timestamp is now |
1260 | $ts = (new DateTime())->getTimestamp(); |
1261 | |
1262 | // do it already ! |
1263 | $qb = $this->db->getQueryBuilder(); |
1264 | $qb->insert('cospend_bills') |
1265 | ->values([ |
1266 | 'projectid' => $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR), |
1267 | 'what' => $qb->createNamedParameter($what, IQueryBuilder::PARAM_STR), |
1268 | 'comment' => $qb->createNamedParameter($comment, IQueryBuilder::PARAM_STR), |
1269 | 'timestamp' => $qb->createNamedParameter($dateTs, IQueryBuilder::PARAM_INT), |
1270 | 'amount' => $qb->createNamedParameter($amount, IQueryBuilder::PARAM_STR), |
1271 | 'payerid' => $qb->createNamedParameter($payer, IQueryBuilder::PARAM_INT), |
1272 | 'repeat' => $qb->createNamedParameter($repeat, IQueryBuilder::PARAM_STR), |
1273 | 'repeatallactive' => $qb->createNamedParameter($repeatallactive, IQueryBuilder::PARAM_INT), |
1274 | 'repeatuntil' => $qb->createNamedParameter($repeatuntil, IQueryBuilder::PARAM_STR), |
1275 | 'repeatfreq' => $qb->createNamedParameter($repeatfreq ?? 1, IQueryBuilder::PARAM_INT), |
1276 | 'categoryid' => $qb->createNamedParameter($categoryid ?? 0, IQueryBuilder::PARAM_INT), |
1277 | 'paymentmode' => $qb->createNamedParameter($paymentmode ?? 'n', IQueryBuilder::PARAM_STR), |
1278 | 'paymentmodeid' => $qb->createNamedParameter($paymentmodeid ?? 0, IQueryBuilder::PARAM_INT), |
1279 | 'lastchanged' => $qb->createNamedParameter($ts, IQueryBuilder::PARAM_INT) |
1280 | ]); |
1281 | $qb->executeStatement(); |
1282 | $qb = $qb->resetQueryParts(); |
1283 | |
1284 | $insertedBillId = $qb->getLastInsertId(); |
1285 | |
1286 | // insert bill owers |
1287 | foreach ($owerIds as $owerId) { |
1288 | $qb->insert('cospend_bill_owers') |
1289 | ->values([ |
1290 | 'billid' => $qb->createNamedParameter($insertedBillId, IQueryBuilder::PARAM_INT), |
1291 | 'memberid' => $qb->createNamedParameter($owerId, IQueryBuilder::PARAM_INT) |
1292 | ]); |
1293 | $qb->executeStatement(); |
1294 | $qb = $qb->resetQueryParts(); |
1295 | } |
1296 | |
1297 | $this->updateProjectLastChanged($projectid, $ts); |
1298 | |
1299 | return ['inserted_id' => $insertedBillId]; |
1300 | } |
1301 | |
1302 | /** |
1303 | * Delete a bill |
1304 | * |
1305 | * @param string $projectid |
1306 | * @param int $billid |
1307 | * @param bool $force Ignores any deletion protection and forces the deletion of the bill |
1308 | * @return array |
1309 | */ |
1310 | public function deleteBill(string $projectid, int $billid, bool $force = false): array { |
1311 | if ($force === false) { |
1312 | $project = $this->getProjectInfo($projectid); |
1313 | if ($project['deletion_disabled']) { |
1314 | return ['message' => 'Forbidden']; |
1315 | } |
1316 | } |
1317 | $billToDelete = $this->getBill($projectid, $billid); |
1318 | if ($billToDelete !== null) { |
1319 | $this->deleteBillOwersOfBill($billid); |
1320 | |
1321 | $qb = $this->db->getQueryBuilder(); |
1322 | $qb->delete('cospend_bills') |
1323 | ->where( |
1324 | $qb->expr()->eq('id', $qb->createNamedParameter($billid, IQueryBuilder::PARAM_INT)) |
1325 | ) |
1326 | ->andWhere( |
1327 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
1328 | ); |
1329 | $qb->executeStatement(); |
1330 | $qb->resetQueryParts(); |
1331 | |
1332 | $ts = (new DateTime())->getTimestamp(); |
1333 | $this->updateProjectLastChanged($projectid, $ts); |
1334 | |
1335 | return ['success' => true]; |
1336 | } else { |
1337 | return ['message' => $this->trans->t('Not Found')]; |
1338 | } |
1339 | } |
1340 | |
1341 | /** |
1342 | * Get a member |
1343 | * |
1344 | * @param string $projectId |
1345 | * @param int $memberId |
1346 | * @return array|null |
1347 | */ |
1348 | public function getMemberById(string $projectId, int $memberId): ?array { |
1349 | $member = null; |
1350 | |
1351 | $qb = $this->db->getQueryBuilder(); |
1352 | $qb->select('id', 'userid', 'name', 'weight', 'color', 'activated') |
1353 | ->from('cospend_members') |
1354 | ->where( |
1355 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
1356 | ) |
1357 | ->andWhere( |
1358 | $qb->expr()->eq('id', $qb->createNamedParameter($memberId, IQueryBuilder::PARAM_INT)) |
1359 | ); |
1360 | $req = $qb->executeQuery(); |
1361 | |
1362 | while ($row = $req->fetch()) { |
1363 | $dbMemberId = (int) $row['id']; |
1364 | $dbWeight = (float) $row['weight']; |
1365 | $dbUserid = $row['userid']; |
1366 | $dbName = $row['name']; |
1367 | $dbActivated = (int) $row['activated']; |
1368 | $dbColor = $row['color']; |
1369 | if ($dbColor === null) { |
1370 | $av = $this->avatarManager->getGuestAvatar($dbName); |
1371 | $dbColor = $av->avatarBackgroundColor($dbName); |
1372 | $dbColor = [ |
1373 | 'r' => $dbColor->red(), |
1374 | 'g' => $dbColor->green(), |
1375 | 'b' => $dbColor->blue(), |
1376 | ]; |
1377 | } else { |
1378 | $dbColor = Utils::hexToRgb($dbColor); |
1379 | } |
1380 | |
1381 | $member = [ |
1382 | 'activated' => ($dbActivated === 1), |
1383 | 'userid' => $dbUserid, |
1384 | 'name' => $dbName, |
1385 | 'id' => $dbMemberId, |
1386 | 'weight' => $dbWeight, |
1387 | 'color' => $dbColor, |
1388 | ]; |
1389 | break; |
1390 | } |
1391 | $req->closeCursor(); |
1392 | $qb->resetQueryParts(); |
1393 | return $member; |
1394 | } |
1395 | |
1396 | /** |
1397 | * Get project info |
1398 | * |
1399 | * @param string $projectId |
1400 | * @return array|null |
1401 | * @throws \OCP\DB\Exception |
1402 | */ |
1403 | public function getProjectById(string $projectId): ?array { |
1404 | $project = null; |
1405 | |
1406 | $qb = $this->db->getQueryBuilder(); |
1407 | $qb->select('id', 'userid', 'name', 'email', 'password', 'currencyname', 'autoexport', 'guestaccesslevel', 'lastchanged') |
1408 | ->from('cospend_projects') |
1409 | ->where( |
1410 | $qb->expr()->eq('id', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
1411 | ); |
1412 | $req = $qb->executeQuery(); |
1413 | |
1414 | while ($row = $req->fetch()){ |
1415 | $dbId = $row['id']; |
1416 | $dbPassword = $row['password']; |
1417 | $dbName = $row['name']; |
1418 | $dbUserId = $row['userid']; |
1419 | $dbEmail = $row['email']; |
1420 | $dbCurrencyName = $row['currencyname']; |
1421 | $dbAutoexport = $row['autoexport']; |
1422 | $dbLastchanged = (int) $row['lastchanged']; |
1423 | $dbGuestAccessLevel = (int) $row['guestaccesslevel']; |
1424 | $project = [ |
1425 | 'id' => $dbId, |
1426 | 'name' => $dbName, |
1427 | 'userid' => $dbUserId, |
1428 | 'password' => $dbPassword, |
1429 | 'email' => $dbEmail, |
1430 | 'lastchanged' => $dbLastchanged, |
1431 | 'currencyname' => $dbCurrencyName, |
1432 | 'autoexport' => $dbAutoexport, |
1433 | 'guestaccesslevel' => $dbGuestAccessLevel, |
1434 | ]; |
1435 | break; |
1436 | } |
1437 | $req->closeCursor(); |
1438 | $qb->resetQueryParts(); |
1439 | return $project; |
1440 | } |
1441 | |
1442 | /** |
1443 | * Get bill info |
1444 | * |
1445 | * @param string $projectId |
1446 | * @param int $billId |
1447 | * @return array|null |
1448 | */ |
1449 | public function getBill(string $projectId, int $billId): ?array { |
1450 | $bill = null; |
1451 | // get bill owers |
1452 | $billOwers = []; |
1453 | $billOwerIds = []; |
1454 | |
1455 | $qb = $this->db->getQueryBuilder(); |
1456 | |
1457 | $qb->select('memberid', 'm.name', 'm.weight', 'm.activated') |
1458 | ->from('cospend_bill_owers', 'bo') |
1459 | ->innerJoin('bo', 'cospend_members', 'm', $qb->expr()->eq('bo.memberid', 'm.id')) |
1460 | ->where( |
1461 | $qb->expr()->eq('bo.billid', $qb->createNamedParameter($billId, IQueryBuilder::PARAM_INT)) |
1462 | ); |
1463 | $req = $qb->executeQuery(); |
1464 | |
1465 | while ($row = $req->fetch()){ |
1466 | $dbWeight = (float) $row['weight']; |
1467 | $dbName = $row['name']; |
1468 | $dbActivated = (((int) $row['activated']) === 1); |
1469 | $dbOwerId= (int) $row['memberid']; |
1470 | $billOwers[] = [ |
1471 | 'id' => $dbOwerId, |
1472 | 'weight' => $dbWeight, |
1473 | 'name' => $dbName, |
1474 | 'activated' => $dbActivated, |
1475 | ]; |
1476 | $billOwerIds[] = $dbOwerId; |
1477 | } |
1478 | $req->closeCursor(); |
1479 | $qb = $qb->resetQueryParts(); |
1480 | |
1481 | // get the bill |
1482 | $qb->select('id', 'what', 'comment', 'timestamp', 'amount', 'payerid', 'repeat', |
1483 | 'repeatallactive', 'paymentmode', 'paymentmodeid', 'categoryid', 'repeatuntil', 'repeatfreq') |
1484 | ->from('cospend_bills') |
1485 | ->where( |
1486 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
1487 | ) |
1488 | ->andWhere( |
1489 | $qb->expr()->eq('id', $qb->createNamedParameter($billId, IQueryBuilder::PARAM_INT)) |
1490 | ); |
1491 | $req = $qb->executeQuery(); |
1492 | while ($row = $req->fetch()){ |
1493 | $bill = [ |
1494 | 'id' => (int) $row['id'], |
1495 | 'amount' => (float) $row['amount'], |
1496 | 'what' => $row['what'], |
1497 | 'comment' => $row['comment'], |
1498 | 'date' => DateTime::createFromFormat('U', $row['timestamp'])->format('Y-m-d'), |
1499 | 'timestamp' => (int) $row['timestamp'], |
1500 | 'payer_id' => (int) $row['payerid'], |
1501 | 'owers' => $billOwers, |
1502 | 'owerIds' => $billOwerIds, |
1503 | 'repeat' => $row['repeat'], |
1504 | 'repeatallactive' => (int) $row['repeatallactive'], |
1505 | 'repeatuntil' => $row['repeatuntil'], |
1506 | 'repeatfreq' => (int) $row['repeatfreq'], |
1507 | 'paymentmode' => $row['paymentmode'], |
1508 | 'paymentmodeid' => (int) $row['paymentmodeid'], |
1509 | 'categoryid' => (int) $row['categoryid'], |
1510 | ]; |
1511 | } |
1512 | $req->closeCursor(); |
1513 | $qb->resetQueryParts(); |
1514 | |
1515 | return $bill; |
1516 | } |
1517 | |
1518 | /** |
1519 | * Delete bill owers of given bill |
1520 | * |
1521 | * @param int $billid |
1522 | * @return void |
1523 | */ |
1524 | private function deleteBillOwersOfBill(int $billid): void { |
1525 | $qb = $this->db->getQueryBuilder(); |
1526 | $qb->delete('cospend_bill_owers') |
1527 | ->where( |
1528 | $qb->expr()->eq('billid', $qb->createNamedParameter($billid, IQueryBuilder::PARAM_INT)) |
1529 | ); |
1530 | $qb->executeStatement(); |
1531 | $qb->resetQueryParts(); |
1532 | } |
1533 | |
1534 | /** |
1535 | * Generate bills to automatically settle a project |
1536 | * |
1537 | * @param string $projectid |
1538 | * @param int|null $centeredOn |
1539 | * @param int $precision |
1540 | * @param int|null $maxTimestamp |
1541 | * @return array |
1542 | */ |
1543 | public function autoSettlement(string $projectid, ?int $centeredOn = null, int $precision = 2, ?int $maxTimestamp = null): array { |
1544 | $settlement = $this->getProjectSettlement($projectid, $centeredOn, $maxTimestamp); |
1545 | $transactions = $settlement['transactions']; |
1546 | if (!is_array($transactions)) { |
1547 | return ['message' => $this->trans->t('Error when getting project settlement transactions')]; |
1548 | } |
1549 | |
1550 | $members = $this->getMembers($projectid); |
1551 | $memberIdToName = []; |
1552 | foreach ($members as $member) { |
1553 | $memberIdToName[$member['id']] = $member['name']; |
1554 | } |
1555 | |
1556 | if ($maxTimestamp) { |
1557 | $ts = $maxTimestamp - 1; |
1558 | } else { |
1559 | $ts = (new DateTime())->getTimestamp(); |
1560 | } |
1561 | |
1562 | $paymentModes = []; |
1563 | foreach ($transactions as $transaction) { |
1564 | $fromId = $transaction['from']; |
1565 | $toId = $transaction['to']; |
1566 | $amount = round((float) $transaction['amount'], $precision); |
1567 | $billTitle = $memberIdToName[$fromId].' → '.$memberIdToName[$toId]; |
1568 | $addBillResult = $this->addBill( |
1569 | $projectid, null, $billTitle, $fromId, $toId, $amount, |
1570 | Application::FREQUENCIES['no'], 'n', 0, Application::CAT_REIMBURSEMENT, |
1571 | 0, null, $ts, null, null, $paymentModes |
1572 | ); |
1573 | if (!isset($addBillResult['inserted_id'])) { |
1574 | return ['message' => $this->trans->t('Error when adding a bill')]; |
1575 | } |
1576 | } |
1577 | return ['success' => true]; |
1578 | } |
1579 | |
1580 | /** |
1581 | * Get project settlement plan |
1582 | * |
1583 | * @param string $projectId |
1584 | * @param int|null $centeredOn |
1585 | * @param int|null $maxTimestamp |
1586 | * @return array |
1587 | */ |
1588 | public function getProjectSettlement(string $projectId, ?int $centeredOn = null, ?int $maxTimestamp = null): array { |
1589 | $balances = $this->getBalance($projectId, $maxTimestamp); |
1590 | if ($centeredOn === null) { |
1591 | $transactions = $this->settle($balances); |
1592 | } else { |
1593 | $transactions = $this->centeredSettle($balances, $centeredOn); |
1594 | } |
1595 | return [ |
1596 | 'transactions' => $transactions, |
1597 | 'balances' => $balances, |
1598 | ]; |
1599 | } |
1600 | |
1601 | /** |
1602 | * Get a settlement plan centered on a member |
1603 | * |
1604 | * @param array $balances |
1605 | * @param int $centeredOn |
1606 | * @return array |
1607 | */ |
1608 | private function centeredSettle(array $balances, int $centeredOn): array { |
1609 | $transactions = []; |
1610 | foreach ($balances as $memberId => $balance) { |
1611 | if ($memberId !== $centeredOn) { |
1612 | if ($balance > 0.0) { |
1613 | $transactions[] = [ |
1614 | 'from' => $centeredOn, |
1615 | 'to' => $memberId, |
1616 | 'amount' => $balance |
1617 | ]; |
1618 | } elseif ($balance < 0.0) { |
1619 | $transactions[] = [ |
1620 | 'from' => $memberId, |
1621 | 'to' => $centeredOn, |
1622 | 'amount' => -$balance |
1623 | ]; |
1624 | } |
1625 | } |
1626 | } |
1627 | return $transactions; |
1628 | } |
1629 | |
1630 | /** |
1631 | * Get optimal settlement of a balance list |
1632 | * |
1633 | * @param array $balances |
1634 | * @return array |
1635 | */ |
1636 | private function settle(array $balances): ?array { |
1637 | $debitersCrediters = $this->orderBalance($balances); |
1638 | $debiters = $debitersCrediters[0]; |
1639 | $crediters = $debitersCrediters[1]; |
1640 | return $this->reduceBalance($crediters, $debiters); |
1641 | } |
1642 | |
1643 | /** |
1644 | * Separate crediter and debiter balances |
1645 | * |
1646 | * @param array $balances |
1647 | * @return array |
1648 | */ |
1649 | private function orderBalance(array $balances): array { |
1650 | $crediters = []; |
1651 | $debiters = []; |
1652 | foreach ($balances as $id => $balance) { |
1653 | if ($balance > 0.0) { |
1654 | $crediters[] = [$id, $balance]; |
1655 | } elseif ($balance < 0.0) { |
1656 | $debiters[] = [$id, $balance]; |
1657 | } |
1658 | } |
1659 | |
1660 | return [$debiters, $crediters]; |
1661 | } |
1662 | |
1663 | /** |
1664 | * Recursively produce transaction list of the settlement plan |
1665 | * |
1666 | * @param array $crediters |
1667 | * @param array $debiters |
1668 | * @param array|null $results |
1669 | * @return array |
1670 | */ |
1671 | private function reduceBalance(array $crediters, array $debiters, ?array $results = null): ?array { |
1672 | if (count($crediters) === 0 || count($debiters) === 0) { |
1673 | return $results; |
1674 | } |
1675 | |
1676 | if ($results === null) { |
1677 | $results = []; |
1678 | } |
1679 | |
1680 | $crediters = $this->sortCreditersDebiters($crediters); |
1681 | $debiters = $this->sortCreditersDebiters($debiters, true); |
1682 | |
1683 | $deb = array_pop($debiters); |
1684 | $debiter = $deb[0]; |
1685 | $debiterBalance = $deb[1]; |
1686 | |
1687 | $cred = array_pop($crediters); |
1688 | $crediter = $cred[0]; |
1689 | $crediterBalance = $cred[1]; |
1690 | |
1691 | if (abs($debiterBalance) > abs($crediterBalance)) { |
1692 | $amount = abs($crediterBalance); |
1693 | } else { |
1694 | $amount = abs($debiterBalance); |
1695 | } |
1696 | |
1697 | $newResults = $results; |
1698 | $newResults[] = ['to' => $crediter, 'amount' => $amount, 'from' => $debiter]; |
1699 | |
1700 | $newDebiterBalance = $debiterBalance + $amount; |
1701 | if ($newDebiterBalance < 0.0) { |
1702 | $debiters[] = [$debiter, $newDebiterBalance]; |
1703 | $debiters = $this->sortCreditersDebiters($debiters, true); |
1704 | } |
1705 | |
1706 | $newCrediterBalance = $crediterBalance - $amount; |
1707 | if ($newCrediterBalance > 0.0) { |
1708 | $crediters[] = [$crediter, $newCrediterBalance]; |
1709 | $crediters = $this->sortCreditersDebiters($crediters); |
1710 | } |
1711 | |
1712 | return $this->reduceBalance($crediters, $debiters, $newResults); |
1713 | } |
1714 | |
1715 | /** |
1716 | * Sort crediters or debiters array by balance value |
1717 | * |
1718 | * @param array $arr |
1719 | * @param bool $reverse |
1720 | * @return array |
1721 | */ |
1722 | private function sortCreditersDebiters(array $arr, bool $reverse = false): array { |
1723 | $res = []; |
1724 | if ($reverse) { |
1725 | foreach ($arr as $elem) { |
1726 | $i = 0; |
1727 | while ($i < count($res) && $elem[1] < $res[$i][1]) { |
1728 | $i++; |
1729 | } |
1730 | array_splice($res, $i, 0, [$elem]); |
1731 | } |
1732 | } else { |
1733 | foreach ($arr as $elem) { |
1734 | $i = 0; |
1735 | while ($i < count($res) && $elem[1] >= $res[$i][1]) { |
1736 | $i++; |
1737 | } |
1738 | array_splice($res, $i, 0, [$elem]); |
1739 | } |
1740 | } |
1741 | return $res; |
1742 | } |
1743 | |
1744 | /** |
1745 | * Edit a member |
1746 | * |
1747 | * @param string $projectid |
1748 | * @param int $memberid |
1749 | * @param string|null $name |
1750 | * @param string|null $userid |
1751 | * @param float|null $weight |
1752 | * @param bool $activated |
1753 | * @param string|null $color |
1754 | * @return array |
1755 | */ |
1756 | public function editMember(string $projectid, int $memberid, ?string $name = null, ?string $userid = null, |
1757 | ?float $weight = null, ?bool $activated = null, ?string $color = null): array { |
1758 | $member = $this->getMemberById($projectid, $memberid); |
1759 | if (!is_null($member)) { |
1760 | $qb = $this->db->getQueryBuilder(); |
1761 | // delete member if it has no bill and we are disabling it |
1762 | if ($member['activated'] |
1763 | && (!is_null($activated) && $activated === false) |
1764 | && count($this->getBillsOfMember($memberid)) === 0 |
1765 | ) { |
1766 | $qb->delete('cospend_members') |
1767 | ->where( |
1768 | $qb->expr()->eq('id', $qb->createNamedParameter($memberid, IQueryBuilder::PARAM_INT)) |
1769 | ); |
1770 | $qb->executeStatement(); |
1771 | $qb->resetQueryParts(); |
1772 | return []; |
1773 | } |
1774 | |
1775 | if (!is_null($name)) { |
1776 | if (strpos($name, '/') !== false) { |
1777 | return ['name' => $this->trans->t('Invalid member name')]; |
1778 | } else { |
1779 | // get existing member with this name |
1780 | $memberWithSameName = $this->getMemberByName($projectid, $name); |
1781 | if ($memberWithSameName && $memberWithSameName['id'] !== $memberid) { |
1782 | return ['name' => $this->trans->t('Name already exists')]; |
1783 | } |
1784 | } |
1785 | } |
1786 | |
1787 | if ($color !== null) { |
1788 | $color = preg_replace('/^#/', '', $color); |
1789 | if ($color === '' |
1790 | || ((strlen($color) === 3 || strlen($color) === 6) |
1791 | && preg_match('/^[0-9A-Fa-f]+/', $color) !== false) |
1792 | ) { |
1793 | // fine |
1794 | } else { |
1795 | return ['color' => $this->trans->t('Invalid value')]; |
1796 | } |
1797 | } |
1798 | |
1799 | $qb->update('cospend_members'); |
1800 | if ($weight !== null) { |
1801 | if ($weight > 0.0) { |
1802 | $qb->set('weight', $qb->createNamedParameter($weight, IQueryBuilder::PARAM_STR)); |
1803 | } else { |
1804 | return ['weight' => $this->trans->t('Not a valid decimal value')]; |
1805 | } |
1806 | } |
1807 | if (!is_null($activated)) { |
1808 | $qb->set('activated', $qb->createNamedParameter(($activated ? 1 : 0), IQueryBuilder::PARAM_INT)); |
1809 | } |
1810 | |
1811 | $ts = (new DateTime())->getTimestamp(); |
1812 | $qb->set('lastchanged', $qb->createNamedParameter($ts, IQueryBuilder::PARAM_INT)); |
1813 | |
1814 | if (!is_null($name)) { |
1815 | $qb->set('name', $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR)); |
1816 | } |
1817 | if ($color !== null) { |
1818 | if ($color === '') { |
1819 | $qb->set('color', $qb->createNamedParameter(null, IQueryBuilder::PARAM_STR)); |
1820 | } else { |
1821 | $qb->set('color', $qb->createNamedParameter($color, IQueryBuilder::PARAM_STR)); |
1822 | } |
1823 | } |
1824 | if ($userid !== null) { |
1825 | if ($userid === '') { |
1826 | $qb->set('userid', $qb->createNamedParameter(null, IQueryBuilder::PARAM_STR)); |
1827 | } else { |
1828 | $qb->set('userid', $qb->createNamedParameter($userid, IQueryBuilder::PARAM_STR)); |
1829 | } |
1830 | } |
1831 | $qb->where( |
1832 | $qb->expr()->eq('id', $qb->createNamedParameter($memberid, IQueryBuilder::PARAM_INT)) |
1833 | ) |
1834 | ->andWhere( |
1835 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
1836 | ); |
1837 | $qb->executeStatement(); |
1838 | $qb->resetQueryParts(); |
1839 | |
1840 | return $this->getMemberById($projectid, $memberid); |
1841 | } else { |
1842 | return ['name' => $this->trans->t('This project have no such member')]; |
1843 | } |
1844 | } |
1845 | |
1846 | /** |
1847 | * Edit a project |
1848 | * |
1849 | * @param string $projectid |
1850 | * @param string|null $name |
1851 | * @param string|null $contact_email |
1852 | * @param string|null $password |
1853 | * @param string|null $autoexport |
1854 | * @param string|null $currencyname |
1855 | * @param bool|null $deletion_disabled |
1856 | * @param string|null $categorysort |
1857 | * @param string|null $paymentmodesort |
1858 | * @return array |
1859 | * @throws \OCP\DB\Exception |
1860 | */ |
1861 | public function editProject(string $projectid, ?string $name = null, ?string $contact_email = null, ?string $password = null, |
1862 | ?string $autoexport = null, ?string $currencyname = null, ?bool $deletion_disabled = null, |
1863 | ?string $categorysort = null, ?string $paymentmodesort = null): array { |
1864 | $qb = $this->db->getQueryBuilder(); |
1865 | $qb->update('cospend_projects'); |
1866 | |
1867 | if ($name !== null) { |
1868 | if ($name === '') { |
1869 | return ['name' => [$this->trans->t('Name can\'t be empty')]]; |
1870 | } |
1871 | $qb->set('name', $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR)); |
1872 | } |
1873 | |
1874 | if ($contact_email !== null && $contact_email !== '') { |
1875 | if (filter_var($contact_email, FILTER_VALIDATE_EMAIL)) { |
1876 | $qb->set('email', $qb->createNamedParameter($contact_email, IQueryBuilder::PARAM_STR)); |
1877 | } else { |
1878 | return ['contact_email' => [$this->trans->t('Invalid email address')]]; |
1879 | } |
1880 | } |
1881 | if ($password !== null && $password !== '') { |
1882 | $dbPassword = password_hash($password, PASSWORD_DEFAULT); |
1883 | $qb->set('password', $qb->createNamedParameter($dbPassword, IQueryBuilder::PARAM_STR)); |
1884 | } |
1885 | if ($autoexport !== null && $autoexport !== '') { |
1886 | if (in_array($autoexport, array_values(Application::FREQUENCIES))) { |
1887 | $qb->set('autoexport', $qb->createNamedParameter($autoexport, IQueryBuilder::PARAM_STR)); |
1888 | } else { |
1889 | return ['autoexport' => [$this->trans->t('Invalid frequency')]]; |
1890 | } |
1891 | } |
1892 | if ($categorysort !== null && $categorysort !== '') { |
1893 | if (in_array($categorysort, array_values(Application::SORT_ORDERS))) { |
1894 | $qb->set('categorysort', $qb->createNamedParameter($categorysort, IQueryBuilder::PARAM_STR)); |
1895 | } else { |
1896 | return ['categorysort' => [$this->trans->t('Invalid sort order')]]; |
1897 | } |
1898 | } |
1899 | if ($paymentmodesort !== null && $paymentmodesort !== '') { |
1900 | if (in_array($paymentmodesort, array_values(Application::SORT_ORDERS))) { |
1901 | $qb->set('paymentmodesort', $qb->createNamedParameter($paymentmodesort, IQueryBuilder::PARAM_STR)); |
1902 | } else { |
1903 | return ['paymentmodesort' => [$this->trans->t('Invalid sort order')]]; |
1904 | } |
1905 | } |
1906 | if ($deletion_disabled !== null) { |
1907 | $qb->set('deletiondisabled', $qb->createNamedParameter($deletion_disabled ? 1 : 0, IQueryBuilder::PARAM_INT)); |
1908 | } |
1909 | if ($currencyname !== null) { |
1910 | if ($currencyname === '') { |
1911 | $qb->set('currencyname', $qb->createNamedParameter(null, IQueryBuilder::PARAM_STR)); |
1912 | } else { |
1913 | $qb->set('currencyname', $qb->createNamedParameter($currencyname, IQueryBuilder::PARAM_STR)); |
1914 | } |
1915 | } |
1916 | if ($this->getProjectById($projectid) !== null) { |
1917 | $ts = (new DateTime())->getTimestamp(); |
1918 | $qb->set('lastchanged', $qb->createNamedParameter($ts, IQueryBuilder::PARAM_INT)); |
1919 | $qb->where( |
1920 | $qb->expr()->eq('id', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
1921 | ); |
1922 | $qb->executeStatement(); |
1923 | $qb->resetQueryParts(); |
1924 | |
1925 | return ['success' => true]; |
1926 | } else { |
1927 | return ['message' => $this->trans->t('There is no such project')]; |
1928 | } |
1929 | } |
1930 | |
1931 | /** |
1932 | * Add a member to a project |
1933 | * |
1934 | * @param string $projectid |
1935 | * @param string $name |
1936 | * @param float|null $weight |
1937 | * @param bool $active |
1938 | * @param string|null $color |
1939 | * @param string|null $userid |
1940 | * @return array |
1941 | * @throws \OCP\DB\Exception |
1942 | */ |
1943 | public function addMember(string $projectid, string $name, ?float $weight = 1.0, bool $active = true, |
1944 | ?string $color = null, ?string $userid = null): array { |
1945 | if ($name !== null && $name !== '') { |
1946 | if ($this->getMemberByName($projectid, $name) === null && $this->getMemberByUserid($projectid, $userid) === null) { |
1947 | if (strpos($name, '/') !== false) { |
1948 | return ['error' => $this->trans->t('Invalid member name')]; |
1949 | } |
1950 | |
1951 | $weightToInsert = 1.0; |
1952 | if ($weight !== null) { |
1953 | if ($weight > 0.0) { |
1954 | $weightToInsert = $weight; |
1955 | } else { |
1956 | return ['error' => $this->trans->t('Weight is not a valid decimal value')]; |
1957 | } |
1958 | } |
1959 | |
1960 | if ($color !== null) { |
1961 | if ($color === '') { |
1962 | $color = null; |
1963 | } elseif ((strlen($color) === 4 || strlen($color) === 7) |
1964 | && preg_match('/^#[0-9A-Fa-f]+/', $color) !== false |
1965 | ) { |
1966 | // fine |
1967 | } else { |
1968 | return ['error' => $this->trans->t('Invalid color value')]; |
1969 | } |
1970 | } |
1971 | |
1972 | $ts = (new DateTime())->getTimestamp(); |
1973 | |
1974 | $qb = $this->db->getQueryBuilder(); |
1975 | $qb->insert('cospend_members') |
1976 | ->values([ |
1977 | 'projectid' => $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR), |
1978 | 'userid' => $qb->createNamedParameter($userid, IQueryBuilder::PARAM_STR), |
1979 | 'name' => $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR), |
1980 | 'weight' => $qb->createNamedParameter($weightToInsert, IQueryBuilder::PARAM_STR), |
1981 | 'activated' => $qb->createNamedParameter($active ? 1 : 0, IQueryBuilder::PARAM_INT), |
1982 | 'color' => $qb->createNamedParameter($color, IQueryBuilder::PARAM_STR), |
1983 | 'lastchanged' => $qb->createNamedParameter($ts, IQueryBuilder::PARAM_INT) |
1984 | ]); |
1985 | $qb->executeStatement(); |
1986 | $qb->resetQueryParts(); |
1987 | |
1988 | return $this->getMemberByName($projectid, $name); |
1989 | } else { |
1990 | return ['error' => $this->trans->t('This project already has this member')]; |
1991 | } |
1992 | } else { |
1993 | return ['error' => $this->trans->t('Name field is required')]; |
1994 | } |
1995 | } |
1996 | |
1997 | /** |
1998 | * Get number of bills in a project |
1999 | * |
2000 | * @param string $projectId |
2001 | * @param int|null $payerId |
2002 | * @return int |
2003 | * @throws \OCP\DB\Exception |
2004 | */ |
2005 | public function getNbBills(string $projectId, ?int $payerId = null, ?int $categoryId = null, ?int $paymentModeId = null): int { |
2006 | $nb = 0; |
2007 | $qb = $this->db->getQueryBuilder(); |
2008 | $qb->selectAlias($qb->createFunction('COUNT(*)'), 'count_bills') |
2009 | ->from('cospend_bills', 'bi') |
2010 | ->where( |
2011 | $qb->expr()->eq('bi.projectid', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
2012 | ); |
2013 | if ($payerId !== null) { |
2014 | $qb->andWhere( |
2015 | $qb->expr()->eq('payerid', $qb->createNamedParameter($payerId, IQueryBuilder::PARAM_INT)) |
2016 | ); |
2017 | } |
2018 | if ($categoryId !== null) { |
2019 | $qb->andWhere( |
2020 | $qb->expr()->eq('categoryid', $qb->createNamedParameter($categoryId, IQueryBuilder::PARAM_INT)) |
2021 | ); |
2022 | } |
2023 | if ($paymentModeId !== null) { |
2024 | $qb->andWhere( |
2025 | $qb->expr()->eq('paymentmodeid', $qb->createNamedParameter($paymentModeId, IQueryBuilder::PARAM_INT)) |
2026 | ); |
2027 | } |
2028 | $req = $qb->executeQuery(); |
2029 | while ($row = $req->fetch()) { |
2030 | $nb = (int) $row['count_bills']; |
2031 | } |
2032 | return $nb; |
2033 | } |
2034 | |
2035 | /** |
2036 | * Get filtered list of bills for a project |
2037 | * |
2038 | * @param string $projectId |
2039 | * @param int|null $tsMin |
2040 | * @param int|null $tsMax |
2041 | * @param string|null $paymentMode |
2042 | * @param int|null $category |
2043 | * @param float|null $amountMin |
2044 | * @param float|null $amountMax |
2045 | * @param int|null $lastchanged |
2046 | * @param int|null $limit |
2047 | * @param bool $reverse |
2048 | * @param int $offset |
2049 | * @return array |
2050 | */ |
2051 | public function getBillsWithLimit(string $projectId, ?int $tsMin = null, ?int $tsMax = null, |
2052 | ?string $paymentMode = null, ?int $paymentModeId = null, |
2053 | ?int $category = null, ?float $amountMin = null, ?float $amountMax = null, |
2054 | ?int $lastchanged = null, ?int $limit = null, |
2055 | bool $reverse = false, ?int $offset = 0, ?int $payerId = null, |
2056 | ?int $includeBillId = null, ?string $searchTerm = null): array { |
2057 | $qb = $this->db->getQueryBuilder(); |
2058 | $qb->select('id', 'what', 'comment', 'timestamp', 'amount', 'payerid', 'repeat', |
2059 | 'paymentmode', 'paymentmodeid', 'categoryid', 'lastchanged', 'repeatallactive', |
2060 | 'repeatuntil', 'repeatfreq') |
2061 | ->from('cospend_bills', 'bi') |
2062 | ->where( |
2063 | $qb->expr()->eq('bi.projectid', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
2064 | ); |
2065 | // take bills that have changed after $lastchanged |
2066 | if ($lastchanged !== null) { |
2067 | $qb->andWhere( |
2068 | $qb->expr()->gt('bi.lastchanged', $qb->createNamedParameter($lastchanged, IQueryBuilder::PARAM_INT)) |
2069 | ); |
2070 | } |
2071 | if ($payerId !== null) { |
2072 | $qb->andWhere( |
2073 | $qb->expr()->eq('payerid', $qb->createNamedParameter($payerId, IQueryBuilder::PARAM_INT)) |
2074 | ); |
2075 | } |
2076 | if ($tsMin !== null) { |
2077 | $qb->andWhere( |
2078 | $qb->expr()->gte('timestamp', $qb->createNamedParameter($tsMin, IQueryBuilder::PARAM_INT)) |
2079 | ); |
2080 | } |
2081 | if ($tsMax !== null) { |
2082 | $qb->andWhere( |
2083 | $qb->expr()->lte('timestamp', $qb->createNamedParameter($tsMax, IQueryBuilder::PARAM_INT)) |
2084 | ); |
2085 | } |
2086 | if ($paymentMode !== null && $paymentMode !== '' && $paymentMode !== 'n') { |
2087 | $qb->andWhere( |
2088 | $qb->expr()->eq('paymentmode', $qb->createNamedParameter($paymentMode, IQueryBuilder::PARAM_STR)) |
2089 | ); |
2090 | } elseif (!is_null($paymentModeId)) { |
2091 | $qb->andWhere( |
2092 | $qb->expr()->eq('paymentmodeid', $qb->createNamedParameter($paymentModeId, IQueryBuilder::PARAM_INT)) |
2093 | ); |
2094 | } |
2095 | if ($category !== null) { |
2096 | if ($category === -100) { |
2097 | $or = $qb->expr()->orx(); |
2098 | $or->add($qb->expr()->isNull('categoryid')); |
2099 | $or->add($qb->expr()->neq('categoryid', $qb->createNamedParameter(Application::CAT_REIMBURSEMENT, IQueryBuilder::PARAM_INT))); |
2100 | $qb->andWhere($or); |
2101 | } else { |
2102 | $qb->andWhere( |
2103 | $qb->expr()->eq('categoryid', $qb->createNamedParameter($category, IQueryBuilder::PARAM_INT)) |
2104 | ); |
2105 | } |
2106 | } |
2107 | if ($amountMin !== null) { |
2108 | $qb->andWhere( |
2109 | $qb->expr()->gte('amount', $qb->createNamedParameter($amountMin, IQueryBuilder::PARAM_STR)) |
2110 | ); |
2111 | } |
2112 | if ($amountMax !== null) { |
2113 | $qb->andWhere( |
2114 | $qb->expr()->lte('amount', $qb->createNamedParameter($amountMax, IQueryBuilder::PARAM_STR)) |
2115 | ); |
2116 | } |
2117 | // handle the search term (what, comment, amount+-1) |
2118 | if ($searchTerm !== null && $searchTerm !== '') { |
2119 | $qb = $this->applyBillSearchTermCondition($qb, $searchTerm, 'bi'); |
2120 | } |
2121 | if ($reverse) { |
2122 | $qb->orderBy('timestamp', 'DESC'); |
2123 | } else { |
2124 | $qb->orderBy('timestamp', 'ASC'); |
2125 | } |
2126 | if ($limit) { |
2127 | $qb->setMaxResults($limit); |
2128 | } |
2129 | if ($offset) { |
2130 | $qb->setFirstResult($offset); |
2131 | } |
2132 | $req = $qb->executeQuery(); |
2133 | |
2134 | $bills = []; |
2135 | $includeBillFound = false; |
2136 | while ($row = $req->fetch()){ |
2137 | if ($includeBillId !== null && $includeBillId === (int) $row['id']) { |
2138 | $includeBillFound = true; |
2139 | } |
2140 | $bills[] = $this->getBillFromRow($row); |
2141 | } |
2142 | $req->closeCursor(); |
2143 | |
2144 | // look further if we want to include a specific bill |
2145 | if ($includeBillId !== null && $includeBillFound === false && $limit && $offset === 0) { |
2146 | $lastResultCount = count($bills); |
2147 | while ($lastResultCount > 0 && $includeBillFound === false) { |
2148 | $offset = $offset + $limit; |
2149 | $qb->setFirstResult($offset); |
2150 | $req = $qb->executeQuery(); |
2151 | $lastResultCount = 0; |
2152 | while ($row = $req->fetch()){ |
2153 | if ($includeBillId === (int) $row['id']) { |
2154 | $includeBillFound = true; |
2155 | } |
2156 | $lastResultCount++; |
2157 | $bills[] = $this->getBillFromRow($row); |
2158 | } |
2159 | $req->closeCursor(); |
2160 | } |
2161 | } |
2162 | |
2163 | $qb = $qb->resetQueryParts(); |
2164 | |
2165 | // get owers |
2166 | foreach ($bills as $i => $bill) { |
2167 | $billId = $bill['id']; |
2168 | $billOwers = []; |
2169 | $billOwerIds = []; |
2170 | |
2171 | $qb->select('memberid', 'm.name', 'm.weight', 'm.activated') |
2172 | ->from('cospend_bill_owers', 'bo') |
2173 | ->innerJoin('bo', 'cospend_members', 'm', $qb->expr()->eq('bo.memberid', 'm.id')) |
2174 | ->where( |
2175 | $qb->expr()->eq('bo.billid', $qb->createNamedParameter($billId, IQueryBuilder::PARAM_INT)) |
2176 | ); |
2177 | $qb->setFirstResult(0); |
2178 | $req = $qb->executeQuery(); |
2179 | while ($row = $req->fetch()){ |
2180 | $dbWeight = (float) $row['weight']; |
2181 | $dbName = $row['name']; |
2182 | $dbActivated = ((int) $row['activated']) === 1; |
2183 | $dbOwerId= (int) $row['memberid']; |
2184 | $billOwers[] = [ |
2185 | 'id' => $dbOwerId, |
2186 | 'weight' => $dbWeight, |
2187 | 'name' => $dbName, |
2188 | 'activated' => $dbActivated, |
2189 | ]; |
2190 | $billOwerIds[] = $dbOwerId; |
2191 | } |
2192 | $req->closeCursor(); |
2193 | $qb = $qb->resetQueryParts(); |
2194 | $bills[$i]['owers'] = $billOwers; |
2195 | $bills[$i]['owerIds'] = $billOwerIds; |
2196 | } |
2197 | |
2198 | return $bills; |
2199 | } |
2200 | |
2201 | private function getBillFromRow(array $row): array { |
2202 | $dbBillId = (int) $row['id']; |
2203 | $dbAmount = (float) $row['amount']; |
2204 | $dbWhat = $row['what']; |
2205 | $dbComment = $row['comment']; |
2206 | $dbTimestamp = (int) $row['timestamp']; |
2207 | $dbDate = DateTime::createFromFormat('U', $dbTimestamp); |
2208 | $dbRepeat = $row['repeat']; |
2209 | $dbPayerId = (int) $row['payerid']; |
2210 | $dbPaymentMode = $row['paymentmode']; |
2211 | $dbPaymentModeId = (int) $row['paymentmodeid']; |
2212 | $dbCategoryId = (int) $row['categoryid']; |
2213 | $dbLastchanged = (int) $row['lastchanged']; |
2214 | $dbRepeatAllActive = (int) $row['repeatallactive']; |
2215 | $dbRepeatUntil = $row['repeatuntil']; |
2216 | $dbRepeatFreq = (int) $row['repeatfreq']; |
2217 | return [ |
2218 | 'id' => $dbBillId, |
2219 | 'amount' => $dbAmount, |
2220 | 'what' => $dbWhat, |
2221 | 'comment' => $dbComment, |
2222 | 'timestamp' => $dbTimestamp, |
2223 | 'date' => $dbDate->format('Y-m-d'), |
2224 | 'payer_id' => $dbPayerId, |
2225 | 'owers' => [], |
2226 | 'owerIds' => [], |
2227 | 'repeat' => $dbRepeat, |
2228 | 'paymentmode' => $dbPaymentMode, |
2229 | 'paymentmodeid' => $dbPaymentModeId, |
2230 | 'categoryid' => $dbCategoryId, |
2231 | 'lastchanged' => $dbLastchanged, |
2232 | 'repeatallactive' => $dbRepeatAllActive, |
2233 | 'repeatuntil' => $dbRepeatUntil, |
2234 | 'repeatfreq' => $dbRepeatFreq, |
2235 | ]; |
2236 | } |
2237 | |
2238 | /** |
2239 | * Get filtered list of bills for a project |
2240 | * |
2241 | * @param string $projectId |
2242 | * @param int|null $tsMin |
2243 | * @param int|null $tsMax |
2244 | * @param string|null $paymentMode |
2245 | * @param int|null $category |
2246 | * @param float|null $amountMin |
2247 | * @param float|null $amountMax |
2248 | * @param int|null $lastchanged |
2249 | * @param int|null $limit |
2250 | * @param bool $reverse |
2251 | * @param int|null $payerId |
2252 | * @return array |
2253 | * @throws \OCP\DB\Exception |
2254 | */ |
2255 | public function getBills(string $projectId, ?int $tsMin = null, ?int $tsMax = null, |
2256 | ?string $paymentMode = null, ?int $paymentModeId = null, |
2257 | ?int $category = null, ?float $amountMin = null, ?float $amountMax = null, |
2258 | ?int $lastchanged = null, ?int $limit = null, |
2259 | bool $reverse = false, ?int $payerId = null): array { |
2260 | $qb = $this->db->getQueryBuilder(); |
2261 | $qb->select('bi.id', 'what', 'comment', 'timestamp', 'amount', 'payerid', 'repeat', |
2262 | 'paymentmode', 'paymentmodeid', 'categoryid', 'bi.lastchanged', 'repeatallactive', 'repeatuntil', 'repeatfreq', |
2263 | 'memberid', 'm.name', 'm.weight', 'm.activated') |
2264 | ->from('cospend_bill_owers', 'bo') |
2265 | ->innerJoin('bo', 'cospend_bills', 'bi', $qb->expr()->eq('bo.billid', 'bi.id')) |
2266 | ->innerJoin('bo', 'cospend_members', 'm', $qb->expr()->eq('bo.memberid', 'm.id')) |
2267 | ->where( |
2268 | $qb->expr()->eq('bi.projectid', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
2269 | ); |
2270 | // take bills that have changed after $lastchanged |
2271 | if ($lastchanged !== null) { |
2272 | $qb->andWhere( |
2273 | $qb->expr()->gt('bi.lastchanged', $qb->createNamedParameter($lastchanged, IQueryBuilder::PARAM_INT)) |
2274 | ); |
2275 | } |
2276 | if ($payerId !== null) { |
2277 | $qb->andWhere( |
2278 | $qb->expr()->eq('bi.payerid', $qb->createNamedParameter($payerId, IQueryBuilder::PARAM_INT)) |
2279 | ); |
2280 | } |
2281 | if ($tsMin !== null) { |
2282 | $qb->andWhere( |
2283 | $qb->expr()->gte('timestamp', $qb->createNamedParameter($tsMin, IQueryBuilder::PARAM_INT)) |
2284 | ); |
2285 | } |
2286 | if ($tsMax !== null) { |
2287 | $qb->andWhere( |
2288 | $qb->expr()->lte('timestamp', $qb->createNamedParameter($tsMax, IQueryBuilder::PARAM_INT)) |
2289 | ); |
2290 | } |
2291 | if ($paymentMode !== null && $paymentMode !== '' && $paymentMode !== 'n') { |
2292 | $qb->andWhere( |
2293 | $qb->expr()->eq('paymentmode', $qb->createNamedParameter($paymentMode, IQueryBuilder::PARAM_STR)) |
2294 | ); |
2295 | } elseif (!is_null($paymentModeId)) { |
2296 | $qb->andWhere( |
2297 | $qb->expr()->eq('paymentmodeid', $qb->createNamedParameter($paymentModeId, IQueryBuilder::PARAM_INT)) |
2298 | ); |
2299 | } |
2300 | if ($category !== null) { |
2301 | if ($category === -100) { |
2302 | $or = $qb->expr()->orx(); |
2303 | $or->add($qb->expr()->isNull('categoryid')); |
2304 | $or->add($qb->expr()->neq('categoryid', $qb->createNamedParameter(Application::CAT_REIMBURSEMENT, IQueryBuilder::PARAM_INT))); |
2305 | $qb->andWhere($or); |
2306 | } else { |
2307 | $qb->andWhere( |
2308 | $qb->expr()->eq('categoryid', $qb->createNamedParameter($category, IQueryBuilder::PARAM_INT)) |
2309 | ); |
2310 | } |
2311 | } |
2312 | if ($amountMin !== null) { |
2313 | $qb->andWhere( |
2314 | $qb->expr()->gte('amount', $qb->createNamedParameter($amountMin, IQueryBuilder::PARAM_STR)) |
2315 | ); |
2316 | } |
2317 | if ($amountMax !== null) { |
2318 | $qb->andWhere( |
2319 | $qb->expr()->lte('amount', $qb->createNamedParameter($amountMax, IQueryBuilder::PARAM_STR)) |
2320 | ); |
2321 | } |
2322 | if ($reverse) { |
2323 | $qb->orderBy('timestamp', 'DESC'); |
2324 | } else { |
2325 | $qb->orderBy('timestamp', 'ASC'); |
2326 | } |
2327 | if ($limit) { |
2328 | $qb->setMaxResults($limit); |
2329 | } |
2330 | $req = $qb->executeQuery(); |
2331 | |
2332 | // bills by id |
2333 | $billDict = []; |
2334 | // ordered list of bill ids |
2335 | $orderedBillIds = []; |
2336 | while ($row = $req->fetch()){ |
2337 | $dbBillId = (int) $row['id']; |
2338 | // if first time we see the bill : add it to bill list |
2339 | if (!array_key_exists($dbBillId, $billDict)) { |
2340 | $dbAmount = (float) $row['amount']; |
2341 | $dbWhat = $row['what']; |
2342 | $dbComment = $row['comment']; |
2343 | $dbTimestamp = (int) $row['timestamp']; |
2344 | $dbDate = DateTime::createFromFormat('U', $dbTimestamp); |
2345 | $dbRepeat = $row['repeat']; |
2346 | $dbPayerId = (int) $row['payerid']; |
2347 | $dbPaymentMode = $row['paymentmode']; |
2348 | $dbPaymentModeId = (int) $row['paymentmodeid']; |
2349 | $dbCategoryId = (int) $row['categoryid']; |
2350 | $dbLastchanged = (int) $row['lastchanged']; |
2351 | $dbRepeatAllActive = (int) $row['repeatallactive']; |
2352 | $dbRepeatUntil = $row['repeatuntil']; |
2353 | $dbRepeatFreq = (int) $row['repeatfreq']; |
2354 | $billDict[$dbBillId] = [ |
2355 | 'id' => $dbBillId, |
2356 | 'amount' => $dbAmount, |
2357 | 'what' => $dbWhat, |
2358 | 'comment' => $dbComment, |
2359 | 'timestamp' => $dbTimestamp, |
2360 | 'date' => $dbDate->format('Y-m-d'), |
2361 | 'payer_id' => $dbPayerId, |
2362 | 'owers' => [], |
2363 | 'owerIds' => [], |
2364 | 'repeat' => $dbRepeat, |
2365 | 'paymentmode' => $dbPaymentMode, |
2366 | 'paymentmodeid' => $dbPaymentModeId, |
2367 | 'categoryid' => $dbCategoryId, |
2368 | 'lastchanged' => $dbLastchanged, |
2369 | 'repeatallactive' => $dbRepeatAllActive, |
2370 | 'repeatuntil' => $dbRepeatUntil, |
2371 | 'repeatfreq' => $dbRepeatFreq, |
2372 | ]; |
2373 | // keep order of bills |
2374 | $orderedBillIds[] = $dbBillId; |
2375 | } |
2376 | // anyway add an ower |
2377 | $dbWeight = (float) $row['weight']; |
2378 | $dbName = $row['name']; |
2379 | $dbActivated = ((int) $row['activated']) === 1; |
2380 | $dbOwerId= (int) $row['memberid']; |
2381 | $billDict[$dbBillId]['owers'][] = [ |
2382 | 'id' => $dbOwerId, |
2383 | 'weight' => $dbWeight, |
2384 | 'name' => $dbName, |
2385 | 'activated' => $dbActivated, |
2386 | ]; |
2387 | $billDict[$dbBillId]['owerIds'][] = $dbOwerId; |
2388 | } |
2389 | $req->closeCursor(); |
2390 | $qb->resetQueryParts(); |
2391 | |
2392 | $resultBills = []; |
2393 | foreach ($orderedBillIds as $bid) { |
2394 | $resultBills[] = $billDict[$bid]; |
2395 | } |
2396 | |
2397 | return $resultBills; |
2398 | } |
2399 | |
2400 | /** |
2401 | * Get all bill IDs of a project |
2402 | * |
2403 | * @param string $projectId |
2404 | * @return array |
2405 | */ |
2406 | public function getAllBillIds(string $projectId): array { |
2407 | $billIds = []; |
2408 | $qb = $this->db->getQueryBuilder(); |
2409 | $qb->select('id') |
2410 | ->from('cospend_bills', 'b') |
2411 | ->where( |
2412 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
2413 | ); |
2414 | $req = $qb->executeQuery(); |
2415 | |
2416 | while ($row = $req->fetch()){ |
2417 | $billIds[] = (int) $row['id']; |
2418 | } |
2419 | $req->closeCursor(); |
2420 | $qb->resetQueryParts(); |
2421 | |
2422 | return $billIds; |
2423 | } |
2424 | |
2425 | /** |
2426 | * Get members of a project |
2427 | * |
2428 | * @param string $projectId |
2429 | * @param string|null $order |
2430 | * @param int|null $lastchanged |
2431 | * @return array |
2432 | */ |
2433 | public function getMembers(string $projectId, ?string $order = null, ?int $lastchanged = null): array { |
2434 | $members = []; |
2435 | $qb = $this->db->getQueryBuilder(); |
2436 | |
2437 | $sqlOrder = 'name'; |
2438 | if ($order !== null) { |
2439 | if ($order === 'lowername') { |
2440 | $sqlOrder = $qb->func()->lower('name'); |
2441 | } else { |
2442 | $sqlOrder = $order; |
2443 | } |
2444 | } |
2445 | |
2446 | $qb->select('id', 'userid', 'name', 'weight', 'color', 'activated', 'lastchanged') |
2447 | ->from('cospend_members') |
2448 | ->where( |
2449 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
2450 | ); |
2451 | if ($lastchanged !== null) { |
2452 | $qb->andWhere( |
2453 | $qb->expr()->gt('lastchanged', $qb->createNamedParameter($lastchanged, IQueryBuilder::PARAM_INT)) |
2454 | ); |
2455 | } |
2456 | $qb->orderBy($sqlOrder, 'ASC'); |
2457 | $req = $qb->executeQuery(); |
2458 | |
2459 | while ($row = $req->fetch()){ |
2460 | $dbMemberId = (int) $row['id']; |
2461 | $dbWeight = (float) $row['weight']; |
2462 | $dbUserid = $row['userid']; |
2463 | $dbName = $row['name']; |
2464 | $dbActivated = (int) $row['activated']; |
2465 | $dbLastchanged = (int) $row['lastchanged']; |
2466 | $dbColor = $row['color']; |
2467 | if ($dbColor === null) { |
2468 | $av = $this->avatarManager->getGuestAvatar($dbName); |
2469 | $avatarBgColor = $av->avatarBackgroundColor($dbName); |
2470 | $dbColor = [ |
2471 | 'r' => $avatarBgColor->red(), |
2472 | 'g' => $avatarBgColor->green(), |
2473 | 'b' => $avatarBgColor->blue(), |
2474 | ]; |
2475 | } else { |
2476 | $dbColor = Utils::hexToRgb($dbColor); |
2477 | } |
2478 | |
2479 | $members[] = [ |
2480 | 'activated' => $dbActivated === 1, |
2481 | 'userid' => $dbUserid, |
2482 | 'name' => $dbName, |
2483 | 'id' => $dbMemberId, |
2484 | 'weight' => $dbWeight, |
2485 | 'color' => $dbColor, |
2486 | 'lastchanged' => $dbLastchanged, |
2487 | ]; |
2488 | } |
2489 | $req->closeCursor(); |
2490 | $qb->resetQueryParts(); |
2491 | return $members; |
2492 | } |
2493 | |
2494 | /** |
2495 | * Get members balances for a project |
2496 | * |
2497 | * @param string $projectId |
2498 | * @param int|null $maxTimestamp |
2499 | * @return array |
2500 | */ |
2501 | private function getBalance(string $projectId, ?int $maxTimestamp = null): array { |
2502 | $membersWeight = []; |
2503 | $membersBalance = []; |
2504 | |
2505 | $members = $this->getMembers($projectId); |
2506 | foreach ($members as $member) { |
2507 | $memberId = $member['id']; |
2508 | $memberWeight = $member['weight']; |
2509 | $membersWeight[$memberId] = $memberWeight; |
2510 | $membersBalance[$memberId] = 0.0; |
2511 | } |
2512 | |
2513 | $bills = $this->getBills($projectId, null, $maxTimestamp); |
2514 | foreach ($bills as $bill) { |
2515 | $payerId = $bill['payer_id']; |
2516 | $amount = $bill['amount']; |
2517 | $owers = $bill['owers']; |
2518 | |
2519 | $membersBalance[$payerId] += $amount; |
2520 | |
2521 | $nbOwerShares = 0.0; |
2522 | foreach ($owers as $ower) { |
2523 | $owerWeight = $ower['weight']; |
2524 | if ($owerWeight === 0.0) { |
2525 | $owerWeight = 1.0; |
2526 | } |
2527 | $nbOwerShares += $owerWeight; |
2528 | } |
2529 | foreach ($owers as $ower) { |
2530 | $owerWeight = $ower['weight']; |
2531 | if ($owerWeight === 0.0) { |
2532 | $owerWeight = 1.0; |
2533 | } |
2534 | $owerId = $ower['id']; |
2535 | $spent = $amount / $nbOwerShares * $owerWeight; |
2536 | $membersBalance[$owerId] -= $spent; |
2537 | } |
2538 | } |
2539 | |
2540 | return $membersBalance; |
2541 | } |
2542 | |
2543 | /** |
2544 | * Check if a user is member of a given circle |
2545 | * |
2546 | * @param string $userId |
2547 | * @param string $circleId |
2548 | * @return bool |
2549 | */ |
2550 | private function isUserInCircle(string $userId, string $circleId): bool { |
2551 | try { |
2552 | $circlesManager = \OC::$server->get(\OCA\Circles\CirclesManager::class); |
2553 | $circlesManager->startSuperSession(); |
2554 | } catch (Exception $e) { |
2555 | return false; |
2556 | } |
2557 | try { |
2558 | $circle = $circlesManager->getCircle($circleId); |
2559 | } catch (\OCA\Circles\Exceptions\CircleNotFoundException $e) { |
2560 | $circlesManager->stopSession(); |
2561 | return false; |
2562 | } |
2563 | // is the circle owner |
2564 | $owner = $circle->getOwner(); |
2565 | // the owner is also a member so this might be useless... |
2566 | if ($owner->getUserType() === 1 && $owner->getUserId() === $userId) { |
2567 | $circlesManager->stopSession(); |
2568 | return true; |
2569 | } else { |
2570 | $members = $circle->getMembers(); |
2571 | foreach ($members as $m) { |
2572 | // is member of this circle |
2573 | if ($m->getUserType() === 1 && $m->getUserId() === $userId) { |
2574 | $circlesManager->stopSession(); |
2575 | return true; |
2576 | } |
2577 | } |
2578 | } |
2579 | $circlesManager->stopSession(); |
2580 | return false; |
2581 | } |
2582 | |
2583 | /** |
2584 | * For all projects the user has access to, get id => name |
2585 | * |
2586 | * @param string $userId |
2587 | * @return array |
2588 | * @throws \OCP\DB\Exception |
2589 | */ |
2590 | public function getProjectNames(?string $userId): array { |
2591 | if (is_null($userId)) { |
2592 | return []; |
2593 | } |
2594 | |
2595 | $projectNames = []; |
2596 | |
2597 | $qb = $this->db->getQueryBuilder(); |
2598 | |
2599 | $qb->select('id', 'name') |
2600 | ->from('cospend_projects', 'p') |
2601 | ->where( |
2602 | $qb->expr()->eq('userid', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) |
2603 | ); |
2604 | $req = $qb->executeQuery(); |
2605 | |
2606 | while ($row = $req->fetch()){ |
2607 | $projectNames[$row['id']] = $row['name']; |
2608 | } |
2609 | $req->closeCursor(); |
2610 | |
2611 | $qb = $qb->resetQueryParts(); |
2612 | |
2613 | // shared with user |
2614 | $qb->select('p.id', 'p.name') |
2615 | ->from('cospend_projects', 'p') |
2616 | ->innerJoin('p', 'cospend_shares', 's', $qb->expr()->eq('p.id', 's.projectid')) |
2617 | ->where( |
2618 | $qb->expr()->eq('s.userid', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) |
2619 | ) |
2620 | ->andWhere( |
2621 | $qb->expr()->eq('s.type', $qb->createNamedParameter(Application::SHARE_TYPES['user'], IQueryBuilder::PARAM_STR)) |
2622 | ); |
2623 | $req = $qb->executeQuery(); |
2624 | |
2625 | while ($row = $req->fetch()){ |
2626 | // avoid putting twice the same project |
2627 | // this can happen with a share loop |
2628 | if (!isset($projectNames[$row['id']])) { |
2629 | $projectNames[$row['id']] = $row['name']; |
2630 | } |
2631 | } |
2632 | $req->closeCursor(); |
2633 | $qb = $qb->resetQueryParts(); |
2634 | |
2635 | // shared with one of the groups the user is member of |
2636 | $userO = $this->userManager->get($userId); |
2637 | |
2638 | // get group with which a project is shared |
2639 | $candidateGroupIds = []; |
2640 | $qb->select('userid') |
2641 | ->from('cospend_shares', 's') |
2642 | ->where( |
2643 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['group'], IQueryBuilder::PARAM_STR)) |
2644 | ) |
2645 | ->groupBy('userid'); |
2646 | $req = $qb->executeQuery(); |
2647 | while ($row = $req->fetch()){ |
2648 | $groupId = $row['userid']; |
2649 | $candidateGroupIds[] = $groupId; |
2650 | } |
2651 | $req->closeCursor(); |
2652 | $qb = $qb->resetQueryParts(); |
2653 | |
2654 | // is the user member of these groups? |
2655 | foreach ($candidateGroupIds as $candidateGroupId) { |
2656 | $group = $this->groupManager->get($candidateGroupId); |
2657 | if ($group !== null && $group->inGroup($userO)) { |
2658 | // get projects shared with this group |
2659 | $qb->select('p.id', 'p.name') |
2660 | ->from('cospend_projects', 'p') |
2661 | ->innerJoin('p', 'cospend_shares', 's', $qb->expr()->eq('p.id', 's.projectid')) |
2662 | ->where( |
2663 | $qb->expr()->eq('s.userid', $qb->createNamedParameter($candidateGroupId, IQueryBuilder::PARAM_STR)) |
2664 | ) |
2665 | ->andWhere( |
2666 | $qb->expr()->eq('s.type', $qb->createNamedParameter(Application::SHARE_TYPES['group'], IQueryBuilder::PARAM_STR)) |
2667 | ); |
2668 | $req = $qb->executeQuery(); |
2669 | |
2670 | while ($row = $req->fetch()){ |
2671 | // avoid putting twice the same project |
2672 | // this can happen with a share loop |
2673 | if (!isset($projectNames[$row['id']])) { |
2674 | $projectNames[$row['id']] = $row['name']; |
2675 | } |
2676 | } |
2677 | $req->closeCursor(); |
2678 | $qb = $qb->resetQueryParts(); |
2679 | } |
2680 | } |
2681 | |
2682 | $circlesEnabled = $this->appManager->isEnabledForUser('circles'); |
2683 | if ($circlesEnabled) { |
2684 | // get circles with which a project is shared |
2685 | $candidateCircleIds = []; |
2686 | $qb->select('userid') |
2687 | ->from('cospend_shares', 's') |
2688 | ->where( |
2689 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['circle'], IQueryBuilder::PARAM_STR)) |
2690 | ) |
2691 | ->groupBy('userid'); |
2692 | $req = $qb->executeQuery(); |
2693 | while ($row = $req->fetch()){ |
2694 | $circleId = $row['userid']; |
2695 | $candidateCircleIds[] = $circleId; |
2696 | } |
2697 | $req->closeCursor(); |
2698 | $qb = $qb->resetQueryParts(); |
2699 | |
2700 | // is the user member of these circles? |
2701 | foreach ($candidateCircleIds as $candidateCircleId) { |
2702 | if ($this->isUserInCircle($userId, $candidateCircleId)) { |
2703 | // get projects shared with this circle |
2704 | $qb->select('p.id', 'p.name') |
2705 | ->from('cospend_projects', 'p') |
2706 | ->innerJoin('p', 'cospend_shares', 's', $qb->expr()->eq('p.id', 's.projectid')) |
2707 | ->where( |
2708 | $qb->expr()->eq('s.userid', $qb->createNamedParameter($candidateCircleId, IQueryBuilder::PARAM_STR)) |
2709 | ) |
2710 | ->andWhere( |
2711 | $qb->expr()->eq('s.type', $qb->createNamedParameter(Application::SHARE_TYPES['circle'], IQueryBuilder::PARAM_STR)) |
2712 | ); |
2713 | $req = $qb->executeQuery(); |
2714 | |
2715 | while ($row = $req->fetch()){ |
2716 | // avoid putting twice the same project |
2717 | // this can happen with a share loop or multiple shares |
2718 | if (!isset($projectNames[$row['id']])) { |
2719 | $projectNames[$row['id']] = $row['name']; |
2720 | } |
2721 | } |
2722 | $req->closeCursor(); |
2723 | $qb = $qb->resetQueryParts(); |
2724 | } |
2725 | } |
2726 | } |
2727 | return $projectNames; |
2728 | } |
2729 | |
2730 | /** |
2731 | * Get detailed project list for a given NC user |
2732 | * |
2733 | * @param string $userId |
2734 | * @return array |
2735 | */ |
2736 | public function getProjects(string $userId): array { |
2737 | $projectids = array_keys($this->getProjectNames($userId)); |
2738 | |
2739 | // get the projects |
2740 | $projects = []; |
2741 | foreach ($projectids as $projectid) { |
2742 | $project = $this->getProjectInfo($projectid); |
2743 | $project['myaccesslevel'] = $this->getUserMaxAccessLevel($userId, $projectid); |
2744 | $projects[] = $project; |
2745 | } |
2746 | |
2747 | return $projects; |
2748 | } |
2749 | |
2750 | /** |
2751 | * Get categories of a given project |
2752 | * |
2753 | * @param string $projectid |
2754 | * @param bool $getCategories |
2755 | * @return array |
2756 | * @throws \OCP\DB\Exception |
2757 | */ |
2758 | public function getCategoriesOrPaymentModes(string $projectid, bool $getCategories = true): array { |
2759 | $elements = []; |
2760 | |
2761 | $qb = $this->db->getQueryBuilder(); |
2762 | |
2763 | if ($getCategories) { |
2764 | $sortOrderField = 'categorysort'; |
2765 | $billTableField = 'categoryid'; |
2766 | $dbTable = 'cospend_categories'; |
2767 | $alias = 'cat'; |
2768 | } else { |
2769 | $sortOrderField = 'paymentmodesort'; |
2770 | $billTableField = 'paymentmodeid'; |
2771 | $dbTable = 'cospend_paymentmodes'; |
2772 | $alias = 'pm'; |
2773 | } |
2774 | |
2775 | // get sort method |
2776 | $qb->select($sortOrderField) |
2777 | ->from('cospend_projects', 'p') |
2778 | ->where( |
2779 | $qb->expr()->eq('id', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
2780 | ); |
2781 | $req = $qb->executeQuery(); |
2782 | $sortMethod = Application::SORT_ORDERS['alpha']; |
2783 | while ($row = $req->fetch()) { |
2784 | $sortMethod = $row[$sortOrderField]; |
2785 | break; |
2786 | } |
2787 | $req->closeCursor(); |
2788 | $qb->resetQueryParts(); |
2789 | |
2790 | if ($sortMethod === Application::SORT_ORDERS['manual'] || $sortMethod === Application::SORT_ORDERS['alpha']) { |
2791 | if ($getCategories) { |
2792 | $qb = $qb->select('name', 'id', 'encoded_icon', 'color', 'order'); |
2793 | } else { |
2794 | $qb = $qb->select('name', 'id', 'encoded_icon', 'color', 'order', 'old_id'); |
2795 | } |
2796 | $qb->from($dbTable, 'c') |
2797 | ->where( |
2798 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
2799 | ); |
2800 | $req = $qb->executeQuery(); |
2801 | while ($row = $req->fetch()) { |
2802 | $dbName = $row['name']; |
2803 | $dbIcon = urldecode($row['encoded_icon']); |
2804 | $dbColor = $row['color']; |
2805 | $dbId = (int) $row['id']; |
2806 | $dbOrder = (int) $row['order']; |
2807 | $elements[$dbId] = [ |
2808 | 'name' => $dbName, |
2809 | 'icon' => $dbIcon, |
2810 | 'color' => $dbColor, |
2811 | 'id' => $dbId, |
2812 | 'order' => $dbOrder, |
2813 | ]; |
2814 | if (!$getCategories) { |
2815 | $elements[$dbId]['old_id'] = $row['old_id']; |
2816 | } |
2817 | } |
2818 | $req->closeCursor(); |
2819 | $qb->resetQueryParts(); |
2820 | } elseif ($sortMethod === Application::SORT_ORDERS['most_used'] || $sortMethod === Application::SORT_ORDERS['most_recently_used']) { |
2821 | // get all categories/paymentmodes |
2822 | if ($getCategories) { |
2823 | $qb = $qb->select('name', 'id', 'encoded_icon', 'color'); |
2824 | } else { |
2825 | $qb = $qb->select('name', 'id', 'encoded_icon', 'color', 'old_id'); |
2826 | } |
2827 | $qb->from($dbTable, 'c') |
2828 | ->where( |
2829 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
2830 | ); |
2831 | $req = $qb->executeQuery(); |
2832 | while ($row = $req->fetch()) { |
2833 | $dbName = $row['name']; |
2834 | $dbIcon = urldecode($row['encoded_icon']); |
2835 | $dbColor = $row['color']; |
2836 | $dbId = (int) $row['id']; |
2837 | $elements[$dbId] = [ |
2838 | 'name' => $dbName, |
2839 | 'icon' => $dbIcon, |
2840 | 'color' => $dbColor, |
2841 | 'id' => $dbId, |
2842 | 'order' => null, |
2843 | ]; |
2844 | if (!$getCategories) { |
2845 | $elements[$dbId]['old_id'] = $row['old_id']; |
2846 | } |
2847 | } |
2848 | $req->closeCursor(); |
2849 | $qb->resetQueryParts(); |
2850 | // now we get the order |
2851 | if ($sortMethod === Application::SORT_ORDERS['most_used']) { |
2852 | // sort by most used |
2853 | // first get list of most used |
2854 | $mostUsedOrder = []; |
2855 | $qb->select($alias . '.id') |
2856 | ->from($dbTable, $alias) |
2857 | ->innerJoin($alias, 'cospend_bills', 'bill', $qb->expr()->eq($alias . '.id', 'bill.' . $billTableField)) |
2858 | ->where( |
2859 | $qb->expr()->eq($alias . '.projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
2860 | ) |
2861 | ->orderBy($qb->func()->count($alias . '.id'), 'DESC') |
2862 | ->groupBy($alias . '.id'); |
2863 | $req = $qb->executeQuery(); |
2864 | $order = 0; |
2865 | while ($row = $req->fetch()) { |
2866 | $dbId = (int) $row['id']; |
2867 | $mostUsedOrder[$dbId] = $order++; |
2868 | } |
2869 | $req->closeCursor(); |
2870 | $qb->resetQueryParts(); |
2871 | // affect order |
2872 | foreach ($elements as $cid => $cat) { |
2873 | // fallback order is more than max order |
2874 | $elements[$cid]['order'] = $mostUsedOrder[$cid] ?? $order; |
2875 | } |
2876 | } elseif ($sortMethod === Application::SORT_ORDERS['most_recently_used']) { |
2877 | // sort by most recently used |
2878 | $mostUsedOrder = []; |
2879 | $qb->select($alias . '.id') |
2880 | ->from($dbTable, $alias) |
2881 | ->innerJoin($alias, 'cospend_bills', 'bill', $qb->expr()->eq($alias . '.id', 'bill.' . $billTableField)) |
2882 | ->where( |
2883 | $qb->expr()->eq($alias . '.projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
2884 | ) |
2885 | ->orderBy($qb->func()->max('bill.timestamp'), 'DESC') |
2886 | ->groupBy($alias . '.id'); |
2887 | $req = $qb->executeQuery(); |
2888 | $order = 0; |
2889 | while ($row = $req->fetch()) { |
2890 | $dbId = (int) $row['id']; |
2891 | $mostUsedOrder[$dbId] = $order++; |
2892 | } |
2893 | $req->closeCursor(); |
2894 | $qb->resetQueryParts(); |
2895 | // affect order |
2896 | foreach ($elements as $elemId => $element) { |
2897 | // fallback order is more than max order |
2898 | $elements[$elemId]['order'] = $mostUsedOrder[$elemId] ?? $order; |
2899 | } |
2900 | } |
2901 | } |
2902 | |
2903 | return $elements; |
2904 | } |
2905 | |
2906 | /** |
2907 | * Get currencies of a project |
2908 | * |
2909 | * @param string $projectid |
2910 | * @return array |
2911 | */ |
2912 | private function getCurrencies(string $projectid): array { |
2913 | $currencies = []; |
2914 | |
2915 | $qb = $this->db->getQueryBuilder(); |
2916 | $qb->select('name', 'id', 'exchange_rate') |
2917 | ->from('cospend_currencies') |
2918 | ->where( |
2919 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
2920 | ); |
2921 | $req = $qb->executeQuery(); |
2922 | while ($row = $req->fetch()){ |
2923 | $dbName = $row['name']; |
2924 | $dbId = (int) $row['id']; |
2925 | $dbExchangeRate = (float) $row['exchange_rate']; |
2926 | $currencies[] = [ |
2927 | 'name' => $dbName, |
2928 | 'exchange_rate' => $dbExchangeRate, |
2929 | 'id' => $dbId, |
2930 | ]; |
2931 | } |
2932 | $req->closeCursor(); |
2933 | $qb->resetQueryParts(); |
2934 | |
2935 | return $currencies; |
2936 | } |
2937 | |
2938 | /** |
2939 | * Get user shared access of a project |
2940 | * |
2941 | * @param string $projectid |
2942 | * @return array |
2943 | */ |
2944 | private function getUserShares(string $projectid): array { |
2945 | $shares = []; |
2946 | $userIdToName = []; |
2947 | $sharesToDelete = []; |
2948 | |
2949 | $qb = $this->db->getQueryBuilder(); |
2950 | $qb->select('projectid', 'userid', 'id', 'accesslevel', 'manually_added') |
2951 | ->from('cospend_shares') |
2952 | ->where( |
2953 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
2954 | ) |
2955 | ->andWhere( |
2956 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['user'], IQueryBuilder::PARAM_STR)) |
2957 | ); |
2958 | $req = $qb->executeQuery(); |
2959 | while ($row = $req->fetch()){ |
2960 | $dbuserId = $row['userid']; |
2961 | $dbId = (int) $row['id']; |
2962 | $dbAccessLevel = (int) $row['accesslevel']; |
2963 | $dbManuallyAdded = (int) $row['manually_added']; |
2964 | if (array_key_exists($dbuserId, $userIdToName)) { |
2965 | $name = $userIdToName[$dbuserId]; |
2966 | } else { |
2967 | $user = $this->userManager->get($dbuserId); |
2968 | if ($user !== null) { |
2969 | $userIdToName[$user->getUID()] = $user->getDisplayName(); |
2970 | $name = $user->getDisplayName(); |
2971 | } else { |
2972 | $sharesToDelete[] = $dbId; |
2973 | continue; |
2974 | } |
2975 | } |
2976 | $shares[] = [ |
2977 | 'userid' => $dbuserId, |
2978 | 'name' => $name, |
2979 | 'id' => $dbId, |
2980 | 'accesslevel' => $dbAccessLevel, |
2981 | 'type' => Application::SHARE_TYPES['user'], |
2982 | 'manually_added' => $dbManuallyAdded === 1, |
2983 | ]; |
2984 | } |
2985 | $req->closeCursor(); |
2986 | $qb->resetQueryParts(); |
2987 | |
2988 | // delete shares pointing to unfound users |
2989 | foreach ($sharesToDelete as $shId) { |
2990 | $this->deleteUserShare($projectid, $shId); |
2991 | } |
2992 | |
2993 | return $shares; |
2994 | } |
2995 | |
2996 | /** |
2997 | * Get public links of a project |
2998 | * |
2999 | * @param string $projectid |
3000 | * @param int|null $maxAccessLevel |
3001 | * @return array |
3002 | */ |
3003 | public function getPublicShares(string $projectid, ?int $maxAccessLevel = null): array { |
3004 | $shares = []; |
3005 | |
3006 | $qb = $this->db->getQueryBuilder(); |
3007 | $qb->select('projectid', 'userid', 'id', 'accesslevel', 'label', 'password') |
3008 | ->from('cospend_shares') |
3009 | ->where( |
3010 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
3011 | ) |
3012 | ->andWhere( |
3013 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['public_link'], IQueryBuilder::PARAM_STR)) |
3014 | ); |
3015 | if (!is_null($maxAccessLevel)) { |
3016 | $qb->andWhere( |
3017 | $qb->expr()->lte('accesslevel', $qb->createNamedParameter($maxAccessLevel, IQueryBuilder::PARAM_INT)) |
3018 | ); |
3019 | } |
3020 | $req = $qb->executeQuery(); |
3021 | while ($row = $req->fetch()){ |
3022 | $dbToken = $row['userid']; |
3023 | $dbId = (int) $row['id']; |
3024 | $dbAccessLevel = (int) $row['accesslevel']; |
3025 | $dbLabel = $row['label']; |
3026 | $dbPassword = $row['password']; |
3027 | $shares[] = [ |
3028 | 'token' => $dbToken, |
3029 | 'id' => $dbId, |
3030 | 'accesslevel' => $dbAccessLevel, |
3031 | 'label' => $dbLabel, |
3032 | 'password' => $dbPassword, |
3033 | 'type' => Application::SHARE_TYPES['public_link'], |
3034 | ]; |
3035 | } |
3036 | $req->closeCursor(); |
3037 | $qb->resetQueryParts(); |
3038 | |
3039 | return $shares; |
3040 | } |
3041 | |
3042 | /** |
3043 | * Get project info for a given public share token |
3044 | * |
3045 | * @param string $token |
3046 | * @return array |
3047 | */ |
3048 | public function getProjectInfoFromShareToken(string $token): ?array { |
3049 | $projectInfo = null; |
3050 | |
3051 | $qb = $this->db->getQueryBuilder(); |
3052 | $qb->select('projectid', 'accesslevel', 'label', 'password') |
3053 | ->from('cospend_shares') |
3054 | ->where( |
3055 | $qb->expr()->eq('userid', $qb->createNamedParameter($token, IQueryBuilder::PARAM_STR)) |
3056 | ) |
3057 | ->andWhere( |
3058 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['public_link'], IQueryBuilder::PARAM_STR)) |
3059 | ); |
3060 | $req = $qb->executeQuery(); |
3061 | while ($row = $req->fetch()){ |
3062 | $projectId = $row['projectid']; |
3063 | $label = $row['label']; |
3064 | $password = $row['password']; |
3065 | $accessLevel = (int) $row['accesslevel']; |
3066 | $projectInfo = [ |
3067 | 'projectid' => $projectId, |
3068 | 'accesslevel' => $accessLevel, |
3069 | 'label' => $label, |
3070 | 'password' => $password, |
3071 | ]; |
3072 | break; |
3073 | } |
3074 | $req->closeCursor(); |
3075 | $qb->resetQueryParts(); |
3076 | |
3077 | return $projectInfo; |
3078 | } |
3079 | |
3080 | /** |
3081 | * Get group shared access list of a project |
3082 | * |
3083 | * @param string $projectid |
3084 | * @return array |
3085 | */ |
3086 | private function getGroupShares(string $projectid): array { |
3087 | $shares = []; |
3088 | $groupIdToName = []; |
3089 | $sharesToDelete = []; |
3090 | |
3091 | $qb = $this->db->getQueryBuilder(); |
3092 | $qb->select('projectid', 'userid', 'id', 'accesslevel') |
3093 | ->from('cospend_shares') |
3094 | ->where( |
3095 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
3096 | ) |
3097 | ->andWhere( |
3098 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['group'], IQueryBuilder::PARAM_STR)) |
3099 | ); |
3100 | $req = $qb->executeQuery(); |
3101 | while ($row = $req->fetch()){ |
3102 | $dbGroupId = $row['userid']; |
3103 | $dbId = (int) $row['id']; |
3104 | $dbAccessLevel = (int) $row['accesslevel']; |
3105 | if (array_key_exists($dbGroupId, $groupIdToName)) { |
3106 | $name = $groupIdToName[$dbGroupId]; |
3107 | } else { |
3108 | if ($this->groupManager->groupExists($dbGroupId)) { |
3109 | $name = $this->groupManager->get($dbGroupId)->getDisplayName(); |
3110 | $groupIdToName[$dbGroupId] = $name; |
3111 | } else { |
3112 | $sharesToDelete[] = $dbId; |
3113 | continue; |
3114 | } |
3115 | } |
3116 | $shares[] = [ |
3117 | 'groupid' => $dbGroupId, |
3118 | 'name' => $name, |
3119 | 'id' => $dbId, |
3120 | 'accesslevel' => $dbAccessLevel, |
3121 | 'type' => Application::SHARE_TYPES['group'], |
3122 | ]; |
3123 | } |
3124 | $req->closeCursor(); |
3125 | $qb->resetQueryParts(); |
3126 | |
3127 | foreach ($sharesToDelete as $shId) { |
3128 | $this->deleteGroupShare($projectid, $shId); |
3129 | } |
3130 | |
3131 | return $shares; |
3132 | } |
3133 | |
3134 | /** |
3135 | * Get circle shared access list of a project |
3136 | * |
3137 | * @param string $projectid |
3138 | * @return array |
3139 | */ |
3140 | private function getCircleShares(string $projectid): array { |
3141 | $shares = []; |
3142 | |
3143 | $circlesEnabled = $this->appManager->isEnabledForUser('circles'); |
3144 | if ($circlesEnabled) { |
3145 | try { |
3146 | $circlesManager = \OC::$server->get(\OCA\Circles\CirclesManager::class); |
3147 | $circlesManager->startSuperSession(); |
3148 | } catch (Exception $e) { |
3149 | return []; |
3150 | } |
3151 | $qb = $this->db->getQueryBuilder(); |
3152 | $qb->select('projectid', 'userid', 'id', 'accesslevel') |
3153 | ->from('cospend_shares') |
3154 | ->where( |
3155 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
3156 | ) |
3157 | ->andWhere( |
3158 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['circle'], IQueryBuilder::PARAM_STR)) |
3159 | ); |
3160 | $req = $qb->executeQuery(); |
3161 | while ($row = $req->fetch()) { |
3162 | $dbCircleId = $row['userid']; |
3163 | $dbId = (int) $row['id']; |
3164 | $dbAccessLevel = (int) $row['accesslevel']; |
3165 | try { |
3166 | $circle = $circlesManager->getCircle($dbCircleId); |
3167 | $shares[] = [ |
3168 | 'circleid' => $dbCircleId, |
3169 | 'name' => $circle->getDisplayName(), |
3170 | 'id' => $dbId, |
3171 | 'accesslevel' => $dbAccessLevel, |
3172 | 'type' => Application::SHARE_TYPES['circle'], |
3173 | ]; |
3174 | } catch (\OCA\Circles\Exceptions\CircleNotFoundException $e) { |
3175 | } |
3176 | } |
3177 | $req->closeCursor(); |
3178 | $qb->resetQueryParts(); |
3179 | $circlesManager->stopSession(); |
3180 | } |
3181 | return $shares; |
3182 | } |
3183 | |
3184 | /** |
3185 | * Delete a member |
3186 | * |
3187 | * @param string $projectid |
3188 | * @param int $memberid |
3189 | * @return array |
3190 | */ |
3191 | public function deleteMember(string $projectid, int $memberid): array { |
3192 | $memberToDelete = $this->getMemberById($projectid, $memberid); |
3193 | if ($memberToDelete !== null) { |
3194 | $qb = $this->db->getQueryBuilder(); |
3195 | if (count($this->getBillsOfMember($memberid)) === 0) { |
3196 | $qb->delete('cospend_members') |
3197 | ->where( |
3198 | $qb->expr()->eq('id', $qb->createNamedParameter($memberid, IQueryBuilder::PARAM_INT)) |
3199 | ); |
3200 | $qb->executeStatement(); |
3201 | $qb->resetQueryParts(); |
3202 | } elseif ($memberToDelete['activated']) { |
3203 | $qb->update('cospend_members'); |
3204 | $qb->set('activated', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)); |
3205 | $qb->where( |
3206 | $qb->expr()->eq('id', $qb->createNamedParameter($memberid, IQueryBuilder::PARAM_INT)) |
3207 | ) |
3208 | ->andWhere( |
3209 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
3210 | ); |
3211 | $qb->executeStatement(); |
3212 | $qb->resetQueryParts(); |
3213 | } |
3214 | return ['success' => true]; |
3215 | } else { |
3216 | return ['Not Found']; |
3217 | } |
3218 | } |
3219 | |
3220 | /** |
3221 | * Get bills involving a member (as a payer or an ower) |
3222 | * |
3223 | * @param int $memberid |
3224 | * @return array |
3225 | * @throws \OCP\DB\Exception |
3226 | */ |
3227 | public function getBillsOfMember(int $memberid): array { |
3228 | $qb = $this->db->getQueryBuilder(); |
3229 | $qb->select('bi.id') |
3230 | ->from('cospend_bill_owers', 'bo') |
3231 | ->innerJoin('bo', 'cospend_bills', 'bi', $qb->expr()->eq('bo.billid', 'bi.id')) |
3232 | ->innerJoin('bo', 'cospend_members', 'm', $qb->expr()->eq('bo.memberid', 'm.id')) |
3233 | ->where( |
3234 | $qb->expr()->eq('bi.payerid', $qb->createNamedParameter($memberid, IQueryBuilder::PARAM_INT)) |
3235 | ) |
3236 | ->orWhere( |
3237 | $qb->expr()->eq('bo.memberid', $qb->createNamedParameter($memberid, IQueryBuilder::PARAM_INT)) |
3238 | ); |
3239 | $req = $qb->executeQuery(); |
3240 | |
3241 | $billIds = []; |
3242 | while ($row = $req->fetch()) { |
3243 | $billIds[] = $row['id']; |
3244 | } |
3245 | return $billIds; |
3246 | } |
3247 | |
3248 | /** |
3249 | * Get a member from its name |
3250 | * |
3251 | * @param string $projectId |
3252 | * @param string $name |
3253 | * @return array|null |
3254 | */ |
3255 | public function getMemberByName(string $projectId, string $name): ?array { |
3256 | $member = null; |
3257 | $qb = $this->db->getQueryBuilder(); |
3258 | $qb->select('id', 'userid', 'name', 'weight', 'color', 'activated') |
3259 | ->from('cospend_members') |
3260 | ->where( |
3261 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
3262 | ) |
3263 | ->andWhere( |
3264 | $qb->expr()->eq('name', $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR)) |
3265 | ); |
3266 | $req = $qb->executeQuery(); |
3267 | |
3268 | while ($row = $req->fetch()){ |
3269 | $dbMemberId = (int) $row['id']; |
3270 | $dbWeight = (float) $row['weight']; |
3271 | $dbUserid = $row['userid']; |
3272 | $dbName = $row['name']; |
3273 | $dbActivated= (int) $row['activated']; |
3274 | $dbColor = $row['color']; |
3275 | if ($dbColor === null) { |
3276 | $av = $this->avatarManager->getGuestAvatar($dbName); |
3277 | $dbColor = $av->avatarBackgroundColor($dbName); |
3278 | $dbColor = [ |
3279 | 'r' => $dbColor->red(), |
3280 | 'g' => $dbColor->green(), |
3281 | 'b' => $dbColor->blue(), |
3282 | ]; |
3283 | } else { |
3284 | $dbColor = Utils::hexToRgb($dbColor); |
3285 | } |
3286 | $member = [ |
3287 | 'activated' => $dbActivated === 1, |
3288 | 'userid' => $dbUserid, |
3289 | 'name' => $dbName, |
3290 | 'id' => $dbMemberId, |
3291 | 'weight' => $dbWeight, |
3292 | 'color' => $dbColor, |
3293 | ]; |
3294 | break; |
3295 | } |
3296 | $req->closeCursor(); |
3297 | $qb->resetQueryParts(); |
3298 | return $member; |
3299 | } |
3300 | |
3301 | /** |
3302 | * Get a member from its user ID |
3303 | * |
3304 | * @param string $projectId |
3305 | * @param string|null $userid |
3306 | * @return array|null |
3307 | */ |
3308 | public function getMemberByUserid(string $projectId, ?string $userid): ?array { |
3309 | $member = null; |
3310 | if ($userid !== null) { |
3311 | $qb = $this->db->getQueryBuilder(); |
3312 | $qb->select('id', 'userid', 'name', 'weight', 'color', 'activated') |
3313 | ->from('cospend_members') |
3314 | ->where( |
3315 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
3316 | ) |
3317 | ->andWhere( |
3318 | $qb->expr()->eq('userid', $qb->createNamedParameter($userid, IQueryBuilder::PARAM_STR)) |
3319 | ); |
3320 | $req = $qb->executeQuery(); |
3321 | |
3322 | while ($row = $req->fetch()){ |
3323 | $dbMemberId = (int) $row['id']; |
3324 | $dbWeight = (float) $row['weight']; |
3325 | $dbUserid = $row['userid']; |
3326 | $dbName = $row['name']; |
3327 | $dbActivated= (int) $row['activated']; |
3328 | $dbColor = $row['color']; |
3329 | if ($dbColor === null) { |
3330 | $av = $this->avatarManager->getGuestAvatar($dbName); |
3331 | $dbColor = $av->avatarBackgroundColor($dbName); |
3332 | $dbColor = [ |
3333 | 'r' => $dbColor->red(), |
3334 | 'g' => $dbColor->green(), |
3335 | 'b' => $dbColor->blue(), |
3336 | ]; |
3337 | } else { |
3338 | $dbColor = Utils::hexToRgb($dbColor); |
3339 | } |
3340 | $member = [ |
3341 | 'activated' => $dbActivated === 1, |
3342 | 'userid' => $dbUserid, |
3343 | 'name' => $dbName, |
3344 | 'id' => $dbMemberId, |
3345 | 'weight' => $dbWeight, |
3346 | 'color' => $dbColor, |
3347 | ]; |
3348 | break; |
3349 | } |
3350 | $req->closeCursor(); |
3351 | $qb->resetQueryParts(); |
3352 | } |
3353 | return $member; |
3354 | } |
3355 | |
3356 | /** |
3357 | * Edit a bill |
3358 | * |
3359 | * @param string $projectid |
3360 | * @param int $billid |
3361 | * @param string|null $date |
3362 | * @param string|null $what |
3363 | * @param int|null $payer |
3364 | * @param string|null $payed_for |
3365 | * @param float|null $amount |
3366 | * @param string|null $repeat |
3367 | * @param string|null $paymentmode |
3368 | * @param int|null $paymentmodeid |
3369 | * @param int|null $categoryid |
3370 | * @param int|null $repeatallactive |
3371 | * @param string|null $repeatuntil |
3372 | * @param int|null $timestamp |
3373 | * @param string|null $comment |
3374 | * @param int|null $repeatfreq |
3375 | * @param array|null $paymentModes |
3376 | * @return array |
3377 | * @throws \OCP\DB\Exception |
3378 | */ |
3379 | public function editBill(string $projectid, int $billid, ?string $date, ?string $what, ?int $payer, ?string $payed_for, |
3380 | ?float $amount, ?string $repeat, ?string $paymentmode = null, ?int $paymentmodeid = null, |
3381 | ?int $categoryid = null, ?int $repeatallactive = null, ?string $repeatuntil = null, |
3382 | ?int $timestamp = null, ?string $comment = null, ?int $repeatfreq = null, |
3383 | ?array $paymentModes = null): array { |
3384 | // if we don't have the payment modes, get them now |
3385 | if (is_null($paymentModes)) { |
3386 | $paymentModes = $this->getCategoriesOrPaymentModes($projectid, false); |
3387 | } |
3388 | |
3389 | $qb = $this->db->getQueryBuilder(); |
3390 | $qb->update('cospend_bills'); |
3391 | |
3392 | // set last modification timestamp |
3393 | $ts = (new DateTime())->getTimestamp(); |
3394 | $qb->set('lastchanged', $qb->createNamedParameter($ts, IQueryBuilder::PARAM_INT)); |
3395 | |
3396 | // first check the bill exists |
3397 | if ($this->getBill($projectid, $billid) === null) { |
3398 | return ['message' => $this->trans->t('There is no such bill')]; |
3399 | } |
3400 | // then edit the hell of it |
3401 | if ($what !== null) { |
3402 | $qb->set('what', $qb->createNamedParameter($what, IQueryBuilder::PARAM_STR)); |
3403 | } |
3404 | |
3405 | if ($comment !== null) { |
3406 | $qb->set('comment', $qb->createNamedParameter($comment, IQueryBuilder::PARAM_STR)); |
3407 | } |
3408 | |
3409 | if ($repeat !== null && $repeat !== '') { |
3410 | if (in_array($repeat, array_values(Application::FREQUENCIES))) { |
3411 | $qb->set('repeat', $qb->createNamedParameter($repeat, IQueryBuilder::PARAM_STR)); |
3412 | } else { |
3413 | return ['repeat' => $this->trans->t('Invalid value')]; |
3414 | } |
3415 | } |
3416 | |
3417 | if ($repeatfreq !== null) { |
3418 | $qb->set('repeatfreq', $qb->createNamedParameter($repeatfreq, IQueryBuilder::PARAM_INT)); |
3419 | } |
3420 | |
3421 | if ($repeatuntil !== null) { |
3422 | if ($repeatuntil === '') { |
3423 | $qb->set('repeatuntil', $qb->createNamedParameter(null, IQueryBuilder::PARAM_STR)); |
3424 | } else { |
3425 | $qb->set('repeatuntil', $qb->createNamedParameter($repeatuntil, IQueryBuilder::PARAM_STR)); |
3426 | } |
3427 | } |
3428 | if ($repeatallactive !== null) { |
3429 | $qb->set('repeatallactive', $qb->createNamedParameter($repeatallactive, IQueryBuilder::PARAM_INT)); |
3430 | } |
3431 | // payment mode |
3432 | if (!is_null($paymentmodeid)) { |
3433 | // is the old_id set for this payment mode? if yes, use it for old 'paymentmode' column |
3434 | $paymentmode = 'n'; |
3435 | if (isset($paymentModes[$paymentmodeid], $paymentModes[$paymentmodeid]['old_id']) |
3436 | && $paymentModes[$paymentmodeid]['old_id'] !== null |
3437 | && $paymentModes[$paymentmodeid]['old_id'] !== '' |
3438 | ) { |
3439 | $paymentmode = $paymentModes[$paymentmodeid]['old_id']; |
3440 | } |
3441 | $qb->set('paymentmodeid', $qb->createNamedParameter($paymentmodeid, IQueryBuilder::PARAM_INT)); |
3442 | $qb->set('paymentmode', $qb->createNamedParameter($paymentmode, IQueryBuilder::PARAM_STR)); |
3443 | } elseif (!is_null($paymentmode)) { |
3444 | // is there a pm with this old id? if yes, use it for new id |
3445 | $paymentmodeid = 0; |
3446 | foreach ($paymentModes as $id => $pm) { |
3447 | if ($pm['old_id'] === $paymentmode) { |
3448 | $paymentmodeid = $id; |
3449 | break; |
3450 | } |
3451 | } |
3452 | $qb->set('paymentmodeid', $qb->createNamedParameter($paymentmodeid, IQueryBuilder::PARAM_INT)); |
3453 | $qb->set('paymentmode', $qb->createNamedParameter($paymentmode, IQueryBuilder::PARAM_STR)); |
3454 | } |
3455 | if ($categoryid !== null) { |
3456 | $qb->set('categoryid', $qb->createNamedParameter($categoryid, IQueryBuilder::PARAM_INT)); |
3457 | } |
3458 | // priority to timestamp (moneybuster might send both for a moment) |
3459 | if ($timestamp !== null) { |
3460 | $qb->set('timestamp', $qb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT)); |
3461 | } elseif ($date !== null && $date !== '') { |
3462 | $datetime = DateTime::createFromFormat('Y-m-d', $date); |
3463 | if ($datetime !== false) { |
3464 | $dateTs = $datetime->getTimestamp(); |
3465 | $qb->set('timestamp', $qb->createNamedParameter($dateTs, IQueryBuilder::PARAM_INT)); |
3466 | } else { |
3467 | return ['date' => $this->trans->t('Invalid value')]; |
3468 | } |
3469 | } |
3470 | if ($amount !== null) { |
3471 | $qb->set('amount', $qb->createNamedParameter($amount, IQueryBuilder::PARAM_STR)); |
3472 | } |
3473 | if ($payer !== null) { |
3474 | $member = $this->getMemberById($projectid, $payer); |
3475 | if ($member === null) { |
3476 | return ['payer' => $this->trans->t('Not a valid choice')]; |
3477 | } else { |
3478 | $qb->set('payerid', $qb->createNamedParameter($payer, IQueryBuilder::PARAM_INT)); |
3479 | } |
3480 | } |
3481 | |
3482 | $owerIds = null; |
3483 | // check owers |
3484 | if ($payed_for !== null && $payed_for !== '') { |
3485 | $owerIds = explode(',', $payed_for); |
3486 | if (count($owerIds) === 0) { |
3487 | return ['payed_for' => $this->trans->t('Invalid value')]; |
3488 | } else { |
3489 | foreach ($owerIds as $owerId) { |
3490 | if (!is_numeric($owerId)) { |
3491 | return ['payed_for' => $this->trans->t('Invalid value')]; |
3492 | } |
3493 | if ($this->getMemberById($projectid, $owerId) === null) { |
3494 | return ['payed_for' => $this->trans->t('Not a valid choice')]; |
3495 | } |
3496 | } |
3497 | } |
3498 | } |
3499 | |
3500 | // do it already! |
3501 | $qb->where( |
3502 | $qb->expr()->eq('id', $qb->createNamedParameter($billid, IQueryBuilder::PARAM_INT)) |
3503 | ) |
3504 | ->andWhere( |
3505 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
3506 | ); |
3507 | $qb->executeStatement(); |
3508 | $qb = $qb->resetQueryParts(); |
3509 | |
3510 | // edit the bill owers |
3511 | if ($owerIds !== null) { |
3512 | // delete old bill owers |
3513 | $this->deleteBillOwersOfBill($billid); |
3514 | // insert bill owers |
3515 | foreach ($owerIds as $owerId) { |
3516 | $qb->insert('cospend_bill_owers') |
3517 | ->values([ |
3518 | 'billid' => $qb->createNamedParameter($billid, IQueryBuilder::PARAM_INT), |
3519 | 'memberid' => $qb->createNamedParameter($owerId, IQueryBuilder::PARAM_INT) |
3520 | ]); |
3521 | $qb->executeStatement(); |
3522 | $qb = $qb->resetQueryParts(); |
3523 | } |
3524 | } |
3525 | |
3526 | $this->updateProjectLastChanged($projectid, $ts); |
3527 | |
3528 | return ['edited_bill_id' => $billid]; |
3529 | } |
3530 | |
3531 | /** |
3532 | * daily check of repeated bills |
3533 | * |
3534 | * @param int|null $billId |
3535 | * @return array |
3536 | */ |
3537 | public function cronRepeatBills(?int $billId = null): array { |
3538 | $result = []; |
3539 | $projects = []; |
3540 | $now = new DateTimeImmutable(); |
3541 | // in case cron job wasn't executed during several days, |
3542 | // continue trying to repeat bills as long as there was at least one repeated |
3543 | $continue = true; |
3544 | while ($continue) { |
3545 | $continue = false; |
3546 | // get bills whith repetition flag |
3547 | $qb = $this->db->getQueryBuilder(); |
3548 | $qb->select('id', 'projectid', 'what', 'timestamp', 'amount', 'payerid', 'repeat', 'repeatallactive', 'repeatfreq') |
3549 | ->from('cospend_bills', 'b') |
3550 | ->where( |
3551 | $qb->expr()->neq('repeat', $qb->createNamedParameter(Application::FREQUENCIES['no'], IQueryBuilder::PARAM_STR)) |
3552 | ); |
3553 | // we only repeat one bill |
3554 | if (!is_null($billId)) { |
3555 | $qb->andWhere( |
3556 | $qb->expr()->eq('id', $qb->createNamedParameter($billId, IQueryBuilder::PARAM_INT)) |
3557 | ); |
3558 | } |
3559 | $req = $qb->executeQuery(); |
3560 | $bills = []; |
3561 | /** @var DateTimeZone[] $timezoneByProjectId */ |
3562 | $timezoneByProjectId = []; |
3563 | while ($row = $req->fetch()) { |
3564 | $id = $row['id']; |
3565 | $what = $row['what']; |
3566 | $repeat = $row['repeat']; |
3567 | $repeatallactive = $row['repeatallactive']; |
3568 | $repeatfreq = (int) $row['repeatfreq']; |
3569 | $timestamp = $row['timestamp']; |
3570 | $projectid = $row['projectid']; |
3571 | $bills[] = [ |
3572 | 'id' => $id, |
3573 | 'what' => $what, |
3574 | 'repeat' => $repeat, |
3575 | 'repeatallactive' => $repeatallactive, |
3576 | 'repeatfreq' => $repeatfreq, |
3577 | 'projectid' => $projectid, |
3578 | 'timestamp' => $timestamp |
3579 | ]; |
3580 | if (!isset($timezoneByProjectId[$projectid])) { |
3581 | $timezoneByProjectId[$projectid] = $this->getProjectTimeZone($projectid); |
3582 | } |
3583 | } |
3584 | $req->closeCursor(); |
3585 | $qb->resetQueryParts(); |
3586 | |
3587 | foreach ($bills as $bill) { |
3588 | $billProjectId = $bill['projectid']; |
3589 | $billDate = (new DateTimeImmutable())->setTimestamp($bill['timestamp'])->setTimezone($timezoneByProjectId[$billProjectId]); |
3590 | $nextDate = $this->getNextRepetitionDate($bill, $billDate); |
3591 | |
3592 | // Unknown repeat interval |
3593 | if ($nextDate === null) { |
3594 | continue; |
3595 | } |
3596 | |
3597 | // Repeat if $nextDate is in the past (or today) |
3598 | $diff = $now->diff($nextDate); |
3599 | if ($nextDate->format('Y-m-d') === $now->format('Y-m-d') || $diff->invert) { |
3600 | $newBillId = $this->repeatBill($bill['projectid'], $bill['id'], $nextDate); |
3601 | // bill was not repeated (because of disabled owers or repeatuntil) |
3602 | if ($newBillId === null) { |
3603 | continue; |
3604 | } |
3605 | if (!array_key_exists($bill['projectid'], $projects)) { |
3606 | $projects[$bill['projectid']] = $this->getProjectInfo($bill['projectid']); |
3607 | } |
3608 | $result[] = [ |
3609 | 'new_bill_id' => $newBillId, |
3610 | 'date_orig' => $billDate->format('Y-m-d'), |
3611 | 'date_repeat' => $nextDate->format('Y-m-d'), |
3612 | 'what' => $bill['what'], |
3613 | 'project_name' => $projects[$bill['projectid']]['name'], |
3614 | ]; |
3615 | $continue = true; |
3616 | // when only repeating one bill, this newly created bill is the one we want to potentially repeat |
3617 | $billId = $newBillId; |
3618 | } |
3619 | } |
3620 | } |
3621 | return $result; |
3622 | } |
3623 | |
3624 | private function getProjectTimeZone(string $projectId): DateTimeZone { |
3625 | $projectInfo = $this->getProjectInfo($projectId); |
3626 | $userId = $projectInfo['userid']; |
3627 | $timeZone = $this->config->getUserValue($userId, 'core', 'timezone', null); |
3628 | $serverTimeZone = date_default_timezone_get() ?: 'UTC'; |
3629 | |
3630 | if ($timeZone === null) { |
3631 | $timeZone = $serverTimeZone; |
3632 | } |
3633 | |
3634 | try { |
3635 | return new DateTimeZone($timeZone); |
3636 | } catch (Exception $e) { |
3637 | return new DateTimeZone($serverTimeZone); |
3638 | } |
3639 | } |
3640 | |
3641 | private function copyBillPaymentModeOver(string $projectid, array $bill, string $toProjectId): int { |
3642 | $originPayments = $this->getCategoriesOrPaymentModes($projectid, false); |
3643 | $destinationPayments = $this->getCategoriesOrPaymentModes($toProjectId, false); |
3644 | |
3645 | if ($bill['paymentmodeid'] !== 0) { |
3646 | $originPayment = array_filter($originPayments, static function ($val) use ($bill) { |
3647 | return $val['id'] === $bill['paymentmodeid']; |
3648 | }); |
3649 | $originPayment = array_shift($originPayment); |
3650 | |
3651 | // find a payment mode with the same name |
3652 | $paymentNameMatches = array_filter($destinationPayments, static function ($val) use ($originPayment) { |
3653 | return $val['name'] === $originPayment['name']; |
3654 | }); |
3655 | |
3656 | // no payment mode match, means new mode |
3657 | if (count($paymentNameMatches) === 0) { |
3658 | return $this->addPaymentMode($toProjectId, $originPayment['name'], $originPayment['icon'], $originPayment['color']); |
3659 | } else { |
3660 | return array_shift($paymentNameMatches)['id']; |
3661 | } |
3662 | } |
3663 | |
3664 | return $bill['paymentmodeid']; |
3665 | } |
3666 | |
3667 | private function copyBillCategoryOver(string $projectid, array $bill, string $toProjectId): int { |
3668 | $originCategories = $this->getCategoriesOrPaymentModes($projectid); |
3669 | $destinationCategories = $this->getCategoriesOrPaymentModes($toProjectId); |
3670 | |
3671 | if ($bill['categoryid'] !== 0) { |
3672 | $originCategory = array_filter($originCategories, static function ($val) use ($bill) { |
3673 | return $val['id'] === $bill['categoryid']; |
3674 | }); |
3675 | $originCategory = array_shift($originCategory); |
3676 | |
3677 | // find a category with the same name |
3678 | $categoryNameMatches = array_filter($destinationCategories, static function ($val) use ($originCategory) { |
3679 | return $val['name'] === $originCategory['name']; |
3680 | }); |
3681 | |
3682 | // no category match, means new category |
3683 | if (count($categoryNameMatches) === 0) { |
3684 | return $this->addCategory($toProjectId, $originCategory['name'], $originCategory['icon'], $originCategory['color']); |
3685 | } else { |
3686 | return array_shift($categoryNameMatches)['id']; |
3687 | } |
3688 | } |
3689 | |
3690 | return $bill['categoryid']; |
3691 | } |
3692 | |
3693 | public function moveBill(string $projectid, int $billid, string $toProjectId): array { |
3694 | $bill = $this->getBill($projectid, $billid); |
3695 | |
3696 | // get all members in all the projects and try to match them |
3697 | $originMembers = $this->getMembers($projectid, 'lowername'); |
3698 | $destinationMembers = $this->getMembers($toProjectId, 'lowername'); |
3699 | |
3700 | // try to match them |
3701 | $originalPayer = $originMembers; |
3702 | $originalPayer = array_filter($originalPayer, static function ($val) use ($bill) { |
3703 | return $val['id'] === $bill['payer_id']; |
3704 | }); |
3705 | $originalPayer = array_shift($originalPayer); |
3706 | |
3707 | $newPayer = $destinationMembers; |
3708 | $newPayer = array_filter($newPayer, static function ($val) use ($originalPayer) { |
3709 | return $val['name'] === $originalPayer['name']; |
3710 | }); |
3711 | |
3712 | if (count($newPayer) < 1) { |
3713 | return ['message' => $this->trans->t('Cannot match payer')]; |
3714 | } |
3715 | |
3716 | $newPayer = array_shift($newPayer); |
3717 | |
3718 | // match owers too, these do not mind that much, the user will be able to modify the new invoice just after moving it |
3719 | $newOwers = array_filter($destinationMembers, static function ($member) use ($bill) { |
3720 | $matches = array_filter($bill['owers'], static function ($oldMember) use ($member) { |
3721 | return $oldMember['name'] === $member['name']; |
3722 | }); |
3723 | |
3724 | if (count($matches) === 0) { |
3725 | return false; |
3726 | } |
3727 | |
3728 | return true; |
3729 | }); |
3730 | |
3731 | $newCategoryId = $this->copyBillCategoryOver($projectid, $bill, $toProjectId); |
3732 | $newPaymentId = $this->copyBillPaymentModeOver($projectid, $bill, $toProjectId); |
3733 | |
3734 | $result = $this->addBill( |
3735 | $toProjectId, null, $bill['what'], $newPayer['id'], |
3736 | implode(',', array_column($newOwers, 'id')), $bill['amount'], $bill['repeat'], |
3737 | $bill['paymentmode'], $newPaymentId, |
3738 | $newCategoryId, $bill['repeatallactive'], $bill['repeatuntil'], |
3739 | $bill['timestamp'], $bill['comment'], $bill['repeatfreq'] |
3740 | ); |
3741 | |
3742 | if (!isset($result['inserted_id'])) { |
3743 | return ['message' => $this->trans->t('Cannot create new bill: %1$s', $result['message'])]; |
3744 | } |
3745 | |
3746 | // remove the old bill |
3747 | $this->deleteBill($projectid, $billid, true); |
3748 | |
3749 | return $result; |
3750 | } |
3751 | |
3752 | /** |
3753 | * duplicate the bill today and give it the repeat flag |
3754 | * remove the repeat flag on original bill |
3755 | * |
3756 | * @param string $projectid |
3757 | * @param int $billid |
3758 | * @param DateTimeImmutable $targetDatetime |
3759 | * @return int|null |
3760 | * @throws \OCP\DB\Exception |
3761 | */ |
3762 | private function repeatBill(string $projectid, int $billid, DateTimeImmutable $targetDatetime): ?int { |
3763 | $bill = $this->getBill($projectid, $billid); |
3764 | |
3765 | $owerIds = []; |
3766 | if (((int) $bill['repeatallactive']) === 1) { |
3767 | $pInfo = $this->getProjectInfo($projectid); |
3768 | foreach ($pInfo['active_members'] as $am) { |
3769 | $owerIds[] = $am['id']; |
3770 | } |
3771 | } else { |
3772 | foreach ($bill['owers'] as $ower) { |
3773 | if ($ower['activated']) { |
3774 | $owerIds[] = $ower['id']; |
3775 | } |
3776 | } |
3777 | } |
3778 | $owerIdsStr = implode(',', $owerIds); |
3779 | // if all owers are disabled, don't try to repeat the bill and remove repeat flag |
3780 | if (count($owerIds) === 0) { |
3781 | $this->editBill( |
3782 | $projectid, $billid, null, null, null, null, |
3783 | null, Application::FREQUENCIES['no'], null, null, |
3784 | null, null |
3785 | ); |
3786 | return null; |
3787 | } |
3788 | |
3789 | // if bill should be repeated only until... |
3790 | if ($bill['repeatuntil'] !== null && $bill['repeatuntil'] !== '') { |
3791 | $untilDate = DateTimeImmutable::createFromFormat('Y-m-d', $bill['repeatuntil']); |
3792 | if ($targetDatetime > $untilDate) { |
3793 | $this->editBill( |
3794 | $projectid, $billid, null, null, null, null, |
3795 | null, Application::FREQUENCIES['no'], null, null, |
3796 | null, null |
3797 | ); |
3798 | return null; |
3799 | } |
3800 | } |
3801 | |
3802 | $addBillResult = $this->addBill( |
3803 | $projectid, null, $bill['what'], $bill['payer_id'], |
3804 | $owerIdsStr, $bill['amount'], $bill['repeat'], |
3805 | $bill['paymentmode'], $bill['paymentmodeid'], |
3806 | $bill['categoryid'], $bill['repeatallactive'], $bill['repeatuntil'], |
3807 | $targetDatetime->getTimestamp(), $bill['comment'], $bill['repeatfreq'] |
3808 | ); |
3809 | |
3810 | $newBillId = $addBillResult['inserted_id'] ?? 0; |
3811 | |
3812 | $billObj = $this->billMapper->find($newBillId); |
3813 | $this->activityManager->triggerEvent( |
3814 | ActivityManager::COSPEND_OBJECT_BILL, $billObj, |
3815 | ActivityManager::SUBJECT_BILL_CREATE, |
3816 | [] |
3817 | ); |
3818 | |
3819 | // now we can remove repeat flag on original bill |
3820 | $this->editBill($projectid, $billid, null, $bill['what'], $bill['payer_id'], null, |
3821 | $bill['amount'], Application::FREQUENCIES['no'], null, null, null, null); |
3822 | return $newBillId; |
3823 | } |
3824 | |
3825 | /** |
3826 | * Get next repetition date of a bill |
3827 | * |
3828 | * @param array $bill |
3829 | * @param DateTimeImmutable $billDate |
3830 | * @return DateTimeImmutable|null |
3831 | * @throws Exception |
3832 | */ |
3833 | private function getNextRepetitionDate(array $bill, DateTimeImmutable $billDate): ?DateTimeImmutable { |
3834 | switch ($bill['repeat']) { |
3835 | case Application::FREQUENCIES['daily']: |
3836 | if ($bill['repeatfreq'] < 2) { |
3837 | return $billDate->add(new DateInterval('P1D')); |
3838 | } else { |
3839 | return $billDate->add(new DateInterval('P' . $bill['repeatfreq'] . 'D')); |
3840 | } |
3841 | break; |
3842 | |
3843 | case Application::FREQUENCIES['weekly']: |
3844 | if ($bill['repeatfreq'] < 2) { |
3845 | return $billDate->add(new DateInterval('P7D')); |
3846 | } else { |
3847 | $nbDays = 7 * $bill['repeatfreq']; |
3848 | return $billDate->add(new DateInterval('P' . $nbDays . 'D')); |
3849 | } |
3850 | break; |
3851 | |
3852 | case Application::FREQUENCIES['bi_weekly']: |
3853 | return $billDate->add(new DateInterval('P14D')); |
3854 | break; |
3855 | |
3856 | case Application::FREQUENCIES['semi_monthly']: |
3857 | $day = (int) $billDate->format('d'); |
3858 | $month = (int) $billDate->format('m'); |
3859 | $year = (int) $billDate->format('Y'); |
3860 | |
3861 | // first of next month |
3862 | if ($day >= 15) { |
3863 | if ($month === 12) { |
3864 | $nextYear = $year + 1; |
3865 | $nextMonth = 1; |
3866 | return $billDate->setDate($nextYear, $nextMonth, 1); |
3867 | } else { |
3868 | $nextMonth = $month + 1; |
3869 | return $billDate->setDate($year, $nextMonth, 1); |
3870 | } |
3871 | } else { |
3872 | // 15 of same month |
3873 | return $billDate->setDate($year, $month, 15); |
3874 | } |
3875 | break; |
3876 | |
3877 | case Application::FREQUENCIES['monthly']: |
3878 | $freq = ($bill['repeatfreq'] < 2) ? 1 : $bill['repeatfreq']; |
3879 | $billMonth = (int) $billDate->format('m'); |
3880 | $yearDelta = intdiv($billMonth + $freq - 1, 12); |
3881 | $nextYear = ((int) $billDate->format('Y')) + $yearDelta; |
3882 | $nextMonth = (($billMonth + $freq - 1) % 12) + 1; |
3883 | |
3884 | // same day of month if possible, otherwise at end of month |
3885 | $firstOfNextMonth = $billDate->setDate($nextYear, $nextMonth, 1); |
3886 | $billDay = (int) $billDate->format('d'); |
3887 | $nbDaysInTargetMonth = (int) $firstOfNextMonth->format('t'); |
3888 | if ($billDay > $nbDaysInTargetMonth) { |
3889 | return $billDate->setDate($nextYear, $nextMonth, $nbDaysInTargetMonth); |
3890 | } else { |
3891 | return $billDate->setDate($nextYear, $nextMonth, $billDay); |
3892 | } |
3893 | break; |
3894 | |
3895 | case Application::FREQUENCIES['yearly']: |
3896 | $freq = ($bill['repeatfreq'] < 2) ? 1 : $bill['repeatfreq']; |
3897 | $billYear = (int) $billDate->format('Y'); |
3898 | $billMonth = (int) $billDate->format('m'); |
3899 | $billDay = (int) $billDate->format('d'); |
3900 | $nextYear = $billYear + $freq; |
3901 | |
3902 | // same day of month if possible, otherwise at end of month + same month |
3903 | $firstDayOfTargetMonth = $billDate->setDate($nextYear, $billMonth, 1); |
3904 | $nbDaysInTargetMonth = (int) $firstDayOfTargetMonth->format('t'); |
3905 | if ($billDay > $nbDaysInTargetMonth) { |
3906 | return $billDate->setDate($nextYear, $billMonth, $nbDaysInTargetMonth); |
3907 | } else { |
3908 | return $billDate->setDate($nextYear, $billMonth, $billDay); |
3909 | } |
3910 | break; |
3911 | } |
3912 | |
3913 | return null; |
3914 | } |
3915 | |
3916 | /** |
3917 | * @param string $projectid |
3918 | * @param string $name |
3919 | * @param string|null $icon |
3920 | * @param string $color |
3921 | * @param int|null $order |
3922 | * @return int |
3923 | */ |
3924 | public function addPaymentMode(string $projectid, string $name, ?string $icon, string $color, ?int $order = 0): int { |
3925 | $qb = $this->db->getQueryBuilder(); |
3926 | |
3927 | $encIcon = $icon; |
3928 | if ($icon !== null && $icon !== '') { |
3929 | $encIcon = urlencode($icon); |
3930 | } |
3931 | $qb->insert('cospend_paymentmodes') |
3932 | ->values([ |
3933 | 'projectid' => $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR), |
3934 | 'encoded_icon' => $qb->createNamedParameter($encIcon, IQueryBuilder::PARAM_STR), |
3935 | 'color' => $qb->createNamedParameter($color, IQueryBuilder::PARAM_STR), |
3936 | 'name' => $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR), |
3937 | 'order' => $qb->createNamedParameter(is_null($order) ? 0 : $order, IQueryBuilder::PARAM_INT) |
3938 | ]); |
3939 | $qb->executeStatement(); |
3940 | $qb = $qb->resetQueryParts(); |
3941 | |
3942 | return $qb->getLastInsertId(); |
3943 | } |
3944 | |
3945 | /** |
3946 | * @param string $projectId |
3947 | * @param int $pmid |
3948 | * @return array|null |
3949 | * @throws \OCP\DB\Exception |
3950 | */ |
3951 | public function getPaymentMode(string $projectId, int $pmid): ?array { |
3952 | $pm = null; |
3953 | |
3954 | $qb = $this->db->getQueryBuilder(); |
3955 | $qb->select('id', 'name', 'projectid', 'encoded_icon', 'color', 'old_id') |
3956 | ->from('cospend_paymentmodes', 'pm') |
3957 | ->where( |
3958 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
3959 | ) |
3960 | ->andWhere( |
3961 | $qb->expr()->eq('id', $qb->createNamedParameter($pmid, IQueryBuilder::PARAM_INT)) |
3962 | ); |
3963 | $req = $qb->executeQuery(); |
3964 | |
3965 | while ($row = $req->fetch()) { |
3966 | $dbPmId = (int) $row['id']; |
3967 | $dbName = $row['name']; |
3968 | $dbIcon = urldecode($row['encoded_icon']); |
3969 | $dbColor = $row['color']; |
3970 | $dbOldId = $row['old_id']; |
3971 | $pm = [ |
3972 | 'name' => $dbName, |
3973 | 'icon' => $dbIcon, |
3974 | 'color' => $dbColor, |
3975 | 'id' => $dbPmId, |
3976 | 'projectid' => $projectId, |
3977 | 'old_id' => $dbOldId, |
3978 | ]; |
3979 | break; |
3980 | } |
3981 | $req->closeCursor(); |
3982 | $qb->resetQueryParts(); |
3983 | return $pm; |
3984 | } |
3985 | |
3986 | /** |
3987 | * @param string $projectid |
3988 | * @param int $pmid |
3989 | * @return array |
3990 | */ |
3991 | public function deletePaymentMode(string $projectid, int $pmid): array { |
3992 | $pmToDelete = $this->getPaymentMode($projectid, $pmid); |
3993 | if ($pmToDelete !== null) { |
3994 | $qb = $this->db->getQueryBuilder(); |
3995 | $qb->delete('cospend_paymentmodes') |
3996 | ->where( |
3997 | $qb->expr()->eq('id', $qb->createNamedParameter($pmid, IQueryBuilder::PARAM_INT)) |
3998 | ) |
3999 | ->andWhere( |
4000 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4001 | ); |
4002 | $qb->executeStatement(); |
4003 | $qb->resetQueryParts(); |
4004 | |
4005 | // then get rid of this pm in bills |
4006 | $qb = $this->db->getQueryBuilder(); |
4007 | $qb->update('cospend_bills'); |
4008 | $qb->set('paymentmodeid', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)); |
4009 | $qb->where( |
4010 | $qb->expr()->eq('paymentmodeid', $qb->createNamedParameter($pmid, IQueryBuilder::PARAM_INT)) |
4011 | ) |
4012 | ->andWhere( |
4013 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4014 | ); |
4015 | $qb->executeStatement(); |
4016 | $qb->resetQueryParts(); |
4017 | |
4018 | return ['success' => true]; |
4019 | } else { |
4020 | return ['message' => $this->trans->t('Not found')]; |
4021 | } |
4022 | } |
4023 | |
4024 | /** |
4025 | * @param string $projectid |
4026 | * @param array $order |
4027 | * @return bool |
4028 | */ |
4029 | public function savePaymentModeOrder(string $projectid, array $order): bool { |
4030 | $qb = $this->db->getQueryBuilder(); |
4031 | foreach ($order as $o) { |
4032 | $qb->update('cospend_paymentmodes'); |
4033 | $qb->set('order', $qb->createNamedParameter($o['order'], IQueryBuilder::PARAM_INT)); |
4034 | $qb->where( |
4035 | $qb->expr()->eq('id', $qb->createNamedParameter($o['id'], IQueryBuilder::PARAM_INT)) |
4036 | ) |
4037 | ->andWhere( |
4038 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4039 | ); |
4040 | $qb->executeStatement(); |
4041 | $qb = $qb->resetQueryParts(); |
4042 | } |
4043 | return true; |
4044 | } |
4045 | |
4046 | /** |
4047 | * @param string $projectid |
4048 | * @param int $pmid |
4049 | * @param string|null $name |
4050 | * @param string|null $icon |
4051 | * @param string|null $color |
4052 | * @return array |
4053 | */ |
4054 | public function editPaymentMode(string $projectid, int $pmid, ?string $name = null, |
4055 | ?string $icon = null, ?string $color = null): array { |
4056 | if ($name !== null && $name !== '') { |
4057 | $encIcon = $icon; |
4058 | if ($icon !== null && $icon !== '') { |
4059 | $encIcon = urlencode($icon); |
4060 | } |
4061 | if ($this->getPaymentMode($projectid, $pmid) !== null) { |
4062 | $qb = $this->db->getQueryBuilder(); |
4063 | $qb->update('cospend_paymentmodes'); |
4064 | $qb->set('name', $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR)); |
4065 | $qb->set('encoded_icon', $qb->createNamedParameter($encIcon, IQueryBuilder::PARAM_STR)); |
4066 | $qb->set('color', $qb->createNamedParameter($color, IQueryBuilder::PARAM_STR)); |
4067 | $qb->where( |
4068 | $qb->expr()->eq('id', $qb->createNamedParameter($pmid, IQueryBuilder::PARAM_INT)) |
4069 | ) |
4070 | ->andWhere( |
4071 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4072 | ); |
4073 | $qb->executeStatement(); |
4074 | $qb->resetQueryParts(); |
4075 | |
4076 | return $this->getPaymentMode($projectid, $pmid); |
4077 | } else { |
4078 | return ['message' => $this->trans->t('This project has no such payment mode')]; |
4079 | } |
4080 | } else { |
4081 | return ['message' => $this->trans->t('Incorrect field values')]; |
4082 | } |
4083 | } |
4084 | |
4085 | /** |
4086 | * Add a new category |
4087 | * |
4088 | * @param string $projectid |
4089 | * @param string $name |
4090 | * @param string|null $icon |
4091 | * @param string $color |
4092 | * @param int|null $order |
4093 | * @return int |
4094 | */ |
4095 | public function addCategory(string $projectid, string $name, ?string $icon, string $color, ?int $order = 0): int { |
4096 | $qb = $this->db->getQueryBuilder(); |
4097 | |
4098 | $encIcon = $icon; |
4099 | if ($icon !== null && $icon !== '') { |
4100 | $encIcon = urlencode($icon); |
4101 | } |
4102 | $qb->insert('cospend_categories') |
4103 | ->values([ |
4104 | 'projectid' => $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR), |
4105 | 'encoded_icon' => $qb->createNamedParameter($encIcon, IQueryBuilder::PARAM_STR), |
4106 | 'color' => $qb->createNamedParameter($color, IQueryBuilder::PARAM_STR), |
4107 | 'name' => $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR), |
4108 | 'order' => $qb->createNamedParameter(is_null($order) ? 0 : $order, IQueryBuilder::PARAM_INT) |
4109 | ]); |
4110 | $qb->executeStatement(); |
4111 | $qb = $qb->resetQueryParts(); |
4112 | |
4113 | return $qb->getLastInsertId(); |
4114 | } |
4115 | |
4116 | /** |
4117 | * Get a category |
4118 | * |
4119 | * @param string $projectId |
4120 | * @param int $categoryid |
4121 | * @return array|null |
4122 | */ |
4123 | public function getCategory(string $projectId, int $categoryid): ?array { |
4124 | $category = null; |
4125 | |
4126 | $qb = $this->db->getQueryBuilder(); |
4127 | $qb->select('id', 'name', 'projectid', 'encoded_icon', 'color') |
4128 | ->from('cospend_categories') |
4129 | ->where( |
4130 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
4131 | ) |
4132 | ->andWhere( |
4133 | $qb->expr()->eq('id', $qb->createNamedParameter($categoryid, IQueryBuilder::PARAM_INT)) |
4134 | ); |
4135 | $req = $qb->executeQuery(); |
4136 | |
4137 | while ($row = $req->fetch()) { |
4138 | $dbCategoryId = (int) $row['id']; |
4139 | $dbName = $row['name']; |
4140 | $dbIcon = urldecode($row['encoded_icon']); |
4141 | $dbColor = $row['color']; |
4142 | $category = [ |
4143 | 'name' => $dbName, |
4144 | 'icon' => $dbIcon, |
4145 | 'color' => $dbColor, |
4146 | 'id' => $dbCategoryId, |
4147 | 'projectid' => $projectId, |
4148 | ]; |
4149 | break; |
4150 | } |
4151 | $req->closeCursor(); |
4152 | $qb->resetQueryParts(); |
4153 | return $category; |
4154 | } |
4155 | |
4156 | /** |
4157 | * Delete a category |
4158 | * |
4159 | * @param string $projectid |
4160 | * @param int $categoryid |
4161 | * @return array |
4162 | */ |
4163 | public function deleteCategory(string $projectid, int $categoryid): array { |
4164 | $categoryToDelete = $this->getCategory($projectid, $categoryid); |
4165 | if ($categoryToDelete !== null) { |
4166 | $qb = $this->db->getQueryBuilder(); |
4167 | $qb->delete('cospend_categories') |
4168 | ->where( |
4169 | $qb->expr()->eq('id', $qb->createNamedParameter($categoryid, IQueryBuilder::PARAM_INT)) |
4170 | ) |
4171 | ->andWhere( |
4172 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4173 | ); |
4174 | $qb->executeStatement(); |
4175 | $qb->resetQueryParts(); |
4176 | |
4177 | // then get rid of this category in bills |
4178 | $qb = $this->db->getQueryBuilder(); |
4179 | $qb->update('cospend_bills'); |
4180 | $qb->set('categoryid', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)); |
4181 | $qb->where( |
4182 | $qb->expr()->eq('categoryid', $qb->createNamedParameter($categoryid, IQueryBuilder::PARAM_INT)) |
4183 | ) |
4184 | ->andWhere( |
4185 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4186 | ); |
4187 | $qb->executeStatement(); |
4188 | $qb->resetQueryParts(); |
4189 | |
4190 | return ['success' => true]; |
4191 | } else { |
4192 | return ['message' => $this->trans->t('Not found')]; |
4193 | } |
4194 | } |
4195 | |
4196 | /** |
4197 | * Save the manual category order |
4198 | * |
4199 | * @param string $projectid |
4200 | * @param array $order |
4201 | * @return bool |
4202 | */ |
4203 | public function saveCategoryOrder(string $projectid, array $order): bool { |
4204 | $qb = $this->db->getQueryBuilder(); |
4205 | foreach ($order as $o) { |
4206 | $qb->update('cospend_categories'); |
4207 | $qb->set('order', $qb->createNamedParameter($o['order'], IQueryBuilder::PARAM_INT)); |
4208 | $qb->where( |
4209 | $qb->expr()->eq('id', $qb->createNamedParameter($o['id'], IQueryBuilder::PARAM_INT)) |
4210 | ) |
4211 | ->andWhere( |
4212 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4213 | ); |
4214 | $qb->executeStatement(); |
4215 | $qb = $qb->resetQueryParts(); |
4216 | } |
4217 | return true; |
4218 | } |
4219 | |
4220 | /** |
4221 | * Edit a category |
4222 | * |
4223 | * @param string $projectid |
4224 | * @param int $categoryid |
4225 | * @param string|null $name |
4226 | * @param string|null $icon |
4227 | * @param string|null $color |
4228 | * @return array |
4229 | */ |
4230 | public function editCategory(string $projectid, int $categoryid, ?string $name = null, |
4231 | ?string $icon = null, ?string $color = null): array { |
4232 | if ($name !== null && $name !== '') { |
4233 | $encIcon = $icon; |
4234 | if ($icon !== null && $icon !== '') { |
4235 | $encIcon = urlencode($icon); |
4236 | } |
4237 | if ($this->getCategory($projectid, $categoryid) !== null) { |
4238 | $qb = $this->db->getQueryBuilder(); |
4239 | $qb->update('cospend_categories'); |
4240 | $qb->set('name', $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR)); |
4241 | $qb->set('encoded_icon', $qb->createNamedParameter($encIcon, IQueryBuilder::PARAM_STR)); |
4242 | $qb->set('color', $qb->createNamedParameter($color, IQueryBuilder::PARAM_STR)); |
4243 | $qb->where( |
4244 | $qb->expr()->eq('id', $qb->createNamedParameter($categoryid, IQueryBuilder::PARAM_INT)) |
4245 | ) |
4246 | ->andWhere( |
4247 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4248 | ); |
4249 | $qb->executeStatement(); |
4250 | $qb->resetQueryParts(); |
4251 | |
4252 | return $this->getCategory($projectid, $categoryid); |
4253 | } else { |
4254 | return ['message' => $this->trans->t('This project has no such category')]; |
4255 | } |
4256 | } else { |
4257 | return ['message' => $this->trans->t('Incorrect field values')]; |
4258 | } |
4259 | } |
4260 | |
4261 | /** |
4262 | * Add a currency |
4263 | * |
4264 | * @param string $projectid |
4265 | * @param string $name |
4266 | * @param float $rate |
4267 | * @return int |
4268 | */ |
4269 | public function addCurrency(string $projectid, string $name, float $rate): int { |
4270 | $qb = $this->db->getQueryBuilder(); |
4271 | |
4272 | $qb->insert('cospend_currencies') |
4273 | ->values([ |
4274 | 'projectid' => $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR), |
4275 | 'name' => $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR), |
4276 | 'exchange_rate' => $qb->createNamedParameter($rate, IQueryBuilder::PARAM_STR) |
4277 | ]); |
4278 | $qb->executeStatement(); |
4279 | $qb = $qb->resetQueryParts(); |
4280 | |
4281 | return $qb->getLastInsertId(); |
4282 | } |
4283 | |
4284 | /** |
4285 | * Get one currency |
4286 | * |
4287 | * @param string $projectId |
4288 | * @param int $currencyid |
4289 | * @return array|null |
4290 | */ |
4291 | private function getCurrency(string $projectId, int $currencyid): ?array { |
4292 | $currency = null; |
4293 | |
4294 | $qb = $this->db->getQueryBuilder(); |
4295 | $qb->select('id', 'name', 'exchange_rate', 'projectid') |
4296 | ->from('cospend_currencies') |
4297 | ->where( |
4298 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
4299 | ) |
4300 | ->andWhere( |
4301 | $qb->expr()->eq('id', $qb->createNamedParameter($currencyid, IQueryBuilder::PARAM_INT)) |
4302 | ); |
4303 | $req = $qb->executeQuery(); |
4304 | |
4305 | while ($row = $req->fetch()) { |
4306 | $dbCurrencyId = (int) $row['id']; |
4307 | $dbRate = (float) $row['exchange_rate']; |
4308 | $dbName = $row['name']; |
4309 | $currency = [ |
4310 | 'name' => $dbName, |
4311 | 'id' => $dbCurrencyId, |
4312 | 'exchange_rate' => $dbRate, |
4313 | 'projectid' => $projectId, |
4314 | ]; |
4315 | break; |
4316 | } |
4317 | $req->closeCursor(); |
4318 | $qb->resetQueryParts(); |
4319 | return $currency; |
4320 | } |
4321 | |
4322 | /** |
4323 | * Delete one currency |
4324 | * |
4325 | * @param string $projectid |
4326 | * @param int $currencyid |
4327 | * @return array |
4328 | */ |
4329 | public function deleteCurrency(string $projectid, int $currencyid): array { |
4330 | $currencyToDelete = $this->getCurrency($projectid, $currencyid); |
4331 | if ($currencyToDelete !== null) { |
4332 | $qb = $this->db->getQueryBuilder(); |
4333 | $qb->delete('cospend_currencies') |
4334 | ->where( |
4335 | $qb->expr()->eq('id', $qb->createNamedParameter($currencyid, IQueryBuilder::PARAM_INT)) |
4336 | ) |
4337 | ->andWhere( |
4338 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4339 | ); |
4340 | $qb->executeStatement(); |
4341 | $qb->resetQueryParts(); |
4342 | |
4343 | return ['success' => true]; |
4344 | } else { |
4345 | return ['message' => $this->trans->t('Not found')]; |
4346 | } |
4347 | } |
4348 | |
4349 | /** |
4350 | * Edit a currency |
4351 | * |
4352 | * @param string $projectid |
4353 | * @param int $currencyid |
4354 | * @param string $name |
4355 | * @param float $exchange_rate |
4356 | * @return array |
4357 | */ |
4358 | public function editCurrency(string $projectid, int $currencyid, string $name, float $exchange_rate): array { |
4359 | if ($name !== '' && $exchange_rate !== 0.0) { |
4360 | if ($this->getCurrency($projectid, $currencyid) !== null) { |
4361 | $qb = $this->db->getQueryBuilder(); |
4362 | $qb->update('cospend_currencies'); |
4363 | $qb->set('exchange_rate', $qb->createNamedParameter($exchange_rate, IQueryBuilder::PARAM_STR)); |
4364 | $qb->set('name', $qb->createNamedParameter($name, IQueryBuilder::PARAM_STR)); |
4365 | $qb->where( |
4366 | $qb->expr()->eq('id', $qb->createNamedParameter($currencyid, IQueryBuilder::PARAM_INT)) |
4367 | ) |
4368 | ->andWhere( |
4369 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4370 | ); |
4371 | $qb->executeStatement(); |
4372 | $qb->resetQueryParts(); |
4373 | |
4374 | return $this->getCurrency($projectid, $currencyid); |
4375 | } else { |
4376 | return ['message' => $this->trans->t('This project have no such currency')]; |
4377 | } |
4378 | } else { |
4379 | return ['message' => $this->trans->t('Incorrect field values')]; |
4380 | } |
4381 | } |
4382 | |
4383 | /** |
4384 | * Add a user shared access to a project |
4385 | * |
4386 | * @param string $projectid |
4387 | * @param string $userid |
4388 | * @param string $fromUserId |
4389 | * @param int $accesslevel |
4390 | * @param bool $manually_added |
4391 | * @return array |
4392 | */ |
4393 | public function addUserShare(string $projectid, string $userid, string $fromUserId, |
4394 | int $accesslevel = Application::ACCESS_LEVELS['participant'], bool $manually_added = true): array { |
4395 | $user = $this->userManager->get($userid); |
4396 | if ($user !== null && $userid !== $fromUserId) { |
4397 | $userName = $user->getDisplayName(); |
4398 | $qb = $this->db->getQueryBuilder(); |
4399 | $projectInfo = $this->getProjectInfo($projectid); |
4400 | // check if someone tries to share the project with its owner |
4401 | if ($userid !== $projectInfo['userid']) { |
4402 | // check if user share exists |
4403 | $qb->select('userid', 'projectid') |
4404 | ->from('cospend_shares', 's') |
4405 | ->where( |
4406 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['user'], IQueryBuilder::PARAM_STR)) |
4407 | ) |
4408 | ->andWhere( |
4409 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4410 | ) |
4411 | ->andWhere( |
4412 | $qb->expr()->eq('userid', $qb->createNamedParameter($userid, IQueryBuilder::PARAM_STR)) |
4413 | ); |
4414 | $req = $qb->executeQuery(); |
4415 | $dbuserId = null; |
4416 | while ($row = $req->fetch()){ |
4417 | $dbuserId = $row['userid']; |
4418 | break; |
4419 | } |
4420 | $req->closeCursor(); |
4421 | $qb = $qb->resetQueryParts(); |
4422 | |
4423 | if ($dbuserId === null) { |
4424 | if ($this->getUserMaxAccessLevel($fromUserId, $projectid) >= $accesslevel) { |
4425 | $qb->insert('cospend_shares') |
4426 | ->values([ |
4427 | 'projectid' => $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR), |
4428 | 'userid' => $qb->createNamedParameter($userid, IQueryBuilder::PARAM_STR), |
4429 | 'type' => $qb->createNamedParameter(Application::SHARE_TYPES['user'], IQueryBuilder::PARAM_STR), |
4430 | 'accesslevel' => $qb->createNamedParameter($accesslevel, IQueryBuilder::PARAM_INT), |
4431 | 'manually_added' => $qb->createNamedParameter($manually_added ? 1 : 0, IQueryBuilder::PARAM_INT), |
4432 | ]); |
4433 | $qb->executeStatement(); |
4434 | $qb = $qb->resetQueryParts(); |
4435 | |
4436 | $insertedShareId = $qb->getLastInsertId(); |
4437 | $response = [ |
4438 | 'id' => $insertedShareId, |
4439 | 'name' => $userName, |
4440 | ]; |
4441 | |
4442 | // activity |
4443 | $projectObj = $this->projectMapper->find($projectid); |
4444 | $this->activityManager->triggerEvent( |
4445 | ActivityManager::COSPEND_OBJECT_PROJECT, $projectObj, |
4446 | ActivityManager::SUBJECT_PROJECT_SHARE, |
4447 | ['who' => $userid, 'type' => Application::SHARE_TYPES['user']] |
4448 | ); |
4449 | |
4450 | // SEND NOTIFICATION |
4451 | $manager = $this->notificationManager; |
4452 | $notification = $manager->createNotification(); |
4453 | |
4454 | $acceptAction = $notification->createAction(); |
4455 | $acceptAction->setLabel('accept') |
4456 | ->setLink('/apps/cospend', 'GET'); |
4457 | |
4458 | $declineAction = $notification->createAction(); |
4459 | $declineAction->setLabel('decline') |
4460 | ->setLink('/apps/cospend', 'GET'); |
4461 | |
4462 | $notification->setApp('cospend') |
4463 | ->setUser($userid) |
4464 | ->setDateTime(new DateTime()) |
4465 | ->setObject('addusershare', $projectid) |
4466 | ->setSubject('add_user_share', [$fromUserId, $projectInfo['name']]) |
4467 | ->addAction($acceptAction) |
4468 | ->addAction($declineAction); |
4469 | |
4470 | $manager->notify($notification); |
4471 | |
4472 | return $response; |
4473 | } else { |
4474 | return ['message' => $this->trans->t('You are not authorized to give such access level')]; |
4475 | } |
4476 | } else { |
4477 | return ['message' => $this->trans->t('Already shared with this user')]; |
4478 | } |
4479 | } else { |
4480 | return ['message' => $this->trans->t('Impossible to share the project with its owner')]; |
4481 | } |
4482 | } else { |
4483 | return ['message' => $this->trans->t('No such user')]; |
4484 | } |
4485 | } |
4486 | |
4487 | /** |
4488 | * Add public share access (public link with token) |
4489 | * |
4490 | * @param string $projectid |
4491 | * @return array |
4492 | */ |
4493 | public function addPublicShare(string $projectid): array { |
4494 | $qb = $this->db->getQueryBuilder(); |
4495 | // generate token |
4496 | $token = md5($projectid.rand()); |
4497 | |
4498 | $qb->insert('cospend_shares') |
4499 | ->values([ |
4500 | 'projectid' => $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR), |
4501 | 'userid' => $qb->createNamedParameter($token, IQueryBuilder::PARAM_STR), |
4502 | 'type' => $qb->createNamedParameter(Application::SHARE_TYPES['public_link'], IQueryBuilder::PARAM_STR) |
4503 | ]); |
4504 | $qb->executeStatement(); |
4505 | $qb = $qb->resetQueryParts(); |
4506 | |
4507 | $insertedShareId = $qb->getLastInsertId(); |
4508 | |
4509 | //// activity |
4510 | //$projectObj = $this->projectMapper->find($projectid); |
4511 | //$this->activityManager->triggerEvent( |
4512 | // ActivityManager::COSPEND_OBJECT_PROJECT, $projectObj, |
4513 | // ActivityManager::SUBJECT_PROJECT_SHARE, |
4514 | // ['who' => $userid, 'type' => 'u'] |
4515 | //); |
4516 | |
4517 | //// SEND NOTIFICATION |
4518 | //$projectInfo = $this->getProjectInfo($projectid); |
4519 | //$manager = $this->notificationManager; |
4520 | //$notification = $manager->createNotification(); |
4521 | |
4522 | //$acceptAction = $notification->createAction(); |
4523 | //$acceptAction->setLabel('accept') |
4524 | // ->setLink('/apps/cospend', 'GET'); |
4525 | |
4526 | //$declineAction = $notification->createAction(); |
4527 | //$declineAction->setLabel('decline') |
4528 | // ->setLink('/apps/cospend', 'GET'); |
4529 | |
4530 | //$notification->setApp('cospend') |
4531 | // ->setUser($userid) |
4532 | // ->setDateTime(new DateTime()) |
4533 | // ->setObject('addusershare', $projectid) |
4534 | // ->setSubject('add_user_share', [$fromUserId, $projectInfo['name']]) |
4535 | // ->addAction($acceptAction) |
4536 | // ->addAction($declineAction) |
4537 | // ; |
4538 | |
4539 | //$manager->notify($notification); |
4540 | |
4541 | return [ |
4542 | 'token' => $token, |
4543 | 'id' => $insertedShareId |
4544 | ]; |
4545 | } |
4546 | |
4547 | /** |
4548 | * Change shared access permissions |
4549 | * |
4550 | * @param string $projectid |
4551 | * @param int $shid |
4552 | * @param int $accesslevel |
4553 | * @return array |
4554 | */ |
4555 | public function editShareAccessLevel(string $projectid, int $shid, int $accesslevel): array { |
4556 | // check if user share exists |
4557 | $qb = $this->db->getQueryBuilder(); |
4558 | $qb->select('id', 'projectid') |
4559 | ->from('cospend_shares', 's') |
4560 | ->where( |
4561 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4562 | ) |
4563 | ->andWhere( |
4564 | $qb->expr()->eq('id', $qb->createNamedParameter($shid, IQueryBuilder::PARAM_INT)) |
4565 | ); |
4566 | $req = $qb->executeQuery(); |
4567 | $dbId = null; |
4568 | while ($row = $req->fetch()){ |
4569 | $dbId = $row['id']; |
4570 | break; |
4571 | } |
4572 | $req->closeCursor(); |
4573 | $qb = $qb->resetQueryParts(); |
4574 | |
4575 | if ($dbId !== null) { |
4576 | // set the accesslevel |
4577 | $qb->update('cospend_shares') |
4578 | ->set('accesslevel', $qb->createNamedParameter($accesslevel, IQueryBuilder::PARAM_INT)) |
4579 | ->where( |
4580 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4581 | ) |
4582 | ->andWhere( |
4583 | $qb->expr()->eq('id', $qb->createNamedParameter($shid, IQueryBuilder::PARAM_INT)) |
4584 | ); |
4585 | $qb->executeStatement(); |
4586 | $qb->resetQueryParts(); |
4587 | |
4588 | return ['success' => true]; |
4589 | } else { |
4590 | return ['message' => $this->trans->t('No such share')]; |
4591 | } |
4592 | } |
4593 | |
4594 | /** |
4595 | * Change shared access permissions |
4596 | * |
4597 | * @param string $projectid |
4598 | * @param int $shid |
4599 | * @param string|null $label |
4600 | * @return array |
4601 | * @throws \OCP\DB\Exception |
4602 | */ |
4603 | public function editShareAccess(string $projectid, int $shid, ?string $label = null, ?string $password = null): array { |
4604 | // check if user share exists |
4605 | $qb = $this->db->getQueryBuilder(); |
4606 | $qb->select('id', 'projectid') |
4607 | ->from('cospend_shares', 's') |
4608 | ->where( |
4609 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4610 | ) |
4611 | ->andWhere( |
4612 | $qb->expr()->eq('id', $qb->createNamedParameter($shid, IQueryBuilder::PARAM_INT)) |
4613 | ); |
4614 | $req = $qb->executeQuery(); |
4615 | $dbId = null; |
4616 | while ($row = $req->fetch()){ |
4617 | $dbId = $row['id']; |
4618 | break; |
4619 | } |
4620 | $req->closeCursor(); |
4621 | $qb = $qb->resetQueryParts(); |
4622 | |
4623 | if (!is_null($dbId) && (!is_null($label) || !is_null($password))) { |
4624 | $qb->update('cospend_shares'); |
4625 | if (!is_null($label)) { |
4626 | if ($label === '') { |
4627 | $label = null; |
4628 | } |
4629 | $qb->set('label', $qb->createNamedParameter($label, IQueryBuilder::PARAM_STR)); |
4630 | } |
4631 | if (!is_null($password)) { |
4632 | if ($password === '') { |
4633 | $password = null; |
4634 | } |
4635 | $qb->set('password', $qb->createNamedParameter($password, IQueryBuilder::PARAM_STR)); |
4636 | } |
4637 | $qb->where( |
4638 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4639 | ) |
4640 | ->andWhere( |
4641 | $qb->expr()->eq('id', $qb->createNamedParameter($shid, IQueryBuilder::PARAM_INT)) |
4642 | ); |
4643 | $qb->executeStatement(); |
4644 | $qb->resetQueryParts(); |
4645 | |
4646 | return ['success' => true]; |
4647 | } else { |
4648 | return ['message' => $this->trans->t('No such share')]; |
4649 | } |
4650 | } |
4651 | |
4652 | /** |
4653 | * Change guest access permissions |
4654 | * |
4655 | * @param string $projectid |
4656 | * @param int $accesslevel |
4657 | * @return array |
4658 | */ |
4659 | public function editGuestAccessLevel(string $projectid, int $accesslevel): array { |
4660 | // check if project exists |
4661 | $qb = $this->db->getQueryBuilder(); |
4662 | |
4663 | // set the access level |
4664 | $qb->update('cospend_projects') |
4665 | ->set('guestaccesslevel', $qb->createNamedParameter($accesslevel, IQueryBuilder::PARAM_INT)) |
4666 | ->where( |
4667 | $qb->expr()->eq('id', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4668 | ); |
4669 | $qb->executeStatement(); |
4670 | $qb->resetQueryParts(); |
4671 | |
4672 | return ['success' => true]; |
4673 | } |
4674 | |
4675 | /** |
4676 | * Delete user shared access |
4677 | * |
4678 | * @param string $projectid |
4679 | * @param int $shid |
4680 | * @param string|null $fromUserId |
4681 | * @return array |
4682 | */ |
4683 | public function deleteUserShare(string $projectid, int $shid, ?string $fromUserId = null): array { |
4684 | // check if user share exists |
4685 | $qb = $this->db->getQueryBuilder(); |
4686 | $qb->select('id', 'userid', 'projectid') |
4687 | ->from('cospend_shares', 's') |
4688 | ->where( |
4689 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['user'], IQueryBuilder::PARAM_STR)) |
4690 | ) |
4691 | ->andWhere( |
4692 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4693 | ) |
4694 | ->andWhere( |
4695 | $qb->expr()->eq('id', $qb->createNamedParameter($shid, IQueryBuilder::PARAM_INT)) |
4696 | ); |
4697 | $req = $qb->executeQuery(); |
4698 | $dbId = null; |
4699 | $dbuserId = null; |
4700 | while ($row = $req->fetch()){ |
4701 | $dbId = $row['id']; |
4702 | $dbuserId = $row['userid']; |
4703 | break; |
4704 | } |
4705 | $req->closeCursor(); |
4706 | $qb = $qb->resetQueryParts(); |
4707 | |
4708 | if ($dbId !== null) { |
4709 | // delete |
4710 | $qb->delete('cospend_shares') |
4711 | ->where( |
4712 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4713 | ) |
4714 | ->andWhere( |
4715 | $qb->expr()->eq('id', $qb->createNamedParameter($shid, IQueryBuilder::PARAM_INT)) |
4716 | ) |
4717 | ->andWhere( |
4718 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['user'], IQueryBuilder::PARAM_STR)) |
4719 | ); |
4720 | $qb->executeStatement(); |
4721 | $qb->resetQueryParts(); |
4722 | |
4723 | // activity |
4724 | $projectObj = $this->projectMapper->find($projectid); |
4725 | $this->activityManager->triggerEvent( |
4726 | ActivityManager::COSPEND_OBJECT_PROJECT, $projectObj, |
4727 | ActivityManager::SUBJECT_PROJECT_UNSHARE, |
4728 | ['who' => $dbuserId, 'type' => Application::SHARE_TYPES['user']] |
4729 | ); |
4730 | |
4731 | // SEND NOTIFICATION |
4732 | if (!is_null($fromUserId)) { |
4733 | $projectInfo = $this->getProjectInfo($projectid); |
4734 | |
4735 | $manager = $this->notificationManager; |
4736 | $notification = $manager->createNotification(); |
4737 | |
4738 | $acceptAction = $notification->createAction(); |
4739 | $acceptAction->setLabel('accept') |
4740 | ->setLink('/apps/cospend', 'GET'); |
4741 | |
4742 | $declineAction = $notification->createAction(); |
4743 | $declineAction->setLabel('decline') |
4744 | ->setLink('/apps/cospend', 'GET'); |
4745 | |
4746 | $notification->setApp('cospend') |
4747 | ->setUser($dbuserId) |
4748 | ->setDateTime(new DateTime()) |
4749 | ->setObject('deleteusershare', $projectid) |
4750 | ->setSubject('delete_user_share', [$fromUserId, $projectInfo['name']]) |
4751 | ->addAction($acceptAction) |
4752 | ->addAction($declineAction) |
4753 | ; |
4754 | |
4755 | $manager->notify($notification); |
4756 | } |
4757 | |
4758 | return ['success' => true]; |
4759 | } else { |
4760 | return ['message' => $this->trans->t('No such share')]; |
4761 | } |
4762 | } |
4763 | |
4764 | /** |
4765 | * Delete public shared access |
4766 | * |
4767 | * @param string $projectid |
4768 | * @param int $shid |
4769 | * @return array |
4770 | */ |
4771 | public function deletePublicShare(string $projectid, int $shid): array { |
4772 | // check if public share exists |
4773 | $qb = $this->db->getQueryBuilder(); |
4774 | $qb->select('id', 'userid', 'projectid') |
4775 | ->from('cospend_shares', 's') |
4776 | ->where( |
4777 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['public_link'], IQueryBuilder::PARAM_STR)) |
4778 | ) |
4779 | ->andWhere( |
4780 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4781 | ) |
4782 | ->andWhere( |
4783 | $qb->expr()->eq('id', $qb->createNamedParameter($shid, IQueryBuilder::PARAM_INT)) |
4784 | ); |
4785 | $req = $qb->executeQuery(); |
4786 | $dbId = null; |
4787 | while ($row = $req->fetch()){ |
4788 | $dbId = $row['id']; |
4789 | break; |
4790 | } |
4791 | $req->closeCursor(); |
4792 | $qb = $qb->resetQueryParts(); |
4793 | |
4794 | if ($dbId !== null) { |
4795 | // delete |
4796 | $qb->delete('cospend_shares') |
4797 | ->where( |
4798 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4799 | ) |
4800 | ->andWhere( |
4801 | $qb->expr()->eq('id', $qb->createNamedParameter($shid, IQueryBuilder::PARAM_INT)) |
4802 | ) |
4803 | ->andWhere( |
4804 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['public_link'], IQueryBuilder::PARAM_STR)) |
4805 | ); |
4806 | $qb->executeStatement(); |
4807 | $qb->resetQueryParts(); |
4808 | |
4809 | //// activity |
4810 | //$projectObj = $this->projectMapper->find($projectid); |
4811 | //$this->activityManager->triggerEvent( |
4812 | // ActivityManager::COSPEND_OBJECT_PROJECT, $projectObj, |
4813 | // ActivityManager::SUBJECT_PROJECT_UNSHARE, |
4814 | // ['who' => $dbuserId, 'type' => 'u'] |
4815 | //); |
4816 | |
4817 | //// SEND NOTIFICATION |
4818 | //$projectInfo = $this->getProjectInfo($projectid); |
4819 | |
4820 | //$manager = $this->notificationManager; |
4821 | //$notification = $manager->createNotification(); |
4822 | |
4823 | //$acceptAction = $notification->createAction(); |
4824 | //$acceptAction->setLabel('accept') |
4825 | // ->setLink('/apps/cospend', 'GET'); |
4826 | |
4827 | //$declineAction = $notification->createAction(); |
4828 | //$declineAction->setLabel('decline') |
4829 | // ->setLink('/apps/cospend', 'GET'); |
4830 | |
4831 | //$notification->setApp('cospend') |
4832 | // ->setUser($dbuserId) |
4833 | // ->setDateTime(new DateTime()) |
4834 | // ->setObject('deleteusershare', $projectid) |
4835 | // ->setSubject('delete_user_share', [$fromUserId, $projectInfo['name']]) |
4836 | // ->addAction($acceptAction) |
4837 | // ->addAction($declineAction) |
4838 | // ; |
4839 | |
4840 | //$manager->notify($notification); |
4841 | |
4842 | return ['success' => true]; |
4843 | } else { |
4844 | return ['message' => $this->trans->t('No such shared access')]; |
4845 | } |
4846 | } |
4847 | |
4848 | /** |
4849 | * Add group shared access |
4850 | * |
4851 | * @param string $projectid |
4852 | * @param string $groupid |
4853 | * @param string|null $fromUserId |
4854 | * @return array |
4855 | */ |
4856 | public function addGroupShare(string $projectid, string $groupid, ?string $fromUserId = null): array { |
4857 | if ($this->groupManager->groupExists($groupid)) { |
4858 | $groupName = $this->groupManager->get($groupid)->getDisplayName(); |
4859 | $qb = $this->db->getQueryBuilder(); |
4860 | // check if user share exists |
4861 | $qb->select('userid', 'projectid') |
4862 | ->from('cospend_shares', 's') |
4863 | ->where( |
4864 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['group'], IQueryBuilder::PARAM_STR)) |
4865 | ) |
4866 | ->andWhere( |
4867 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4868 | ) |
4869 | ->andWhere( |
4870 | $qb->expr()->eq('userid', $qb->createNamedParameter($groupid, IQueryBuilder::PARAM_STR)) |
4871 | ); |
4872 | $req = $qb->executeQuery(); |
4873 | $dbGroupId = null; |
4874 | while ($row = $req->fetch()){ |
4875 | $dbGroupId = $row['userid']; |
4876 | break; |
4877 | } |
4878 | $req->closeCursor(); |
4879 | $qb = $qb->resetQueryParts(); |
4880 | |
4881 | if ($dbGroupId === null) { |
4882 | $qb->insert('cospend_shares') |
4883 | ->values([ |
4884 | 'projectid' => $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR), |
4885 | 'userid' => $qb->createNamedParameter($groupid, IQueryBuilder::PARAM_STR), |
4886 | 'type' => $qb->createNamedParameter(Application::SHARE_TYPES['group'], IQueryBuilder::PARAM_STR) |
4887 | ]); |
4888 | $qb->executeStatement(); |
4889 | $qb = $qb->resetQueryParts(); |
4890 | |
4891 | $insertedShareId = $qb->getLastInsertId(); |
4892 | |
4893 | // activity |
4894 | $projectObj = $this->projectMapper->find($projectid); |
4895 | $this->activityManager->triggerEvent( |
4896 | ActivityManager::COSPEND_OBJECT_PROJECT, $projectObj, |
4897 | ActivityManager::SUBJECT_PROJECT_SHARE, |
4898 | ['who' => $groupid, 'type' => Application::SHARE_TYPES['group']] |
4899 | ); |
4900 | |
4901 | return [ |
4902 | 'id' => $insertedShareId, |
4903 | 'name' => $groupName, |
4904 | ]; |
4905 | } else { |
4906 | return ['message' => $this->trans->t('Already shared with this group')]; |
4907 | } |
4908 | } else { |
4909 | return ['message' => $this->trans->t('No such group')]; |
4910 | } |
4911 | } |
4912 | |
4913 | /** |
4914 | * Delete group shared access |
4915 | * |
4916 | * @param string $projectid |
4917 | * @param int shid |
4918 | * @param string|null $fromUserId |
4919 | * @return array |
4920 | */ |
4921 | public function deleteGroupShare(string $projectid, int $shid, ?string $fromUserId = null): array { |
4922 | // check if group share exists |
4923 | $qb = $this->db->getQueryBuilder(); |
4924 | $qb->select('userid', 'projectid', 'id') |
4925 | ->from('cospend_shares', 's') |
4926 | ->where( |
4927 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['group'], IQueryBuilder::PARAM_STR)) |
4928 | ) |
4929 | ->andWhere( |
4930 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4931 | ) |
4932 | ->andWhere( |
4933 | $qb->expr()->eq('id', $qb->createNamedParameter($shid, IQueryBuilder::PARAM_INT)) |
4934 | ); |
4935 | $req = $qb->executeQuery(); |
4936 | $dbGroupId = null; |
4937 | while ($row = $req->fetch()){ |
4938 | $dbGroupId = $row['userid']; |
4939 | break; |
4940 | } |
4941 | $req->closeCursor(); |
4942 | $qb = $qb->resetQueryParts(); |
4943 | |
4944 | if ($dbGroupId !== null) { |
4945 | // delete |
4946 | $qb->delete('cospend_shares') |
4947 | ->where( |
4948 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
4949 | ) |
4950 | ->andWhere( |
4951 | $qb->expr()->eq('id', $qb->createNamedParameter($shid, IQueryBuilder::PARAM_INT)) |
4952 | ) |
4953 | ->andWhere( |
4954 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['group'], IQueryBuilder::PARAM_STR)) |
4955 | ); |
4956 | $qb->executeStatement(); |
4957 | $qb->resetQueryParts(); |
4958 | |
4959 | // activity |
4960 | $projectObj = $this->projectMapper->find($projectid); |
4961 | $this->activityManager->triggerEvent( |
4962 | ActivityManager::COSPEND_OBJECT_PROJECT, $projectObj, |
4963 | ActivityManager::SUBJECT_PROJECT_UNSHARE, |
4964 | ['who' => $dbGroupId, 'type' => Application::SHARE_TYPES['group']] |
4965 | ); |
4966 | |
4967 | return ['success' => true]; |
4968 | } else { |
4969 | return ['message' => $this->trans->t('No such share')]; |
4970 | } |
4971 | } |
4972 | |
4973 | /** |
4974 | * Add circle shaed access |
4975 | * |
4976 | * @param string $projectid |
4977 | * @param string $circleid |
4978 | * @param string|null $fromUserId |
4979 | * @return array |
4980 | */ |
4981 | public function addCircleShare(string $projectid, string $circleid, ?string $fromUserId = null): array { |
4982 | // check if circleId exists |
4983 | $circlesEnabled = $this->appManager->isEnabledForUser('circles'); |
4984 | if ($circlesEnabled) { |
4985 | try { |
4986 | $circlesManager = \OC::$server->get(\OCA\Circles\CirclesManager::class); |
4987 | $circlesManager->startSuperSession(); |
4988 | } catch (Exception $e) { |
4989 | return ['message' => $this->trans->t('Impossible to get the circle manager')]; |
4990 | } |
4991 | |
4992 | $exists = true; |
4993 | $circleName = ''; |
4994 | try { |
4995 | $circle = $circlesManager->getCircle($circleid); |
4996 | $circleName = $circle->getDisplayName(); |
4997 | } catch (\OCA\Circles\Exceptions\CircleNotFoundException $e) { |
4998 | $exists = false; |
4999 | } |
5000 | |
5001 | if ($circleid !== '' && $exists) { |
5002 | $qb = $this->db->getQueryBuilder(); |
5003 | // check if circle share exists |
5004 | $qb->select('userid', 'projectid') |
5005 | ->from('cospend_shares', 's') |
5006 | ->where( |
5007 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['circle'], IQueryBuilder::PARAM_STR)) |
5008 | ) |
5009 | ->andWhere( |
5010 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
5011 | ) |
5012 | ->andWhere( |
5013 | $qb->expr()->eq('userid', $qb->createNamedParameter($circleid, IQueryBuilder::PARAM_STR)) |
5014 | ); |
5015 | $req = $qb->executeQuery(); |
5016 | $dbCircleId = null; |
5017 | while ($row = $req->fetch()){ |
5018 | $dbCircleId = $row['userid']; |
5019 | break; |
5020 | } |
5021 | $req->closeCursor(); |
5022 | $qb = $qb->resetQueryParts(); |
5023 | |
5024 | if ($dbCircleId === null) { |
5025 | $qb->insert('cospend_shares') |
5026 | ->values([ |
5027 | 'projectid' => $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR), |
5028 | 'userid' => $qb->createNamedParameter($circleid, IQueryBuilder::PARAM_STR), |
5029 | 'type' => $qb->createNamedParameter(Application::SHARE_TYPES['circle'], IQueryBuilder::PARAM_STR) |
5030 | ]); |
5031 | $qb->executeStatement(); |
5032 | $qb = $qb->resetQueryParts(); |
5033 | |
5034 | $insertedShareId = $qb->getLastInsertId(); |
5035 | |
5036 | // activity |
5037 | $projectObj = $this->projectMapper->find($projectid); |
5038 | $this->activityManager->triggerEvent( |
5039 | ActivityManager::COSPEND_OBJECT_PROJECT, $projectObj, |
5040 | ActivityManager::SUBJECT_PROJECT_SHARE, |
5041 | ['who' => $circleid, 'type' => Application::SHARE_TYPES['circle']] |
5042 | ); |
5043 | |
5044 | $circlesManager->stopSession(); |
5045 | return [ |
5046 | 'id' => $insertedShareId, |
5047 | 'name' => $circleName, |
5048 | ]; |
5049 | } else { |
5050 | $circlesManager->stopSession(); |
5051 | return ['message' => $this->trans->t('Already shared with this circle')]; |
5052 | } |
5053 | } else { |
5054 | $circlesManager->stopSession(); |
5055 | return ['message' => $this->trans->t('No such circle')]; |
5056 | } |
5057 | } else { |
5058 | return ['message' => $this->trans->t('Circles app is not enabled')]; |
5059 | } |
5060 | } |
5061 | |
5062 | /** |
5063 | * Delete circle shared access |
5064 | * |
5065 | * @param string $projectid |
5066 | * @param int $shid |
5067 | * @param string|null $fromUserId |
5068 | * @return array |
5069 | */ |
5070 | public function deleteCircleShare(string $projectid, int $shid, ?string $fromUserId = null): array { |
5071 | // check if circle share exists |
5072 | $qb = $this->db->getQueryBuilder(); |
5073 | $qb->select('userid', 'projectid', 'id') |
5074 | ->from('cospend_shares', 's') |
5075 | ->where( |
5076 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['circle'], IQueryBuilder::PARAM_STR)) |
5077 | ) |
5078 | ->andWhere( |
5079 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
5080 | ) |
5081 | ->andWhere( |
5082 | $qb->expr()->eq('id', $qb->createNamedParameter($shid, IQueryBuilder::PARAM_INT)) |
5083 | ); |
5084 | $req = $qb->executeQuery(); |
5085 | $dbCircleId = null; |
5086 | while ($row = $req->fetch()){ |
5087 | $dbCircleId = $row['userid']; |
5088 | break; |
5089 | } |
5090 | $req->closeCursor(); |
5091 | $qb = $qb->resetQueryParts(); |
5092 | |
5093 | if ($dbCircleId !== null) { |
5094 | // delete |
5095 | $qb->delete('cospend_shares') |
5096 | ->where( |
5097 | $qb->expr()->eq('projectid', $qb->createNamedParameter($projectid, IQueryBuilder::PARAM_STR)) |
5098 | ) |
5099 | ->andWhere( |
5100 | $qb->expr()->eq('id', $qb->createNamedParameter($shid, IQueryBuilder::PARAM_INT)) |
5101 | ) |
5102 | ->andWhere( |
5103 | $qb->expr()->eq('type', $qb->createNamedParameter(Application::SHARE_TYPES['circle'], IQueryBuilder::PARAM_STR)) |
5104 | ); |
5105 | $qb->executeStatement(); |
5106 | $qb->resetQueryParts(); |
5107 | |
5108 | // activity |
5109 | $projectObj = $this->projectMapper->find($projectid); |
5110 | $this->activityManager->triggerEvent( |
5111 | ActivityManager::COSPEND_OBJECT_PROJECT, $projectObj, |
5112 | ActivityManager::SUBJECT_PROJECT_UNSHARE, |
5113 | ['who' => $dbCircleId, 'type' => Application::SHARE_TYPES['circle']] |
5114 | ); |
5115 | |
5116 | $response = ['success' => true]; |
5117 | } else { |
5118 | $response = ['message' => $this->trans->t('No such share')]; |
5119 | } |
5120 | return $response; |
5121 | } |
5122 | |
5123 | /** |
5124 | * Export settlement plan in CSV |
5125 | * |
5126 | * @param string $projectid |
5127 | * @param string $userId |
5128 | * @param int|null $centeredOn |
5129 | * @param int|null $maxTimestamp |
5130 | * @return array |
5131 | */ |
5132 | public function exportCsvSettlement(string $projectid, string $userId, ?int $centeredOn = null, ?int $maxTimestamp = null): array { |
5133 | // create export directory if needed |
5134 | $outPath = $this->config->getUserValue($userId, 'cospend', 'outputDirectory', '/Cospend'); |
5135 | $userFolder = $this->root->getUserFolder($userId); |
5136 | $msg = $this->createAndCheckExportDirectory($userFolder, $outPath); |
5137 | if ($msg !== '') { |
5138 | return ['message' => $msg]; |
5139 | } |
5140 | $folder = $userFolder->get($outPath); |
5141 | |
5142 | // create file |
5143 | if ($folder->nodeExists($projectid.'-settlement.csv')) { |
5144 | $folder->get($projectid.'-settlement.csv')->delete(); |
5145 | } |
5146 | $file = $folder->newFile($projectid.'-settlement.csv'); |
5147 | $handler = $file->fopen('w'); |
5148 | fwrite( |
5149 | $handler, |
5150 | '"' . $this->trans->t('Who pays?') |
5151 | . '","' . $this->trans->t('To whom?') |
5152 | . '","' . $this->trans->t('How much?') |
5153 | . '"' . "\n" |
5154 | ); |
5155 | $settlement = $this->getProjectSettlement($projectid, $centeredOn, $maxTimestamp); |
5156 | $transactions = $settlement['transactions']; |
5157 | |
5158 | $members = $this->getMembers($projectid); |
5159 | $memberIdToName = []; |
5160 | foreach ($members as $member) { |
5161 | $memberIdToName[$member['id']] = $member['name']; |
5162 | } |
5163 | |
5164 | foreach ($transactions as $transaction) { |
5165 | fwrite( |
5166 | $handler, |
5167 | '"' . $memberIdToName[$transaction['from']] |
5168 | . '","' . $memberIdToName[$transaction['to']] |
5169 | . '",' . (float) $transaction['amount'] |
5170 | . "\n" |
5171 | ); |
5172 | } |
5173 | |
5174 | fclose($handler); |
5175 | $file->touch(); |
5176 | return ['path' => $outPath . '/' . $projectid . '-settlement.csv']; |
5177 | } |
5178 | |
5179 | /** |
5180 | * Create directory where things will be exported |
5181 | * |
5182 | * @param Folder $userFolder |
5183 | * @param string $outPath |
5184 | * @return string |
5185 | */ |
5186 | private function createAndCheckExportDirectory(Folder $userFolder, string $outPath): string { |
5187 | if (!$userFolder->nodeExists($outPath)) { |
5188 | $userFolder->newFolder($outPath); |
5189 | } |
5190 | if ($userFolder->nodeExists($outPath)) { |
5191 | $folder = $userFolder->get($outPath); |
5192 | if ($folder->getType() !== FileInfo::TYPE_FOLDER) { |
5193 | return $this->trans->t('%1$s is not a folder', [$outPath]); |
5194 | } elseif (!$folder->isCreatable()) { |
5195 | return $this->trans->t('%1$s is not writeable', [$outPath]); |
5196 | } else { |
5197 | return ''; |
5198 | } |
5199 | } else { |
5200 | return $this->trans->t('Impossible to create %1$s', [$outPath]); |
5201 | } |
5202 | } |
5203 | |
5204 | /** |
5205 | * @param string $projectid |
5206 | * @param string $userId |
5207 | * @param int|null $tsMin |
5208 | * @param int|null $tsMax |
5209 | * @param int|null $paymentModeId |
5210 | * @param int|null $category |
5211 | * @param float|null $amountMin |
5212 | * @param float|null $amountMax |
5213 | * @param bool $showDisabled |
5214 | * @param int|null $currencyId |
5215 | * @return array |
5216 | * @throws \OCP\DB\Exception |
5217 | * @throws \OCP\Files\NotFoundException |
5218 | * @throws \OCP\Files\NotPermittedException |
5219 | * @throws \OC\User\NoUserException |
5220 | */ |
5221 | public function exportCsvStatistics(string $projectid, string $userId, ?int $tsMin = null, ?int $tsMax = null, |
5222 | ?int $paymentModeId = null, ?int $category = null, |
5223 | ?float $amountMin = null, ?float $amountMax = null, |
5224 | bool $showDisabled = true, ?int $currencyId = null): array { |
5225 | // create export directory if needed |
5226 | $outPath = $this->config->getUserValue($userId, 'cospend', 'outputDirectory', '/Cospend'); |
5227 | $userFolder = $this->root->getUserFolder($userId); |
5228 | $msg = $this->createAndCheckExportDirectory($userFolder, $outPath); |
5229 | if ($msg !== '') { |
5230 | return ['message' => $msg]; |
5231 | } |
5232 | $folder = $userFolder->get($outPath); |
5233 | |
5234 | // create file |
5235 | if ($folder->nodeExists($projectid.'-stats.csv')) { |
5236 | $folder->get($projectid.'-stats.csv')->delete(); |
5237 | } |
5238 | $file = $folder->newFile($projectid.'-stats.csv'); |
5239 | $handler = $file->fopen('w'); |
5240 | fwrite( |
5241 | $handler, |
5242 | $this->trans->t('Member name') |
5243 | . ',' . $this->trans->t('Paid') |
5244 | . ',' . $this->trans->t('Spent') |
5245 | . ',' . $this->trans->t('Balance') |
5246 | . "\n" |
5247 | ); |
5248 | $allStats = $this->getProjectStatistics( |
5249 | $projectid, 'lowername', $tsMin, $tsMax, $paymentModeId, |
5250 | $category, $amountMin, $amountMax, $showDisabled, $currencyId |
5251 | ); |
5252 | $stats = $allStats['stats']; |
5253 | |
5254 | foreach ($stats as $stat) { |
5255 | fwrite( |
5256 | $handler, |
5257 | '"' . $stat['member']['name'] |
5258 | . '",' . (float) $stat['paid'] |
5259 | . ',' . (float) $stat['spent'] |
5260 | . ',' . (float) $stat['balance'] |
5261 | . "\n" |
5262 | ); |
5263 | } |
5264 | |
5265 | fclose($handler); |
5266 | $file->touch(); |
5267 | return ['path' => $outPath . '/' . $projectid . '-stats.csv']; |
5268 | } |
5269 | |
5270 | /** |
5271 | * Export project in CSV |
5272 | * |
5273 | * @param string $projectid |
5274 | * @param string|null $name |
5275 | * @param string $userId |
5276 | * @return array |
5277 | * @throws \OCP\Files\NotFoundException |
5278 | * @throws \OCP\Files\NotPermittedException |
5279 | * @throws \OC\User\NoUserException |
5280 | */ |
5281 | public function exportCsvProject(string $projectid, string $userId, ?string $name = null): array { |
5282 | // create export directory if needed |
5283 | $outPath = $this->config->getUserValue($userId, 'cospend', 'outputDirectory', '/Cospend'); |
5284 | $userFolder = $this->root->getUserFolder($userId); |
5285 | $msg = $this->createAndCheckExportDirectory($userFolder, $outPath); |
5286 | if ($msg !== '') { |
5287 | return ['message' => $msg]; |
5288 | } |
5289 | $folder = $userFolder->get($outPath); |
5290 | |
5291 | |
5292 | // create file |
5293 | $filename = $projectid.'.csv'; |
5294 | if ($name !== null) { |
5295 | $filename = $name; |
5296 | if (!Utils::endswith($filename, '.csv')) { |
5297 | $filename .= '.csv'; |
5298 | } |
5299 | } |
5300 | if ($folder->nodeExists($filename)) { |
5301 | $folder->get($filename)->delete(); |
5302 | } |
5303 | $file = $folder->newFile($filename); |
5304 | $handler = $file->fopen('w'); |
5305 | foreach ($this->getJsonProject($projectid) as $chunk) { |
5306 | fwrite($handler, $chunk); |
5307 | } |
5308 | |
5309 | fclose($handler); |
5310 | $file->touch(); |
5311 | return ['path' => $outPath . '/' . $filename]; |
5312 | } |
5313 | |
5314 | public function getJsonProject(string $projectId): Generator { |
5315 | // members |
5316 | yield "name,weight,active,color\n"; |
5317 | $projectInfo = $this->getProjectInfo($projectId); |
5318 | $members = $projectInfo['members']; |
5319 | $memberIdToName = []; |
5320 | $memberIdToWeight = []; |
5321 | $memberIdToActive = []; |
5322 | foreach ($members as $member) { |
5323 | $memberIdToName[$member['id']] = $member['name']; |
5324 | $memberIdToWeight[$member['id']] = $member['weight']; |
5325 | $memberIdToActive[$member['id']] = (int) $member['activated']; |
5326 | $c = $member['color']; |
5327 | yield '"' . $member['name'] . '",' |
5328 | . (float) $member['weight'] . ',' |
5329 | . (int) $member['activated'] . ',"' |
5330 | . sprintf("#%02x%02x%02x", $c['r'] ?? 0, $c['g'] ?? 0, $c['b'] ?? 0) . '"' |
5331 | . "\n"; |
5332 | } |
5333 | // bills |
5334 | yield "\nwhat,amount,date,timestamp,payer_name,payer_weight,payer_active,owers,repeat,repeatfreq,repeatallactive,repeatuntil,categoryid,paymentmode,paymentmodeid,comment\n"; |
5335 | $bills = $this->getBills($projectId); |
5336 | foreach ($bills as $bill) { |
5337 | $owerNames = []; |
5338 | foreach ($bill['owers'] as $ower) { |
5339 | $owerNames[] = $ower['name']; |
5340 | } |
5341 | $owersTxt = implode(',', $owerNames); |
5342 | |
5343 | $payer_id = $bill['payer_id']; |
5344 | $payer_name = $memberIdToName[$payer_id]; |
5345 | $payer_weight = $memberIdToWeight[$payer_id]; |
5346 | $payer_active = $memberIdToActive[$payer_id]; |
5347 | $dateTime = DateTime::createFromFormat('U', $bill['timestamp']); |
5348 | $oldDateStr = $dateTime->format('Y-m-d'); |
5349 | yield '"' . $bill['what'] . '",' |
5350 | . (float) $bill['amount'] . ',' |
5351 | . $oldDateStr . ',' |
5352 | . $bill['timestamp'] . ',"' |
5353 | . $payer_name . '",' |
5354 | . (float) $payer_weight . ',' |
5355 | . $payer_active . ',"' |
5356 | . $owersTxt . '",' |
5357 | . $bill['repeat'] . ',' |
5358 | . $bill['repeatfreq'] . ',' |
5359 | . $bill['repeatallactive'] .',' |
5360 | . $bill['repeatuntil'] . ',' |
5361 | . $bill['categoryid'] . ',' |
5362 | . $bill['paymentmode'] . ',' |
5363 | . $bill['paymentmodeid'] . ',"' |
5364 | . urlencode($bill['comment']) . '"' |
5365 | . "\n"; |
5366 | } |
5367 | |
5368 | // write categories |
5369 | $categories = $projectInfo['categories']; |
5370 | if (count($categories) > 0) { |
5371 | yield "\ncategoryname,categoryid,icon,color\n"; |
5372 | foreach ($categories as $id => $cat) { |
5373 | yield '"' . $cat['name'] . '",' . |
5374 | (int) $id . ',"' . |
5375 | $cat['icon'] . '","' . |
5376 | $cat['color'] . '"' . |
5377 | "\n"; |
5378 | } |
5379 | } |
5380 | |
5381 | // write payment modes |
5382 | $paymentModes = $projectInfo['paymentmodes']; |
5383 | if (count($paymentModes) > 0) { |
5384 | yield "\npaymentmodename,paymentmodeid,icon,color\n"; |
5385 | foreach ($paymentModes as $id => $pm) { |
5386 | yield '"' . $pm['name'] . '",' . |
5387 | (int) $id . ',"' . |
5388 | $pm['icon'] . '","' . |
5389 | $pm['color'] . '"' . |
5390 | "\n"; |
5391 | } |
5392 | } |
5393 | |
5394 | // write currencies |
5395 | $currencies = $projectInfo['currencies']; |
5396 | if (count($currencies) > 0) { |
5397 | yield "\ncurrencyname,exchange_rate\n"; |
5398 | // main currency |
5399 | yield '"' . $projectInfo['currencyname'] . '",1' . "\n"; |
5400 | foreach ($currencies as $cur) { |
5401 | yield '"' . $cur['name'] |
5402 | . '",' . (float) $cur['exchange_rate'] |
5403 | . "\n"; |
5404 | } |
5405 | } |
5406 | |
5407 | return []; |
5408 | } |
5409 | |
5410 | /** |
5411 | * Wrap the import process in an atomic DB transaction |
5412 | * This increases insert performance a lot |
5413 | * |
5414 | * importCsvProject() still takes care of cleaning up created entities in case of error |
5415 | * but this could be done by rollBack |
5416 | * |
5417 | * This could be done with TTransactional::atomic() when we drop support for NC < 24 |
5418 | * |
5419 | * @param $handle |
5420 | * @param string $userId |
5421 | * @param string $projectName |
5422 | * @return array |
5423 | * @throws Throwable |
5424 | * @throws \OCP\DB\Exception |
5425 | */ |
5426 | public function importCsvProjectAtomicWrapper($handle, string $userId, string $projectName): array { |
5427 | $this->db->beginTransaction(); |
5428 | try { |
5429 | $result = $this->importCsvProjectStream($handle, $userId, $projectName); |
5430 | $this->db->commit(); |
5431 | return $result; |
5432 | } catch (Throwable $e) { |
5433 | $this->db->rollBack(); |
5434 | throw $e; |
5435 | } |
5436 | } |
5437 | |
5438 | /** |
5439 | * Import CSV project file |
5440 | * |
5441 | * @param string $path |
5442 | * @param string $userId |
5443 | * @return array |
5444 | */ |
5445 | public function importCsvProject(string $path, string $userId): array { |
5446 | $cleanPath = str_replace(['../', '..\\'], '', $path); |
5447 | $userFolder = $this->root->getUserFolder($userId); |
5448 | if ($userFolder->nodeExists($cleanPath)) { |
5449 | $file = $userFolder->get($cleanPath); |
5450 | if ($file->getType() === FileInfo::TYPE_FILE) { |
5451 | if (($handle = $file->fopen('r')) !== false) { |
5452 | $projectName = preg_replace('/\.csv$/', '', $file->getName()); |
5453 | return $this->importCsvProjectAtomicWrapper($handle, $userId, $projectName); |
5454 | } else { |
5455 | return ['message' => $this->trans->t('Access denied')]; |
5456 | } |
5457 | } else { |
5458 | return ['message' => $this->trans->t('Access denied')]; |
5459 | } |
5460 | } else { |
5461 | return ['message' => $this->trans->t('Access denied')]; |
5462 | } |
5463 | } |
5464 | |
5465 | public function importCsvProjectStream($handle, string $userId, string $projectName): array { |
5466 | $columns = []; |
5467 | $membersByName = []; |
5468 | $bills = []; |
5469 | $currencies = []; |
5470 | $mainCurrencyName = null; |
5471 | $categories = []; |
5472 | $categoryIdConv = []; |
5473 | $paymentModes = []; |
5474 | $paymentModeIdConv = []; |
5475 | $previousLineEmpty = false; |
5476 | $currentSection = null; |
5477 | $row = 0; |
5478 | while (($data = fgetcsv($handle, 0, ',')) !== false) { |
5479 | $uni = array_unique($data); |
5480 | if ($data === [null] || (count($uni) === 1 && $uni[0] === '')) { |
5481 | $previousLineEmpty = true; |
5482 | } elseif ($row === 0 || $previousLineEmpty) { |
5483 | // determine which section we're entering |
5484 | $previousLineEmpty = false; |
5485 | $nbCol = count($data); |
5486 | $columns = []; |
5487 | for ($c = 0; $c < $nbCol; $c++) { |
5488 | if ($data[$c] !== '') { |
5489 | $columns[$data[$c]] = $c; |
5490 | } |
5491 | } |
5492 | if (array_key_exists('what', $columns) |
5493 | && array_key_exists('amount', $columns) |
5494 | && (array_key_exists('date', $columns) || array_key_exists('timestamp', $columns)) |
5495 | && array_key_exists('payer_name', $columns) |
5496 | && array_key_exists('payer_weight', $columns) |
5497 | && array_key_exists('owers', $columns) |
5498 | ) { |
5499 | $currentSection = 'bills'; |
5500 | } elseif (array_key_exists('name', $columns) |
5501 | && array_key_exists('weight', $columns) |
5502 | && array_key_exists('active', $columns) |
5503 | && array_key_exists('color', $columns) |
5504 | ) { |
5505 | $currentSection = 'members'; |
5506 | } elseif (array_key_exists('icon', $columns) |
5507 | && array_key_exists('color', $columns) |
5508 | && array_key_exists('paymentmodeid', $columns) |
5509 | && array_key_exists('paymentmodename', $columns) |
5510 | ) { |
5511 | $currentSection = 'paymentmodes'; |
5512 | } elseif (array_key_exists('icon', $columns) |
5513 | && array_key_exists('color', $columns) |
5514 | && array_key_exists('categoryid', $columns) |
5515 | && array_key_exists('categoryname', $columns) |
5516 | ) { |
5517 | $currentSection = 'categories'; |
5518 | } elseif (array_key_exists('exchange_rate', $columns) |
5519 | && array_key_exists('currencyname', $columns) |
5520 | ) { |
5521 | $currentSection = 'currencies'; |
5522 | } else { |
5523 | fclose($handle); |
5524 | return ['message' => $this->trans->t('Malformed CSV, bad column names at line %1$s', [$row + 1])]; |
5525 | } |
5526 | } else { |
5527 | // normal line: bill/category/payment mode/currency |
5528 | $previousLineEmpty = false; |
5529 | if ($currentSection === 'categories') { |
5530 | if (mb_strlen($data[$columns['icon']], 'UTF-8') && preg_match('!\S!u', $data[$columns['icon']])) { |
5531 | $icon = $data[$columns['icon']]; |
5532 | } else { |
5533 | $icon = null; |
5534 | } |
5535 | $color = $data[$columns['color']]; |
5536 | $categoryid = $data[$columns['categoryid']]; |
5537 | $categoryname = $data[$columns['categoryname']]; |
5538 | $categories[] = [ |
5539 | 'icon' => $icon, |
5540 | 'color' => $color, |
5541 | 'id' => $categoryid, |
5542 | 'name' => $categoryname, |
5543 | ]; |
5544 | } elseif ($currentSection === 'paymentmodes') { |
5545 | if (mb_strlen($data[$columns['icon']], 'UTF-8') && preg_match('!\S!u', $data[$columns['icon']])) { |
5546 | $icon = $data[$columns['icon']]; |
5547 | } else { |
5548 | $icon = null; |
5549 | } |
5550 | $color = $data[$columns['color']]; |
5551 | $paymentmodeid = $data[$columns['paymentmodeid']]; |
5552 | $paymentmodename = $data[$columns['paymentmodename']]; |
5553 | $paymentModes[] = [ |
5554 | 'icon' => $icon, |
5555 | 'color' => $color, |
5556 | 'id' => $paymentmodeid, |
5557 | 'name' => $paymentmodename, |
5558 | ]; |
5559 | } elseif ($currentSection === 'currencies') { |
5560 | $name = $data[$columns['currencyname']]; |
5561 | $exchange_rate = $data[$columns['exchange_rate']]; |
5562 | if (((float) $exchange_rate) === 1.0) { |
5563 | $mainCurrencyName = $name; |
5564 | } else { |
5565 | $currencies[] = [ |
5566 | 'name' => $name, |
5567 | 'exchange_rate' => $exchange_rate, |
5568 | ]; |
5569 | } |
5570 | } elseif ($currentSection === 'members') { |
5571 | $name = trim($data[$columns['name']]); |
5572 | $weight = $data[$columns['weight']]; |
5573 | $active = $data[$columns['active']]; |
5574 | $color = $data[$columns['color']]; |
5575 | if (strlen($name) > 0 |
5576 | && is_numeric($weight) |
5577 | && is_numeric($active) |
5578 | && preg_match('/^#[0-9A-Fa-f]+$/', $color) !== false |
5579 | ) { |
5580 | $membersByName[$name] = [ |
5581 | 'weight' => $weight, |
5582 | 'active' => (int) $active !== 0, |
5583 | 'color' => $color, |
5584 | ]; |
5585 | } else { |
5586 | fclose($handle); |
5587 | return ['message' => $this->trans->t('Malformed CSV, invalid member on line %1$s', [$row + 1])]; |
5588 | } |
5589 | } elseif ($currentSection === 'bills') { |
5590 | $what = $data[$columns['what']]; |
5591 | $amount = $data[$columns['amount']]; |
5592 | $timestamp = null; |
5593 | // priority to timestamp |
5594 | if (array_key_exists('timestamp', $columns)) { |
5595 | $timestamp = $data[$columns['timestamp']]; |
5596 | } elseif (array_key_exists('date', $columns)) { |
5597 | $date = $data[$columns['date']]; |
5598 | $datetime = DateTime::createFromFormat('Y-m-d', $date); |
5599 | if ($datetime !== false) { |
5600 | $timestamp = $datetime->getTimestamp(); |
5601 | } |
5602 | } |
5603 | if ($timestamp === null) { |
5604 | fclose($handle); |
5605 | return ['message' => $this->trans->t('Malformed CSV, missing or invalid date/timestamp on line %1$s', [$row + 1])]; |
5606 | } |
5607 | $payer_name = $data[$columns['payer_name']]; |
5608 | $payer_weight = $data[$columns['payer_weight']]; |
5609 | $owers = $data[$columns['owers']]; |
5610 | $payer_active = array_key_exists('payer_active', $columns) ? $data[$columns['payer_active']] : 1; |
5611 | $repeat = array_key_exists('repeat', $columns) ? $data[$columns['repeat']] : Application::FREQUENCIES['no']; |
5612 | $categoryid = array_key_exists('categoryid', $columns) ? (int) $data[$columns['categoryid']] : null; |
5613 | $paymentmode = array_key_exists('paymentmode', $columns) ? $data[$columns['paymentmode']] : null; |
5614 | $paymentmodeid = array_key_exists('paymentmodeid', $columns) ? $data[$columns['paymentmodeid']] : null; |
5615 | $repeatallactive = array_key_exists('repeatallactive', $columns) ? $data[$columns['repeatallactive']] : 0; |
5616 | $repeatuntil = array_key_exists('repeatuntil', $columns) ? $data[$columns['repeatuntil']] : null; |
5617 | $repeatfreq = array_key_exists('repeatfreq', $columns) ? $data[$columns['repeatfreq']] : 1; |
5618 | $comment = array_key_exists('comment', $columns) ? urldecode($data[$columns['comment']] ?? '') : null; |
5619 | |
5620 | // manage members |
5621 | if (!isset($membersByName[$payer_name])) { |
5622 | $membersByName[$payer_name] = [ |
5623 | 'active' => ((int) $payer_active) !== 0, |
5624 | 'weight' => 1.0, |
5625 | 'color' => null, |
5626 | ]; |
5627 | if (is_numeric($payer_weight)) { |
5628 | $membersByName[$payer_name]['weight'] = (float) $payer_weight; |
5629 | } else { |
5630 | fclose($handle); |
5631 | return ['message' => $this->trans->t('Malformed CSV, invalid payer weight on line %1$s', [$row + 1])]; |
5632 | } |
5633 | } |
5634 | if (strlen($owers) === 0) { |
5635 | fclose($handle); |
5636 | return ['message' => $this->trans->t('Malformed CSV, invalid owers on line %1$s', [$row + 1])]; |
5637 | } |
5638 | if ($what !== 'deleteMeIfYouWant') { |
5639 | $owersArray = explode(',', $owers); |
5640 | foreach ($owersArray as $ower) { |
5641 | $strippedOwer = trim($ower); |
5642 | if (strlen($strippedOwer) === 0) { |
5643 | fclose($handle); |
5644 | return ['message' => $this->trans->t('Malformed CSV, invalid owers on line %1$s', [$row + 1])]; |
5645 | } |
5646 | if (!isset($membersByName[$strippedOwer])) { |
5647 | $membersByName[$strippedOwer]['weight'] = 1.0; |
5648 | $membersByName[$strippedOwer]['active'] = true; |
5649 | $membersByName[$strippedOwer]['color'] = null; |
5650 | } |
5651 | } |
5652 | if (!is_numeric($amount)) { |
5653 | fclose($handle); |
5654 | return ['message' => $this->trans->t('Malformed CSV, invalid amount on line %1$s', [$row + 1])]; |
5655 | } |
5656 | $bills[] = [ |
5657 | 'what' => $what, |
5658 | 'comment' => $comment, |
5659 | 'timestamp' => $timestamp, |
5660 | 'amount' => $amount, |
5661 | 'payer_name' => $payer_name, |
5662 | 'owers' => $owersArray, |
5663 | 'paymentmode' => $paymentmode, |
5664 | 'paymentmodeid' => $paymentmodeid, |
5665 | 'categoryid' => $categoryid, |
5666 | 'repeat' => $repeat, |
5667 | 'repeatuntil' => $repeatuntil, |
5668 | 'repeatallactive' => $repeatallactive, |
5669 | 'repeatfreq' => $repeatfreq, |
5670 | ]; |
5671 | } |
5672 | } |
5673 | } |
5674 | $row++; |
5675 | } |
5676 | fclose($handle); |
5677 | |
5678 | $memberNameToId = []; |
5679 | |
5680 | // add project |
5681 | $user = $this->userManager->get($userId); |
5682 | $userEmail = $user->getEMailAddress(); |
5683 | $projectid = Utils::slugify($projectName); |
5684 | $createDefaultCategories = (count($categories) === 0); |
5685 | $createDefaultPaymentModes = (count($paymentModes) === 0); |
5686 | $projResult = $this->createProject( |
5687 | $projectName, $projectid, '', $userEmail, $userId, |
5688 | $createDefaultCategories, $createDefaultPaymentModes |
5689 | ); |
5690 | if (!isset($projResult['id'])) { |
5691 | return ['message' => $this->trans->t('Error in project creation, %1$s', [$projResult['message'] ?? ''])]; |
5692 | } |
5693 | // set project main currency |
5694 | if ($mainCurrencyName !== null) { |
5695 | $this->editProject($projectid, $projectName, null, null, null, $mainCurrencyName); |
5696 | } |
5697 | // add payment modes |
5698 | foreach ($paymentModes as $pm) { |
5699 | $insertedPmId = $this->addPaymentMode($projectid, $pm['name'], $pm['icon'], $pm['color']); |
5700 | if (!is_numeric($insertedPmId)) { |
5701 | $this->deleteProject($projectid); |
5702 | return ['message' => $this->trans->t('Error when adding payment mode %1$s', [$pm['name']])]; |
5703 | } |
5704 | $paymentModeIdConv[$pm['id']] = $insertedPmId; |
5705 | } |
5706 | // add categories |
5707 | foreach ($categories as $cat) { |
5708 | $insertedCatId = $this->addCategory($projectid, $cat['name'], $cat['icon'], $cat['color']); |
5709 | if (!is_numeric($insertedCatId)) { |
5710 | $this->deleteProject($projectid); |
5711 | return ['message' => $this->trans->t('Error when adding category %1$s', [$cat['name']])]; |
5712 | } |
5713 | $categoryIdConv[$cat['id']] = $insertedCatId; |
5714 | } |
5715 | // add currencies |
5716 | foreach ($currencies as $cur) { |
5717 | $insertedCurId = $this->addCurrency($projectid, $cur['name'], $cur['exchange_rate']); |
5718 | if (!is_numeric($insertedCurId)) { |
5719 | $this->deleteProject($projectid); |
5720 | return ['message' => $this->trans->t('Error when adding currency %1$s', [$cur['name']])]; |
5721 | } |
5722 | } |
5723 | // add members |
5724 | foreach ($membersByName as $memberName => $member) { |
5725 | $insertedMember = $this->addMember( |
5726 | $projectid, $memberName, $member['weight'], $member['active'], $member['color'] ?? null |
5727 | ); |
5728 | if (!is_array($insertedMember)) { |
5729 | $this->deleteProject($projectid); |
5730 | return ['message' => $this->trans->t('Error when adding member %1$s', [$memberName])]; |
5731 | } |
5732 | $memberNameToId[$memberName] = $insertedMember['id']; |
5733 | } |
5734 | $dbPaymentModes = $this->getCategoriesOrPaymentModes($projectid, false); |
5735 | // add bills |
5736 | foreach ($bills as $bill) { |
5737 | // manage category id if this is a custom category |
5738 | $catId = $bill['categoryid']; |
5739 | if (is_numeric($catId) && (int) $catId > 0) { |
5740 | $catId = $categoryIdConv[$catId]; |
5741 | } |
5742 | // manage payment mode id if this is a custom payment mode |
5743 | $pmId = $bill['paymentmodeid']; |
5744 | if (is_numeric($pmId) && (int) $pmId > 0) { |
5745 | $pmId = $paymentModeIdConv[$pmId]; |
5746 | } |
5747 | $payerId = $memberNameToId[$bill['payer_name']]; |
5748 | $owerIds = []; |
5749 | foreach ($bill['owers'] as $owerName) { |
5750 | $strippedOwer = trim($owerName); |
5751 | $owerIds[] = $memberNameToId[$strippedOwer]; |
5752 | } |
5753 | $owerIdsStr = implode(',', $owerIds); |
5754 | $addBillResult = $this->addBill( |
5755 | $projectid, null, $bill['what'], $payerId, |
5756 | $owerIdsStr, $bill['amount'], $bill['repeat'], |
5757 | $bill['paymentmode'], $pmId, |
5758 | $catId, $bill['repeatallactive'], |
5759 | $bill['repeatuntil'], $bill['timestamp'], $bill['comment'], $bill['repeatfreq'], |
5760 | $dbPaymentModes |
5761 | ); |
5762 | if (!isset($addBillResult['inserted_id'])) { |
5763 | $this->deleteProject($projectid); |
5764 | return ['message' => $this->trans->t('Error when adding bill %1$s', [$bill['what']])]; |
5765 | } |
5766 | } |
5767 | return ['project_id' => $projectid]; |
5768 | } |
5769 | |
5770 | /** |
5771 | * Import SplitWise project file |
5772 | * |
5773 | * @param string $path |
5774 | * @param string $userId |
5775 | * @return array |
5776 | */ |
5777 | public function importSWProject(string $path, string $userId): array { |
5778 | $cleanPath = str_replace(['../', '..\\'], '', $path); |
5779 | $userFolder = $this->root->getUserFolder($userId); |
5780 | if ($userFolder->nodeExists($cleanPath)) { |
5781 | $file = $userFolder->get($cleanPath); |
5782 | if ($file->getType() === FileInfo::TYPE_FILE) { |
5783 | if (($handle = $file->fopen('r')) !== false) { |
5784 | $columns = []; |
5785 | $membersWeight = []; |
5786 | $bills = []; |
5787 | $owersArray = []; |
5788 | $categoryNames = []; |
5789 | $row = 0; |
5790 | $nbCol = 0; |
5791 | |
5792 | $columnNamesLineFound = false; |
5793 | while (($data = fgetcsv($handle, 1000, ',')) !== false) { |
5794 | // look for column order line |
5795 | if (!$columnNamesLineFound) { |
5796 | $nbCol = count($data); |
5797 | for ($c = 0; $c < $nbCol; $c++) { |
5798 | $columns[$data[$c]] = $c; |
5799 | } |
5800 | if (!array_key_exists('Date', $columns) |
5801 | || !array_key_exists('Description', $columns) |
5802 | || !array_key_exists('Category', $columns) |
5803 | || !array_key_exists('Cost', $columns) |
5804 | || !array_key_exists('Currency', $columns) |
5805 | ) { |
5806 | $columns = []; |
5807 | $row++; |
5808 | continue; |
5809 | } |
5810 | $columnNamesLineFound = true; |
5811 | // manage members |
5812 | $m = 0; |
5813 | for ($c = 5; $c < $nbCol; $c++) { |
5814 | $owersArray[$m] = $data[$c]; |
5815 | $m++; |
5816 | } |
5817 | foreach ($owersArray as $ower) { |
5818 | if (strlen($ower) === 0) { |
5819 | fclose($handle); |
5820 | return ['message' => $this->trans->t('Malformed CSV, cannot have an empty ower')]; |
5821 | } |
5822 | if (!array_key_exists($ower, $membersWeight)) { |
5823 | $membersWeight[$ower] = 1.0; |
5824 | } |
5825 | } |
5826 | } elseif (!isset($data[$columns['Date']]) || empty($data[$columns['Date']])) { |
5827 | // skip empty lines |
5828 | } elseif (isset($data[$columns['Description']]) && $data[$columns['Description']] === 'Total balance') { |
5829 | // skip the total lines |
5830 | } else { |
5831 | // normal line : bill |
5832 | $what = $data[$columns['Description']]; |
5833 | $cost = trim($data[$columns['Cost']]); |
5834 | if (empty($cost)) { |
5835 | // skip lines with no cost, it might be the balances line |
5836 | $row++; |
5837 | continue; |
5838 | } |
5839 | $date = $data[$columns['Date']]; |
5840 | $datetime = DateTime::createFromFormat('Y-m-d', $date); |
5841 | if ($datetime === false) { |
5842 | fclose($handle); |
5843 | return ['message' => $this->trans->t('Malformed CSV, missing or invalid date/timestamp on line %1$s', [$row])]; |
5844 | } |
5845 | $timestamp = $datetime->getTimestamp(); |
5846 | |
5847 | $categoryName = null; |
5848 | // manage categories |
5849 | if (array_key_exists('Category', $columns) |
5850 | && $data[$columns['Category']] !== null |
5851 | && $data[$columns['Category']] !== '') { |
5852 | $categoryName = $data[$columns['Category']]; |
5853 | if (!in_array($categoryName, $categoryNames)) { |
5854 | $categoryNames[] = $categoryName; |
5855 | } |
5856 | } |
5857 | |
5858 | // new algorithm |
5859 | // get those with a negative value, they will be the owers in generated bills |
5860 | $negativeCols = []; |
5861 | for ($c = 5; $c < $nbCol; $c++) { |
5862 | $amount = $data[$c]; |
5863 | if (!is_numeric($amount)) { |
5864 | fclose($handle); |
5865 | return ['message' => $this->trans->t('Malformed CSV, bad amount on line %1$s', [$row])]; |
5866 | } |
5867 | if ($amount < 0) { |
5868 | $negativeCols[] = $c; |
5869 | } |
5870 | } |
5871 | $owersList = array_map(static function($c) use ($owersArray) { |
5872 | return $owersArray[$c - 5]; |
5873 | }, $negativeCols); |
5874 | // each positive one: bill with member-specific amount (not the full amount), owers are the negative ones |
5875 | for ($c = 5; $c < $nbCol; $c++) { |
5876 | $amount = $data[$c]; |
5877 | if ($amount > 0) { |
5878 | $payer_name = $owersArray[$c - 5]; |
5879 | if (empty($payer_name)) { |
5880 | fclose($handle); |
5881 | return ['message' => $this->trans->t('Malformed CSV, no payer on line %1$s', [$row])]; |
5882 | } |
5883 | $bill = [ |
5884 | 'what' => $what, |
5885 | 'timestamp' => $timestamp, |
5886 | 'amount' => $amount, |
5887 | 'payer_name' => $payer_name, |
5888 | 'owers' => $owersList |
5889 | ]; |
5890 | if ($categoryName !== null) { |
5891 | $bill['category_name'] = $categoryName; |
5892 | } |
5893 | $bills[] = $bill; |
5894 | } |
5895 | } |
5896 | } |
5897 | $row++; |
5898 | } |
5899 | fclose($handle); |
5900 | |
5901 | if (!$columnNamesLineFound) { |
5902 | return ['message' => $this->trans->t('Malformed CSV, impossible to find the column names')]; |
5903 | } |
5904 | |
5905 | $memberNameToId = []; |
5906 | |
5907 | // add project |
5908 | $user = $this->userManager->get($userId); |
5909 | $userEmail = $user->getEMailAddress(); |
5910 | $projectName = preg_replace('/\.csv$/', '', $file->getName()); |
5911 | $projectid = Utils::slugify($projectName); |
5912 | // create default categories only if none are found in the CSV |
5913 | $createDefaultCategories = (count($categoryNames) === 0); |
5914 | $projResult = $this->createProject( |
5915 | $projectName, $projectid, '', $userEmail, |
5916 | $userId, $createDefaultCategories |
5917 | ); |
5918 | if (!isset($projResult['id'])) { |
5919 | return ['message' => $this->trans->t('Error in project creation, %1$s', [$projResult['message'] ?? ''])]; |
5920 | } |
5921 | // add categories |
5922 | $catNameToId = []; |
5923 | foreach ($categoryNames as $categoryName) { |
5924 | $insertedCatId = $this->addCategory($projectid, $categoryName, null, '#000000'); |
5925 | if (!is_numeric($insertedCatId)) { |
5926 | $this->deleteProject($projectid); |
5927 | return ['message' => $this->trans->t('Error when adding category %1$s', [$categoryName])]; |
5928 | } |
5929 | $catNameToId[$categoryName] = $insertedCatId; |
5930 | } |
5931 | // add members |
5932 | foreach ($membersWeight as $memberName => $weight) { |
5933 | $insertedMember = $this->addMember($projectid, $memberName, $weight); |
5934 | if (!is_array($insertedMember)) { |
5935 | $this->deleteProject($projectid); |
5936 | return ['message' => $this->trans->t('Error when adding member %1$s', [$memberName])]; |
5937 | } |
5938 | $memberNameToId[$memberName] = $insertedMember['id']; |
5939 | } |
5940 | // add bills |
5941 | foreach ($bills as $bill) { |
5942 | $payerId = $memberNameToId[$bill['payer_name']]; |
5943 | $owerIds = []; |
5944 | foreach ($bill['owers'] as $owerName) { |
5945 | $owerIds[] = $memberNameToId[$owerName]; |
5946 | } |
5947 | $owerIdsStr = implode(',', $owerIds); |
5948 | // category |
5949 | $catId = null; |
5950 | if (array_key_exists('category_name', $bill) |
5951 | && array_key_exists($bill['category_name'], $catNameToId)) { |
5952 | $catId = $catNameToId[$bill['category_name']]; |
5953 | } |
5954 | $addBillResult = $this->addBill( |
5955 | $projectid, null, $bill['what'], $payerId, $owerIdsStr, |
5956 | $bill['amount'], Application::FREQUENCIES['no'],null, 0, $catId, |
5957 | 0, null, $bill['timestamp'], null, null, [] |
5958 | ); |
5959 | if (!isset($addBillResult['inserted_id'])) { |
5960 | $this->deleteProject($projectid); |
5961 | return ['message' => $this->trans->t('Error when adding bill %1$s', [$bill['what']])]; |
5962 | } |
5963 | } |
5964 | return ['project_id' => $projectid]; |
5965 | } else { |
5966 | return ['message' => $this->trans->t('Access denied')]; |
5967 | } |
5968 | } else { |
5969 | return ['message' => $this->trans->t('Access denied')]; |
5970 | } |
5971 | } else { |
5972 | return ['message' => $this->trans->t('Access denied')]; |
5973 | } |
5974 | } |
5975 | |
5976 | /** |
5977 | * auto export |
5978 | * triggered by NC cron job |
5979 | * |
5980 | * export projects |
5981 | */ |
5982 | public function cronAutoExport(): void { |
5983 | date_default_timezone_set('UTC'); |
5984 | // last day |
5985 | $now = new DateTime(); |
5986 | $y = $now->format('Y'); |
5987 | $m = $now->format('m'); |
5988 | $d = $now->format('d'); |
5989 | |
5990 | // get begining of today |
5991 | $dateMaxDay = new DateTime($y.'-'.$m.'-'.$d); |
5992 | $maxDayTimestamp = $dateMaxDay->getTimestamp(); |
5993 | $minDayTimestamp = $maxDayTimestamp - 24*60*60; |
5994 | |
5995 | $dateMaxDay->modify('-1 day'); |
5996 | $dailySuffix = '_'.$this->trans->t('daily').'_'.$dateMaxDay->format('Y-m-d'); |
5997 | |
5998 | // last week |
5999 | $now = new DateTime(); |
6000 | while (((int) $now->format('N')) !== 1) { |
6001 | $now->modify('-1 day'); |
6002 | } |
6003 | $y = $now->format('Y'); |
6004 | $m = $now->format('m'); |
6005 | $d = $now->format('d'); |
6006 | $dateWeekMax = new DateTime($y.'-'.$m.'-'.$d); |
6007 | $maxWeekTimestamp = $dateWeekMax->getTimestamp(); |
6008 | $minWeekTimestamp = $maxWeekTimestamp - 7*24*60*60; |
6009 | $dateWeekMin = new DateTime($y.'-'.$m.'-'.$d); |
6010 | $dateWeekMin->modify('-7 day'); |
6011 | $weeklySuffix = '_'.$this->trans->t('weekly').'_'.$dateWeekMin->format('Y-m-d'); |
6012 | |
6013 | // last month |
6014 | $now = new DateTime(); |
6015 | while (((int) $now->format('d')) !== 1) { |
6016 | $now->modify('-1 day'); |
6017 | } |
6018 | $y = $now->format('Y'); |
6019 | $m = $now->format('m'); |
6020 | $d = $now->format('d'); |
6021 | $dateMonthMax = new DateTime($y.'-'.$m.'-'.$d); |
6022 | $maxMonthTimestamp = $dateMonthMax->getTimestamp(); |
6023 | $now->modify('-1 day'); |
6024 | while (((int) $now->format('d')) !== 1) { |
6025 | $now->modify('-1 day'); |
6026 | } |
6027 | $y = (int) $now->format('Y'); |
6028 | $m = (int) $now->format('m'); |
6029 | $d = (int) $now->format('d'); |
6030 | $dateMonthMin = new DateTime($y.'-'.$m.'-'.$d); |
6031 | $minMonthTimestamp = $dateMonthMin->getTimestamp(); |
6032 | $monthlySuffix = '_'.$this->trans->t('monthly').'_'.$dateMonthMin->format('Y-m'); |
6033 | |
6034 | // $weekFilterArray = []; |
6035 | // $weekFilterArray['tsmin'] = $minWeekTimestamp; |
6036 | // $weekFilterArray['tsmax'] = $maxWeekTimestamp; |
6037 | // $dayFilterArray = []; |
6038 | // $dayFilterArray['tsmin'] = $minDayTimestamp; |
6039 | // $dayFilterArray['tsmax'] = $maxDayTimestamp; |
6040 | // $monthFilterArray = []; |
6041 | // $monthFilterArray['tsmin'] = $minMonthTimestamp; |
6042 | // $monthFilterArray['tsmax'] = $maxMonthTimestamp; |
6043 | |
6044 | $qb = $this->db->getQueryBuilder(); |
6045 | |
6046 | foreach ($this->userManager->search('') as $u) { |
6047 | $uid = $u->getUID(); |
6048 | $outPath = $this->config->getUserValue($uid, 'cospend', 'outputDirectory', '/Cospend'); |
6049 | |
6050 | $qb->select('id', 'name', 'autoexport') |
6051 | ->from('cospend_projects') |
6052 | ->where( |
6053 | $qb->expr()->eq('userid', $qb->createNamedParameter($uid, IQueryBuilder::PARAM_STR)) |
6054 | ) |
6055 | ->andWhere( |
6056 | $qb->expr()->neq('autoexport', $qb->createNamedParameter(Application::FREQUENCIES['no'], IQueryBuilder::PARAM_STR)) |
6057 | ); |
6058 | $req = $qb->executeQuery(); |
6059 | |
6060 | $dbProjectId = null; |
6061 | while ($row = $req->fetch()) { |
6062 | $dbProjectId = $row['id']; |
6063 | $autoexport = $row['autoexport']; |
6064 | |
6065 | $suffix = $dailySuffix; |
6066 | // TODO add suffix for all frequencies |
6067 | if ($autoexport === Application::FREQUENCIES['weekly']) { |
6068 | $suffix = $weeklySuffix; |
6069 | } elseif ($autoexport === Application::FREQUENCIES['monthly']) { |
6070 | $suffix = $monthlySuffix; |
6071 | } |
6072 | // check if file already exists |
6073 | $exportName = $dbProjectId . $suffix . '.csv'; |
6074 | |
6075 | $userFolder = $this->root->getUserFolder($uid); |
6076 | if (!$userFolder->nodeExists($outPath . '/' . $exportName)) { |
6077 | $this->exportCsvProject($dbProjectId, $uid, $exportName); |
6078 | } |
6079 | } |
6080 | $req->closeCursor(); |
6081 | $qb = $qb->resetQueryParts(); |
6082 | } |
6083 | } |
6084 | |
6085 | /** |
6086 | * Search bills with query string |
6087 | * |
6088 | * @param string $projectId |
6089 | * @param string $term |
6090 | * @return array |
6091 | */ |
6092 | public function searchBills(string $projectId, string $term): array { |
6093 | $qb = $this->db->getQueryBuilder(); |
6094 | $qb->select( |
6095 | 'b.id', 'what', 'comment', 'amount', 'timestamp', |
6096 | 'paymentmode', 'paymentmodeid', 'categoryid', |
6097 | 'pr.currencyname', 'me.name', 'me.userid' |
6098 | ) |
6099 | ->from('cospend_bills', 'b') |
6100 | ->innerJoin('b', 'cospend_projects', 'pr', $qb->expr()->eq('b.projectid', 'pr.id')) |
6101 | ->innerJoin('b', 'cospend_members', 'me', $qb->expr()->eq('b.payerid', 'me.id')) |
6102 | ->where( |
6103 | $qb->expr()->eq('b.projectid', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
6104 | ); |
6105 | $qb = $this->applyBillSearchTermCondition($qb, $term, 'b'); |
6106 | $qb->orderBy('timestamp', 'ASC'); |
6107 | $req = $qb->executeQuery(); |
6108 | |
6109 | // bills by id |
6110 | $bills = []; |
6111 | while ($row = $req->fetch()){ |
6112 | $dbBillId = (int) $row['id']; |
6113 | $dbAmount = (float) $row['amount']; |
6114 | $dbWhat = $row['what']; |
6115 | $dbTimestamp = (int) $row['timestamp']; |
6116 | $dbComment = $row['comment']; |
6117 | $dbPaymentMode = $row['paymentmode']; |
6118 | $dbPaymentModeId = (int) $row['paymentmodeid']; |
6119 | $dbCategoryId = (int) $row['categoryid']; |
6120 | $dbProjectCurrencyName = $row['currencyname']; |
6121 | $dbPayerName = $row['name']; |
6122 | $dbPayerUserId = $row['userid']; |
6123 | $bills[] = [ |
6124 | 'id' => $dbBillId, |
6125 | 'projectId' => $projectId, |
6126 | 'amount' => $dbAmount, |
6127 | 'what' => $dbWhat, |
6128 | 'timestamp' => $dbTimestamp, |
6129 | 'comment' => $dbComment, |
6130 | 'paymentmode' => $dbPaymentMode, |
6131 | 'paymentmodeid' => $dbPaymentModeId, |
6132 | 'categoryid' => $dbCategoryId, |
6133 | 'currencyname' => $dbProjectCurrencyName, |
6134 | 'payer_name' => $dbPayerName, |
6135 | 'payer_user_id' => $dbPayerUserId, |
6136 | ]; |
6137 | } |
6138 | $req->closeCursor(); |
6139 | $qb->resetQueryParts(); |
6140 | |
6141 | return $bills; |
6142 | } |
6143 | |
6144 | private function applyBillSearchTermCondition(IQueryBuilder $qb, string $term, string $billTableAlias): IQueryBuilder { |
6145 | $term = strtolower($term); |
6146 | $or = $qb->expr()->orx(); |
6147 | $or->add( |
6148 | $qb->expr()->iLike($billTableAlias . '.what', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($term) . '%', IQueryBuilder::PARAM_STR)) |
6149 | ); |
6150 | $or->add( |
6151 | $qb->expr()->iLike($billTableAlias . '.comment', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($term) . '%', IQueryBuilder::PARAM_STR)) |
6152 | ); |
6153 | // search amount |
6154 | $noCommaTerm = str_replace(',', '.', $term); |
6155 | if (is_numeric($noCommaTerm)) { |
6156 | $amount = (float) $noCommaTerm; |
6157 | $amountMin = $amount - 1.0; |
6158 | $amountMax = $amount + 1.0; |
6159 | $andExpr = $qb->expr()->andX(); |
6160 | $andExpr->add( |
6161 | $qb->expr()->gte($billTableAlias . '.amount', $qb->createNamedParameter($amountMin, IQueryBuilder::PARAM_STR)) |
6162 | ); |
6163 | $andExpr->add( |
6164 | $qb->expr()->lte($billTableAlias . '.amount', $qb->createNamedParameter($amountMax, IQueryBuilder::PARAM_STR)) |
6165 | ); |
6166 | $or->add($andExpr); |
6167 | } |
6168 | $qb->andWhere($or); |
6169 | return $qb; |
6170 | } |
6171 | |
6172 | /** |
6173 | * Get Cospend bill activity |
6174 | * |
6175 | * @param string $userId |
6176 | * @param int|null $since |
6177 | * @return array |
6178 | */ |
6179 | public function getBillActivity(string $userId, ?int $since): array { |
6180 | // get projects |
6181 | $projects = $this->getProjects($userId); |
6182 | |
6183 | // get bills (7 max) |
6184 | $bills = []; |
6185 | foreach ($projects as $project) { |
6186 | $pid = $project['id']; |
6187 | $bl = $this->getBills($pid, null, null, null, null, null, null, null, $since, 20, true); |
6188 | |
6189 | // get members by id |
6190 | $membersById = []; |
6191 | foreach ($project['members'] as $m) { |
6192 | $membersById[$m['id']] = $m; |
6193 | } |
6194 | // add information |
6195 | foreach ($bl as $i => $bill) { |
6196 | $payerId = $bill['payer_id']; |
6197 | $bl[$i]['payer'] = $membersById[$payerId]; |
6198 | $bl[$i]['project_id'] = $pid; |
6199 | $bl[$i]['project_name'] = $project['name']; |
6200 | } |
6201 | |
6202 | $bills = array_merge($bills, $bl); |
6203 | } |
6204 | |
6205 | // sort bills by date |
6206 | usort($bills, function($a, $b) { |
6207 | $ta = $a['timestamp']; |
6208 | $tb = $b['timestamp']; |
6209 | return ($ta > $tb) ? -1 : 1; |
6210 | }); |
6211 | |
6212 | // take 7 firsts |
6213 | return array_slice($bills, 0, 7); |
6214 | } |
6215 | |
6216 | /** |
6217 | * Touch a project |
6218 | * |
6219 | * @param string $projectId |
6220 | * @param int $timestamp |
6221 | * @return void |
6222 | */ |
6223 | private function updateProjectLastChanged(string $projectId, int $timestamp): void { |
6224 | $qb = $this->db->getQueryBuilder(); |
6225 | $qb->update('cospend_projects'); |
6226 | $qb->set('lastchanged', $qb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT)); |
6227 | $qb->where( |
6228 | $qb->expr()->eq('id', $qb->createNamedParameter($projectId, IQueryBuilder::PARAM_STR)) |
6229 | ); |
6230 | $qb->executeStatement(); |
6231 | $qb->resetQueryParts(); |
6232 | } |
6233 | } |