Teren-app/app/Services/Import/Handlers/PaymentHandler.php
Simon Pocrnjič dea7432deb changes
2025-12-26 22:39:58 +01:00

225 lines
7.9 KiB
PHP

<?php
namespace App\Services\Import\Handlers;
use App\Models\Account;
use App\Models\Booking;
use App\Models\Import;
use App\Models\Payment;
use App\Models\PaymentSetting;
use App\Services\Import\DateNormalizer;
use App\Services\Import\BaseEntityHandler;
use Illuminate\Support\Facades\Log;
class PaymentHandler extends BaseEntityHandler
{
public function getEntityClass(): string
{
return Payment::class;
}
/**
* Override validate to skip validation if amount is empty.
*/
public function validate(array $mapped): array
{
$amount = $mapped['amount'] ?? null;
if (empty($amount) || !is_numeric($amount)) {
return ['valid' => 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(),
]);
}
}
}