Files
ladill-pos/app/Http/Controllers/Pos/SettingsController.php
T
isaacclad 600aedb59d
Deploy Ladill POS / deploy (push) Successful in 31s
Email digital receipts, add promo settings, and apply tips at charge.
Send real receipt emails from the customer display and sale page, store
branch promo content for the idle screen, and fold selected tips into totals.
2026-07-15 16:10:20 +00:00

244 lines
9.8 KiB
PHP

<?php
namespace App\Http\Controllers\Pos;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Pos\Concerns\ScopesToAccount;
use App\Models\PosLocation;
use App\Models\PosMember;
use App\Models\PaymentGatewaySetting;
use App\Models\PosTable;
use App\Models\User;
use App\Services\Import\CrmProductImportService;
use App\Services\Import\MerchantCatalogImportService;
use App\Services\Payments\MerchantGatewayService;
use App\Services\Pos\PosLocationService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Illuminate\View\View;
use RuntimeException;
class SettingsController extends Controller
{
use ScopesToAccount;
public function __construct(
private PosLocationService $locations,
private \App\Services\Pos\SubscriptionService $subscriptions,
private MerchantGatewayService $gateway,
) {}
public function index(Request $request): View
{
$owner = $this->ownerRef($request);
$location = $this->location($request);
$account = ladill_account() ?? $request->user();
$scope = $this->locationScope($request);
$branches = $scope === null
? PosLocation::owned($owner)->orderBy('name')->get()
: PosLocation::owned($owner)->whereKey($scope)->orderBy('name')->get();
return view('pos.settings', [
'location' => $location,
'branches' => $branches,
'members' => PosMember::owned($owner)->with('location')->orderBy('created_at')->get(),
'roles' => config('pos.roles', []),
'hasMultiLocation' => $this->subscriptions->canUseMultiLocation($account),
'hasTeamFeatures' => $this->subscriptions->canManageTeam($account),
'canAddBranch' => $this->subscriptions->canAddLocation($account, $owner),
'merchantImportEnabled' => (bool) config('pos.merchant_import_enabled', true),
'tables' => $this->scopeToLocation($request, PosTable::owned($owner))
->orderBy('area')->orderBy('position')->orderBy('label')->get(),
'gateway' => $this->gateway->settingFor($account),
]);
}
public function update(Request $request): RedirectResponse
{
$data = $request->validate([
'name' => ['required', 'string', 'max:120'],
'currency' => ['required', 'string', 'size:3'],
'service_style' => ['required', 'in:retail,restaurant'],
'receipt_footer' => ['nullable', 'string', 'max:1000'],
'receipt_header' => ['nullable', 'string', 'max:500'],
'receipt_logo' => ['nullable', 'image', 'mimes:jpeg,jpg,png,gif,webp', 'max:2048'],
'remove_receipt_logo' => ['sometimes', 'boolean'],
'printer_paper_mm' => ['required', 'in:58,80'],
'printer_auto_print' => ['sometimes', 'boolean'],
'customer_promo_headline' => ['nullable', 'string', 'max:160'],
'customer_promo_body' => ['nullable', 'string', 'max:500'],
'customer_promo_image' => ['nullable', 'image', 'mimes:jpeg,jpg,png,gif,webp', 'max:2048'],
'remove_customer_promo_image' => ['sometimes', 'boolean'],
'gateway_provider' => ['nullable', Rule::in([
PaymentGatewaySetting::PROVIDER_PAYSTACK,
PaymentGatewaySetting::PROVIDER_FLUTTERWAVE,
PaymentGatewaySetting::PROVIDER_HUBTEL,
'',
])],
'gateway_public_key' => ['nullable', 'string', 'max:2000'],
'gateway_secret_key' => ['nullable', 'string', 'max:2000'],
'gateway_webhook_secret' => ['nullable', 'string', 'max:2000'],
'gateway_is_active' => ['nullable', 'boolean'],
]);
$user = ladill_account() ?? $request->user();
if ($data['service_style'] === 'restaurant' && ! $this->subscriptions->canUseRestaurantMode($user)) {
return redirect()->route('pos.pro.index')
->with('upsell', 'Restaurant mode is part of Ladill POS Pro.');
}
$location = $this->location($request);
if ($request->boolean('remove_receipt_logo') && $location->receipt_logo_path) {
Storage::disk('public')->delete($location->receipt_logo_path);
$location->receipt_logo_path = null;
}
if ($request->hasFile('receipt_logo')) {
if ($location->receipt_logo_path) {
Storage::disk('public')->delete($location->receipt_logo_path);
}
$location->receipt_logo_path = $request->file('receipt_logo')->store(
'pos/receipt-logos/'.$this->ownerRef($request),
'public'
);
}
if ($request->boolean('remove_customer_promo_image') && $location->customer_promo_image_path) {
Storage::disk('public')->delete($location->customer_promo_image_path);
$location->customer_promo_image_path = null;
}
if ($request->hasFile('customer_promo_image')) {
if ($location->customer_promo_image_path) {
Storage::disk('public')->delete($location->customer_promo_image_path);
}
$location->customer_promo_image_path = $request->file('customer_promo_image')->store(
'pos/customer-promo/'.$this->ownerRef($request),
'public'
);
}
$location->update([
'name' => $data['name'],
'currency' => strtoupper($data['currency']),
'service_style' => $data['service_style'],
'receipt_footer' => $data['receipt_footer'] ?? null,
'receipt_header' => $data['receipt_header'] ?? null,
'printer_paper_mm' => (int) $data['printer_paper_mm'],
'printer_auto_print' => $request->boolean('printer_auto_print'),
'receipt_logo_path' => $location->receipt_logo_path,
'customer_promo_headline' => $data['customer_promo_headline'] ?? null,
'customer_promo_body' => $data['customer_promo_body'] ?? null,
'customer_promo_image_path' => $location->customer_promo_image_path,
]);
$this->persistGateway($request, $user);
return back()->with('success', 'Settings saved.');
}
/**
* Upsert merchant gateway credentials for the acting account.
* Blank secret fields keep the previously stored encrypted values.
*/
protected function persistGateway(Request $request, User $user): void
{
$ownerRef = (string) $user->public_id;
$existing = PaymentGatewaySetting::query()->where('owner_ref', $ownerRef)->first();
$provider = trim((string) $request->input('gateway_provider', ''));
$publicKey = trim((string) $request->input('gateway_public_key', ''));
$secretKey = trim((string) $request->input('gateway_secret_key', ''));
$webhookSecret = trim((string) $request->input('gateway_webhook_secret', ''));
// No provider chosen and no prior config — nothing to persist.
if ($provider === '' && $existing === null) {
return;
}
$attributes = [
'provider' => $provider !== '' ? $provider : $existing->provider,
'is_active' => $request->boolean('gateway_is_active'),
];
// Leave blank on the form means "keep the saved credential".
if ($publicKey !== '') {
$attributes['public_key'] = $publicKey;
}
if ($secretKey !== '') {
$attributes['secret_key'] = $secretKey;
}
if ($webhookSecret !== '') {
$attributes['webhook_secret'] = $webhookSecret;
}
PaymentGatewaySetting::query()->updateOrCreate(
['owner_ref' => $ownerRef],
$attributes,
);
}
public function storeTable(Request $request): RedirectResponse
{
$data = $request->validate([
'area' => ['nullable', 'string', 'max:60'],
'label' => ['required', 'string', 'max:60'],
'seats' => ['nullable', 'integer', 'min:1', 'max:99'],
]);
$owner = $this->ownerRef($request);
$location = $this->location($request);
PosTable::create([
'owner_ref' => $owner,
'location_id' => $location->id,
'area' => $data['area'] ?? null,
'label' => $data['label'],
'seats' => $data['seats'] ?? 2,
'status' => PosTable::STATUS_FREE,
'position' => (int) PosTable::owned($owner)->max('position') + 1,
]);
return back()->with('success', 'Table added.');
}
public function destroyTable(Request $request, PosTable $table): RedirectResponse
{
$this->authorizeOwner($request, $table);
if (! $table->isFree()) {
return back()->with('error', 'Settle the open ticket before removing this table.');
}
$table->delete();
return back()->with('success', 'Table removed.');
}
public function importCrm(Request $request, CrmProductImportService $import): RedirectResponse
{
try {
$result = $import->import($this->ownerRef($request));
return back()->with('success', "Imported {$result['imported']} product(s), updated {$result['updated']}.");
} catch (RuntimeException $e) {
return back()->with('error', $e->getMessage());
}
}
public function importMerchant(Request $request, MerchantCatalogImportService $import): RedirectResponse
{
try {
$result = $import->import($this->ownerRef($request));
return back()->with('success', "Imported {$result['imported']} item(s) from {$result['storefronts']} storefront(s), updated {$result['updated']}.");
} catch (RuntimeException $e) {
return back()->with('error', $e->getMessage());
}
}
}