SMS service
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\SmsLog;
|
||||
use App\Models\SmsProfile;
|
||||
use App\Models\SmsTemplate;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class SmsLogController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = SmsLog::query()->with(['profile:id,name', 'template:id,name,slug']);
|
||||
|
||||
// Filters
|
||||
$status = $request->string('status')->toString();
|
||||
$profileId = $request->integer('profile_id');
|
||||
$templateId = $request->integer('template_id');
|
||||
$search = trim((string) $request->input('search', ''));
|
||||
$from = $request->date('from');
|
||||
$to = $request->date('to');
|
||||
|
||||
if ($status !== '') {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
if ($profileId) {
|
||||
$query->where('profile_id', $profileId);
|
||||
}
|
||||
if ($templateId) {
|
||||
$query->where('template_id', $templateId);
|
||||
}
|
||||
if ($search !== '') {
|
||||
$query->where(function ($q) use ($search): void {
|
||||
$q->where('to_number', 'ILIKE', "%$search%")
|
||||
->orWhere('sender', 'ILIKE', "%$search%")
|
||||
->orWhere('provider_message_id', 'ILIKE', "%$search%")
|
||||
->orWhere('message', 'ILIKE', "%$search%");
|
||||
});
|
||||
}
|
||||
if ($from) {
|
||||
$query->whereDate('created_at', '>=', $from);
|
||||
}
|
||||
if ($to) {
|
||||
$query->whereDate('created_at', '<=', $to);
|
||||
}
|
||||
|
||||
$logs = $query->orderByDesc('id')->paginate(20)->withQueryString();
|
||||
|
||||
if ($request->wantsJson() || $request->expectsJson()) {
|
||||
return response()->json($logs);
|
||||
}
|
||||
|
||||
$profiles = SmsProfile::query()->orderBy('name')->get(['id', 'name']);
|
||||
$templates = SmsTemplate::query()->orderBy('name')->get(['id', 'name', 'slug']);
|
||||
|
||||
return Inertia::render('Admin/SmsLogs/Index', [
|
||||
'logs' => $logs,
|
||||
'profiles' => $profiles,
|
||||
'templates' => $templates,
|
||||
'filters' => [
|
||||
'status' => $status ?: null,
|
||||
'profile_id' => $profileId ?: null,
|
||||
'template_id' => $templateId ?: null,
|
||||
'search' => $search ?: null,
|
||||
'from' => $from ? $from->format('Y-m-d') : null,
|
||||
'to' => $to ? $to->format('Y-m-d') : null,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(SmsLog $smsLog)
|
||||
{
|
||||
$smsLog->load(['profile:id,name', 'template:id,name,slug']);
|
||||
|
||||
return Inertia::render('Admin/SmsLogs/Show', [
|
||||
'log' => $smsLog,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreSmsProfileRequest;
|
||||
use App\Http\Requests\TestSendSmsRequest;
|
||||
use App\Jobs\SendSmsJob;
|
||||
use App\Models\SmsProfile;
|
||||
use App\Models\SmsSender;
|
||||
use App\Services\Sms\SmsService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class SmsProfileController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$profiles = SmsProfile::query()->with(['senders:id,profile_id,sname,active'])->orderBy('name')->get([
|
||||
'id', 'uuid', 'name', 'active', 'api_username', 'default_sender_id', 'settings', 'created_at', 'updated_at',
|
||||
]);
|
||||
|
||||
// Inertia requests must receive an Inertia response
|
||||
if ($request->headers->has('X-Inertia')) {
|
||||
return Inertia::render('Admin/SmsProfiles/Index', [
|
||||
'initialProfiles' => $profiles,
|
||||
]);
|
||||
}
|
||||
|
||||
// JSON/AJAX API
|
||||
if ($request->wantsJson() || $request->expectsJson()) {
|
||||
return response()->json(['profiles' => $profiles]);
|
||||
}
|
||||
|
||||
// Default to Inertia page for normal browser navigation
|
||||
return Inertia::render('Admin/SmsProfiles/Index', [
|
||||
'initialProfiles' => $profiles,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreSmsProfileRequest $request)
|
||||
{
|
||||
$data = $request->validated();
|
||||
$profile = new SmsProfile;
|
||||
$profile->uuid = (string) Str::uuid();
|
||||
$profile->name = $data['name'];
|
||||
$profile->active = (bool) ($data['active'] ?? true);
|
||||
$profile->api_username = $data['api_username'];
|
||||
// write-only attribute setter will encrypt and store to encrypted_api_password
|
||||
$profile->api_password = $data['api_password'];
|
||||
$profile->save();
|
||||
|
||||
if ($request->wantsJson() || $request->expectsJson()) {
|
||||
return response()->json(['profile' => $profile], 201);
|
||||
}
|
||||
|
||||
return back()->with('success', 'SMS profil je ustvarjen.');
|
||||
}
|
||||
|
||||
public function testSend(SmsProfile $profile, TestSendSmsRequest $request, SmsService $sms)
|
||||
{
|
||||
$data = $request->validated();
|
||||
$sender = null;
|
||||
if (! empty($data['sender_id'])) {
|
||||
$sender = SmsSender::query()->where('id', $data['sender_id'])->where('profile_id', $profile->id)->firstOrFail();
|
||||
}
|
||||
|
||||
// Queue the SMS send (admin test send - no activity created)
|
||||
SendSmsJob::dispatch(
|
||||
profileId: $profile->id,
|
||||
to: (string) $data['to'],
|
||||
content: (string) $data['message'],
|
||||
senderId: $sender?->id,
|
||||
countryCode: $data['country_code'] ?? null,
|
||||
deliveryReport: (bool) ($data['delivery_report'] ?? false),
|
||||
clientReference: null,
|
||||
);
|
||||
|
||||
if ($request->wantsJson() || $request->expectsJson()) {
|
||||
return response()->json(['queued' => true]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Testni SMS je bil dodan v čakalno vrsto.');
|
||||
}
|
||||
|
||||
public function balance(SmsProfile $smsProfile, SmsService $sms)
|
||||
{
|
||||
try {
|
||||
$balance = (string) $sms->getCreditBalance($smsProfile);
|
||||
|
||||
return response()->json(['balance' => $balance]);
|
||||
} catch (\Throwable $e) {
|
||||
// Return a graceful payload so UI doesn't break; also include message for optional UI/tooling
|
||||
return response()->json([
|
||||
'balance' => '—',
|
||||
'error' => 'Unable to fetch balance: '.$e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function price(SmsProfile $smsProfile, SmsService $sms)
|
||||
{
|
||||
$quotes = $sms->getPriceQuotes($smsProfile);
|
||||
|
||||
return response()->json(['quotes' => $quotes]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreSmsSenderRequest;
|
||||
use App\Http\Requests\UpdateSmsSenderRequest;
|
||||
use App\Models\SmsProfile;
|
||||
use App\Models\SmsSender;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class SmsSenderController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$senders = SmsSender::query()
|
||||
->with(['profile:id,name'])
|
||||
->orderBy('id', 'desc')
|
||||
->get(['id', 'profile_id', 'sname', 'phone_number', 'description', 'active', 'created_at']);
|
||||
|
||||
$profiles = SmsProfile::query()->orderBy('name')->get(['id', 'name']);
|
||||
|
||||
return Inertia::render('Admin/SmsSenders/Index', [
|
||||
'initialSenders' => $senders,
|
||||
'profiles' => $profiles,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreSmsSenderRequest $request)
|
||||
{
|
||||
$data = $request->validated();
|
||||
$sender = SmsSender::create([
|
||||
'profile_id' => $data['profile_id'],
|
||||
'sname' => $data['sname'],
|
||||
'phone_number' => $data['phone_number'] ?? null,
|
||||
'description' => $data['description'] ?? null,
|
||||
'active' => (bool) ($data['active'] ?? true),
|
||||
]);
|
||||
|
||||
if ($request->wantsJson() || $request->expectsJson()) {
|
||||
return response()->json(['sender' => $sender], 201);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Pošiljatelj je ustvarjen.');
|
||||
}
|
||||
|
||||
public function update(UpdateSmsSenderRequest $request, SmsSender $smsSender)
|
||||
{
|
||||
$data = $request->validated();
|
||||
$smsSender->forceFill([
|
||||
'profile_id' => $data['profile_id'],
|
||||
'sname' => $data['sname'],
|
||||
'phone_number' => $data['phone_number'] ?? null,
|
||||
'description' => $data['description'] ?? null,
|
||||
'active' => (bool) ($data['active'] ?? $smsSender->active),
|
||||
])->save();
|
||||
|
||||
if ($request->wantsJson() || $request->expectsJson()) {
|
||||
return response()->json(['sender' => $smsSender]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Pošiljatelj je posodobljen.');
|
||||
}
|
||||
|
||||
public function toggle(Request $request, SmsSender $smsSender)
|
||||
{
|
||||
$smsSender->active = ! $smsSender->active;
|
||||
$smsSender->save();
|
||||
|
||||
if ($request->wantsJson() || $request->expectsJson()) {
|
||||
return response()->json(['sender' => $smsSender]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Stanje pošiljatelja je posodobljeno.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, SmsSender $smsSender)
|
||||
{
|
||||
$smsSender->delete();
|
||||
|
||||
if ($request->wantsJson() || $request->expectsJson()) {
|
||||
return response()->json(['deleted' => true]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Pošiljatelj je izbrisan.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreSmsTemplateRequest;
|
||||
use App\Http\Requests\TestSendSmsTemplateRequest;
|
||||
use App\Http\Requests\UpdateSmsTemplateRequest;
|
||||
use App\Models\SmsProfile;
|
||||
use App\Models\SmsSender;
|
||||
use App\Models\SmsTemplate;
|
||||
use App\Services\Sms\SmsService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class SmsTemplateController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$templates = SmsTemplate::query()
|
||||
->with(['defaultProfile:id,name', 'defaultSender:id,sname'])
|
||||
->orderBy('name')
|
||||
->get(['id', 'uuid', 'name', 'slug', 'content', 'variables_json', 'is_active', 'default_profile_id', 'default_sender_id', 'created_at']);
|
||||
|
||||
$profiles = SmsProfile::query()->orderBy('name')->get(['id', 'name']);
|
||||
$senders = SmsSender::query()->orderBy('sname')->get(['id', 'profile_id', 'sname', 'active']);
|
||||
|
||||
if ($request->wantsJson() || $request->expectsJson()) {
|
||||
return response()->json([
|
||||
'templates' => $templates,
|
||||
'profiles' => $profiles,
|
||||
'senders' => $senders,
|
||||
]);
|
||||
}
|
||||
|
||||
return Inertia::render('Admin/SmsTemplates/Index', [
|
||||
'initialTemplates' => $templates,
|
||||
'profiles' => $profiles,
|
||||
'senders' => $senders,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$profiles = SmsProfile::query()->orderBy('name')->get(['id', 'name']);
|
||||
$senders = SmsSender::query()->orderBy('sname')->get(['id', 'profile_id', 'sname', 'active']);
|
||||
$actions = \App\Models\Action::query()
|
||||
->with(['decisions:id,name'])
|
||||
->orderBy('name')
|
||||
->get(['id', 'name']);
|
||||
|
||||
return Inertia::render('Admin/SmsTemplates/Edit', [
|
||||
'template' => null,
|
||||
'profiles' => $profiles,
|
||||
'senders' => $senders,
|
||||
'actions' => $actions,
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(SmsTemplate $smsTemplate)
|
||||
{
|
||||
$profiles = SmsProfile::query()->orderBy('name')->get(['id', 'name']);
|
||||
$senders = SmsSender::query()->orderBy('sname')->get(['id', 'profile_id', 'sname', 'active']);
|
||||
$actions = \App\Models\Action::query()
|
||||
->with(['decisions:id,name'])
|
||||
->orderBy('name')
|
||||
->get(['id', 'name']);
|
||||
|
||||
return Inertia::render('Admin/SmsTemplates/Edit', [
|
||||
'template' => $smsTemplate->only(['id', 'uuid', 'name', 'slug', 'content', 'variables_json', 'is_active', 'default_profile_id', 'default_sender_id', 'allow_custom_body', 'action_id', 'decision_id']),
|
||||
'profiles' => $profiles,
|
||||
'senders' => $senders,
|
||||
'actions' => $actions,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreSmsTemplateRequest $request)
|
||||
{
|
||||
$data = $request->validated();
|
||||
$tpl = new SmsTemplate;
|
||||
$tpl->uuid = (string) Str::uuid();
|
||||
$tpl->name = $data['name'];
|
||||
$tpl->slug = $data['slug'];
|
||||
$tpl->content = $data['content'] ?? '';
|
||||
$tpl->variables_json = $data['variables_json'] ?? null;
|
||||
$tpl->is_active = (bool) ($data['is_active'] ?? true);
|
||||
$tpl->default_profile_id = $data['default_profile_id'] ?? null;
|
||||
$tpl->default_sender_id = $data['default_sender_id'] ?? null;
|
||||
$tpl->allow_custom_body = (bool) ($data['allow_custom_body'] ?? false);
|
||||
$tpl->action_id = $data['action_id'] ?? null;
|
||||
$tpl->decision_id = $data['decision_id'] ?? null;
|
||||
$tpl->save();
|
||||
|
||||
if ($request->wantsJson() || $request->expectsJson()) {
|
||||
return response()->json(['template' => $tpl], 201);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.sms-templates.edit', $tpl);
|
||||
}
|
||||
|
||||
public function update(UpdateSmsTemplateRequest $request, SmsTemplate $smsTemplate)
|
||||
{
|
||||
$data = $request->validated();
|
||||
$smsTemplate->forceFill([
|
||||
'name' => $data['name'],
|
||||
'slug' => $data['slug'],
|
||||
'content' => $data['content'] ?? '',
|
||||
'variables_json' => $data['variables_json'] ?? null,
|
||||
'is_active' => (bool) ($data['is_active'] ?? $smsTemplate->is_active),
|
||||
'default_profile_id' => $data['default_profile_id'] ?? null,
|
||||
'default_sender_id' => $data['default_sender_id'] ?? null,
|
||||
'allow_custom_body' => (bool) ($data['allow_custom_body'] ?? $smsTemplate->allow_custom_body),
|
||||
'action_id' => $data['action_id'] ?? null,
|
||||
'decision_id' => $data['decision_id'] ?? null,
|
||||
])->save();
|
||||
|
||||
if ($request->wantsJson() || $request->expectsJson()) {
|
||||
return response()->json(['template' => $smsTemplate]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'SMS predloga je posodobljena.');
|
||||
}
|
||||
|
||||
public function toggle(Request $request, SmsTemplate $smsTemplate)
|
||||
{
|
||||
$smsTemplate->is_active = ! $smsTemplate->is_active;
|
||||
$smsTemplate->save();
|
||||
|
||||
if ($request->wantsJson() || $request->expectsJson()) {
|
||||
return response()->json(['template' => $smsTemplate]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Stanje predloge je posodobljeno.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, SmsTemplate $smsTemplate)
|
||||
{
|
||||
$smsTemplate->delete();
|
||||
|
||||
if ($request->wantsJson() || $request->expectsJson()) {
|
||||
return response()->json(['deleted' => true]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Predloga je izbrisana.');
|
||||
}
|
||||
|
||||
public function sendTest(TestSendSmsTemplateRequest $request, SmsTemplate $smsTemplate, SmsService $sms)
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
$profile = null;
|
||||
if (! empty($data['profile_id'])) {
|
||||
$profile = SmsProfile::query()->findOrFail($data['profile_id']);
|
||||
}
|
||||
$sender = null;
|
||||
if (! empty($data['sender_id'])) {
|
||||
$sender = SmsSender::query()->findOrFail($data['sender_id']);
|
||||
}
|
||||
|
||||
$variables = (array) ($data['variables'] ?? []);
|
||||
|
||||
if (! empty($data['custom_content']) && $smsTemplate->allow_custom_body) {
|
||||
// Use custom content when allowed
|
||||
if (! $profile) {
|
||||
$profile = $smsTemplate->defaultProfile;
|
||||
}
|
||||
if (! $profile) {
|
||||
throw new \InvalidArgumentException('SMS profile is required to send a message.');
|
||||
}
|
||||
$log = $sms->sendRaw(
|
||||
profile: $profile,
|
||||
to: $data['to'],
|
||||
content: (string) $data['custom_content'],
|
||||
sender: $sender,
|
||||
countryCode: $data['country_code'] ?? null,
|
||||
deliveryReport: (bool) ($data['delivery_report'] ?? false),
|
||||
);
|
||||
$log->template_id = $smsTemplate->id;
|
||||
$log->save();
|
||||
} else {
|
||||
$log = $sms->sendFromTemplate(
|
||||
template: $smsTemplate,
|
||||
to: $data['to'],
|
||||
variables: $variables,
|
||||
profile: $profile,
|
||||
sender: $sender,
|
||||
countryCode: $data['country_code'] ?? null,
|
||||
deliveryReport: (bool) ($data['delivery_report'] ?? false),
|
||||
);
|
||||
}
|
||||
|
||||
if ($request->wantsJson() || $request->expectsJson()) {
|
||||
return response()->json(['log' => $log]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Testni SMS je bil poslan.');
|
||||
}
|
||||
}
|
||||
@@ -1343,6 +1343,22 @@ function ($p) {
|
||||
'segments' => $case->segments()->wherePivot('active', true)->get(['segments.id', 'segments.name']),
|
||||
'all_segments' => \App\Models\Segment::query()->where('active', true)->get(['id', 'name']),
|
||||
'current_segment' => $currentSegment,
|
||||
// SMS helpers for per-case sending UI
|
||||
'sms_profiles' => \App\Models\SmsProfile::query()
|
||||
->select(['id', 'name', 'default_sender_id'])
|
||||
->where('active', true)
|
||||
->orderBy('name')
|
||||
->get(),
|
||||
'sms_senders' => \App\Models\SmsSender::query()
|
||||
->select(['id', 'profile_id'])
|
||||
->addSelect(\DB::raw('sname as name'))
|
||||
->addSelect(\DB::raw('phone_number as phone'))
|
||||
->orderBy('sname')
|
||||
->get(),
|
||||
'sms_templates' => \App\Models\SmsTemplate::query()
|
||||
->select(['id', 'name', 'content', 'allow_custom_body'])
|
||||
->orderBy('name')
|
||||
->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1665,4 +1681,124 @@ public function emergencyCreatePerson(ClientCase $clientCase, Request $request)
|
||||
'person_uuid' => $newPerson?->uuid,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an SMS to a specific phone that belongs to the client case person.
|
||||
*/
|
||||
public function sendSmsToPhone(ClientCase $clientCase, Request $request, int $phone_id)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'message' => ['required', 'string', 'max:1000'],
|
||||
'delivery_report' => ['sometimes', 'boolean'],
|
||||
'template_id' => ['sometimes', 'nullable', 'integer', 'exists:sms_templates,id'],
|
||||
'profile_id' => ['sometimes', 'nullable', 'integer', 'exists:sms_profiles,id'],
|
||||
'sender_id' => ['sometimes', 'nullable', 'integer', 'exists:sms_senders,id'],
|
||||
]);
|
||||
|
||||
// Ensure the phone belongs to the person of this case
|
||||
/** @var \App\Models\Person\PersonPhone|null $phone */
|
||||
$phone = \App\Models\Person\PersonPhone::query()
|
||||
->where('id', $phone_id)
|
||||
->where('person_id', $clientCase->person_id)
|
||||
->first();
|
||||
|
||||
if (! $phone) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// Resolve explicit profile/sender if provided; otherwise fallback to first active profile and its default sender
|
||||
/** @var \App\Models\SmsProfile|null $profile */
|
||||
$profile = null;
|
||||
/** @var \App\Models\SmsSender|null $sender */
|
||||
$sender = null;
|
||||
|
||||
if (! empty($validated['sender_id']) && empty($validated['profile_id'])) {
|
||||
// Infer profile from sender if not explicitly provided
|
||||
$sender = \App\Models\SmsSender::query()->find($validated['sender_id']);
|
||||
if ($sender) {
|
||||
$profile = \App\Models\SmsProfile::query()->find($sender->profile_id);
|
||||
}
|
||||
}
|
||||
if (! empty($validated['profile_id'])) {
|
||||
$profile = \App\Models\SmsProfile::query()->where('id', $validated['profile_id'])->first();
|
||||
if (! $profile) {
|
||||
return back()->with('error', 'Izbran SMS profil ne obstaja.');
|
||||
}
|
||||
if (property_exists($profile, 'active') && ! $profile->active) {
|
||||
return back()->with('error', 'Izbran SMS profil ni aktiven.');
|
||||
}
|
||||
}
|
||||
if (! empty($validated['sender_id'])) {
|
||||
$sender = \App\Models\SmsSender::query()->find($validated['sender_id']);
|
||||
if (! $sender) {
|
||||
return back()->with('error', 'Izbran pošiljatelj ne obstaja.');
|
||||
}
|
||||
if ($profile && (int) $sender->profile_id !== (int) $profile->id) {
|
||||
return back()->with('error', 'Izbran pošiljatelj ne pripada izbranemu profilu.');
|
||||
}
|
||||
}
|
||||
if (! $profile) {
|
||||
$profile = \App\Models\SmsProfile::query()
|
||||
->where('active', true)
|
||||
->orderByRaw('CASE WHEN default_sender_id IS NULL THEN 1 ELSE 0 END')
|
||||
->orderBy('id')
|
||||
->first();
|
||||
}
|
||||
if (! $profile) {
|
||||
return back()->with('warning', 'Ni aktivnega SMS profila.');
|
||||
}
|
||||
if (! $sender && ! empty($profile->default_sender_id)) {
|
||||
$sender = \App\Models\SmsSender::query()->find($profile->default_sender_id);
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var \App\Services\Sms\SmsService $sms */
|
||||
$sms = app(\App\Services\Sms\SmsService::class);
|
||||
// Check available credits before enqueueing (fail-closed)
|
||||
try {
|
||||
$raw = (string) $sms->getCreditBalance($profile);
|
||||
$num = null;
|
||||
if ($raw !== '') {
|
||||
$normalized = str_replace(',', '.', trim($raw));
|
||||
if (preg_match('/-?\d+(?:\.\d+)?/', $normalized, $m)) {
|
||||
$num = (float) ($m[0] ?? null);
|
||||
}
|
||||
}
|
||||
if (! is_null($num) && $num <= 0.0) {
|
||||
return back()->with('error', 'No credits left.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
\Log::warning('SMS credit balance check failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'profile_id' => $profile->id,
|
||||
]);
|
||||
|
||||
return back()->with('error', 'Unable to verify SMS credits.');
|
||||
}
|
||||
|
||||
// Queue the SMS send; activity will be created in the job on success if a template is provided
|
||||
\App\Jobs\SendSmsJob::dispatch(
|
||||
profileId: $profile->id,
|
||||
to: (string) $phone->nu,
|
||||
content: (string) $validated['message'],
|
||||
senderId: $sender?->id,
|
||||
countryCode: $phone->country_code ?: null,
|
||||
deliveryReport: (bool) ($validated['delivery_report'] ?? false),
|
||||
clientReference: null,
|
||||
templateId: $validated['template_id'] ?? null,
|
||||
clientCaseId: $clientCase->id,
|
||||
userId: optional($request->user())->id,
|
||||
);
|
||||
|
||||
return back()->with('success', 'SMS je bil dodan v čakalno vrsto.');
|
||||
} catch (\Throwable $e) {
|
||||
\Log::warning('SMS enqueue failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'case_id' => $clientCase->id,
|
||||
'phone_id' => $phone_id,
|
||||
]);
|
||||
|
||||
return back()->with('error', 'SMS ni bil dodan v čakalno vrsto.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
use App\Models\Document; // assuming model name Import
|
||||
use App\Models\FieldJob; // if this model exists
|
||||
use App\Models\Import;
|
||||
use App\Models\SmsLog;
|
||||
use App\Models\SmsProfile;
|
||||
use App\Services\Sms\SmsService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Inertia\Inertia;
|
||||
@@ -15,7 +18,7 @@
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function __invoke(): Response
|
||||
public function __invoke(SmsService $sms): Response
|
||||
{
|
||||
$today = now()->startOfDay();
|
||||
$yesterday = now()->subDay()->startOfDay();
|
||||
@@ -223,6 +226,52 @@ public function __invoke(): Response
|
||||
'fieldJobsAssignedToday' => fn () => $fieldJobsAssignedToday,
|
||||
'importsInProgress' => fn () => $importsInProgress,
|
||||
'activeTemplates' => fn () => $activeTemplates,
|
||||
'smsStats' => function () use ($sms, $today) {
|
||||
// Aggregate counts per profile for today
|
||||
$counts = SmsLog::query()
|
||||
->whereDate('created_at', $today)
|
||||
->selectRaw('profile_id, status, COUNT(*) as c')
|
||||
->groupBy('profile_id', 'status')
|
||||
->get()
|
||||
->groupBy('profile_id')
|
||||
->map(function ($rows) {
|
||||
$map = [
|
||||
'queued' => 0,
|
||||
'sent' => 0,
|
||||
'delivered' => 0,
|
||||
'failed' => 0,
|
||||
];
|
||||
foreach ($rows as $r) {
|
||||
$map[$r->status] = (int) $r->c;
|
||||
}
|
||||
$map['total'] = array_sum($map);
|
||||
|
||||
return $map;
|
||||
});
|
||||
|
||||
// Important: include credential fields so provider calls have proper credentials
|
||||
$profiles = SmsProfile::query()
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'active', 'api_username', 'encrypted_api_password']);
|
||||
|
||||
return $profiles->map(function (SmsProfile $p) use ($sms, $counts) {
|
||||
// Provider balance may fail; guard and present a placeholder.
|
||||
try {
|
||||
$balance = $sms->getCreditBalance($p);
|
||||
} catch (\Throwable $e) {
|
||||
$balance = '—';
|
||||
}
|
||||
$c = $counts->get($p->id) ?? ['queued' => 0, 'sent' => 0, 'delivered' => 0, 'failed' => 0, 'total' => 0];
|
||||
|
||||
return [
|
||||
'id' => $p->id,
|
||||
'name' => $p->name,
|
||||
'active' => (bool) $p->active,
|
||||
'balance' => $balance,
|
||||
'today' => $c,
|
||||
];
|
||||
})->values();
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Activity;
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientCase;
|
||||
use App\Models\Contract;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -21,9 +22,13 @@ public function unread(Request $request)
|
||||
$perPage = max(1, min(100, (int) $request->integer('perPage', 15)));
|
||||
$search = trim((string) $request->input('search', ''));
|
||||
$clientUuid = trim((string) $request->input('client', ''));
|
||||
$clientCaseId = null;
|
||||
$clientId = null;
|
||||
$clientCaseIdsForFilter = collect();
|
||||
if ($clientUuid !== '') {
|
||||
$clientCaseId = ClientCase::query()->where('uuid', $clientUuid)->value('id');
|
||||
$clientId = Client::query()->where('uuid', $clientUuid)->value('id');
|
||||
if ($clientId) {
|
||||
$clientCaseIdsForFilter = ClientCase::query()->where('client_id', $clientId)->pluck('id');
|
||||
}
|
||||
}
|
||||
|
||||
$query = Activity::query()
|
||||
@@ -36,13 +41,13 @@ public function unread(Request $request)
|
||||
->where('anr.user_id', $user->id)
|
||||
->whereColumn('anr.due_date', 'activities.due_date');
|
||||
})
|
||||
->when($clientCaseId, function ($q) use ($clientCaseId) {
|
||||
// Match activities for the client case directly OR via contracts belonging to the case
|
||||
$q->where(function ($qq) use ($clientCaseId) {
|
||||
$qq->where('activities.client_case_id', $clientCaseId)
|
||||
->when($clientCaseIdsForFilter->isNotEmpty(), function ($q) use ($clientCaseIdsForFilter) {
|
||||
// Filter by clients: activities directly on any of the client's cases OR via contracts under those cases
|
||||
$q->where(function ($qq) use ($clientCaseIdsForFilter) {
|
||||
$qq->whereIn('activities.client_case_id', $clientCaseIdsForFilter)
|
||||
->orWhereIn('activities.contract_id', Contract::query()
|
||||
->select('id')
|
||||
->where('client_case_id', $clientCaseId)
|
||||
->whereIn('client_case_id', $clientCaseIdsForFilter)
|
||||
);
|
||||
});
|
||||
})
|
||||
@@ -65,7 +70,7 @@ public function unread(Request $request)
|
||||
$qq->select(['client_cases.id', 'client_cases.uuid', 'client_cases.client_id'])
|
||||
->with([
|
||||
'client' => function ($qqq) {
|
||||
$qqq->select(['clients.id', 'clients.person_id'])
|
||||
$qqq->select(['clients.id', 'clients.uuid', 'clients.person_id'])
|
||||
->with([
|
||||
'person' => function ($qqqq) {
|
||||
$qqqq->select(['person.id', 'person.full_name']);
|
||||
@@ -86,7 +91,7 @@ public function unread(Request $request)
|
||||
$qq->select(['person.id', 'person.full_name']);
|
||||
},
|
||||
'client' => function ($qq) {
|
||||
$qq->select(['clients.id', 'clients.person_id'])
|
||||
$qq->select(['clients.id', 'clients.uuid', 'clients.person_id'])
|
||||
->with([
|
||||
'person' => function ($qqq) {
|
||||
$qqq->select(['person.id', 'person.full_name']);
|
||||
@@ -102,7 +107,7 @@ public function unread(Request $request)
|
||||
// Use a custom page parameter name to match the frontend DataTableServer
|
||||
$activities = $query->paginate($perPage, ['*'], 'unread-page')->withQueryString();
|
||||
|
||||
// Build a distinct clients list for the filter (client_case UUID + person.full_name)
|
||||
// Build a distinct clients list for the filter (client UUID + client.person.full_name)
|
||||
// Collect client_case_ids from both direct activities and via contracts
|
||||
$baseForClients = Activity::query()
|
||||
->select(['contract_id', 'client_case_id'])
|
||||
@@ -114,10 +119,10 @@ public function unread(Request $request)
|
||||
->where('anr.user_id', $user->id)
|
||||
->whereColumn('anr.due_date', 'activities.due_date');
|
||||
})
|
||||
->when($clientCaseId, function ($q) use ($clientCaseId) {
|
||||
$q->where(function ($qq) use ($clientCaseId) {
|
||||
$qq->where('activities.client_case_id', $clientCaseId)
|
||||
->orWhereIn('activities.contract_id', Contract::query()->select('id')->where('client_case_id', $clientCaseId));
|
||||
->when($clientCaseIdsForFilter->isNotEmpty(), function ($q) use ($clientCaseIdsForFilter) {
|
||||
$q->where(function ($qq) use ($clientCaseIdsForFilter) {
|
||||
$qq->whereIn('activities.client_case_id', $clientCaseIdsForFilter)
|
||||
->orWhereIn('activities.contract_id', Contract::query()->select('id')->whereIn('client_case_id', $clientCaseIdsForFilter));
|
||||
});
|
||||
})
|
||||
->get();
|
||||
@@ -133,16 +138,23 @@ public function unread(Request $request)
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
$clients = ClientCase::query()
|
||||
->whereIn('id', $caseIds)
|
||||
->with(['person:id,full_name'])
|
||||
->get(['id', 'uuid', 'person_id'])
|
||||
->map(fn ($cc) => [
|
||||
'value' => $cc->uuid,
|
||||
'label' => optional($cc->person)->full_name ?: '(neznana stranka)',
|
||||
])
|
||||
->sortBy('label', SORT_NATURAL | SORT_FLAG_CASE)
|
||||
->values();
|
||||
// Map caseIds -> clientIds, then load clients and present as value(label)
|
||||
$clientIds = $caseIds->isNotEmpty()
|
||||
? ClientCase::query()->whereIn('id', $caseIds)->pluck('client_id')->filter()->unique()->values()
|
||||
: collect();
|
||||
|
||||
$clients = $clientIds->isNotEmpty()
|
||||
? Client::query()
|
||||
->whereIn('id', $clientIds)
|
||||
->with(['person:id,full_name'])
|
||||
->get(['id', 'uuid', 'person_id'])
|
||||
->map(fn ($c) => [
|
||||
'value' => $c->uuid,
|
||||
'label' => optional($c->person)->full_name ?: '(neznana stranka)',
|
||||
])
|
||||
->sortBy('label', SORT_NATURAL | SORT_FLAG_CASE)
|
||||
->values()
|
||||
: collect();
|
||||
|
||||
return Inertia::render('Notifications/Unread', [
|
||||
'activities' => $activities,
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\SmsLog;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SmsWebhookController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle smsapi.si delivery reports (GET) and replies (POST).
|
||||
* This endpoint accepts both methods as the provider may use either.
|
||||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
// Delivery report via GET: id (int) and status (string)
|
||||
if ($request->query->has('id')) {
|
||||
$providerId = (string) ((int) $request->query('id'));
|
||||
$status = trim(strip_tags((string) $request->query('status', '')));
|
||||
|
||||
$log = SmsLog::query()->where('provider_message_id', $providerId)->first();
|
||||
if ($log) {
|
||||
$meta = (array) $log->meta;
|
||||
$meta['delivery_report'] = [
|
||||
'raw_status' => $status,
|
||||
'received_at' => now()->toIso8601String(),
|
||||
];
|
||||
|
||||
// Naive mapping: mark delivered for common success statuses
|
||||
$normalized = strtoupper($status);
|
||||
if (in_array($normalized, ['DELIVERED', 'DELIVRD', 'OK'], true)) {
|
||||
$log->status = 'delivered';
|
||||
$log->delivered_at = now();
|
||||
} elseif (in_array($normalized, ['FAILED', 'UNDELIV', 'UNDELIVERED', 'ERROR'], true)) {
|
||||
$log->status = 'failed';
|
||||
$log->failed_at = now();
|
||||
$log->error_code = $normalized;
|
||||
}
|
||||
|
||||
$log->meta = $meta;
|
||||
$log->save();
|
||||
} else {
|
||||
Log::warning('sms.webhook.delivery.unknown_id', ['provider_id' => $providerId, 'status' => $status]);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
// Reply via POST: smsId, m (message), from, to, time
|
||||
if ($request->isMethod('post') && $request->post('smsId')) {
|
||||
$providerId = (string) ((int) $request->post('smsId'));
|
||||
$msg = trim(strip_tags((string) $request->post('m', '')));
|
||||
$fromNumber = (string) $request->post('from', '');
|
||||
$toNumber = (string) $request->post('to', '');
|
||||
$timestamp = (int) $request->post('time', time());
|
||||
|
||||
$log = SmsLog::query()->where('provider_message_id', $providerId)->first();
|
||||
if ($log) {
|
||||
$meta = (array) $log->meta;
|
||||
$replies = isset($meta['replies']) && is_array($meta['replies']) ? $meta['replies'] : [];
|
||||
$replies[] = [
|
||||
'message' => $msg,
|
||||
'from' => $fromNumber,
|
||||
'to' => $toNumber,
|
||||
'time' => date('c', $timestamp),
|
||||
];
|
||||
$meta['replies'] = $replies;
|
||||
$log->meta = $meta;
|
||||
$log->save();
|
||||
} else {
|
||||
Log::warning('sms.webhook.reply.unknown_id', [
|
||||
'provider_id' => $providerId,
|
||||
'from' => $fromNumber,
|
||||
'to' => $toNumber,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
// Unknown payload
|
||||
return response()->json(['ok' => false, 'reason' => 'unsupported payload'], 400);
|
||||
}
|
||||
}
|
||||
@@ -139,7 +139,7 @@ public function share(Request $request): array
|
||||
? \App\Models\Client::query()
|
||||
->whereIn('clients.id', $clientIds)
|
||||
->with(['person:id,full_name'])
|
||||
->get(['clients.id', 'clients.person_id'])
|
||||
->get(['clients.id', 'clients.uuid', 'clients.person_id'])
|
||||
->keyBy('id')
|
||||
: collect();
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreSmsProfileRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('manage-settings') ?? false;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:190'],
|
||||
'active' => ['sometimes', 'boolean'],
|
||||
'api_username' => ['required', 'string', 'max:190'],
|
||||
'api_password' => ['required', 'string', 'max:500'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreSmsSenderRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('manage-settings') ?? false;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$pid = (int) $this->input('profile_id');
|
||||
|
||||
return [
|
||||
'profile_id' => ['required', 'integer', 'exists:sms_profiles,id'],
|
||||
'sname' => [
|
||||
'nullable', 'string', 'max:20',
|
||||
Rule::unique('sms_senders', 'sname')->where(fn ($q) => $q->where('profile_id', $pid)),
|
||||
],
|
||||
'phone_number' => ['nullable', 'string', 'max:30'],
|
||||
'description' => ['nullable', 'string', 'max:190'],
|
||||
'active' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreSmsTemplateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('manage-settings') ?? false;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:190'],
|
||||
'slug' => ['required', 'string', 'max:190', 'alpha_dash', 'unique:sms_templates,slug'],
|
||||
// Content is required unless template allows custom body
|
||||
'content' => [Rule::requiredIf(fn () => ! (bool) $this->input('allow_custom_body')), 'nullable', 'string', 'max:1000'],
|
||||
'variables_json' => ['nullable', 'array'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
'default_profile_id' => ['nullable', 'integer', 'exists:sms_profiles,id'],
|
||||
'default_sender_id' => ['nullable', 'integer', 'exists:sms_senders,id'],
|
||||
'allow_custom_body' => ['sometimes', 'boolean'],
|
||||
'action_id' => ['nullable', 'integer', 'exists:actions,id'],
|
||||
'decision_id' => ['nullable', 'integer', 'exists:decisions,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class TestSendSmsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('manage-settings') ?? false;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'to' => ['required', 'string', 'max:30'], // E.164-ish; we can refine later
|
||||
'message' => ['required', 'string', 'max:1000'],
|
||||
'sender_id' => ['nullable', 'integer', 'exists:sms_senders,id'],
|
||||
'delivery_report' => ['sometimes', 'boolean'],
|
||||
'country_code' => ['nullable', 'string', 'max:5'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class TestSendSmsTemplateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('manage-settings') ?? false;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'to' => ['required', 'string', 'max:30'],
|
||||
'variables' => ['nullable', 'array'],
|
||||
'profile_id' => ['nullable', 'integer', 'exists:sms_profiles,id'],
|
||||
'sender_id' => ['nullable', 'integer', 'exists:sms_senders,id'],
|
||||
'delivery_report' => ['sometimes', 'boolean'],
|
||||
'country_code' => ['nullable', 'string', 'max:5'],
|
||||
'custom_content' => ['nullable', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateSmsSenderRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('manage-settings') ?? false;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$pid = (int) $this->input('profile_id');
|
||||
$id = (int) ($this->route('smsSender')?->id ?? 0);
|
||||
|
||||
return [
|
||||
'profile_id' => ['required', 'integer', 'exists:sms_profiles,id'],
|
||||
'sname' => [
|
||||
'nullable', 'string', 'max:20',
|
||||
Rule::unique('sms_senders', 'sname')
|
||||
->ignore($id)
|
||||
->where(fn ($q) => $q->where('profile_id', $pid)),
|
||||
],
|
||||
'phone_number' => ['nullable', 'string', 'max:30'],
|
||||
'description' => ['nullable', 'string', 'max:190'],
|
||||
'active' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateSmsTemplateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()?->can('manage-settings') ?? false;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$id = (int) ($this->route('smsTemplate')?->id ?? 0);
|
||||
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:190'],
|
||||
'slug' => ['required', 'string', 'max:190', 'alpha_dash', Rule::unique('sms_templates', 'slug')->ignore($id)],
|
||||
// Content is required unless template allows custom body
|
||||
'content' => [Rule::requiredIf(fn () => ! (bool) $this->input('allow_custom_body')), 'nullable', 'string', 'max:1000'],
|
||||
'variables_json' => ['nullable', 'array'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
'default_profile_id' => ['nullable', 'integer', 'exists:sms_profiles,id'],
|
||||
'default_sender_id' => ['nullable', 'integer', 'exists:sms_senders,id'],
|
||||
'allow_custom_body' => ['sometimes', 'boolean'],
|
||||
'action_id' => ['nullable', 'integer', 'exists:actions,id'],
|
||||
'decision_id' => ['nullable', 'integer', 'exists:decisions,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user