1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* AccountServiceTrait.php |
4
|
|
|
* Copyright (c) 2018 [email protected] |
5
|
|
|
* |
6
|
|
|
* This file is part of Firefly III. |
7
|
|
|
* |
8
|
|
|
* Firefly III is free software: you can redistribute it and/or modify |
9
|
|
|
* it under the terms of the GNU General Public License as published by |
10
|
|
|
* the Free Software Foundation, either version 3 of the License, or |
11
|
|
|
* (at your option) any later version. |
12
|
|
|
* |
13
|
|
|
* Firefly III is distributed in the hope that it will be useful, |
14
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
15
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16
|
|
|
* GNU General Public License for more details. |
17
|
|
|
* |
18
|
|
|
* You should have received a copy of the GNU General Public License |
19
|
|
|
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>. |
20
|
|
|
*/ |
21
|
|
|
|
22
|
|
|
declare(strict_types=1); |
23
|
|
|
|
24
|
|
|
namespace FireflyIII\Services\Internal\Support; |
25
|
|
|
|
26
|
|
|
use FireflyIII\Factory\AccountFactory; |
27
|
|
|
use FireflyIII\Factory\AccountMetaFactory; |
28
|
|
|
use FireflyIII\Factory\TransactionFactory; |
29
|
|
|
use FireflyIII\Factory\TransactionJournalFactory; |
30
|
|
|
use FireflyIII\Models\Account; |
31
|
|
|
use FireflyIII\Models\AccountMeta; |
32
|
|
|
use FireflyIII\Models\AccountType; |
33
|
|
|
use FireflyIII\Models\Note; |
34
|
|
|
use FireflyIII\Models\Transaction; |
35
|
|
|
use FireflyIII\Models\TransactionJournal; |
36
|
|
|
use FireflyIII\Models\TransactionType; |
37
|
|
|
use FireflyIII\Services\Internal\Destroy\JournalDestroyService; |
38
|
|
|
use FireflyIII\User; |
39
|
|
|
use Log; |
40
|
|
|
use Validator; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Trait AccountServiceTrait |
44
|
|
|
* |
45
|
|
|
* @package FireflyIII\Services\Internal\Support |
46
|
|
|
*/ |
47
|
|
|
trait AccountServiceTrait |
48
|
|
|
{ |
49
|
|
|
/** @var array */ |
50
|
|
|
public $validAssetFields = ['accountRole', 'accountNumber', 'currency_id', 'BIC']; |
51
|
|
|
/** @var array */ |
52
|
|
|
public $validCCFields = ['accountRole', 'ccMonthlyPaymentDate', 'ccType', 'accountNumber', 'currency_id', 'BIC']; |
53
|
|
|
/** @var array */ |
54
|
|
|
public $validFields = ['accountNumber', 'currency_id', 'BIC']; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @param Account $account |
58
|
|
|
* |
59
|
|
|
* @return bool |
60
|
|
|
*/ |
61
|
|
|
public function deleteIB(Account $account): bool |
62
|
|
|
{ |
63
|
|
|
Log::debug(sprintf('deleteIB() for account #%d', $account->id)); |
64
|
|
|
$openingBalance = $this->getIBJournal($account); |
65
|
|
|
|
66
|
|
|
// opening balance data? update it! |
67
|
|
|
if (null !== $openingBalance) { |
68
|
|
|
Log::debug('Opening balance journal found, delete journal.'); |
69
|
|
|
/** @var JournalDestroyService $service */ |
70
|
|
|
$service = app(JournalDestroyService::class); |
71
|
|
|
$service->destroy($openingBalance); |
72
|
|
|
|
73
|
|
|
return true; |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
return true; |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* @param null|string $iban |
81
|
|
|
* |
82
|
|
|
* @return null|string |
83
|
|
|
*/ |
84
|
|
|
public function filterIban(?string $iban) |
85
|
|
|
{ |
86
|
|
|
if (null === $iban) { |
87
|
|
|
return null; |
88
|
|
|
} |
89
|
|
|
$data = ['iban' => $iban]; |
90
|
|
|
$rules = ['iban' => 'required|iban']; |
91
|
|
|
$validator = Validator::make($data, $rules); |
92
|
|
|
if ($validator->fails()) { |
93
|
|
|
Log::error(sprintf('Detected invalid IBAN ("%s"). Return NULL instead.', $iban)); |
94
|
|
|
|
95
|
|
|
return null; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
|
99
|
|
|
return $iban; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Find existing opening balance. |
104
|
|
|
* |
105
|
|
|
* @param Account $account |
106
|
|
|
* |
107
|
|
|
* @return TransactionJournal|null |
108
|
|
|
*/ |
109
|
|
|
public function getIBJournal(Account $account): ?TransactionJournal |
110
|
|
|
{ |
111
|
|
|
$journal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') |
112
|
|
|
->where('transactions.account_id', $account->id) |
113
|
|
|
->transactionTypes([TransactionType::OPENING_BALANCE]) |
114
|
|
|
->first(['transaction_journals.*']); |
115
|
|
|
if (null === $journal) { |
116
|
|
|
Log::debug('Could not find a opening balance journal, return NULL.'); |
117
|
|
|
|
118
|
|
|
return null; |
119
|
|
|
} |
120
|
|
|
Log::debug(sprintf('Found opening balance: journal #%d.', $journal->id)); |
121
|
|
|
|
122
|
|
|
return $journal; |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* @param Account $account |
127
|
|
|
* @param array $data |
128
|
|
|
* |
129
|
|
|
* @return TransactionJournal|null |
130
|
|
|
*/ |
131
|
|
|
public function storeIBJournal(Account $account, array $data): ?TransactionJournal |
132
|
|
|
{ |
133
|
|
|
$amount = (string)$data['openingBalance']; |
134
|
|
|
Log::debug(sprintf('Submitted amount is %s', $amount)); |
135
|
|
|
|
136
|
|
|
if (0 === bccomp($amount, '0')) { |
137
|
|
|
return null; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
// store journal, without transactions: |
141
|
|
|
$name = $data['name']; |
142
|
|
|
$currencyId = $data['currency_id']; |
143
|
|
|
$journalData = [ |
144
|
|
|
'type' => TransactionType::OPENING_BALANCE, |
145
|
|
|
'user' => $account->user->id, |
146
|
|
|
'transaction_currency_id' => $currencyId, |
147
|
|
|
'description' => (string)trans('firefly.initial_balance_description', ['account' => $account->name]), |
148
|
|
|
'completed' => true, |
149
|
|
|
'date' => $data['openingBalanceDate'], |
150
|
|
|
'bill_id' => null, |
151
|
|
|
'bill_name' => null, |
152
|
|
|
'piggy_bank_id' => null, |
153
|
|
|
'piggy_bank_name' => null, |
154
|
|
|
'tags' => null, |
155
|
|
|
'notes' => null, |
156
|
|
|
'transactions' => [], |
157
|
|
|
|
158
|
|
|
]; |
159
|
|
|
/** @var TransactionJournalFactory $factory */ |
160
|
|
|
$factory = app(TransactionJournalFactory::class); |
161
|
|
|
$factory->setUser($account->user); |
162
|
|
|
$journal = $factory->create($journalData); |
163
|
|
|
$opposing = $this->storeOpposingAccount($account->user, $name); |
164
|
|
|
Log::notice(sprintf('Created new opening balance journal: #%d', $journal->id)); |
165
|
|
|
|
166
|
|
|
$firstAccount = $account; |
167
|
|
|
$secondAccount = $opposing; |
168
|
|
|
$firstAmount = $amount; |
169
|
|
|
$secondAmount = bcmul($amount, '-1'); |
170
|
|
|
Log::notice(sprintf('First amount is %s, second amount is %s', $firstAmount, $secondAmount)); |
171
|
|
|
|
172
|
|
|
if (bccomp($amount, '0') === -1) { |
173
|
|
|
Log::debug(sprintf('%s is a negative number.', $amount)); |
174
|
|
|
$firstAccount = $opposing; |
175
|
|
|
$secondAccount = $account; |
176
|
|
|
$firstAmount = bcmul($amount, '-1'); |
177
|
|
|
$secondAmount = $amount; |
178
|
|
|
Log::notice(sprintf('First amount is %s, second amount is %s', $firstAmount, $secondAmount)); |
179
|
|
|
} |
180
|
|
|
/** @var TransactionFactory $factory */ |
181
|
|
|
$factory = app(TransactionFactory::class); |
182
|
|
|
$factory->setUser($account->user); |
183
|
|
|
$one = $factory->create( |
184
|
|
|
[ |
185
|
|
|
'account' => $firstAccount, |
186
|
|
|
'transaction_journal' => $journal, |
187
|
|
|
'amount' => $firstAmount, |
188
|
|
|
'currency_id' => $currencyId, |
189
|
|
|
'description' => null, |
190
|
|
|
'identifier' => 0, |
191
|
|
|
'foreign_amount' => null, |
192
|
|
|
'reconciled' => false, |
193
|
|
|
] |
194
|
|
|
); |
195
|
|
|
$two = $factory->create( |
196
|
|
|
[ |
197
|
|
|
'account' => $secondAccount, |
198
|
|
|
'transaction_journal' => $journal, |
199
|
|
|
'amount' => $secondAmount, |
200
|
|
|
'currency_id' => $currencyId, |
201
|
|
|
'description' => null, |
202
|
|
|
'identifier' => 0, |
203
|
|
|
'foreign_amount' => null, |
204
|
|
|
'reconciled' => false, |
205
|
|
|
] |
206
|
|
|
); |
207
|
|
|
Log::notice(sprintf('Stored two transactions for new account, #%d and #%d', $one->id, $two->id)); |
208
|
|
|
|
209
|
|
|
return $journal; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* @param User $user |
214
|
|
|
* @param string $name |
215
|
|
|
* |
216
|
|
|
* @return Account |
217
|
|
|
*/ |
218
|
|
|
public function storeOpposingAccount(User $user, string $name): Account |
219
|
|
|
{ |
220
|
|
|
$name .= ' initial balance'; |
221
|
|
|
Log::debug('Going to create an opening balance opposing account.'); |
222
|
|
|
/** @var AccountFactory $factory */ |
223
|
|
|
$factory = app(AccountFactory::class); |
224
|
|
|
$factory->setUser($user); |
225
|
|
|
|
226
|
|
|
return $factory->findOrCreate($name, AccountType::INITIAL_BALANCE); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* @param Account $account |
231
|
|
|
* @param array $data |
232
|
|
|
* |
233
|
|
|
* @return bool |
234
|
|
|
*/ |
235
|
|
|
public function updateIB(Account $account, array $data): bool |
236
|
|
|
{ |
237
|
|
|
Log::debug(sprintf('updateInitialBalance() for account #%d', $account->id)); |
238
|
|
|
$openingBalance = $this->getIBJournal($account); |
239
|
|
|
|
240
|
|
|
// no opening balance journal? create it: |
241
|
|
|
if (null === $openingBalance) { |
242
|
|
|
Log::debug('No opening balance journal yet, create journal.'); |
243
|
|
|
$this->storeIBJournal($account, $data); |
244
|
|
|
|
245
|
|
|
return true; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
// opening balance data? update it! |
249
|
|
|
if (null !== $openingBalance->id) { |
250
|
|
|
Log::debug('Opening balance journal found, update journal.'); |
251
|
|
|
$this->updateIBJournal($account, $openingBalance, $data); |
252
|
|
|
|
253
|
|
|
return true; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
return true; // @codeCoverageIgnore |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* @param Account $account |
261
|
|
|
* @param TransactionJournal $journal |
262
|
|
|
* @param array $data |
263
|
|
|
* |
264
|
|
|
* @return bool |
265
|
|
|
*/ |
266
|
|
|
public function updateIBJournal(Account $account, TransactionJournal $journal, array $data): bool |
267
|
|
|
{ |
268
|
|
|
$date = $data['openingBalanceDate']; |
269
|
|
|
$amount = (string)$data['openingBalance']; |
270
|
|
|
$negativeAmount = bcmul($amount, '-1'); |
271
|
|
|
$currencyId = (int)$data['currency_id']; |
272
|
|
|
|
273
|
|
|
Log::debug(sprintf('Submitted amount for opening balance to update is "%s"', $amount)); |
274
|
|
|
if (0 === bccomp($amount, '0')) { |
275
|
|
|
Log::notice(sprintf('Amount "%s" is zero, delete opening balance.', $amount)); |
276
|
|
|
/** @var JournalDestroyService $service */ |
277
|
|
|
$service = app(JournalDestroyService::class); |
278
|
|
|
$service->destroy($journal); |
279
|
|
|
|
280
|
|
|
|
281
|
|
|
return true; |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
// update date: |
285
|
|
|
$journal->date = $date; |
286
|
|
|
$journal->transaction_currency_id = $currencyId; |
|
|
|
|
287
|
|
|
$journal->save(); |
288
|
|
|
|
289
|
|
|
// update transactions: |
290
|
|
|
/** @var Transaction $transaction */ |
291
|
|
|
foreach ($journal->transactions()->get() as $transaction) { |
292
|
|
|
if ((int)$account->id === (int)$transaction->account_id) { |
293
|
|
|
Log::debug(sprintf('Will (eq) change transaction #%d amount from "%s" to "%s"', $transaction->id, $transaction->amount, $amount)); |
294
|
|
|
$transaction->amount = $amount; |
295
|
|
|
$transaction->transaction_currency_id = $currencyId; |
296
|
|
|
$transaction->save(); |
297
|
|
|
} |
298
|
|
|
if (!((int)$account->id === (int)$transaction->account_id)) { |
299
|
|
|
Log::debug(sprintf('Will (neq) change transaction #%d amount from "%s" to "%s"', $transaction->id, $transaction->amount, $negativeAmount)); |
300
|
|
|
$transaction->amount = $negativeAmount; |
301
|
|
|
$transaction->transaction_currency_id = $currencyId; |
302
|
|
|
$transaction->save(); |
303
|
|
|
} |
304
|
|
|
} |
305
|
|
|
Log::debug('Updated opening balance journal.'); |
306
|
|
|
|
307
|
|
|
return true; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* Update meta data for account. Depends on type which fields are valid. |
312
|
|
|
* |
313
|
|
|
* @param Account $account |
314
|
|
|
* @param array $data |
315
|
|
|
*/ |
316
|
|
|
public function updateMetaData(Account $account, array $data) |
317
|
|
|
{ |
318
|
|
|
$fields = $this->validFields; |
319
|
|
|
|
320
|
|
|
if ($account->accountType->type === AccountType::ASSET) { |
|
|
|
|
321
|
|
|
$fields = $this->validAssetFields; |
322
|
|
|
} |
323
|
|
|
if ($account->accountType->type === AccountType::ASSET && $data['accountRole'] === 'ccAsset') { |
324
|
|
|
$fields = $this->validCCFields; |
325
|
|
|
} |
326
|
|
|
/** @var AccountMetaFactory $factory */ |
327
|
|
|
$factory = app(AccountMetaFactory::class); |
328
|
|
|
foreach ($fields as $field) { |
329
|
|
|
/** @var AccountMeta $entry */ |
330
|
|
|
$entry = $account->accountMeta()->where('name', $field)->first(); |
331
|
|
|
|
332
|
|
|
// if $data has field and $entry is null, create new one: |
333
|
|
|
if (isset($data[$field]) && null === $entry) { |
334
|
|
|
Log::debug(sprintf('Created meta-field "%s":"%s" for account #%d ("%s") ', $field, $data[$field], $account->id, $account->name)); |
335
|
|
|
$factory->create(['account_id' => $account->id, 'name' => $field, 'data' => $data[$field],]); |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
// if $data has field and $entry is not null, update $entry: |
339
|
|
|
// let's not bother with a service. |
340
|
|
|
if (isset($data[$field]) && null !== $entry) { |
341
|
|
|
$entry->data = $data[$field]; |
|
|
|
|
342
|
|
|
$entry->save(); |
343
|
|
|
Log::debug(sprintf('Updated meta-field "%s":"%s" for #%d ("%s") ', $field, $data[$field], $account->id, $account->name)); |
344
|
|
|
} |
345
|
|
|
} |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* @param Account $account |
350
|
|
|
* @param string $note |
351
|
|
|
* |
352
|
|
|
* @return bool |
353
|
|
|
*/ |
354
|
|
|
public function updateNote(Account $account, string $note): bool |
355
|
|
|
{ |
356
|
|
|
if (0 === strlen($note)) { |
357
|
|
|
$dbNote = $account->notes()->first(); |
358
|
|
|
if (null !== $dbNote) { |
359
|
|
|
$dbNote->delete(); |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
return true; |
363
|
|
|
} |
364
|
|
|
$dbNote = $account->notes()->first(); |
365
|
|
|
if (null === $dbNote) { |
366
|
|
|
$dbNote = new Note; |
367
|
|
|
$dbNote->noteable()->associate($account); |
368
|
|
|
} |
369
|
|
|
$dbNote->text = trim($note); |
370
|
|
|
$dbNote->save(); |
371
|
|
|
|
372
|
|
|
return true; |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
/** |
376
|
|
|
* Verify if array contains valid data to possibly store or update the opening balance. |
377
|
|
|
* |
378
|
|
|
* @param array $data |
379
|
|
|
* |
380
|
|
|
* @return bool |
381
|
|
|
*/ |
382
|
|
|
public function validIBData(array $data): bool |
383
|
|
|
{ |
384
|
|
|
$data['openingBalance'] = (string)($data['openingBalance'] ?? ''); |
385
|
|
|
if (isset($data['openingBalance'], $data['openingBalanceDate']) && \strlen($data['openingBalance']) > 0) { |
386
|
|
|
Log::debug('Array has valid opening balance data.'); |
387
|
|
|
|
388
|
|
|
return true; |
389
|
|
|
} |
390
|
|
|
Log::debug('Array does not have valid opening balance data.'); |
391
|
|
|
|
392
|
|
|
return false; |
393
|
|
|
} |
394
|
|
|
} |
395
|
|
|
|