Feat: Adding Translations to Payment and Shipping Methods, removing unecessary shipping method fulfillment type
This commit is contained in:
@@ -11,37 +11,110 @@ use Filament\Forms\Components\Select;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Lunar\Admin\Support\Extending\ResourceExtension;
|
||||
use Lunar\Admin\Support\Forms\Components\TranslatedText;
|
||||
use Lunar\Shipping\Facades\Shipping;
|
||||
use Modules\Core\Shipping\Contracts\DeclaresFulfillmentType;
|
||||
use Modules\Core\Shipping\Contracts\SupportsLivePricing;
|
||||
use Modules\Core\Shipping\Support\ShippingMethodName;
|
||||
|
||||
class ShippingMethodResourceExtension extends ResourceExtension
|
||||
{
|
||||
public function extendForm(Schema $schema): Schema
|
||||
{
|
||||
return $schema->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);
|
||||
|
||||
Reference in New Issue
Block a user