Deploy Ladill POS / deploy (push) Successful in 1m32s
Implement the missing SettingsController::persistGateway method so merchant Paystack/Flutterwave/Hubtel keys can be saved, and cover create + keep-blank-secrets paths with tests.
222 lines
8.5 KiB
PHP
222 lines
8.5 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'],
|
|
'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'
|
|
);
|
|
}
|
|
|
|
$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,
|
|
]);
|
|
|
|
$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());
|
|
}
|
|
}
|
|
}
|