Files
Vasyka 09fd0bada2 Faza 2 (din continuare): Email notifications
4 Mailables auto-trigger pe model events:
- WorkOrderReadyMail: la WO.status → 'ready', către client.email
  • Atașat PDF fișa lucru (via WorkOrderPdfService)
  • Total/achitat/rest, recomandări (warning box)
- PaymentReceivedMail: la Payment::created, confirmare cu sumă/metodă/ref
- AppointmentConfirmedMail: la Appointment::created status='scheduled'
- ServiceReminderMail: dispatch manual (vehicle, type=itp/oil/general, note)

Layout email branded (resources/views/emails/layout.blade.php):
- Header cu logo tenant + theme_color border-bottom
- Footer cu telefon/email/disclaimer
- Stiluri inline (compatibil tot mail client)

Settings page extins cu 4 toggle:
- 'Mașina e gata de ridicat'
- 'Confirmare plată primită'
- 'Programare confirmată'
- 'Reminder ITP / revizie'
Salvate în companies.settings.notify (JSON), default true.

NotificationDispatcher service centralizat:
- Verifică isEnabled() pe settings.notify[$key]
- Skip dacă client n-are email
- Try/catch + Log::warning pe eșec (nu crapă request-ul)

Mailables folosesc UsesTenantBranding trait pentru context unitar.
Test prin Mailpit: https://mailpit.service.mir.md (capturează toate).
2026-05-07 13:20:19 +00:00

85 lines
2.2 KiB
PHP

<?php
namespace App\Models\Tenant;
use App\Models\Concerns\BelongsToTenant;
use App\Models\Concerns\Auditable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class Payment extends Model
{
use Auditable, BelongsToTenant, SoftDeletes;
public const METHODS = [
'cash' => 'Numerar',
'card' => 'Card',
'transfer' => 'Virament',
'mobile' => 'Mobile pay',
];
protected $fillable = [
'company_id', 'client_id', 'work_order_id', 'user_id',
'paid_at', 'amount', 'method', 'reference', 'notes',
];
protected $casts = [
'paid_at' => 'date',
'amount' => 'decimal:2',
];
public function client(): BelongsTo
{
return $this->belongsTo(Client::class);
}
public function workOrder(): BelongsTo
{
return $this->belongsTo(WorkOrder::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* After save/delete, recompute the work order's pay_status.
*/
protected static function booted(): void
{
$sync = function (self $payment) {
if (! $payment->work_order_id) {
return;
}
$wo = WorkOrder::withoutGlobalScopes()->find($payment->work_order_id);
if (! $wo) {
return;
}
$paid = (float) static::withoutGlobalScopes()
->where('work_order_id', $wo->id)
->whereNull('deleted_at')
->sum('amount');
$total = (float) $wo->total;
if ($paid <= 0) {
$wo->pay_status = 'unpaid';
} elseif ($paid + 0.01 < $total) {
$wo->pay_status = 'partial';
} else {
$wo->pay_status = 'paid';
}
$wo->save();
};
static::saved($sync);
static::deleted($sync);
// Auto-send confirmation email on new payment.
static::created(function (self $payment) {
app(\App\Services\NotificationDispatcher::class)->paymentReceived($payment);
});
}
}