From 97004234f035e6c6b2efc1533886816f51ac3ee7 Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Tue, 15 Sep 2026 23:51:10 +0300 Subject: [PATCH] Feat: Adding Translations to Payment and Shipping Methods, removing unecessary shipping method fulfillment type --- ...make_payment_methods_name_translatable.php | 65 ++++++++ ...ake_shipping_methods_name_translatable.php | 74 +++++++++ src/Command/InstallLunarCommand.php | 15 +- .../OrderPaymentMethodSummaryExtension.php | 4 +- .../Resources/PaymentMethodResource.php | 8 +- src/Payment/Models/PaymentMethod.php | 15 +- src/Providers/ShippingServiceProvider.php | 12 +- src/Shipping/Carriers/Acs/AcsRateDriver.php | 11 +- .../Carriers/BoxNow/BoxNowRateDriver.php | 8 +- .../Concerns/ResolvesFixedPricing.php | 5 +- .../Contracts/DeclaresFulfillmentType.php | 25 +++ .../ShippingMethodResourceExtension.php | 146 +++++++++++++++--- src/Shipping/Support/FulfillmentType.php | 60 +++++++ src/Shipping/Support/ShippingMethodName.php | 35 +++++ 14 files changed, 447 insertions(+), 36 deletions(-) create mode 100644 database/migrations/2026_09_15_000001_make_payment_methods_name_translatable.php create mode 100644 database/migrations/2026_09_15_000002_make_shipping_methods_name_translatable.php create mode 100644 src/Shipping/Contracts/DeclaresFulfillmentType.php create mode 100644 src/Shipping/Support/FulfillmentType.php create mode 100644 src/Shipping/Support/ShippingMethodName.php diff --git a/database/migrations/2026_09_15_000001_make_payment_methods_name_translatable.php b/database/migrations/2026_09_15_000001_make_payment_methods_name_translatable.php new file mode 100644 index 0000000..fc97701 --- /dev/null +++ b/database/migrations/2026_09_15_000001_make_payment_methods_name_translatable.php @@ -0,0 +1,65 @@ +value('code') ?? 'en'; + + $existing = DB::table('payment_methods')->pluck('name', 'id'); + + DB::statement('ALTER TABLE payment_methods ALTER COLUMN name DROP DEFAULT'); + DB::statement("ALTER TABLE payment_methods ALTER COLUMN name TYPE json USING NULL"); + + foreach ($existing as $id => $name) { + if ($name === null) { + continue; + } + + DB::table('payment_methods') + ->where('id', $id) + ->update(['name' => json_encode([$defaultLocale => $name])]); + } + } + + public function down(): void + { + $defaultLocale = Language::where('default', true)->value('code') ?? 'en'; + + $existing = DB::table('payment_methods')->pluck('name', 'id'); + + DB::statement('ALTER TABLE payment_methods ALTER COLUMN name TYPE varchar(255) USING NULL'); + + foreach ($existing as $id => $name) { + $decoded = json_decode((string) $name, true); + $flat = is_array($decoded) ? ($decoded[$defaultLocale] ?? reset($decoded) ?: null) : $name; + + DB::table('payment_methods')->where('id', $id)->update(['name' => $flat]); + } + } +}; diff --git a/database/migrations/2026_09_15_000002_make_shipping_methods_name_translatable.php b/database/migrations/2026_09_15_000002_make_shipping_methods_name_translatable.php new file mode 100644 index 0000000..2a64fdd --- /dev/null +++ b/database/migrations/2026_09_15_000002_make_shipping_methods_name_translatable.php @@ -0,0 +1,74 @@ +prefix.'shipping_methods'; + $defaultLocale = Language::where('default', true)->value('code') ?? 'en'; + + // The column is NOT NULL (vendor migration never marked it + // nullable) — converting via `USING NULL` first, then + // backfilling with a second UPDATE, violates that constraint + // before the backfill ever runs. json_build_object() converts + // each existing string in place, in the same statement, so the + // column is never transiently NULL. $defaultLocale is inlined + // (not bound) — parameter binding inside an ALTER TABLE ... USING + // expression isn't reliable across drivers; it's a Language::code + // value we control, not user input, so quote_literal-safe + // interpolation here is fine. + $quotedLocale = DB::getPdo()->quote($defaultLocale); + + DB::statement("ALTER TABLE {$table} ALTER COLUMN name TYPE json USING json_build_object({$quotedLocale}, name)"); + } + + public function down(): void + { + $table = $this->prefix.'shipping_methods'; + $defaultLocale = Language::where('default', true)->value('code') ?? 'en'; + + // Same NOT NULL constraint applies going back — ->>'{locale}' + // extracts the default locale's text value directly in the + // USING clause, falling back to the first key present via + // COALESCE for any row missing that locale (e.g. one only ever + // filled in via a non-default language). + $quotedLocale = DB::getPdo()->quote($defaultLocale); + + DB::statement( + "ALTER TABLE {$table} ALTER COLUMN name TYPE varchar(255) ". + "USING COALESCE(name->>{$quotedLocale}, (SELECT value FROM json_each_text(name) LIMIT 1))" + ); + } +}; diff --git a/src/Command/InstallLunarCommand.php b/src/Command/InstallLunarCommand.php index 1c0c040..c20f636 100644 --- a/src/Command/InstallLunarCommand.php +++ b/src/Command/InstallLunarCommand.php @@ -66,6 +66,16 @@ class InstallLunarCommand extends Command ]); } + if (! Language::where('code', 'el')->exists()) { + $this->components->info('Adding Greek language'); + + Language::create([ + 'code' => 'el', + 'name' => 'Greek', + 'default' => false, + ]); + } + if (! Currency::whereDefault(true)->exists()) { $this->components->info('Adding a default currency (USD)'); @@ -310,7 +320,10 @@ class InstallLunarCommand extends Command PaymentMethod::create([ 'type' => 'cash-on-delivery', - 'name' => 'Cash on Delivery', + 'name' => [ + 'en' => 'Cash on Delivery', + 'el' => 'Αντικαταβολή', + ], 'driver' => 'cash-on-delivery', 'capture_mode' => 'pay', 'position' => 0, diff --git a/src/Order/Filament/Extensions/OrderPaymentMethodSummaryExtension.php b/src/Order/Filament/Extensions/OrderPaymentMethodSummaryExtension.php index 426697f..29bb4f2 100644 --- a/src/Order/Filament/Extensions/OrderPaymentMethodSummaryExtension.php +++ b/src/Order/Filament/Extensions/OrderPaymentMethodSummaryExtension.php @@ -42,6 +42,8 @@ class OrderPaymentMethodSummaryExtension extends ViewPageExtension return null; } - return PaymentMethod::where('type', $type)->value('name') ?? $type; + $method = PaymentMethod::where('type', $type)->first(); + + return $method?->translate('name') ?? $type; } } diff --git a/src/Payment/Filament/Resources/PaymentMethodResource.php b/src/Payment/Filament/Resources/PaymentMethodResource.php index 363d24f..5a441d6 100644 --- a/src/Payment/Filament/Resources/PaymentMethodResource.php +++ b/src/Payment/Filament/Resources/PaymentMethodResource.php @@ -12,6 +12,7 @@ use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\ToggleColumn; use Filament\Tables\Table; use Illuminate\Support\Facades\Event; +use Lunar\Admin\Support\Forms\Components\TranslatedText; use Modules\Core\Payment\Contracts\Configurable; use Modules\Core\Payment\Events\PaymentMethodsReordered; use Modules\Core\Payment\Filament\Resources\PaymentMethodResource\Pages\ListPaymentMethods; @@ -76,7 +77,7 @@ class PaymentMethodResource extends Resource ->sortable(), TextColumn::make('name') ->label('Name') - ->searchable(), + ->state(fn (PaymentMethod $record) => $record->translate('name')), TextColumn::make('type') ->label('Type'), TextColumn::make('driver') @@ -124,10 +125,9 @@ class PaymentMethodResource extends Resource public static function getFormComponents(): array { return [ - TextInput::make('name') + TranslatedText::make('name') ->label('Name') - ->required() - ->maxLength(255), + ->required(), TextInput::make('type') ->label('Type') ->helperText('Machine-facing slug — stored on the cart/order, used by other code to identify this method. Cannot be changed once orders reference it.') diff --git a/src/Payment/Models/PaymentMethod.php b/src/Payment/Models/PaymentMethod.php index 39a6016..6ff63ea 100644 --- a/src/Payment/Models/PaymentMethod.php +++ b/src/Payment/Models/PaymentMethod.php @@ -4,6 +4,7 @@ namespace Modules\Core\Payment\Models; use Illuminate\Database\Eloquent\Casts\AsArrayObject; use Illuminate\Database\Eloquent\Model; +use Lunar\Base\Traits\HasTranslations; /** * A merchant-configured payment method — the DB-instance layer, admin @@ -11,7 +12,16 @@ use Illuminate\Database\Eloquent\Model; * shipping_methods table already has (see docs/payments.md): * - type: unique, machine-facing slug (Cart::meta['payment_method'], * ApplyPaymentMethodFee's lookup key, every Payment event's $type). - * - name: admin-facing label. + * - name: admin-facing label, locale-keyed JSON (e.g. + * {"en": "Cash On Delivery", "el": "Αντικαταβολή"}) — same shape/ + * resolution as Product/Collection names (Lunar\Base\Traits\ + * HasTranslations), just applied directly to this column rather than + * through attribute_data, since this is a merchant settings row, not + * a catalog attribute. Rendered in Filament via Lunar's own + * Lunar\Admin\Support\Forms\Components\TranslatedText — one input per + * configured Language row, no bespoke translation UI. Resolve a + * display string with $method->translate('name') (locale defaults to + * app()->getLocale(), falling back to the store's default language). * - driver: the Modules\Core\Payment\Services\PaymentDriverRegistry key * — NOT the same as `type`, and not unique (two rows can share one * driver, e.g. two differently-named offline-style methods). @@ -28,12 +38,15 @@ use Illuminate\Database\Eloquent\Model; */ class PaymentMethod extends Model { + use HasTranslations; + protected $guarded = []; protected $casts = [ 'enabled' => 'boolean', 'position' => 'integer', 'driver_missing_at' => 'datetime', + 'name' => 'array', 'data' => AsArrayObject::class, ]; } diff --git a/src/Providers/ShippingServiceProvider.php b/src/Providers/ShippingServiceProvider.php index 7e23709..55e8121 100644 --- a/src/Providers/ShippingServiceProvider.php +++ b/src/Providers/ShippingServiceProvider.php @@ -27,6 +27,7 @@ use Modules\Core\Shipping\Contracts\CarrierFulfillmentInterface; use Modules\Core\Shipping\Filament\Pages\ManageShippingRates; use Modules\Core\Shipping\Jobs\PollShipmentTrackingJob; use Modules\Core\Shipping\Listeners\InvalidateShippingOptions; +use Modules\Core\Shipping\Support\FulfillmentType; use Modules\Core\Shipping\Models\Shipment; class ShippingServiceProvider extends ServiceProvider @@ -80,8 +81,10 @@ class ShippingServiceProvider extends ServiceProvider // resolveCarrier() for the same lookup pattern already used to // resolve a carrier driver from it). // - // Reads ShippingMethod.data['fulfillment_type'] directly rather - // than through a ShippingMethod::macro('isStorePickup', ...) — + // Resolves via Modules\Core\Shipping\Support\FulfillmentType (driver- + // declared for acs/box-now, merchant-configured data['fulfillment_type'] + // fallback for table-rate-shipping's generic drivers) rather than + // through a ShippingMethod::macro('isStorePickup', ...) — // Lunar\Base\Traits\HasModelExtending::__callStatic() (used by // Lunar\Shipping\Models\ShippingMethod via Lunar\Base\BaseModel) // intercepts EVERY unmatched static call, including macro() @@ -91,8 +94,7 @@ class ShippingServiceProvider extends ServiceProvider // false. (Lunar\Models\Order is unaffected because it declares // its own macro() method directly, bypassing __callStatic // entirely — that's why Order::macro('isStorePickupOrder', ...) - // below still works.) Defaults to 'carrier' (false) for any row - // saved before this field existed. + // below still works.) Order::macro('isStorePickupOrder', function () { /** @var Order $this */ $code = $this->shippingAddress?->shipping_option; @@ -108,7 +110,7 @@ class ShippingServiceProvider extends ServiceProvider // attribute avoids that entirely. $method = ShippingMethod::where('code', $code)->first(); - return ($method?->data['fulfillment_type'] ?? 'carrier') === 'store_pickup'; + return $method && FulfillmentType::isStorePickup($method); }); foreach ([CartLineAdded::class, CartLineUpdated::class, CartLineRemoved::class, CartCleared::class, ShippingAddressSet::class] as $event) { diff --git a/src/Shipping/Carriers/Acs/AcsRateDriver.php b/src/Shipping/Carriers/Acs/AcsRateDriver.php index 74ce760..5a780a6 100644 --- a/src/Shipping/Carriers/Acs/AcsRateDriver.php +++ b/src/Shipping/Carriers/Acs/AcsRateDriver.php @@ -10,10 +10,12 @@ use Lunar\Shipping\Models\ShippingRate; use Modules\Core\Shipping\Carriers\Acs\Exceptions\AcsApiException; use Modules\Core\Shipping\Concerns\CachesLivePricing; use Modules\Core\Shipping\Concerns\ResolvesFixedPricing; +use Modules\Core\Shipping\Contracts\DeclaresFulfillmentType; use Modules\Core\Shipping\Contracts\SupportsLivePricing; +use Modules\Core\Shipping\Support\ShippingMethodName; use Modules\Core\Shipping\Support\WeightCalculator; -class AcsRateDriver implements ShippingRateInterface, SupportsLivePricing +class AcsRateDriver implements ShippingRateInterface, SupportsLivePricing, DeclaresFulfillmentType { use ResolvesFixedPricing; use CachesLivePricing; @@ -30,6 +32,11 @@ class AcsRateDriver implements ShippingRateInterface, SupportsLivePricing return 'ACS Courier'; } + public function fulfillmentType(): string + { + return 'carrier'; + } + public function description(): string { return 'Live rate quote from ACS Courier.'; @@ -84,7 +91,7 @@ class AcsRateDriver implements ShippingRateInterface, SupportsLivePricing $amount = (int) round(($response->valueOutput['Total_Ammount'] ?? 0) * 100); return new ShippingOption( - name: $shippingMethod->name ?: $this->name(), + name: ShippingMethodName::resolve($shippingMethod) ?: $this->name(), description: $shippingMethod->description ?: $this->description(), identifier: $shippingRate->getIdentifier(), price: new Price($amount, $cart->currency, 1), diff --git a/src/Shipping/Carriers/BoxNow/BoxNowRateDriver.php b/src/Shipping/Carriers/BoxNow/BoxNowRateDriver.php index 46fe211..cd9c392 100644 --- a/src/Shipping/Carriers/BoxNow/BoxNowRateDriver.php +++ b/src/Shipping/Carriers/BoxNow/BoxNowRateDriver.php @@ -7,6 +7,7 @@ use Lunar\Shipping\DataTransferObjects\ShippingOptionRequest; use Lunar\Shipping\Interfaces\ShippingRateInterface; use Lunar\Shipping\Models\ShippingRate; use Modules\Core\Shipping\Concerns\ResolvesFixedPricing; +use Modules\Core\Shipping\Contracts\DeclaresFulfillmentType; /** * Box Now has no pricing API, so this always resolves the method's normal @@ -14,7 +15,7 @@ use Modules\Core\Shipping\Concerns\ResolvesFixedPricing; * flat-rate/ship-by drivers use. Does not implement SupportsLivePricing: * there is no live option to offer. */ -class BoxNowRateDriver implements ShippingRateInterface +class BoxNowRateDriver implements ShippingRateInterface, DeclaresFulfillmentType { use ResolvesFixedPricing; @@ -25,6 +26,11 @@ class BoxNowRateDriver implements ShippingRateInterface return 'Box Now Locker Delivery'; } + public function fulfillmentType(): string + { + return 'carrier'; + } + public function description(): string { return 'Deliver to a Box Now parcel locker.'; diff --git a/src/Shipping/Concerns/ResolvesFixedPricing.php b/src/Shipping/Concerns/ResolvesFixedPricing.php index d0a31bd..ea2ce52 100644 --- a/src/Shipping/Concerns/ResolvesFixedPricing.php +++ b/src/Shipping/Concerns/ResolvesFixedPricing.php @@ -6,6 +6,8 @@ use Lunar\DataTypes\ShippingOption; use Lunar\Facades\Pricing; use Lunar\Shipping\Models\ShippingMethod; use Lunar\Shipping\Models\ShippingRate; +use Modules\Core\Shipping\Support\FulfillmentType; +use Modules\Core\Shipping\Support\ShippingMethodName; /** * Shared by any carrier driver that also supports Lunar's own price-break @@ -31,12 +33,13 @@ trait ResolvesFixedPricing } return new ShippingOption( - name: $shippingMethod->name ?: $this->name(), + name: ShippingMethodName::resolve($shippingMethod) ?: $this->name(), description: $shippingMethod->description ?: $this->description(), identifier: $shippingRate->getIdentifier(), price: $pricing->matched->price, taxClass: $shippingRate->getTaxClass(), taxReference: $shippingRate->getTaxReference(), + collect: FulfillmentType::isStorePickup($shippingMethod), ); } } diff --git a/src/Shipping/Contracts/DeclaresFulfillmentType.php b/src/Shipping/Contracts/DeclaresFulfillmentType.php new file mode 100644 index 0000000..7eced62 --- /dev/null +++ b/src/Shipping/Contracts/DeclaresFulfillmentType.php @@ -0,0 +1,25 @@ +components([ - ...$this->replaceChargeByField( - $this->replaceDriverField($schema->getComponents()) - ), - $this->fulfillmentTypeSelect(), - ]); + return $schema->components( + $this->replaceFulfillmentTypeField( + $this->replaceChargeByField( + $this->replaceNameField( + $this->replaceDriverField($schema->getComponents()) + ) + ) + ) + ); } /** - * ShippingMethod.data['fulfillment_type'] — 'carrier' (default) or - * 'store_pickup'. Same free-form-`data`-column pattern as charge_by - * above, not a migrated column: ShippingMethod is a vendor - * (lunarphp/table-rate-shipping) table, and this codebase avoids - * forking vendor migrations for a merchant-configurable extra (see - * PaymentMethod.data.fee for the same convention on a different - * vendor-adjacent model). - * - * What this actually gates: Modules\Core\Shipping\Extensions\ - * OrderViewExtension's "Create Shipment" action only makes sense for - * a 'carrier' method (it books a real carrier voucher) — a - * 'store_pickup' order instead moves through Order.status - * 'ready-for-pickup' -> a staff "Mark Picked Up" action, no shipment - * ever created. See docs/checkout.md for the full status-flow design. + * Replaces the vendor's plain-string `name` TextInput with + * Lunar's own TranslatedText — `name` is now a locale-keyed JSON + * column (see database/migrations/..._make_shipping_methods_name_translatable.php), + * same shape/resolution as PaymentMethod.name and Product/Collection + * names (Lunar\Base\Traits\HasTranslations). */ + private function replaceNameField(array $components): array + { + return array_map(function (Component $component) { + if (method_exists($component, 'getName') && $component->getName() === 'name') { + return $this->translatedNameField(); + } + + if (in_array(HasChildComponents::class, class_uses_recursive($component), true)) { + $component->schema($this->replaceNameField($component->getChildComponents())); + } + + return $component; + }, $components); + } + + /** + * ShippingMethod.name is a locale-keyed JSON column (see database/ + * migrations/..._make_shipping_methods_name_translatable.php), but + * ShippingMethod is a vendor Eloquent model with no cast declared for + * it — Lunar\Shipping\Models\ShippingMethod only casts `data`, and + * there's no ModelManifest contract wired up to swap in a first-party + * subclass that adds one (Contracts\ShippingMethod exists but is + * never bound — see this class's own git history/nameColumn() for + * the same gap on the read side). TranslatedText itself round-trips + * plain array state, so afterStateHydrated()/dehydrateStateUsing() + * decode/encode the raw JSON string at the field boundary instead — + * the model attribute is a string on the way in and out, only ever + * an array while Filament's schema state holds it. + */ + private function translatedNameField(): TranslatedText + { + $field = TranslatedText::make('name') + ->label('Name') + ->required() + ->afterStateHydrated(function (TranslatedText $component, $state) { + $decoded = json_decode((string) $state, true); + $component->state(is_array($decoded) ? $decoded : []); + }) + ->dehydrateStateUsing(fn ($state) => json_encode(is_array($state) ? $state : [])); + + $field->expanded = true; + + return $field; + } + + /** + * Inserts the `fulfillment_type` Select right after `charge_by`, in + * the SAME Group (vendor's own `Group::make([getChargeByFormComponent()]) + * ->columns(2)`) — formerly appended at the very end of the whole + * form, disconnected from `driver`/`charge_by`, the decisions it + * actually relates to. Only rendered at all for a driver that DOESN'T + * already declare its own fulfillment type (see Modules\Core\Shipping\ + * Contracts\DeclaresFulfillmentType, Modules\Core\Shipping\Support\ + * FulfillmentType) — acs/box-now are unambiguously carrier-only, so + * asking a merchant to also pick "Carrier delivery" for every ACS/Box + * Now method was redundant, error-prone config with no real decision + * behind it. Still offered for table-rate-shipping's generic drivers + * (flat-rate, ship-by, free-shipping), which are genuinely ambiguous. + */ + private function replaceFulfillmentTypeField(array $components): array + { + $result = []; + + foreach ($components as $component) { + $result[] = $component; + + if (method_exists($component, 'getName') && $component->getName() === 'charge_by') { + $result[] = $this->fulfillmentTypeSelect(); + } elseif (in_array(HasChildComponents::class, class_uses_recursive($component), true)) { + $component->schema($this->replaceFulfillmentTypeField($component->getChildComponents())); + } + } + + return $result; + } + private function fulfillmentTypeSelect(): Select { return Select::make('data.fulfillment_type') @@ -52,9 +125,23 @@ class ShippingMethodResourceExtension extends ResourceExtension ]) ->default('carrier') ->required() + ->visible(fn (Get $get) => $this->driverIsFulfillmentAmbiguous($get('../driver'))) ->helperText('Whether an order using this method is handed to a carrier, or collected by the customer in person.'); } + private function driverIsFulfillmentAmbiguous(?string $driver): bool + { + if (! $driver) { + return true; + } + + try { + return ! Shipping::driver($driver) instanceof DeclaresFulfillmentType; + } catch (InvalidArgumentException) { + return true; + } + } + /** * Extend the vendor's cart_total/weight charge_by Select with a third * "live" option — only offered when the currently selected driver @@ -127,6 +214,10 @@ class ShippingMethodResourceExtension extends ResourceExtension return $this->driverColumn(); } + if (method_exists($column, 'getName') && $column->getName() === 'name') { + return $this->nameColumn(); + } + return $column; }, $table->getColumns()) ); @@ -139,6 +230,21 @@ class ShippingMethodResourceExtension extends ResourceExtension ->formatStateUsing(fn ($state) => $this->driverLabel($state)); } + /** + * `name` is a locale-keyed JSON column (see Modules\Core\Shipping\ + * Support\ShippingMethodName's own docblock for why it needs manual + * decoding rather than a model cast). Uses ->state() rather than + * ->formatStateUsing(), which would otherwise have Filament iterate a + * would-be array state as a multi-value list (one formatted cell per + * locale) instead of a single string. + */ + private function nameColumn(): TextColumn + { + return TextColumn::make('name') + ->label('Name') + ->state(fn ($record) => ShippingMethodName::resolve($record)); + } + private function driverLabel(string $key): string { $driver = collect(Shipping::getSupportedDrivers())->get($key); diff --git a/src/Shipping/Support/FulfillmentType.php b/src/Shipping/Support/FulfillmentType.php new file mode 100644 index 0000000..84b6373 --- /dev/null +++ b/src/Shipping/Support/FulfillmentType.php @@ -0,0 +1,60 @@ +get($method->driver); + + if ($driver instanceof DeclaresFulfillmentType) { + return $driver->fulfillmentType(); + } + + return $method->data['fulfillment_type'] ?? 'carrier'; + } + + public static function isStorePickup(ShippingMethod $method): bool + { + return static::resolve($method) === 'store_pickup'; + } + + /** + * Whether the merchant-facing "Fulfillment type" Select should be + * shown at all for a given driver — hidden entirely for a driver that + * already declares its own fulfillment type, since there is no real + * decision left for the merchant to make. + */ + public static function isConfigurableFor(?string $driverKey): bool + { + if ($driverKey === null) { + return true; + } + + $driver = collect(Shipping::getSupportedDrivers())->get($driverKey); + + return ! $driver instanceof DeclaresFulfillmentType; + } +} diff --git a/src/Shipping/Support/ShippingMethodName.php b/src/Shipping/Support/ShippingMethodName.php new file mode 100644 index 0000000..572f6a1 --- /dev/null +++ b/src/Shipping/Support/ShippingMethodName.php @@ -0,0 +1,35 @@ + + * name` returns the raw JSON string, not a decoded array — this resolves + * it the same way Lunar\Base\Traits\HasTranslations::translate() would, + * shared by every rate driver that builds a Lunar\DataTypes\ShippingOption + * (Modules\Core\Shipping\Concerns\ResolvesFixedPricing, Modules\Core\ + * Shipping\Carriers\Acs\AcsRateDriver) plus the Filament table column. + */ +class ShippingMethodName +{ + public static function resolve(ShippingMethod $method, ?string $locale = null): ?string + { + $decoded = json_decode((string) $method->getRawOriginal('name'), true); + + if (! is_array($decoded)) { + return $method->getRawOriginal('name'); + } + + return Arr::get($decoded, $locale ?: app()->getLocale()) ?: Arr::first($decoded); + } +}