client = $client; } public function isConfigured(): bool { return $this->settings->isStripeConfigured(); } /** * Create (or, when one already exists, return) a PaymentIntent for the billed * total of a payment. The payment id is stored in metadata so the webhook can * reconcile the charge back to our ledger row. Returns null on any Stripe error * so callers can fail gracefully. */ public function createIntent( Payment $payment ): ?PaymentIntent { if ( ! $this->isConfigured() ) { return null; } try { return $this->paymentIntentsCreate( [ 'amount' => $this->toMinorUnits( $payment->total() ), 'currency' => strtolower( $payment->currency ), 'metadata' => [ 'payment_id' => (string) $payment->id, 'registration_type' => $payment->registrationType, 'registration_id' => (string) $payment->registrationId, 'student_id' => (string) $payment->studentId, ], 'description' => sprintf( 'Lesson payment #%d', (int) $payment->id ), ], [ 'idempotency_key' => 'usc-payment-' . $payment->id ] ); } catch ( \Throwable $e ) { return null; } } /** * Seam around the Stripe PaymentIntents create call so tests can stub the * network request. * * @param array{amount: int, currency: string, metadata: array, description: string} $params * @param array{idempotency_key?: string} $options */ protected function paymentIntentsCreate( array $params, array $options ): PaymentIntent { return $this->client()->paymentIntents->create( $params, $options ); } /** * Verify a raw webhook payload against its signature header and return the * decoded Stripe event, or null when verification fails or no signing secret is * configured. */ public function verifyWebhook( string $payload, string $signatureHeader ): ?Event { $secret = $this->settings->webhookSecret(); if ( '' === $secret || '' === $signatureHeader ) { return null; } try { return $this->constructEvent( $payload, $signatureHeader, $secret ); } catch ( \Throwable $e ) { return null; } } /** * Seam around the static Stripe verifier so tests can stub signature checking. */ protected function constructEvent( string $payload, string $signatureHeader, string $secret ): Event { return Webhook::constructEvent( $payload, $signatureHeader, $secret ); } private function client(): StripeClient { if ( null === $this->client ) { $this->client = new StripeClient( $this->settings->secretKey() ); } return $this->client; } /** * Convert a dollar amount to the integer minor units (cents) Stripe expects. */ private function toMinorUnits( float $amount ): int { return (int) round( $amount * 100 ); } }