Replace Ladill Pay POS checkouts with merchant gateways.
Deploy Ladill POS / deploy (push) Successful in 30s

Register Card/MoMo payments through merchant Paystack, Flutterwave, or Hubtel settings so takings settle 100% to the business.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-15 01:26:22 +00:00
co-authored by Cursor
parent 2e969f8db2
commit 3ea9fdfe33
10 changed files with 473 additions and 68 deletions
@@ -6,13 +6,16 @@ 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\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;
@@ -23,6 +26,7 @@ class SettingsController extends Controller
public function __construct(
private PosLocationService $locations,
private \App\Services\Pos\SubscriptionService $subscriptions,
private MerchantGatewayService $gateway,
) {}
public function index(Request $request): View
@@ -47,6 +51,7 @@ class SettingsController extends Controller
'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),
]);
}
@@ -62,6 +67,16 @@ class SettingsController extends Controller
'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();
@@ -98,6 +113,8 @@ class SettingsController extends Controller
'receipt_logo_path' => $location->receipt_logo_path,
]);
$this->persistGateway($request, $user);
return back()->with('success', 'Settings saved.');
}
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PaymentGatewaySetting extends Model
{
public const PROVIDER_PAYSTACK = 'paystack';
public const PROVIDER_FLUTTERWAVE = 'flutterwave';
public const PROVIDER_HUBTEL = 'hubtel';
protected $fillable = [
'owner_ref',
'provider',
'public_key',
'secret_key',
'webhook_secret',
'is_active',
'metadata',
];
protected function casts(): array
{
return [
'public_key' => 'encrypted',
'secret_key' => 'encrypted',
'webhook_secret' => 'encrypted',
'is_active' => 'boolean',
'metadata' => 'array',
];
}
public function isConfigured(): bool
{
if (! $this->is_active) {
return false;
}
$secret = trim((string) $this->secret_key);
return $secret !== '' && in_array($this->provider, [
self::PROVIDER_PAYSTACK,
self::PROVIDER_FLUTTERWAVE,
self::PROVIDER_HUBTEL,
], true);
}
}
+3 -3
View File
@@ -91,17 +91,17 @@ class AfiaService
return <<<PROMPT
You are Afia, the assistant inside Ladill POS (pos.ladill.com).
Help users run the in-store register taking sales, charging by cash or Ladill Pay, managing the product catalog, and reading their sales history. Be concise, friendly, and actionable: short numbered steps and name the screen or section to use.
Help users run the in-store register taking sales, charging by cash or Card / MoMo, managing the product catalog, and reading their sales history. Be concise, friendly, and actionable: short numbered steps and name the screen or section to use.
What Ladill POS does and where things live:
- Overview (Dashboard): today's takings, number of sales, active products, and recent sales.
- Register: the point-of-sale screen add catalog products or a quick custom amount to the cart, then charge by cash or Ladill Pay (MoMo/card).
- Register: the point-of-sale screen add catalog products or a quick custom amount to the cart, then charge by cash or Card / MoMo (MoMo/card).
- Products: the local catalog (name, SKU, price). Products can also be imported from Ladill CRM in Settings.
- Sales: history of completed sales with receipts; a paid sale can be turned into a Ladill Invoice.
- Settings: store details, default currency, and importing products from CRM or the Merchant storefront.
Rules:
- Only answer questions about Ladill POS the register, sales, products, checkout (cash/Ladill Pay), receipts, and settings.
- Only answer questions about Ladill POS the register, sales, products, checkout (cash/Card / MoMo), receipts, and settings.
- If asked about contacts, invoices, domains, hosting, email mailboxes, or QR codes, briefly say those live in other Ladill apps and suggest the app launcher.
- Never invent figures (takings, counts) use the user context below or tell them where to check.
@@ -0,0 +1,288 @@
<?php
namespace App\Services\Payments;
use App\Models\PaymentGatewaySetting;
use App\Models\User;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class MerchantGatewayService
{
public function settingFor(User|string $owner): ?PaymentGatewaySetting
{
$ownerRef = $owner instanceof User ? (string) $owner->public_id : $owner;
return PaymentGatewaySetting::query()->where('owner_ref', $ownerRef)->first();
}
public function isConfigured(User|string $owner): bool
{
return (bool) $this->settingFor($owner)?->isConfigured();
}
/**
* @param array<string, mixed> $metadata
* @return array{checkout_url: string, reference: string, provider: string}
*/
public function initializeCheckout(
User|string $owner,
int $amountMinor,
string $currency,
string $email,
string $callbackUrl,
string $reference,
array $metadata = [],
): array {
$setting = $this->requireConfigured($owner);
$currency = strtoupper($currency ?: 'GHS');
return match ($setting->provider) {
PaymentGatewaySetting::PROVIDER_PAYSTACK => $this->paystackInitialize($setting, $amountMinor, $currency, $email, $callbackUrl, $reference, $metadata),
PaymentGatewaySetting::PROVIDER_FLUTTERWAVE => $this->flutterwaveInitialize($setting, $amountMinor, $currency, $email, $callbackUrl, $reference, $metadata),
PaymentGatewaySetting::PROVIDER_HUBTEL => $this->hubtelInitialize($setting, $amountMinor, $currency, $email, $callbackUrl, $reference, $metadata),
default => throw new RuntimeException('Unsupported payment provider.'),
};
}
/**
* @return array{paid: bool, amount_minor: int, reference: string, provider: string, raw: array<string, mixed>}
*/
public function verify(User|string $owner, string $reference): array
{
$setting = $this->requireConfigured($owner);
return match ($setting->provider) {
PaymentGatewaySetting::PROVIDER_PAYSTACK => $this->paystackVerify($setting, $reference),
PaymentGatewaySetting::PROVIDER_FLUTTERWAVE => $this->flutterwaveVerify($setting, $reference),
PaymentGatewaySetting::PROVIDER_HUBTEL => $this->hubtelVerify($setting, $reference),
default => throw new RuntimeException('Unsupported payment provider.'),
};
}
protected function requireConfigured(User|string $owner): PaymentGatewaySetting
{
$setting = $this->settingFor($owner);
if (! $setting?->isConfigured()) {
throw new RuntimeException('Connect Paystack, Flutterwave, or Hubtel in Settings before accepting online payments.');
}
return $setting;
}
/**
* @param array<string, mixed> $metadata
* @return array{checkout_url: string, reference: string, provider: string}
*/
protected function paystackInitialize(
PaymentGatewaySetting $setting,
int $amountMinor,
string $currency,
string $email,
string $callbackUrl,
string $reference,
array $metadata,
): array {
$response = Http::withToken((string) $setting->secret_key)
->acceptJson()
->timeout(20)
->post('https://api.paystack.co/transaction/initialize', [
'email' => $email !== '' ? $email : 'payer@example.com',
'amount' => $amountMinor,
'currency' => $currency,
'reference' => $reference,
'callback_url' => $callbackUrl,
'metadata' => $metadata,
]);
if (! $response->successful() || ! ($response->json('status') ?? false)) {
throw new RuntimeException($response->json('message') ?: 'Paystack could not start checkout.');
}
$url = (string) $response->json('data.authorization_url');
if ($url === '') {
throw new RuntimeException('Paystack did not return a checkout URL.');
}
return [
'checkout_url' => $url,
'reference' => (string) ($response->json('data.reference') ?: $reference),
'provider' => PaymentGatewaySetting::PROVIDER_PAYSTACK,
];
}
/**
* @return array{paid: bool, amount_minor: int, reference: string, provider: string, raw: array<string, mixed>}
*/
protected function paystackVerify(PaymentGatewaySetting $setting, string $reference): array
{
$response = Http::withToken((string) $setting->secret_key)
->acceptJson()
->timeout(20)
->get('https://api.paystack.co/transaction/verify/'.rawurlencode($reference));
$data = (array) ($response->json('data') ?? []);
$paid = ($response->json('status') ?? false)
&& strtolower((string) ($data['status'] ?? '')) === 'success';
return [
'paid' => $paid,
'amount_minor' => (int) ($data['amount'] ?? 0),
'reference' => (string) ($data['reference'] ?? $reference),
'provider' => PaymentGatewaySetting::PROVIDER_PAYSTACK,
'raw' => $data,
];
}
/**
* @param array<string, mixed> $metadata
* @return array{checkout_url: string, reference: string, provider: string}
*/
protected function flutterwaveInitialize(
PaymentGatewaySetting $setting,
int $amountMinor,
string $currency,
string $email,
string $callbackUrl,
string $reference,
array $metadata,
): array {
$response = Http::withToken((string) $setting->secret_key)
->acceptJson()
->timeout(20)
->post('https://api.flutterwave.com/v3/payments', [
'tx_ref' => $reference,
'amount' => round($amountMinor / 100, 2),
'currency' => $currency,
'redirect_url' => $callbackUrl,
'customer' => [
'email' => $email !== '' ? $email : 'payer@example.com',
],
'customizations' => [
'title' => (string) ($metadata['title'] ?? 'Payment'),
],
'meta' => $metadata,
]);
if (! $response->successful() || ($response->json('status') ?? '') !== 'success') {
throw new RuntimeException($response->json('message') ?: 'Flutterwave could not start checkout.');
}
$url = (string) $response->json('data.link');
if ($url === '') {
throw new RuntimeException('Flutterwave did not return a checkout URL.');
}
return [
'checkout_url' => $url,
'reference' => $reference,
'provider' => PaymentGatewaySetting::PROVIDER_FLUTTERWAVE,
];
}
/**
* @return array{paid: bool, amount_minor: int, reference: string, provider: string, raw: array<string, mixed>}
*/
protected function flutterwaveVerify(PaymentGatewaySetting $setting, string $reference): array
{
$response = Http::withToken((string) $setting->secret_key)
->acceptJson()
->timeout(20)
->get('https://api.flutterwave.com/v3/transactions/verify_by_reference', [
'tx_ref' => $reference,
]);
$data = (array) ($response->json('data') ?? []);
$paid = ($response->json('status') ?? '') === 'success'
&& strtolower((string) ($data['status'] ?? '')) === 'successful';
$amountMajor = (float) ($data['amount'] ?? 0);
return [
'paid' => $paid,
'amount_minor' => (int) round($amountMajor * 100),
'reference' => (string) ($data['tx_ref'] ?? $reference),
'provider' => PaymentGatewaySetting::PROVIDER_FLUTTERWAVE,
'raw' => $data,
];
}
/**
* @param array<string, mixed> $metadata
* @return array{checkout_url: string, reference: string, provider: string}
*/
protected function hubtelInitialize(
PaymentGatewaySetting $setting,
int $amountMinor,
string $currency,
string $email,
string $callbackUrl,
string $reference,
array $metadata,
): array {
// Hubtel: public_key = merchant account number, secret_key = API key (client secret),
// webhook_secret optionally stores client id for Basic auth as "clientId:clientSecret".
$auth = trim((string) ($setting->webhook_secret ?: $setting->secret_key));
if (! str_contains($auth, ':')) {
$auth = trim((string) $setting->public_key).':'.trim((string) $setting->secret_key);
}
$response = Http::withBasicAuth(...explode(':', $auth, 2))
->acceptJson()
->timeout(20)
->post('https://payproxyapi.hubtel.com/items/initiate', [
'totalAmount' => round($amountMinor / 100, 2),
'description' => (string) ($metadata['title'] ?? 'Payment'),
'callbackUrl' => $callbackUrl,
'returnUrl' => $callbackUrl,
'merchantAccountNumber' => (string) $setting->public_key,
'cancellationUrl' => $callbackUrl,
'clientReference' => $reference,
]);
if (! $response->successful()) {
throw new RuntimeException($response->json('message') ?: 'Hubtel could not start checkout.');
}
$url = (string) ($response->json('data.checkoutUrl') ?? $response->json('data.checkoutDirectUrl') ?? '');
if ($url === '') {
throw new RuntimeException('Hubtel did not return a checkout URL.');
}
return [
'checkout_url' => $url,
'reference' => $reference,
'provider' => PaymentGatewaySetting::PROVIDER_HUBTEL,
];
}
/**
* @return array{paid: bool, amount_minor: int, reference: string, provider: string, raw: array<string, mixed>}
*/
protected function hubtelVerify(PaymentGatewaySetting $setting, string $reference): array
{
$auth = trim((string) ($setting->webhook_secret ?: $setting->secret_key));
if (! str_contains($auth, ':')) {
$auth = trim((string) $setting->public_key).':'.trim((string) $setting->secret_key);
}
$response = Http::withBasicAuth(...explode(':', $auth, 2))
->acceptJson()
->timeout(20)
->get('https://api-txnstatus.hubtel.com/transactions/'.$setting->public_key.'/status', [
'clientReference' => $reference,
]);
$data = (array) ($response->json('data') ?? $response->json() ?? []);
$status = strtolower((string) ($data['status'] ?? $data['transactionStatus'] ?? ''));
$paid = in_array($status, ['success', 'successful', 'paid', 'completed'], true);
$amountMajor = (float) ($data['amount'] ?? $data['totalAmount'] ?? 0);
return [
'paid' => $paid,
'amount_minor' => (int) round($amountMajor * 100),
'reference' => $reference,
'provider' => PaymentGatewaySetting::PROVIDER_HUBTEL,
'raw' => $data,
];
}
}
+43 -59
View File
@@ -10,14 +10,14 @@ use App\Models\PosSaleLine;
use App\Models\PosSaleLineModifier;
use App\Models\PosTable;
use App\Models\User;
use App\Services\Pay\PayClient;
use App\Services\Payments\MerchantGatewayService;
use Illuminate\Support\Str;
use RuntimeException;
class PosSaleService
{
public function __construct(
private PayClient $pay,
private MerchantGatewayService $gateway,
private PosTimelineService $timeline,
) {}
@@ -281,7 +281,7 @@ class PosSaleService
}
/**
* Start a Ladill Pay checkout for part (or all) of a ticket's balance.
* Start a Card / MoMo checkout for part (or all) of a ticket's balance.
*
* @return array{payment: PosPayment, checkout_url: string}
*/
@@ -300,48 +300,39 @@ class PosSaleService
'status' => PosPayment::STATUS_PENDING,
]);
$payOrder = $this->pay->createCheckout([
'merchant' => $merchant->public_id,
'fee_tier' => 'sales',
'source_service' => 'pos',
'source_ref' => 'payment:'.$payment->id,
'callback_url' => route('pos.payments.callback'),
'customer_name' => $sale->customer_name,
'customer_email' => $sale->customer_email,
'customer_phone' => $sale->customer_phone,
'line_items' => [[
'name' => 'Ticket '.$sale->reference.' payment',
'unit_price_minor' => $amount,
'quantity' => 1,
]],
'metadata' => ['pos_sale_id' => $sale->id, 'pos_payment_id' => $payment->id],
]);
$checkoutUrl = (string) ($payOrder['checkout_url'] ?? '');
if ($checkoutUrl === '') {
throw new RuntimeException('Could not start checkout. Please try again.');
}
$reference = 'POSP-'.strtoupper(Str::random(16));
$checkout = $this->gateway->initializeCheckout(
$merchant,
$amount,
(string) ($sale->currency ?? 'GHS'),
(string) ($sale->customer_email ?: $merchant->email ?: 'payer@example.com'),
route('pos.payments.callback'),
$reference,
['title' => 'Ticket '.$sale->reference.' payment', 'pos_sale_id' => $sale->id, 'pos_payment_id' => $payment->id],
);
$payment->forceFill([
'pay_order_id' => $payOrder['id'] ?? null,
'payment_reference' => $payOrder['reference'] ?? null,
'payment_reference' => $checkout['reference'],
])->save();
return ['payment' => $payment, 'checkout_url' => $checkoutUrl];
return ['payment' => $payment, 'checkout_url' => $checkout['checkout_url']];
}
public function completePayPayment(string $reference): PosPayment
{
$payment = PosPayment::where('payment_reference', $reference)
->where('status', PosPayment::STATUS_PENDING)
->with('sale')
->firstOrFail();
$payOrder = $this->pay->verify($reference);
$result = $this->gateway->verify($payment->owner_ref, $reference);
if (! $result['paid']) {
throw new RuntimeException('Payment was not completed.');
}
$payment->forceFill([
'status' => PosPayment::STATUS_PAID,
'pay_order_id' => $payOrder['id'] ?? $payment->pay_order_id,
'amount_minor' => (int) ($payOrder['amount_minor'] ?? $payment->amount_minor),
'amount_minor' => (int) ($result['amount_minor'] ?: $payment->amount_minor),
'paid_at' => now(),
])->save();
@@ -505,36 +496,27 @@ class PosSaleService
$sale->load('lines');
$callbackUrl = route('pos.sales.callback', $sale);
$payOrder = $this->pay->createCheckout([
'merchant' => $merchant->public_id,
'fee_tier' => 'sales',
'source_service' => 'pos',
'source_ref' => (string) $sale->id,
'callback_url' => $callbackUrl,
'customer_name' => $sale->customer_name,
'customer_email' => $sale->customer_email,
'customer_phone' => $sale->customer_phone,
'line_items' => $sale->lines->map(fn (PosSaleLine $line) => [
'name' => $line->name,
'unit_price_minor' => $line->unit_price_minor,
'quantity' => $line->quantity,
])->all(),
'metadata' => [
'pos_sale_id' => $sale->id,
'pos_reference' => $sale->reference,
],
]);
$checkoutUrl = (string) ($payOrder['checkout_url'] ?? '');
if ($checkoutUrl === '') {
throw new RuntimeException('Could not start checkout. Please try again.');
$reference = 'POSS-'.strtoupper(Str::random(16));
$amount = (int) $sale->balanceMinor();
if ($amount < 1) {
$amount = (int) $sale->total_minor;
}
$checkout = $this->gateway->initializeCheckout(
$merchant,
$amount,
(string) ($sale->currency ?? 'GHS'),
(string) ($sale->customer_email ?: $merchant->email ?: 'payer@example.com'),
$callbackUrl,
$reference,
['title' => 'POS '.$sale->reference, 'pos_sale_id' => $sale->id, 'pos_reference' => $sale->reference],
);
$checkoutUrl = $checkout['checkout_url'];
$sale->forceFill([
'payment_method' => PosSale::METHOD_PAY,
'pay_order_id' => $payOrder['id'] ?? null,
'payment_reference' => $payOrder['reference'] ?? null,
'payment_reference' => $checkout['reference'],
])->save();
return [
@@ -570,12 +552,14 @@ class PosSaleService
->where('status', PosSale::STATUS_PENDING)
->firstOrFail();
$payOrder = $this->pay->verify($paymentReference);
$result = $this->gateway->verify($sale->owner_ref, $paymentReference);
if (! $result['paid']) {
throw new RuntimeException('Payment was not completed.');
}
$sale->forceFill([
'status' => PosSale::STATUS_PAID,
'total_minor' => (int) ($payOrder['amount_minor'] ?? $sale->total_minor),
'pay_order_id' => $payOrder['id'] ?? $sale->pay_order_id,
'total_minor' => (int) ($result['amount_minor'] ?: $sale->total_minor),
'paid_at' => now(),
])->save();
-3
View File
@@ -18,10 +18,7 @@ $root = config('app.platform_domain', 'ladill.com');
return [
'apps' => [
['name' => 'Merchant', 'url' => 'https://merchant.'.$root.'/sso/connect?redirect='.urlencode('https://merchant.'.$root.'/dashboard'), 'icon' => 'merchant.svg'],
['name' => 'POS', 'url' => 'https://pos.'.$root.'/sso/connect?redirect='.urlencode('https://pos.'.$root.'/dashboard'), 'icon' => 'pos.svg'],
['name' => 'Mini', 'url' => 'https://mini.'.$root.'/sso/connect?redirect='.urlencode('https://mini.'.$root.'/dashboard'), 'icon' => 'mini.svg'],
['name' => 'Give', 'url' => 'https://give.'.$root.'/sso/connect?redirect='.urlencode('https://give.'.$root.'/dashboard'), 'icon' => 'give.svg'],
['name' => 'Woo Manager', 'url' => 'https://woo.'.$root.'/sso/connect?redirect='.urlencode('https://woo.'.$root.'/dashboard'), 'icon' => 'woomanager.svg'],
['name' => 'Transfer', 'url' => 'https://transfer.'.$root.'/sso/connect?redirect='.urlencode('https://transfer.'.$root.'/dashboard'), 'icon' => 'transfer.svg'],
['name' => 'Accounting', 'url' => 'https://accounting.'.$root.'/sso/connect?redirect='.urlencode('https://accounting.'.$root.'/dashboard'), 'icon' => 'accounting.svg'],
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('payment_gateway_settings', function (Blueprint $table) {
$table->id();
$table->string('owner_ref', 64)->unique();
$table->string('provider', 32); // paystack|flutterwave|hubtel
$table->text('public_key')->nullable();
$table->text('secret_key')->nullable();
$table->text('webhook_secret')->nullable();
$table->boolean('is_active')->default(true);
$table->json('metadata')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('payment_gateway_settings');
}
};
+2 -2
View File
@@ -1,8 +1,8 @@
@php
$afiaGreeting = "Hi, I'm Afia 👋 Ask me about taking a sale, charging by cash or Ladill Pay, managing products, or your sales history…";
$afiaGreeting = "Hi, I'm Afia 👋 Ask me about taking a sale, charging by cash or Card / MoMo, managing products, or your sales history…";
$afiaSuggestions = [
'How do I take a sale?',
'How do I charge with Ladill Pay?',
'How do I charge with Card / MoMo?',
'How do I add a product?',
'How do I import products from CRM?',
];
+41
View File
@@ -79,6 +79,47 @@
<span class="mt-0.5 block text-xs text-slate-500">Opens the receipt print dialog when you record a cash payment.</span>
</span>
</label>
<div class="border-t border-slate-100 pt-4">
<p class="text-sm font-semibold text-slate-900">Payment gateway</p>
<p class="mt-1 text-xs text-slate-400">Connect Paystack, Flutterwave, or Hubtel. Online checkouts go 100% to you 0% Ladill fee.</p>
</div>
<div>
<label class="text-sm font-medium text-slate-700">Provider</label>
<select name="gateway_provider" class="mt-1.5 block w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value="">Select a provider</option>
<option value="paystack" @selected(old('gateway_provider', $gateway?->provider ?? null) === 'paystack')>Paystack</option>
<option value="flutterwave" @selected(old('gateway_provider', $gateway?->provider ?? null) === 'flutterwave')>Flutterwave</option>
<option value="hubtel" @selected(old('gateway_provider', $gateway?->provider ?? null) === 'hubtel')>Hubtel</option>
</select>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label class="text-sm font-medium text-slate-700">Public key / Merchant account</label>
<input type="text" name="gateway_public_key" value="{{ old('gateway_public_key') }}" placeholder="{{ ($gateway?->public_key ?? null) ? '•••• saved — leave blank to keep' : 'pk_live_… or Hubtel account number' }}" class="mt-1.5 block w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
</div>
<div>
<label class="text-sm font-medium text-slate-700">Secret / API key</label>
<input type="password" name="gateway_secret_key" value="" placeholder="{{ ($gateway?->secret_key ?? null) ? '•••• saved — leave blank to keep' : 'sk_live_…' }}" class="mt-1.5 block w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500" autocomplete="new-password">
</div>
</div>
<div>
<label class="text-sm font-medium text-slate-700">Webhook secret (optional)</label>
<input type="password" name="gateway_webhook_secret" value="" placeholder="{{ ($gateway?->webhook_secret ?? null) ? '•••• saved — leave blank to keep' : 'Optional' }}" class="mt-1.5 block w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500" autocomplete="new-password">
</div>
<label class="flex items-start gap-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3">
<input type="hidden" name="gateway_is_active" value="0">
<input type="checkbox" name="gateway_is_active" value="1" @checked(old('gateway_is_active', $gateway?->is_active ?? true)) class="mt-0.5 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
<span>
<span class="block text-sm font-medium text-slate-800">Gateway enabled</span>
<span class="mt-0.5 block text-xs text-slate-500">Required for card / MoMo checkouts on the register.</span>
</span>
</label>
@if ($gateway?->isConfigured())
<p class="rounded-xl bg-emerald-50 px-3 py-2 text-sm text-emerald-800">Gateway connected ({{ ucfirst($gateway->provider) }}).</p>
@else
<p class="rounded-xl bg-amber-50 px-3 py-2 text-sm text-amber-800">Online checkouts stay disabled until a gateway is connected.</p>
@endif
<div class="flex justify-end">
<button type="submit" class="btn-primary">Save settings</button>
</div>
+1 -1
View File
@@ -146,7 +146,7 @@
<button name="payment_method" value="cash" :disabled="lines.length === 0"
class="rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-50 disabled:opacity-40">Cash</button>
<button name="payment_method" value="pay" :disabled="lines.length === 0"
class="btn-primary disabled:opacity-40">Ladill Pay</button>
class="btn-primary disabled:opacity-40">Card / MoMo</button>
</div>
<p class="text-center text-[11px] text-slate-400">Pay the full balance, or a partial amount to split the bill.</p>
</form>