126 lines
4.5 KiB
PHP
126 lines
4.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Payment;
|
|
|
|
use App\Models\Payment;
|
|
use Illuminate\Http\Client\ConnectionException;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* PayMongo driver scaffold. Wires the SDK calls but is gated on
|
|
* PAYMONGO_SECRET_KEY being set. When the key is missing we treat the
|
|
* driver as misconfigured and return failure — the caller can then
|
|
* fall back to ManualPaymentDriver.
|
|
*/
|
|
class PayMongoDriver implements PaymentDriver
|
|
{
|
|
public function __construct(
|
|
private readonly string $secretKey,
|
|
private readonly string $webhookSecret,
|
|
private readonly string $endpoint = 'https://api.paymongo.com/v1',
|
|
private readonly string $successUrl = 'https://verde.local/payment/success',
|
|
private readonly string $cancelUrl = 'https://verde.local/payment/cancel',
|
|
) {}
|
|
|
|
public function initiate(Payment $payment): InitiateResult
|
|
{
|
|
if (empty($this->secretKey)) {
|
|
return InitiateResult::failure('paymongo_not_configured');
|
|
}
|
|
|
|
try {
|
|
$response = Http::withBasicAuth($this->secretKey, '')
|
|
->timeout(15)
|
|
->post($this->endpoint.'/checkout_sessions', [
|
|
'data' => [
|
|
'attributes' => [
|
|
'line_items' => [[
|
|
'name' => $payment->purpose,
|
|
'amount' => $payment->amount_centavos,
|
|
'currency' => $payment->currency,
|
|
'quantity' => 1,
|
|
]],
|
|
'payment_method_types' => ['gcash', 'paymaya', 'card'],
|
|
'success_url' => $this->successUrl.'?p='.$payment->uuid,
|
|
'cancel_url' => $this->cancelUrl.'?p='.$payment->uuid,
|
|
'metadata' => ['payment_uuid' => $payment->uuid],
|
|
],
|
|
],
|
|
]);
|
|
} catch (ConnectionException $e) {
|
|
Log::warning('PayMongo connection failed', ['error' => $e->getMessage()]);
|
|
|
|
return InitiateResult::failure('connection_failed');
|
|
}
|
|
|
|
if (! $response->successful()) {
|
|
Log::warning('PayMongo error', ['status' => $response->status(), 'body' => $response->body()]);
|
|
|
|
return InitiateResult::failure('provider_error_'.$response->status());
|
|
}
|
|
|
|
$data = $response->json('data') ?? [];
|
|
$sessionId = $data['id'] ?? null;
|
|
$checkoutUrl = $data['attributes']['checkout_url'] ?? null;
|
|
|
|
$payment->forceFill([
|
|
'provider' => Payment::PROVIDER_PAYMONGO,
|
|
'provider_payment_id' => $sessionId,
|
|
'provider_data' => $data,
|
|
'status' => Payment::STATUS_PROCESSING,
|
|
])->save();
|
|
|
|
return InitiateResult::success($checkoutUrl, $sessionId);
|
|
}
|
|
|
|
public function verifyWebhook(string $rawBody, string $signature): bool
|
|
{
|
|
if (empty($this->webhookSecret)) {
|
|
return false;
|
|
}
|
|
|
|
// PayMongo webhook header format: t=timestamp,te=signature,li=...
|
|
$parts = [];
|
|
foreach (explode(',', $signature) as $kv) {
|
|
[$k, $v] = array_pad(explode('=', $kv, 2), 2, '');
|
|
$parts[$k] = $v;
|
|
}
|
|
|
|
$timestamp = $parts['t'] ?? '';
|
|
$signed = $parts['te'] ?? '';
|
|
$expected = hash_hmac('sha256', "{$timestamp}.{$rawBody}", $this->webhookSecret);
|
|
|
|
return hash_equals($expected, $signed);
|
|
}
|
|
|
|
public function applyWebhook(array $payload): ?Payment
|
|
{
|
|
$eventType = $payload['data']['attributes']['type'] ?? null;
|
|
$paymentUuid = $payload['data']['attributes']['data']['attributes']['metadata']['payment_uuid'] ?? null;
|
|
if (! $paymentUuid) {
|
|
return null;
|
|
}
|
|
|
|
$payment = Payment::where('uuid', $paymentUuid)->first();
|
|
if (! $payment) {
|
|
return null;
|
|
}
|
|
|
|
if (in_array($eventType, ['checkout_session.payment.paid', 'payment.paid'], true)) {
|
|
$payment->forceFill([
|
|
'status' => Payment::STATUS_PAID,
|
|
'paid_at' => now(),
|
|
'provider_data' => $payload,
|
|
])->save();
|
|
} elseif (in_array($eventType, ['payment.failed'], true)) {
|
|
$payment->forceFill([
|
|
'status' => Payment::STATUS_FAILED,
|
|
'provider_data' => $payload,
|
|
])->save();
|
|
}
|
|
|
|
return $payment->fresh();
|
|
}
|
|
}
|