225 lines
7.9 KiB
PHP
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(),
|
|
]);
|
|
}
|
|
}
|
|
}
|