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(); $devicesQuery = PosDevice::owned($owner)->with('location')->orderBy('name'); $this->scopeToLocation($request, $devicesQuery); return view('pos.settings', [ 'location' => $location, 'branches' => $branches, 'devices' => $devicesQuery->limit(10)->get(), 'deviceTypes' => config('pos.device_types', []), '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), 'canUsePaymentGateway' => $this->subscriptions->canUsePaymentGateway($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. * Free plan cannot connect or change gateway credentials. */ 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; } if (! $this->subscriptions->canUsePaymentGateway($user)) { // Ignore gateway fields on free plan (other settings may still save). 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()); } } }