16235369cb
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.
133 lines
5.1 KiB
PHP
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']];
|
|
}
|
|
}
|