- 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.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
<?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']];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user