Merge pull request #253 from copenhagentruckwash/fix-stripe-payment-intent-reuse-issue
Validate Stripe payment intent amount before reuse
This commit is contained in:
@@ -629,6 +629,7 @@ class ordersRoute
|
||||
|
||||
$stripe = new stripe();
|
||||
$stripePaymentIntents = new stripe_payment_intents_o();
|
||||
$expectedPaymentIntentAmount = $this->getStripePaymentIntentAmountForOrder($order, $tax_percentage ?? 0);
|
||||
|
||||
if ($stripePaymentIntents->doesOrderHavePaymentIntent((int)$order->id)) {
|
||||
$stripePaymentIntents->selectOrderPaymentIntent((int)$order->id);
|
||||
@@ -641,7 +642,10 @@ class ordersRoute
|
||||
$stripePaymentIntents->tax_percentage->set($tax_percentage);
|
||||
}
|
||||
|
||||
if ($this->isStripePaymentIntentReusable($storedPaymentIntent)) {
|
||||
if (
|
||||
$this->isStripePaymentIntentReusable($storedPaymentIntent)
|
||||
&& $this->doesStripePaymentIntentMatchOrder($storedPaymentIntent, $expectedPaymentIntentAmount, $tax_percentage ?? 0)
|
||||
) {
|
||||
(new logs_o())->add('orders', $order->department_id->value(), 1, $user->id, 'CHARGE_ORDER', 'Reused Stripe payment intent for order (ID: ' . $data['id'] . ')');
|
||||
$response->success($this->buildStripePaymentIntentResponse($storedPaymentIntent, $stripePaymentIntents, [
|
||||
'reused' => true,
|
||||
@@ -655,10 +659,7 @@ class ordersRoute
|
||||
}
|
||||
|
||||
$paymentIntent = $stripe->payment_intents->create(
|
||||
(int)round($this->addTaxNetAmount(
|
||||
(float)$order->getNetAmount() * 100,
|
||||
$tax_percentage ?? 0
|
||||
)),
|
||||
$expectedPaymentIntentAmount,
|
||||
[
|
||||
'description' => 'Order ID: ' . $order->id,
|
||||
'metadata' => [
|
||||
@@ -952,6 +953,35 @@ class ordersRoute
|
||||
return $net_amount + ($net_amount * ($tax_percentage / 100));
|
||||
}
|
||||
|
||||
private function getStripePaymentIntentAmountForOrder(orders_o $order, ?int $tax_percentage): int
|
||||
{
|
||||
return (int)round($this->addTaxNetAmount(
|
||||
(float)$order->getNetAmount() * 100,
|
||||
$tax_percentage ?? 0
|
||||
));
|
||||
}
|
||||
|
||||
private function doesStripePaymentIntentMatchOrder(object $paymentIntent, int $expectedAmount, ?int $tax_percentage): bool
|
||||
{
|
||||
if (!isset($paymentIntent->amount) || (int)$paymentIntent->amount !== $expectedAmount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$metadata = $paymentIntent->metadata ?? null;
|
||||
$storedTaxPercentage = null;
|
||||
if (is_array($metadata)) {
|
||||
$storedTaxPercentage = $metadata['tax_percentage'] ?? null;
|
||||
} elseif (is_object($metadata)) {
|
||||
$storedTaxPercentage = $metadata->tax_percentage ?? null;
|
||||
}
|
||||
|
||||
if ($storedTaxPercentage === null || !is_numeric($storedTaxPercentage)) {
|
||||
return ($tax_percentage ?? 0) === 0;
|
||||
}
|
||||
|
||||
return (int)$storedTaxPercentage === ($tax_percentage ?? 0);
|
||||
}
|
||||
|
||||
private function isStripePaymentIntentReusable(object $paymentIntent): bool
|
||||
{
|
||||
$status = strtolower((string)($paymentIntent->status ?? ''));
|
||||
|
||||
+8
@@ -14,7 +14,11 @@ it('wires mobile stripe payment intent routes to normalized lifecycle handling',
|
||||
$stripeSection = substr($content, (int)$start, (int)$end - (int)$start);
|
||||
|
||||
expect($stripeSection)->toContain('buildStripePaymentIntentResponse(');
|
||||
expect($stripeSection)->toContain('$expectedPaymentIntentAmount = $this->getStripePaymentIntentAmountForOrder($order, $tax_percentage ?? 0);');
|
||||
expect($stripeSection)->toContain('isStripePaymentIntentReusable($storedPaymentIntent)');
|
||||
expect($stripeSection)->toContain('doesStripePaymentIntentMatchOrder($storedPaymentIntent, $expectedPaymentIntentAmount, $tax_percentage ?? 0)');
|
||||
expect($stripeSection)->toContain('$stripe->payment_intents->create(
|
||||
$expectedPaymentIntentAmount,');
|
||||
expect($stripeSection)->toContain("'reused' => true");
|
||||
expect($stripeSection)->toContain("'message' => 'No active payment intent for this order.'");
|
||||
expect($stripeSection)->toContain("'message' => 'Payment intent cleared successfully.'");
|
||||
@@ -24,4 +28,8 @@ it('wires mobile stripe payment intent routes to normalized lifecycle handling',
|
||||
expect($stripeSection)->toContain("Payment intent is not ready to capture.");
|
||||
expect($stripeSection)->toContain("paidWithStripe(\$paymentIntent->id);");
|
||||
expect($stripeSection)->not->toContain("Order does not have a payment intent");
|
||||
|
||||
expect($content)->toContain('private function doesStripePaymentIntentMatchOrder(object $paymentIntent, int $expectedAmount, ?int $tax_percentage): bool');
|
||||
expect($content)->toContain('!isset($paymentIntent->amount) || (int)$paymentIntent->amount !== $expectedAmount');
|
||||
expect($content)->toContain("\$storedTaxPercentage = \$metadata['tax_percentage'] ?? null;");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user