true, 'errors' => []]; } return parent::validate($mapped); } public function resolve(array $mapped, array $context = []): mixed { $accountId = $mapped['account_id'] ?? $context['account']?->entity?->id ?? null; $reference = $mapped['reference'] ?? null; if (! $accountId || ! $reference) { return null; } return Payment::where('account_id', $accountId) ->where('reference', $reference) ->first(); } public function process(Import $import, array $mapped, array $raw, array $context = []): array { // Skip if amount is empty or invalid $amount = $mapped['amount'] ?? null; if (empty($amount) || !is_numeric($amount)) { return [ 'action' => 'skipped', 'message' => 'Payment amount is empty or invalid', ]; } // Resolve account - either from mapped data or context $accountId = $mapped['account_id'] ?? $context['account']?->entity?->id ?? null; if (! $accountId) { return [ 'action' => 'skipped', 'message' => 'Payment requires an account', ]; } // Check for duplicates if configured if ($this->getOption('deduplicate_by', [])) { $existing = $this->resolve($mapped, ['account' => (object) ['entity' => (object) ['id' => $accountId]]]); if ($existing) { return [ 'action' => 'skipped', 'entity' => $existing, 'message' => 'Payment already exists (duplicate by reference)', ]; } } // Build payment payload $payload = $this->buildPayload($mapped, new Payment); $payload['account_id'] = $accountId; $payload['created_by'] = $context['user']?->getAuthIdentifier(); // Get account balance before payment $account = Account::find($accountId); $balanceBefore = $account ? (float) ($account->balance_amount ?? 0) : 0; // Create payment $payment = new Payment; $payment->fill($payload); $payment->balance_before = $balanceBefore; try { $payment->save(); } catch (\Throwable $e) { // Handle unique constraint violations gracefully if (str_contains($e->getMessage(), 'payments_account_id_reference_unique')) { return [ 'action' => 'skipped', 'message' => 'Payment duplicate detected (database constraint)', ]; } throw $e; } // Create booking if configured if ($this->getOption('create_booking', true) && isset($payment->amount)) { try { Booking::create([ 'account_id' => $accountId, 'payment_id' => $payment->id, 'amount_cents' => (int) round(((float) $payment->amount) * 100), 'type' => 'credit', 'description' => $payment->reference ? ('Plačilo '.$payment->reference) : 'Plačilo', 'booked_at' => $payment->paid_at ?? now(), ]); } catch (\Throwable $e) { Log::warning('Failed to create booking for payment', [ 'payment_id' => $payment->id, 'error' => $e->getMessage(), ]); } } // Create activity if configured if ($this->getOption('create_activity', false)) { $this->createPaymentActivity($payment, $account, $balanceBefore); } return [ 'action' => 'inserted', 'entity' => $payment, 'applied_fields' => array_keys($payload), ]; } protected function buildPayload(array $mapped, $model): array { $payload = []; // Map payment fields if (isset($mapped['reference'])) { $payload['reference'] = is_string($mapped['reference']) ? trim($mapped['reference']) : $mapped['reference']; } // Handle amount - support both amount and amount_cents if (array_key_exists('amount', $mapped)) { $payload['amount'] = is_string($mapped['amount']) ? (float) str_replace(',', '.', $mapped['amount']) : (float) $mapped['amount']; } elseif (array_key_exists('amount_cents', $mapped)) { $payload['amount'] = ((int) $mapped['amount_cents']) / 100.0; } // Payment date - support both paid_at and payment_date $dateValue = $mapped['paid_at'] ?? $mapped['payment_date'] ?? null; if ($dateValue) { $payload['paid_at'] = DateNormalizer::toDate((string) $dateValue); } $payload['currency'] = $mapped['currency'] ?? 'EUR'; // Handle meta $meta = []; if (is_array($mapped['meta'] ?? null)) { $meta = $mapped['meta']; } if (! empty($mapped['payment_nu'])) { $meta['payment_nu'] = trim((string) $mapped['payment_nu']); } if (! empty($meta)) { $payload['meta'] = $meta; } return $payload; } protected function createPaymentActivity(Payment $payment, ?Account $account, float $balanceBefore): void { try { $settings = PaymentSetting::first(); if (! $settings || ! ($settings->create_activity_on_payment ?? false)) { return; } $amountCents = (int) round(((float) $payment->amount) * 100); $note = $settings->activity_note_template ?? 'Prejeto plačilo'; $note = str_replace( ['{amount}', '{currency}'], [number_format($amountCents / 100, 2, ',', '.'), $payment->currency ?? 'EUR'], $note ); // Get updated balance $account?->refresh(); $balanceAfter = $account ? (float) ($account->balance_amount ?? 0) : 0; $beforeStr = number_format($balanceBefore, 2, ',', '.').' '.($payment->currency ?? 'EUR'); $afterStr = number_format($balanceAfter, 2, ',', '.').' '.($payment->currency ?? 'EUR'); $note .= " (Stanje pred: {$beforeStr}, Stanje po: {$afterStr}; Izvor: plačilo)"; // Resolve client_case_id $account?->loadMissing('contract'); $clientCaseId = $account?->contract?->client_case_id; if ($clientCaseId) { $activity = \App\Models\Activity::create([ 'due_date' => null, 'amount' => $amountCents / 100, 'note' => $note, 'action_id' => $settings->default_action_id, 'decision_id' => $settings->default_decision_id, 'client_case_id' => $clientCaseId, 'contract_id' => $account->contract_id, ]); $payment->update(['activity_id' => $activity->id]); } } catch (\Throwable $e) { Log::warning('Failed to create activity for payment', [ 'payment_id' => $payment->id, 'error' => $e->getMessage(), ]); } } }