Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
332bf92e44 | ||
|
|
cceeb83d5e | ||
|
|
1a7e8704d0 |
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
|
## [0.27.2] - 2026-09-29
|
||||||
|
### Fixed
|
||||||
|
- `Modules\Core\Order\Services\TransactionRecorder::record()` — made idempotent
|
||||||
|
on `(order_id, type, reference)`. A successful Stripe payment can legitimately
|
||||||
|
report `PaymentCaptured` twice for the same PaymentIntent (checkout's
|
||||||
|
synchronous capture via `pay()`, then the webhook confirming the same
|
||||||
|
outcome asynchronously via `handleCallback()`), both routing through
|
||||||
|
`resultFromIntent()`. With no dedupe check, this wrote two identical
|
||||||
|
`Transaction` rows for one real payment. Now returns the existing row
|
||||||
|
instead of creating a duplicate.
|
||||||
|
|
||||||
|
## [0.27.1] - 2026-09-29
|
||||||
|
### Added
|
||||||
|
- Temp logger for Stripe webhook
|
||||||
|
|
||||||
## [0.27.0] - 2026-09-29
|
## [0.27.0] - 2026-09-29
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "boboko/core",
|
"name": "boboko/core",
|
||||||
"description": "Core module — authentication and shared panel behaviour",
|
"description": "Core module — authentication and shared panel behaviour",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"version": "0.27.0",
|
"version": "0.27.2",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Modules\\Core\\": "src/"
|
"Modules\\Core\\": "src/"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@boboko/core",
|
"name": "@boboko/core",
|
||||||
"version": "0.27.0",
|
"version": "0.27.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Portable Stimulus controllers and styles for boboko-core's cart + checkout module. Installed as a real npm dependency (file:../boboko-core in dev, a tagged git install in prod) so a consuming app's `npm install` resolves this package's own dependencies (leaflet, @hotwired/stimulus) transitively, the same way `composer update boboko/*` does for PHP. See CONTRIBUTE.md's \"JS/CSS: a real npm package\" section.",
|
"description": "Portable Stimulus controllers and styles for boboko-core's cart + checkout module. Installed as a real npm dependency (file:../boboko-core in dev, a tagged git install in prod) so a consuming app's `npm install` resolves this package's own dependencies (leaflet, @hotwired/stimulus) transitively, the same way `composer update boboko/*` does for PHP. See CONTRIBUTE.md's \"JS/CSS: a real npm package\" section.",
|
||||||
|
|||||||
@@ -39,8 +39,26 @@ class TransactionRecorder
|
|||||||
* elsewhere in this codebase (see the old, now-removed
|
* elsewhere in this codebase (see the old, now-removed
|
||||||
* TransactionRecorder this replaces).
|
* TransactionRecorder this replaces).
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Idempotent on (order_id, type, reference): a successful payment
|
||||||
|
* outcome can legitimately be reported twice for the same gateway
|
||||||
|
* reference — e.g. Stripe's pay()/handleCallback() both call
|
||||||
|
* resultFromIntent() and both dispatch PaymentCaptured once a
|
||||||
|
* PaymentIntent reaches "succeeded" (checkout's synchronous capture,
|
||||||
|
* then the webhook confirming the same outcome asynchronously) — so
|
||||||
|
* this returns the existing row instead of writing a duplicate.
|
||||||
|
*/
|
||||||
public function record(Order $order, string $type, string $driver, PaymentResult $result): Transaction
|
public function record(Order $order, string $type, string $driver, PaymentResult $result): Transaction
|
||||||
{
|
{
|
||||||
|
$existing = $order->transactions()
|
||||||
|
->where('type', $type)
|
||||||
|
->where('reference', $result->reference)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existing !== null) {
|
||||||
|
return $existing;
|
||||||
|
}
|
||||||
|
|
||||||
return $order->transactions()->create([
|
return $order->transactions()->create([
|
||||||
'success' => $result->status === PaymentResultStatus::Succeeded,
|
'success' => $result->status === PaymentResultStatus::Succeeded,
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
|
|||||||
@@ -33,6 +33,17 @@ class StripeWebhookMiddleware
|
|||||||
$secret
|
$secret
|
||||||
);
|
);
|
||||||
} catch (UnexpectedValueException|SignatureVerificationException $e) {
|
} catch (UnexpectedValueException|SignatureVerificationException $e) {
|
||||||
|
\Illuminate\Support\Facades\Log::error('Stripe webhook signature verification failed', [
|
||||||
|
'signature_header' => $stripeSig,
|
||||||
|
'secret_prefix' => substr((string) $secret, 0, 12),
|
||||||
|
'secret_length' => strlen((string) $secret),
|
||||||
|
'body_length' => strlen($request->getContent()),
|
||||||
|
'body_sha256' => hash('sha256', $request->getContent()),
|
||||||
|
'body_raw' => $request->getContent(),
|
||||||
|
'content_type' => $request->header('Content-Type'),
|
||||||
|
'content_encoding' => $request->header('Content-Encoding'),
|
||||||
|
]);
|
||||||
|
|
||||||
abort(400, $e->getMessage());
|
abort(400, $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user