Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47851d36ec | ||
|
|
c43682ef0c | ||
|
|
332bf92e44 | ||
|
|
cceeb83d5e | ||
|
|
1a7e8704d0 |
@@ -4,6 +4,36 @@ 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/).
|
||||
|
||||
## [0.27.3] - 2026-09-29
|
||||
### Added
|
||||
- The checkout confirmation page now ends with the store's bank transfer
|
||||
instructions (Store Details → Bank transfer) for a bank transfer order,
|
||||
via `StoreDetailsService::bankTransferInstructionsFor()`. New translation
|
||||
line `checkout.page.confirmation_bank_transfer_heading` — re-run
|
||||
`CheckoutTranslationsSeeder` in consuming apps to add it.
|
||||
|
||||
### Fixed
|
||||
- `StoreDetailsService::bankTransferInstructionsFor()` now fills a
|
||||
`{order_reference}` (or `{{ order_reference }}`) typed into the bank
|
||||
transfer instructions with the order's display reference
|
||||
(`OrderReferenceDisplay`). It previously rendered literally in the order
|
||||
confirmation email.
|
||||
|
||||
## [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
|
||||
|
||||
### Added
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "boboko/core",
|
||||
"description": "Core module — authentication and shared panel behaviour",
|
||||
"type": "library",
|
||||
"version": "0.27.0",
|
||||
"version": "0.27.3",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\Core\\": "src/"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@boboko/core",
|
||||
"version": "0.27.0",
|
||||
"version": "0.27.3",
|
||||
"private": true,
|
||||
"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.",
|
||||
|
||||
@@ -975,12 +975,41 @@ textarea.bbk-field-input { resize: vertical; }
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.bbk-confirmation-address-heading {
|
||||
.bbk-confirmation-address-heading,
|
||||
.bbk-confirmation-bank-transfer-heading {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bbk-confirmation-bank-transfer {
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--bbk-color-border);
|
||||
}
|
||||
|
||||
/* Store-authored rich text (ManageStoreDetails' RichEditor) — may be
|
||||
paragraphs, bold text or a bank/IBAN/BIC table. */
|
||||
.bbk-confirmation-bank-transfer-body {
|
||||
font-size: 0.875rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.bbk-confirmation-bank-transfer-body > :first-child { margin-top: 0; }
|
||||
.bbk-confirmation-bank-transfer-body > :last-child { margin-bottom: 0; }
|
||||
|
||||
.bbk-confirmation-bank-transfer-body table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.bbk-confirmation-bank-transfer-body th,
|
||||
.bbk-confirmation-bank-transfer-body td {
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: 1px solid var(--bbk-color-border);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.bbk-address-lines {
|
||||
font-style: normal;
|
||||
display: flex;
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
Order confirmation. Reached only via a session flash of the placed order id
|
||||
(CheckoutController::confirmation) — not deep-linkable. $order is a
|
||||
Lunar\Models\Order with lines + shipping/billing addresses eager-loaded.
|
||||
$bankTransferInstructions is already-sanitized HTML from
|
||||
StoreDetailsService::bankTransferInstructionsFor(), or null unless this
|
||||
is a bank transfer order with instructions filled in for this locale.
|
||||
--}}
|
||||
@extends('layouts.app')
|
||||
|
||||
@@ -128,5 +131,12 @@
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($bankTransferInstructions)
|
||||
<section class="bbk-confirmation-bank-transfer">
|
||||
<h2 class="bbk-confirmation-bank-transfer-heading">{{ __('checkout.page.confirmation_bank_transfer_heading') }}</h2>
|
||||
<div class="bbk-confirmation-bank-transfer-body">{!! $bankTransferInstructions !!}</div>
|
||||
</section>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@@ -190,6 +190,10 @@ class CheckoutTranslationsSeeder extends Seeder
|
||||
'Θέλεις να παρακολουθείς την παραγγελία σου; Δημιούργησε λογαριασμό ή',
|
||||
],
|
||||
'page.confirmation_billing' => ['Billing', 'Χρέωση'],
|
||||
'page.confirmation_bank_transfer_heading' => [
|
||||
'Bank transfer details',
|
||||
'Στοιχεία τραπεζικής μεταφοράς',
|
||||
],
|
||||
'page.confirmation_continue' => ['Continue shopping', 'Συνέχεια αγορών'],
|
||||
'page.box_now_locker_label' => [
|
||||
'Choose a Box Now locker',
|
||||
|
||||
@@ -28,6 +28,7 @@ use Modules\Core\Customer\Services\CustomerAccountService;
|
||||
use Modules\Core\Payment\Enums\PaymentResultStatus;
|
||||
use Modules\Core\Payment\Models\PaymentMethod;
|
||||
use Modules\Core\Shipping\Carriers\BoxNow\BoxNowClient;
|
||||
use Modules\Core\Store\Services\StoreDetailsService;
|
||||
|
||||
/**
|
||||
* The checkout page — one page, sections (contact / billing / shipping /
|
||||
@@ -554,6 +555,8 @@ class CheckoutController extends Controller
|
||||
return view('checkout::confirmation', [
|
||||
'order' => $order,
|
||||
'paymentMethodName' => $paymentMethodName,
|
||||
'bankTransferInstructions' => app(StoreDetailsService::class)
|
||||
->bankTransferInstructionsFor($order, $locale),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,26 @@ class TransactionRecorder
|
||||
* elsewhere in this codebase (see the old, now-removed
|
||||
* 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
|
||||
{
|
||||
$existing = $order->transactions()
|
||||
->where('type', $type)
|
||||
->where('reference', $result->reference)
|
||||
->first();
|
||||
|
||||
if ($existing !== null) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
return $order->transactions()->create([
|
||||
'success' => $result->status === PaymentResultStatus::Succeeded,
|
||||
'type' => $type,
|
||||
|
||||
@@ -33,6 +33,17 @@ class StripeWebhookMiddleware
|
||||
$secret
|
||||
);
|
||||
} 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());
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Event;
|
||||
use Lunar\Models\Language;
|
||||
use Lunar\Models\Order;
|
||||
use Modules\Core\Order\Services\OrderStatusFlow;
|
||||
use Modules\Core\Order\Support\OrderReferenceDisplay;
|
||||
use Modules\Core\Store\Events\StoreDetailsUpdated;
|
||||
use Modules\Core\Store\Models\StoreDetails;
|
||||
|
||||
@@ -39,8 +40,8 @@ class StoreDetailsService
|
||||
}
|
||||
|
||||
/**
|
||||
* Null for any non-bank-transfer order — the confirmation email only
|
||||
* shows this block when there's actually a wire to send (see
|
||||
* Null for any non-bank-transfer order — the confirmation email and page
|
||||
* only show this block when there's actually a wire to send (see
|
||||
* BankTransferPaymentDriver's own docblock for why a bank transfer
|
||||
* order stays at 'awaiting_payment' until staff confirm the wire
|
||||
* arrived). Null also when the store hasn't filled the field in for
|
||||
@@ -66,7 +67,30 @@ class StoreDetailsService
|
||||
return null;
|
||||
}
|
||||
|
||||
return RichContentRenderer::make($content)->toHtml();
|
||||
return $this->fillOrderReference(
|
||||
RichContentRenderer::make($content)->toHtml(),
|
||||
$order,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces a `{order_reference}` (or `{{ order_reference }}`) the shop
|
||||
* owner typed into the instructions with the order's display reference
|
||||
* (OrderReferenceDisplay — same form as the email subject).
|
||||
*
|
||||
* A plain text replace rather than RichContentRenderer::mergeTags():
|
||||
* that only fills genuine Tiptap mergeTag nodes, which this editor never
|
||||
* creates — Lunar's TranslatedText can't pass mergeTags() through to its
|
||||
* per-locale RichEditors, so the placeholder is always stored as
|
||||
* ordinary typed text.
|
||||
*/
|
||||
private function fillOrderReference(string $html, Order $order): string
|
||||
{
|
||||
return preg_replace(
|
||||
'/\{\{?\s*order_reference\s*\}\}?/',
|
||||
e(OrderReferenceDisplay::resolve($order)),
|
||||
$html,
|
||||
);
|
||||
}
|
||||
|
||||
public function update(array $attributes): StoreDetails
|
||||
|
||||
Reference in New Issue
Block a user