| @@ -5,8 +5,10 @@ | ||
| 5 | 5 | * @package Timetics |
| 6 | 6 | */ |
| 7 | 7 | namespace Timetics\Core\Integrations\Stripe; |
| 8 | 8 | |
| 9 | +defined( 'ABSPATH' ) || exit; | |
| 10 | + | |
| 9 | 11 | /** |
| 10 | 12 | * Class StripePayment |
| 11 | 13 | */ |
| 12 | 14 | class StripePayment { |
| @@ -187,6 +189,56 @@ | ||
| 187 | 189 | return new \WP_Error( 'timetics_stripe_invalid_response', __( 'Unexpected Stripe response.', 'timetics' ) ); |
| 188 | 190 | } |
| 189 | 191 | |
| 190 | 192 | return $body; |
| 193 | + } | |
| 194 | + | |
| 195 | + /** | |
| 196 | + * Cancel a Stripe PaymentIntent. Used by the unpaid-booking cleanup sweep | |
| 197 | + * so a card can't be charged after we've already released the slot. | |
| 198 | + * Stripe refuses this if the intent already succeeded — that failure is | |
| 199 | + * the caller's signal to leave the booking alone instead of cancelling it. | |
| 200 | + * | |
| 201 | + * @param string $intent_id PaymentIntent id (pi_...). | |
| 202 | + * | |
| 203 | + * @return array|\WP_Error Decoded Stripe response, or WP_Error on failure. | |
| 204 | + */ | |
| 205 | + public function cancel_payment_intent( $intent_id ) { | |
| 206 | + $intent_id = is_string( $intent_id ) ? trim( $intent_id ) : ''; | |
| 207 | + | |
| 208 | + if ( '' === $intent_id || strpos( $intent_id, 'pi_' ) !== 0 ) { | |
| 209 | + return new \WP_Error( 'timetics_stripe_invalid_intent', __( 'Invalid Stripe payment intent id.', 'timetics' ) ); | |
| 210 | + } | |
| 211 | + | |
| 212 | + $secret = timetics_get_option( 'stripe_secret_key' ); | |
| 213 | + | |
| 214 | + if ( empty( $secret ) ) { | |
| 215 | + return new \WP_Error( 'timetics_stripe_missing_secret', __( 'Stripe secret key is not configured.', 'timetics' ) ); | |
| 216 | + } | |
| 217 | + | |
| 218 | + $response = wp_remote_post( | |
| 219 | + $this->payment_intent_url . '/' . rawurlencode( $intent_id ) . '/cancel', | |
| 220 | + [ | |
| 221 | + 'headers' => [ | |
| 222 | + 'Authorization' => 'Bearer ' . $secret, | |
| 223 | + ], | |
| 224 | + 'timeout' => 15, | |
| 225 | + ] | |
| 226 | + ); | |
| 227 | + | |
| 228 | + if ( is_wp_error( $response ) ) { | |
| 229 | + return $response; | |
| 230 | + } | |
| 231 | + | |
| 232 | + $code = (int) wp_remote_retrieve_response_code( $response ); | |
| 233 | + $body = json_decode( wp_remote_retrieve_body( $response ), true ); | |
| 234 | + | |
| 235 | + if ( $code < 200 || $code >= 300 ) { | |
| 236 | + $message = is_array( $body ) && ! empty( $body['error']['message'] ) | |
| 237 | + ? $body['error']['message'] | |
| 238 | + : __( 'Stripe payment intent cancellation failed.', 'timetics' ); | |
| 239 | + return new \WP_Error( 'timetics_stripe_cancel_failed', $message, [ 'status' => $code ] ); | |
| 240 | + } | |
| 241 | + | |
| 242 | + return is_array( $body ) ? $body : []; | |
| 191 | 243 | } |
| 192 | 244 | } |