Files
order/app/Services/PaymentGateway.php
T
Ty Clifford 16235369cb - Implemented the full Fat Bottom Grille storefront and staff dashboard.
Highlights include configurable menus, specials, cart/checkout, taxes, 
customer accounts with email 2FA, newsletters, analytics, refunds, 
PDF/CSV exports, Square/Mailgun adapters, and optional MySQL migration.

See README.md for setup and production configuration. Verified 
PHP/JavaScript syntax, responsive layouts, registration, 2FA, checkout, 
dashboard workflows, settings persistence, and PDF generation.
Real Square, Mailgun, and MySQL connections require credentials/server 
access. Square implementation follows the official Payments API and 
Refunds API.
2026-06-10 13:27:42 -04:00

133 lines
5.1 KiB
PHP

<?php
declare(strict_types=1);
namespace FatBottom\Services;
use FatBottom\Core\Config;
use RuntimeException;
final class PaymentGateway
{
public function __construct(private readonly Config $config)
{
}
public function charge(string $sourceId, int $amountCents, string $orderNumber): array
{
if (!(bool) $this->config->get('square.enabled', false)) {
return [
'success' => true,
'provider' => 'demo',
'reference' => 'DEMO-' . strtoupper(bin2hex(random_bytes(5))),
'status' => 'COMPLETED',
];
}
if ($sourceId === '') {
throw new RuntimeException('Card details are required.');
}
if (!function_exists('curl_init')) {
throw new RuntimeException('The PHP cURL extension is required for Square payments.');
}
$environment = (string) $this->config->get('square.environment', 'sandbox');
$baseUrl = $environment === 'production'
? 'https://connect.squareup.com'
: 'https://connect.squareupsandbox.com';
$accessToken = (string) $this->config->get('square.access_token');
$locationId = (string) $this->config->get('square.location_id');
if ($accessToken === '' || $locationId === '') {
throw new RuntimeException('Square is enabled but its server credentials are incomplete.');
}
$payload = json_encode([
'source_id' => $sourceId,
'idempotency_key' => substr(hash('sha256', 'fatbottom-payment|' . $orderNumber), 0, 45),
'amount_money' => [
'amount' => $amountCents,
'currency' => 'USD',
],
'location_id' => $locationId,
'reference_id' => $orderNumber,
'note' => 'Online pickup order ' . $orderNumber,
'autocomplete' => true,
], JSON_THROW_ON_ERROR);
$curl = curl_init($baseUrl . '/v2/payments');
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $accessToken,
'Content-Type: application/json',
'Square-Version: 2026-05-20',
],
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($curl);
$status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
$error = curl_error($curl);
curl_close($curl);
$decoded = is_string($response) ? json_decode($response, true) : null;
if ($response === false || $status < 200 || $status >= 300 || !isset($decoded['payment'])) {
$message = $decoded['errors'][0]['detail'] ?? $error ?: 'Payment was declined.';
throw new RuntimeException((string) $message);
}
return [
'success' => true,
'provider' => 'square',
'reference' => (string) $decoded['payment']['id'],
'status' => (string) $decoded['payment']['status'],
];
}
public function refund(string $paymentReference, int $amountCents, string $reason): array
{
if (!(bool) $this->config->get('square.enabled', false) || str_starts_with($paymentReference, 'DEMO-')) {
return ['success' => true, 'reference' => 'REF-DEMO-' . strtoupper(bin2hex(random_bytes(4)))];
}
if (!function_exists('curl_init')) {
throw new RuntimeException('The PHP cURL extension is required for Square refunds.');
}
$environment = (string) $this->config->get('square.environment', 'sandbox');
$baseUrl = $environment === 'production'
? 'https://connect.squareup.com'
: 'https://connect.squareupsandbox.com';
$payload = json_encode([
'idempotency_key' => bin2hex(random_bytes(16)),
'payment_id' => $paymentReference,
'amount_money' => ['amount' => $amountCents, 'currency' => 'USD'],
'reason' => substr($reason, 0, 190),
], JSON_THROW_ON_ERROR);
$curl = curl_init($baseUrl . '/v2/refunds');
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $this->config->get('square.access_token'),
'Content-Type: application/json',
'Square-Version: 2026-05-20',
],
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($curl);
$status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$decoded = is_string($response) ? json_decode($response, true) : null;
if ($status < 200 || $status >= 300 || !isset($decoded['refund'])) {
throw new RuntimeException((string) ($decoded['errors'][0]['detail'] ?? 'Refund failed.'));
}
return ['success' => true, 'reference' => (string) $decoded['refund']['id']];
}
}