Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

TransactionService.php 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. <?php
  2. namespace App\Services;
  3. use App\Enums\Accounting\AccountCategory;
  4. use App\Enums\Accounting\AccountType;
  5. use App\Models\Accounting\Account;
  6. use App\Models\Accounting\Transaction;
  7. use App\Models\Banking\BankAccount;
  8. use App\Models\Company;
  9. use App\Scopes\CurrentCompanyScope;
  10. use Illuminate\Support\Carbon;
  11. class TransactionService
  12. {
  13. public function createStartingBalanceIfNeeded(Company $company, Account $account, BankAccount $bankAccount, array $transactions, float $currentBalance, string $startDate): void
  14. {
  15. if ($account->transactions()->withoutGlobalScope(CurrentCompanyScope::class)->doesntExist()) {
  16. $accountSign = $account->category === AccountCategory::Asset ? 1 : -1;
  17. $sumOfTransactions = collect($transactions)->reduce(static function ($carry, $transaction) {
  18. return bcadd($carry, (string) -$transaction->amount, 2);
  19. }, '0.00');
  20. $adjustedBalance = (string) ($currentBalance * $accountSign);
  21. $startingBalance = bcsub($adjustedBalance, $sumOfTransactions, 2);
  22. $this->createStartingBalanceTransaction($company, $account, $bankAccount, (float) $startingBalance, $startDate);
  23. }
  24. }
  25. public function storeTransactions(Company $company, BankAccount $bankAccount, array $transactions): void
  26. {
  27. foreach ($transactions as $transaction) {
  28. $this->storeTransaction($company, $bankAccount, $transaction);
  29. }
  30. }
  31. public function createStartingBalanceTransaction(Company $company, Account $account, BankAccount $bankAccount, float $startingBalance, string $startDate): void
  32. {
  33. $transactionType = $startingBalance >= 0 ? 'deposit' : 'withdrawal';
  34. $chartAccount = $account->where('category', AccountCategory::Equity)->where('name', 'Owner\'s Equity')->first();
  35. $postedAt = Carbon::parse($startDate)->subDay()->toDateTimeString();
  36. Transaction::create([
  37. 'company_id' => $company->id,
  38. 'account_id' => $chartAccount->id,
  39. 'bank_account_id' => $bankAccount->id,
  40. 'type' => $transactionType,
  41. 'amount' => abs($startingBalance),
  42. 'payment_channel' => 'other',
  43. 'posted_at' => $postedAt,
  44. 'description' => 'Starting Balance',
  45. 'pending' => false,
  46. 'reviewed' => false,
  47. ]);
  48. }
  49. public function storeTransaction(Company $company, BankAccount $bankAccount, object $transaction): void
  50. {
  51. $transactionType = $transaction->amount < 0 ? 'deposit' : 'withdrawal';
  52. $paymentChannel = $transaction->payment_channel;
  53. $chartAccount = $this->getAccountFromTransaction($company, $transaction, $transactionType);
  54. $postedAt = $transaction->datetime ?? Carbon::parse($transaction->date)->toDateTimeString();
  55. $description = $transaction->name;
  56. Transaction::create([
  57. 'company_id' => $company->id,
  58. 'account_id' => $chartAccount->id,
  59. 'bank_account_id' => $bankAccount->id,
  60. 'type' => $transactionType,
  61. 'amount' => abs($transaction->amount),
  62. 'payment_channel' => $paymentChannel,
  63. 'posted_at' => $postedAt,
  64. 'description' => $description,
  65. 'pending' => false,
  66. 'reviewed' => false,
  67. ]);
  68. }
  69. public function getAccountFromTransaction(Company $company, object $transaction, string $transactionType): Account
  70. {
  71. $accountCategory = match ($transactionType) {
  72. 'deposit' => AccountCategory::Revenue,
  73. 'withdrawal' => AccountCategory::Expense,
  74. };
  75. $accounts = $company->accounts()
  76. ->where('category', $accountCategory)
  77. ->whereNotIn('type', [AccountType::UncategorizedRevenue, AccountType::UncategorizedExpense])
  78. ->get();
  79. $bestMatchName = $this->findBestAccountMatch($transaction, $accounts->pluck('name')->toArray());
  80. if ($bestMatchName === null) {
  81. return $this->getUncategorizedAccount($company, $transactionType);
  82. }
  83. return $accounts->firstWhere('name', $bestMatchName) ?: $this->getUncategorizedAccount($company, $transactionType);
  84. }
  85. private function findBestAccountMatch(object $transaction, array $accountNames): ?string
  86. {
  87. $acceptableConfidenceLevels = ['VERY_HIGH', 'HIGH'];
  88. $similarityThreshold = 70.0;
  89. $plaidDetail = $transaction->personal_finance_category->detailed ?? null;
  90. $plaidPrimary = $transaction->personal_finance_category->primary ?? null;
  91. $bestMatchName = null;
  92. $bestMatchPercent = 0.0;
  93. foreach ([$plaidDetail, $plaidPrimary] as $plaidCategory) {
  94. if ($plaidCategory !== null && in_array($transaction->personal_finance_category->confidence_level, $acceptableConfidenceLevels, true)) {
  95. foreach ($accountNames as $accountName) {
  96. $normalizedPlaidCategory = strtolower(str_replace('_', ' ', $plaidCategory));
  97. $normalizedAccountName = strtolower(str_replace('_', ' ', $accountName));
  98. $currentMatchPercent = 0.0;
  99. similar_text($normalizedPlaidCategory, $normalizedAccountName, $currentMatchPercent);
  100. if ($currentMatchPercent >= $similarityThreshold && $currentMatchPercent > $bestMatchPercent) {
  101. $bestMatchPercent = $currentMatchPercent;
  102. $bestMatchName = $accountName; // Use and return the original account name for the best match, not the normalized one
  103. }
  104. }
  105. }
  106. }
  107. return $bestMatchName;
  108. }
  109. public function getUncategorizedAccount(Company $company, string $transactionType): Account
  110. {
  111. [$type, $name] = match ($transactionType) {
  112. 'deposit' => [AccountType::UncategorizedRevenue, 'Uncategorized Income'],
  113. 'withdrawal' => [AccountType::UncategorizedExpense, 'Uncategorized Expense'],
  114. };
  115. return $company->accounts()
  116. ->where('type', $type)
  117. ->where('name', $name)
  118. ->firstOrFail();
  119. }
  120. }