137 lines
5.3 KiB
PHP
137 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace Modules\Core\Store\Services;
|
|
|
|
use Filament\Forms\Components\RichEditor\RichContentRenderer;
|
|
use Illuminate\Support\Facades\Cache;
|
|
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;
|
|
|
|
/**
|
|
* The only entrypoint that creates/updates the StoreDetails singleton — same
|
|
* shape as Modules\Core\Localization\Services\TranslationService: every
|
|
* write goes through here so it can dispatch StoreDetailsUpdated, which
|
|
* Modules\Core\Store\Listeners\FlushStoreDetailsCache reacts to. Never call
|
|
* StoreDetails::query()->update(...) or $storeDetails->save() directly — a
|
|
* write bypassing this service leaves current()'s forever-cache stale.
|
|
*/
|
|
class StoreDetailsService
|
|
{
|
|
public const CACHE_KEY = 'store-details';
|
|
|
|
/**
|
|
* Forever-cached — read on every storefront request that shows store
|
|
* details (e.g. the checkout confirmation page's bank transfer
|
|
* instructions), so this should never re-query the database on a normal
|
|
* request. Only ever invalidated by update() below, via
|
|
* FlushStoreDetailsCache reacting to StoreDetailsUpdated.
|
|
*/
|
|
public function current(): StoreDetails
|
|
{
|
|
return Cache::rememberForever(
|
|
self::CACHE_KEY,
|
|
fn () => $this->firstOrCreate(),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* $locale, so the caller's own @if($bankTransferInstructions) guard
|
|
* covers both cases identically.
|
|
*
|
|
* bank_transfer_instructions is a TranslatedRichEditor field (see
|
|
* ManageStoreDetails), so translate() returns Filament's Tiptap JSON
|
|
* document structure for that locale, not a plain string —
|
|
* RichContentRenderer::make() is Filament's own converter from that
|
|
* structure to sanitized HTML (the same one the admin panel itself
|
|
* uses to render a RichEditor's content read-only).
|
|
*/
|
|
public function bankTransferInstructionsFor(Order $order, string $locale): ?string
|
|
{
|
|
if (! app(OrderStatusFlow::class)->isBankTransfer($order)) {
|
|
return null;
|
|
}
|
|
|
|
$content = $this->current()->translate('bank_transfer_instructions', $locale);
|
|
|
|
if (blank($content)) {
|
|
return null;
|
|
}
|
|
|
|
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
|
|
{
|
|
$storeDetails = $this->firstOrCreate();
|
|
|
|
$storeDetails->update($attributes);
|
|
|
|
Event::dispatch(new StoreDetailsUpdated($storeDetails));
|
|
|
|
return $storeDetails;
|
|
}
|
|
|
|
/**
|
|
* A freshly-created row must never leave a translatable column
|
|
* genuinely NULL — Lunar's own TranslatedText component (Modules\Core\
|
|
* Store\Filament\Pages\ManageStoreDetails's `name`/`address`/
|
|
* `bank_transfer_instructions` fields) silently drops every keystroke
|
|
* on re-render when the field it's editing starts out NULL rather than
|
|
* an empty per-locale array. Real-world precedent (PaymentMethod's own
|
|
* translatable `name` column) never hits this, because every
|
|
* PaymentMethod row is created THROUGH the same Filament form that
|
|
* immediately fills `name` — this singleton is instead created blank
|
|
* and opened for editing in the same visit, which is exactly the gap
|
|
* that surfaces the bug. Caught and fixed after the fact, verified via
|
|
* tinker: seeding a real (non-null) array made typing into the field
|
|
* persist correctly, confirming NULL was the trigger.
|
|
*/
|
|
private function firstOrCreate(): StoreDetails
|
|
{
|
|
return StoreDetails::query()->firstOrCreate([], [
|
|
'name' => $this->emptyPerLocale(),
|
|
'address' => $this->emptyPerLocale(),
|
|
'bank_transfer_instructions' => $this->emptyPerLocale(),
|
|
'legal_name' => $this->emptyPerLocale(),
|
|
]);
|
|
}
|
|
|
|
private function emptyPerLocale(): array
|
|
{
|
|
return Language::query()->pluck('code')->mapWithKeys(fn (string $code) => [$code => ''])->all();
|
|
}
|
|
}
|