Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d680200ab1 | ||
|
|
9529036585 | ||
|
|
2db1e1331f | ||
|
|
d873cb4931 | ||
|
|
661e8b9a96 | ||
|
|
637da37b9b | ||
|
|
808c769595 |
@@ -4,6 +4,23 @@ 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.10.0] - 2026-08-31
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Breaking:** Upgraded `lunarphp/lunar`, `lunarphp/core`, `lunarphp/stripe`, `lunarphp/table-rate-shipping`, and `lunarphp/search` to `1.5.0`, and `filament/filament` to `v4.12.6` — the first Filament v4 admin panel on this codebase. `lunarphp/filament3-2fa` and `kalnoy/nestedset` are gone, replaced by Filament v4's native two-factor auth and `lunarphp/nestedset`. Ran Filament's automated `filament-v4` migration tool across `src/`, then hand-fixed three bugs it introduced or left behind: a stale `$infolist` variable reference in `CartResource`'s `ViewCart` page (the parameter had been renamed to `$schema` but the body wasn't updated), `ShippingMethodResourceExtension` rewritten to call `getDefaultChildComponents()` (returns `array|Schema`) instead of the type-safe `getChildComponents()` (always `array<Component>`), and — unrelated to the tool, but surfaced by the same PHP version bump — `InvalidCouponException`'s `readonly $code` property illegally shadowing the built-in `Exception::$code`, renamed to `$couponCode`. `LunarStaff::addActivitylogExcept()` updated for the renamed `two_factor_secret`/`two_factor_recovery_codes` staff columns (now `app_authentication_secret`/`app_authentication_recovery_codes`; `two_factor_confirmed_at` removed). Consuming apps must run `composer update boboko/core --with-all-dependencies` and `php artisan migrate`.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `Modules\Core\Checkout\Contracts\PaymentDriver` — the abstraction every payment provider implements: `confirm(Cart $cart, string $type, string $fingerprint, array $data): Order` and `isConfigured(): bool`. A driver only ever calls `CheckoutService::placeOrder()` once it has, by whatever mechanism is native to that gateway, independently confirmed payment — never Lunar's raw `Cart::createOrder()`. This is what lets the storefront checkout sequence stay uniform regardless of which provider is active: set addresses, select shipping, hand off to whichever driver is configured, and the driver decides when (or whether) the order gets created.
|
||||||
|
- `Modules\Core\Payment\Drivers\OfflinePaymentDriver` — shared by every payment type with no real gateway to confirm against (`cash-in-hand`, `cash-on-delivery`): places the order immediately via `CheckoutService::placeOrder()`, then sets the order status from `config("lunar.payments.types.{$type}.authorized")` using the type actually confirmed, not a hardcoded key, since one driver instance serves multiple types.
|
||||||
|
- `Modules\Core\Payment\Drivers\StripePaymentDriver` — a fork, not a decoration, of `lunarphp/stripe`'s `StripePaymentType::authorize()`: that method is `final` and calls `Cart::createOrder()` directly with no seam to redirect into our fingerprint-checked `placeOrder()`, so this class reimplements its logic (intent retrieval, capture-on-policy, status mapping via `UpdateOrderFromIntent`) with that one substitution. Throws the new `Modules\Core\Payment\Exceptions\PaymentNotConfirmedException` on anything short of a genuinely confirmed payment intent — never falls through to placing an order on ambiguity.
|
||||||
|
- `CheckoutService::getPaymentMethods(): array` — every payment type currently offered to the storefront: every key in `config('lunar.payments.types')` that is both administratively enabled (`Modules\Core\Payment\Models\PaymentMethod::enabled`) and whose driver reports `isConfigured()` (e.g. Stripe with no API key set is never offered, regardless of the enabled toggle). `selectPaymentMethod(string $type)` and `confirmPayment(string $type, array $data)` both validate against this list, throwing the new `UnknownPaymentTypeException` for a type that isn't currently offered — re-checked in `confirmPayment()` too, since a type could be disabled between selection and confirmation.
|
||||||
|
- `CheckoutService::selectPaymentMethod()` snapshots `Cart::fingerprint()` into `cart->meta['checkout_fingerprint']` *after* saving the chosen type and recalculating — the fingerprint has to reflect the final total including any payment-type-specific adjustment (e.g. a COD surcharge), which only exists once `payment_method` is set. `confirmPayment()` reads this stored fingerprint internally rather than taking one as a parameter: a storefront should never need to know `Cart::fingerprint()` exists or capture it at exactly the right moment itself.
|
||||||
|
- `Modules\Core\Payment\Models\PaymentMethod` — one DB row per payment type key (matching `config('lunar.payments.types')`), `enabled` boolean plus a `data` jsonb column (starting with `fee`, the flat cash-on-delivery surcharge) — mirrors Lunar's own `Discount` model (a single jsonb column of keyed settings, not a fixed column per setting or a separate conditions table). Seeded idempotently by `InstallLunarCommand` (skip-if-exists per type, safe to re-run after installing a new payment-provider package), always `enabled: false` — a newly-seeded type shouldn't go live for shoppers before staff have configured and reviewed it. Admin-editable via the new `PaymentMethodResource` (inline enabled toggle, modal fee editor) under Settings.
|
||||||
|
- `ApplyCashOnDeliveryFee` now reads its surcharge from `PaymentMethod` instead of static config, so it's admin-editable without a deploy.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `CashOnDeliveryPaymentDriver` renamed to `OfflinePaymentDriver` and generalized to work for any offline-style type — it previously hardcoded `'cash-on-delivery'` when reading the post-placement order status from config, which would have silently read the wrong type's status the moment a second offline type (`cash-in-hand`) used it.
|
||||||
|
|
||||||
## [0.9.0] - 2026-08-29
|
## [0.9.0] - 2026-08-29
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+8
-5
@@ -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.9.0",
|
"version": "0.10.0",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Modules\\Core\\": "src/"
|
"Modules\\Core\\": "src/"
|
||||||
@@ -10,14 +10,15 @@
|
|||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.5",
|
"php": "^8.5",
|
||||||
"lunarphp/lunar": "1.3.0",
|
"lunarphp/lunar": "1.5.0",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^3.0",
|
||||||
"symfony/yaml": "^7.0",
|
"symfony/yaml": "^7.0",
|
||||||
"lunarphp/table-rate-shipping": "^1.3",
|
"lunarphp/table-rate-shipping": "1.5.0",
|
||||||
"lunarphp/search": "*",
|
"lunarphp/search": "*",
|
||||||
"lunarphp/meilisearch": "*",
|
"lunarphp/meilisearch": "*",
|
||||||
"spatie/laravel-translation-loader": "^2.8"
|
"spatie/laravel-translation-loader": "^2.8",
|
||||||
|
"lunarphp/stripe": "^1.5"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
@@ -27,7 +28,8 @@
|
|||||||
"mockery/mockery": "^1.6",
|
"mockery/mockery": "^1.6",
|
||||||
"nunomaduro/collision": "^8.6",
|
"nunomaduro/collision": "^8.6",
|
||||||
"pestphp/pest": "^4.6",
|
"pestphp/pest": "^4.6",
|
||||||
"pestphp/pest-plugin-laravel": "^4.1"
|
"pestphp/pest-plugin-laravel": "^4.1",
|
||||||
|
"filament/upgrade": "^4.0"
|
||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
"laravel": {
|
"laravel": {
|
||||||
@@ -35,6 +37,7 @@
|
|||||||
"Modules\\Core\\Providers\\CoreServiceProvider",
|
"Modules\\Core\\Providers\\CoreServiceProvider",
|
||||||
"Modules\\Core\\Providers\\AuthServiceProvider",
|
"Modules\\Core\\Providers\\AuthServiceProvider",
|
||||||
"Modules\\Core\\Providers\\CustomerServiceProvider",
|
"Modules\\Core\\Providers\\CustomerServiceProvider",
|
||||||
|
"Modules\\Core\\Providers\\PaymentServiceProvider",
|
||||||
"Modules\\Core\\Providers\\LocalizationServiceProvider",
|
"Modules\\Core\\Providers\\LocalizationServiceProvider",
|
||||||
"Modules\\Core\\Providers\\CatalogServiceProvider",
|
"Modules\\Core\\Providers\\CatalogServiceProvider",
|
||||||
"Modules\\Core\\Providers\\CartServiceProvider",
|
"Modules\\Core\\Providers\\CartServiceProvider",
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Modules\Core\Payment\Drivers\OfflinePaymentDriver;
|
||||||
|
use Modules\Core\Payment\Pipelines\Cart\ApplyCashOnDeliveryFee;
|
||||||
|
|
||||||
|
return [
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Lunar payment types merged in by Boboko Core
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These are merged into config('lunar.payments.types') so every app using
|
||||||
|
| boboko-core gets cash-on-delivery out of the box, without publishing
|
||||||
|
| Lunar's own config.
|
||||||
|
|
|
||||||
|
| 'payment_driver' is boboko-owned, alongside Lunar's own 'driver' key —
|
||||||
|
| it's the Modules\Core\Checkout\Contracts\PaymentDriver class
|
||||||
|
| CheckoutService::confirmPayment() resolves via the container and calls
|
||||||
|
| confirm() on. Kept on the same row as 'driver' rather than a second,
|
||||||
|
| separately-keyed map, so a type's full definition — Lunar's driver,
|
||||||
|
| its config, and its PaymentDriver — lives in one place.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'types' => [
|
||||||
|
'cash-on-delivery' => [
|
||||||
|
'driver' => 'offline',
|
||||||
|
'payment_driver' => OfflinePaymentDriver::class,
|
||||||
|
'authorized' => 'awaiting-payment',
|
||||||
|
'fee' => 0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Lunar cart pipeline additions
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Appended to config('lunar.cart.pipelines.cart') after ApplyShipping so
|
||||||
|
| the cash-on-delivery fee is added to the shipping total before the
|
||||||
|
| final Calculate step sums everything up.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'cart_pipeline' => [
|
||||||
|
ApplyCashOnDeliveryFee::class,
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('payment_methods', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('type')->unique();
|
||||||
|
$table->boolean('enabled')->default(true);
|
||||||
|
$table->json('data')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('payment_methods');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,18 +2,18 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Auth\Extensions;
|
namespace Modules\Core\Auth\Extensions;
|
||||||
|
|
||||||
use Filament\Forms\Form;
|
use Filament\Schemas\Schema;
|
||||||
use Lunar\Admin\Support\Extending\ResourceExtension;
|
use Lunar\Admin\Support\Extending\ResourceExtension;
|
||||||
|
|
||||||
class StaffResourceExtension extends ResourceExtension
|
class StaffResourceExtension extends ResourceExtension
|
||||||
{
|
{
|
||||||
public function extendForm(Form $form): Form
|
public function extendForm(Schema $form): Schema
|
||||||
{
|
{
|
||||||
$schema = collect($form->getComponents())
|
$schema = collect($form->getComponents())
|
||||||
->reject(fn ($component) => method_exists($component, 'getName') && $component->getName() == 'password')
|
->reject(fn ($component) => method_exists($component, 'getName') && $component->getName() == 'password')
|
||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
return $form->schema($schema);
|
return $form->components($schema);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class Login extends SimplePage
|
|||||||
{
|
{
|
||||||
use WithRateLimiting;
|
use WithRateLimiting;
|
||||||
|
|
||||||
protected static string $view = 'core::auth.filament.pages.login';
|
protected string $view = 'core::auth.filament.pages.login';
|
||||||
|
|
||||||
public ?string $email = '';
|
public ?string $email = '';
|
||||||
public ?string $otp = '';
|
public ?string $otp = '';
|
||||||
@@ -78,7 +78,7 @@ class Login extends SimplePage
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($staff instanceof FilamentUser && !$staff->canAccessPanel(Filament::getCurrentPanel())) {
|
if ($staff instanceof FilamentUser && !$staff->canAccessPanel(Filament::getCurrentOrDefaultPanel())) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'email' => 'You do not have access to this panel.',
|
'email' => 'You do not have access to this panel.',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ use RuntimeException;
|
|||||||
*/
|
*/
|
||||||
class InvalidCouponException extends RuntimeException
|
class InvalidCouponException extends RuntimeException
|
||||||
{
|
{
|
||||||
public function __construct(public readonly string $code)
|
public function __construct(public readonly string $couponCode)
|
||||||
{
|
{
|
||||||
parent::__construct("The coupon code \"{$code}\" is not valid.");
|
parent::__construct("The coupon code \"{$couponCode}\" is not valid.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Cart\Filament\Resources;
|
namespace Modules\Core\Cart\Filament\Resources;
|
||||||
|
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Actions\ViewAction;
|
||||||
|
use Modules\Core\Cart\Filament\Resources\CartResource\Pages\ListCarts;
|
||||||
|
use Modules\Core\Cart\Filament\Resources\CartResource\Pages\ViewCart;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
@@ -24,9 +28,9 @@ class CartResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = Cart::class;
|
protected static ?string $model = Cart::class;
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'heroicon-o-shopping-cart';
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-shopping-cart';
|
||||||
|
|
||||||
protected static ?string $navigationGroup = 'Sales';
|
protected static string | \UnitEnum | null $navigationGroup = 'Sales';
|
||||||
|
|
||||||
protected static ?string $modelLabel = 'Cart';
|
protected static ?string $modelLabel = 'Cart';
|
||||||
|
|
||||||
@@ -70,37 +74,37 @@ class CartResource extends Resource
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
Tables\Columns\TextColumn::make('id')
|
TextColumn::make('id')
|
||||||
->label('Cart')
|
->label('Cart')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
Tables\Columns\TextColumn::make('customer.full_name')
|
TextColumn::make('customer.full_name')
|
||||||
->label('Customer')
|
->label('Customer')
|
||||||
->placeholder('—')
|
->placeholder('—')
|
||||||
->searchable()
|
->searchable()
|
||||||
->url(fn (Cart $record) => $record->customer_id !== null
|
->url(fn (Cart $record) => $record->customer_id !== null
|
||||||
? CustomerResource::getUrl('view', ['record' => $record->customer_id])
|
? CustomerResource::getUrl('view', ['record' => $record->customer_id])
|
||||||
: null),
|
: null),
|
||||||
Tables\Columns\TextColumn::make('user.email')
|
TextColumn::make('user.email')
|
||||||
->label('User')
|
->label('User')
|
||||||
->placeholder('—')
|
->placeholder('—')
|
||||||
->searchable(),
|
->searchable(),
|
||||||
Tables\Columns\TextColumn::make('lines_count')
|
TextColumn::make('lines_count')
|
||||||
->label('Lines')
|
->label('Lines')
|
||||||
->counts('lines')
|
->counts('lines')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
Tables\Columns\TextColumn::make('lines_sum_quantity')
|
TextColumn::make('lines_sum_quantity')
|
||||||
->label('Items')
|
->label('Items')
|
||||||
->sum('lines', 'quantity')
|
->sum('lines', 'quantity')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
Tables\Columns\TextColumn::make('currency.code')
|
TextColumn::make('currency.code')
|
||||||
->label('Currency'),
|
->label('Currency'),
|
||||||
Tables\Columns\TextColumn::make('updated_at')
|
TextColumn::make('updated_at')
|
||||||
->label('Last activity')
|
->label('Last activity')
|
||||||
->dateTime()
|
->dateTime()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
])
|
])
|
||||||
->actions([
|
->recordActions([
|
||||||
Tables\Actions\ViewAction::make(),
|
ViewAction::make(),
|
||||||
])
|
])
|
||||||
->defaultSort('updated_at', 'desc');
|
->defaultSort('updated_at', 'desc');
|
||||||
}
|
}
|
||||||
@@ -108,8 +112,8 @@ class CartResource extends Resource
|
|||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => Pages\ListCarts::route('/'),
|
'index' => ListCarts::route('/'),
|
||||||
'view' => Pages\ViewCart::route('/{record}'),
|
'view' => ViewCart::route('/{record}'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Cart\Filament\Resources\CartResource\Pages;
|
namespace Modules\Core\Cart\Filament\Resources\CartResource\Pages;
|
||||||
|
|
||||||
use Filament\Resources\Components\Tab;
|
use Filament\Schemas\Components\Tabs\Tab;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Modules\Core\Cart\Filament\Resources\CartResource;
|
use Modules\Core\Cart\Filament\Resources\CartResource;
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Cart\Filament\Resources\CartResource\Pages;
|
namespace Modules\Core\Cart\Filament\Resources\CartResource\Pages;
|
||||||
|
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Schemas\Components\Section;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Infolists\Components\RepeatableEntry;
|
use Filament\Infolists\Components\RepeatableEntry;
|
||||||
use Filament\Infolists\Components\Section;
|
|
||||||
use Filament\Infolists\Components\TextEntry;
|
use Filament\Infolists\Components\TextEntry;
|
||||||
use Filament\Infolists\Infolist;
|
|
||||||
use Filament\Resources\Pages\ViewRecord;
|
use Filament\Resources\Pages\ViewRecord;
|
||||||
use Lunar\Admin\Filament\Resources\CustomerResource;
|
use Lunar\Admin\Filament\Resources\CustomerResource;
|
||||||
use Lunar\Models\Cart;
|
use Lunar\Models\Cart;
|
||||||
@@ -44,10 +44,10 @@ class ViewCart extends ViewRecord
|
|||||||
return $cart->calculate();
|
return $cart->calculate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function infolist(Infolist $infolist): Infolist
|
public function infolist(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $infolist
|
return $schema
|
||||||
->schema([
|
->components([
|
||||||
Section::make('Cart')
|
Section::make('Cart')
|
||||||
->columns(3)
|
->columns(3)
|
||||||
->schema([
|
->schema([
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Catalog\Contracts;
|
namespace Modules\Core\Catalog\Contracts;
|
||||||
|
|
||||||
use Filament\Forms\Components\Component;
|
use Filament\Schemas\Components\Component;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Product Option Type describes how a category of Lunar `ProductOption` (e.g.
|
* A Product Option Type describes how a category of Lunar `ProductOption` (e.g.
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Catalog\Filament\Extensions;
|
namespace Modules\Core\Catalog\Filament\Extensions;
|
||||||
|
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Form;
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Lunar\Admin\Support\Extending\ResourceExtension;
|
use Lunar\Admin\Support\Extending\ResourceExtension;
|
||||||
use Modules\Core\Catalog\Services\ProductOptionTypeManager;
|
use Modules\Core\Catalog\Services\ProductOptionTypeManager;
|
||||||
@@ -16,7 +16,7 @@ use Modules\Core\Catalog\Services\ProductOptionTypeManager;
|
|||||||
*/
|
*/
|
||||||
class ProductOptionResourceExtension extends ResourceExtension
|
class ProductOptionResourceExtension extends ResourceExtension
|
||||||
{
|
{
|
||||||
public function extendForm(Form $form): Form
|
public function extendForm(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
$options = collect(ProductOptionTypeManager::get()->all())
|
$options = collect(ProductOptionTypeManager::get()->all())
|
||||||
->keys()
|
->keys()
|
||||||
@@ -24,11 +24,11 @@ class ProductOptionResourceExtension extends ResourceExtension
|
|||||||
->all();
|
->all();
|
||||||
|
|
||||||
if ($options === []) {
|
if ($options === []) {
|
||||||
return $form;
|
return $schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $form->schema([
|
return $schema->components([
|
||||||
...$form->getComponents(),
|
...$schema->getComponents(),
|
||||||
Select::make('meta.option_type')
|
Select::make('meta.option_type')
|
||||||
->label('Option Type')
|
->label('Option Type')
|
||||||
->options($options)
|
->options($options)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Catalog\Filament\Extensions;
|
namespace Modules\Core\Catalog\Filament\Extensions;
|
||||||
|
|
||||||
use Filament\Forms\Form;
|
use Filament\Schemas\Schema;
|
||||||
use Lunar\Admin\Support\Extending\RelationManagerExtension;
|
use Lunar\Admin\Support\Extending\RelationManagerExtension;
|
||||||
use Lunar\Models\ProductOption;
|
use Lunar\Models\ProductOption;
|
||||||
use Modules\Core\Catalog\Services\ProductOptionTypeManager;
|
use Modules\Core\Catalog\Services\ProductOptionTypeManager;
|
||||||
@@ -15,7 +15,7 @@ use Modules\Core\Catalog\Services\ProductOptionTypeManager;
|
|||||||
*/
|
*/
|
||||||
class ValuesRelationManagerExtension extends RelationManagerExtension
|
class ValuesRelationManagerExtension extends RelationManagerExtension
|
||||||
{
|
{
|
||||||
public function extendForm(Form $form): Form
|
public function extendForm(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
/** @var ProductOption $option */
|
/** @var ProductOption $option */
|
||||||
$option = $this->caller->getOwnerRecord();
|
$option = $this->caller->getOwnerRecord();
|
||||||
@@ -23,11 +23,11 @@ class ValuesRelationManagerExtension extends RelationManagerExtension
|
|||||||
$type = ProductOptionTypeManager::get()->resolve($option->meta['option_type'] ?? null);
|
$type = ProductOptionTypeManager::get()->resolve($option->meta['option_type'] ?? null);
|
||||||
|
|
||||||
if ($type === null) {
|
if ($type === null) {
|
||||||
return $form;
|
return $schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $form->schema([
|
return $schema->components([
|
||||||
...$form->getComponents(),
|
...$schema->getComponents(),
|
||||||
...$type->getMetaForm(),
|
...$type->getMetaForm(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Checkout\Contracts;
|
||||||
|
|
||||||
|
use Lunar\Exceptions\FingerprintMismatchException;
|
||||||
|
use Lunar\Exceptions\Carts\CartException;
|
||||||
|
use Lunar\Models\Cart;
|
||||||
|
use Lunar\Models\Order;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boboko-owned payment driver — wraps a payment gateway's own confirmation
|
||||||
|
* mechanics (Stripe's synchronous authorize() call, a redirect-based
|
||||||
|
* provider's async callback/webhook, anything else) behind one uniform
|
||||||
|
* moment: "payment is confirmed, place the order."
|
||||||
|
*
|
||||||
|
* confirm() is the only thing a driver is required to do: once it has,
|
||||||
|
* by whatever mechanism is native to that gateway, independently decided
|
||||||
|
* the payment succeeded, it calls Modules\Core\Checkout\Services\
|
||||||
|
* CheckoutService::placeOrder($fingerprint) itself — no driver ever calls
|
||||||
|
* Lunar\Models\Cart::createOrder() directly. This is what lets the
|
||||||
|
* storefront checkout sequence stay uniform regardless of which provider is
|
||||||
|
* active: set addresses, select shipping, hand off to whichever driver is
|
||||||
|
* configured, and the driver decides when (or whether) the order actually
|
||||||
|
* gets created. See docs/checkout.md / docs/payments.md.
|
||||||
|
*/
|
||||||
|
interface PaymentDriver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Whether this driver can actually be used right now — e.g. Stripe
|
||||||
|
* checking its own API key is present, an offline-style driver always
|
||||||
|
* returning true since it has no external dependency. Independent of
|
||||||
|
* Modules\Core\Payment\Models\PaymentMethod::enabled (the admin
|
||||||
|
* on/off toggle) — CheckoutService::getPaymentMethods() combines both:
|
||||||
|
* a type is only offered to the storefront if it's administratively
|
||||||
|
* enabled AND its driver reports itself configured.
|
||||||
|
*/
|
||||||
|
public function isConfigured(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* $type is the payment type key being confirmed (e.g. 'cash-in-hand',
|
||||||
|
* 'cash-on-delivery', 'stripe') — passed through even though most
|
||||||
|
* drivers only ever serve one type, because a driver shared across
|
||||||
|
* several types (e.g. one "no real confirmation" offline driver behind
|
||||||
|
* both cash-in-hand and cash-on-delivery) needs it to look up that
|
||||||
|
* type's own config (e.g. its 'authorized' status) rather than another
|
||||||
|
* type's.
|
||||||
|
*
|
||||||
|
* $data carries whatever the gateway needs to confirm this specific
|
||||||
|
* payment (Stripe: ['payment_intent' => $id], a redirect-based
|
||||||
|
* provider: its callback payload) — passed explicitly by the caller
|
||||||
|
* (a controller, a webhook job) rather than a driver reaching into the
|
||||||
|
* global request(), so confirm() works the same whether it's called
|
||||||
|
* from a synchronous HTTP request or an async webhook/job with no
|
||||||
|
* active request at all.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
*
|
||||||
|
* @throws FingerprintMismatchException
|
||||||
|
* @throws CartException
|
||||||
|
*/
|
||||||
|
public function confirm(Cart $cart, string $type, string $fingerprint, array $data): Order;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Checkout\Events;
|
||||||
|
|
||||||
|
use Lunar\Models\Cart;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatched by CheckoutService::selectPaymentMethod() — carries the plain
|
||||||
|
* type key (e.g. 'cash-on-delivery', 'stripe'), same convention as
|
||||||
|
* CartService's events (a plain reference the listener resolves further
|
||||||
|
* itself, rather than an already-resolved object) since a payment type key
|
||||||
|
* has nothing further to eagerly resolve the way a ShippingOption does.
|
||||||
|
*/
|
||||||
|
class PaymentMethodSelected
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly Cart $cart,
|
||||||
|
public readonly string $type,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Checkout\Exceptions;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown by CheckoutService::selectPaymentMethod()/confirmPayment() when
|
||||||
|
* $type doesn't resolve to a registered Modules\Core\Checkout\Contracts\
|
||||||
|
* PaymentDriver (config('payment.drivers')) — same reasoning as
|
||||||
|
* InvalidShippingOptionException: nothing here has a matching Lunar
|
||||||
|
* exception type to reuse, so this is the boboko-owned signal instead of a
|
||||||
|
* silent no-op or an opaque container-resolution error.
|
||||||
|
*/
|
||||||
|
class UnknownPaymentTypeException extends RuntimeException
|
||||||
|
{
|
||||||
|
public function __construct(public readonly string $type)
|
||||||
|
{
|
||||||
|
parent::__construct("The payment type \"{$type}\" is not registered.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Checkout\Services;
|
namespace Modules\Core\Checkout\Services;
|
||||||
|
|
||||||
|
use Lunar\Exceptions\FingerprintMismatchException;
|
||||||
|
use Lunar\Exceptions\Carts\CartException;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Lunar\Base\Addressable;
|
use Lunar\Base\Addressable;
|
||||||
@@ -10,11 +12,15 @@ use Lunar\Facades\ShippingManifest;
|
|||||||
use Lunar\Models\Cart;
|
use Lunar\Models\Cart;
|
||||||
use Lunar\Models\Order;
|
use Lunar\Models\Order;
|
||||||
use Modules\Core\Cart\Services\CartService;
|
use Modules\Core\Cart\Services\CartService;
|
||||||
|
use Modules\Core\Checkout\Contracts\PaymentDriver;
|
||||||
use Modules\Core\Checkout\Events\BillingAddressSet;
|
use Modules\Core\Checkout\Events\BillingAddressSet;
|
||||||
use Modules\Core\Checkout\Events\OrderPlaced;
|
use Modules\Core\Checkout\Events\OrderPlaced;
|
||||||
|
use Modules\Core\Checkout\Events\PaymentMethodSelected;
|
||||||
use Modules\Core\Checkout\Events\ShippingAddressSet;
|
use Modules\Core\Checkout\Events\ShippingAddressSet;
|
||||||
use Modules\Core\Checkout\Events\ShippingOptionSelected;
|
use Modules\Core\Checkout\Events\ShippingOptionSelected;
|
||||||
use Modules\Core\Checkout\Exceptions\InvalidShippingOptionException;
|
use Modules\Core\Checkout\Exceptions\InvalidShippingOptionException;
|
||||||
|
use Modules\Core\Checkout\Exceptions\UnknownPaymentTypeException;
|
||||||
|
use Modules\Core\Payment\Models\PaymentMethod;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Storefront-facing checkout operations, mirroring
|
* Storefront-facing checkout operations, mirroring
|
||||||
@@ -99,6 +105,10 @@ class CheckoutService
|
|||||||
* stock adjusted the total, another tab modified the cart) rather than
|
* stock adjusted the total, another tab modified the cart) rather than
|
||||||
* silently placing an order at a different total than what was shown.
|
* silently placing an order at a different total than what was shown.
|
||||||
*
|
*
|
||||||
|
* Not called directly by a storefront — see confirmPayment(), which is
|
||||||
|
* the only caller and supplies the fingerprint captured in
|
||||||
|
* selectPaymentMethod(), not one the storefront has to obtain itself.
|
||||||
|
*
|
||||||
* No exception wrapping: Lunar\Validation\Cart\ValidateCartForOrderCreation
|
* No exception wrapping: Lunar\Validation\Cart\ValidateCartForOrderCreation
|
||||||
* (run inside Cart::createOrder()) already throws
|
* (run inside Cart::createOrder()) already throws
|
||||||
* Lunar\Exceptions\Carts\CartException with a field-keyed MessageBag
|
* Lunar\Exceptions\Carts\CartException with a field-keyed MessageBag
|
||||||
@@ -107,8 +117,8 @@ class CheckoutService
|
|||||||
* render as form errors directly. FingerprintMismatchException
|
* render as form errors directly. FingerprintMismatchException
|
||||||
* propagates the same way, for the same reason.
|
* propagates the same way, for the same reason.
|
||||||
*
|
*
|
||||||
* @throws \Lunar\Exceptions\FingerprintMismatchException
|
* @throws FingerprintMismatchException
|
||||||
* @throws \Lunar\Exceptions\Carts\CartException
|
* @throws CartException
|
||||||
*/
|
*/
|
||||||
public function placeOrder(string $fingerprint): Order
|
public function placeOrder(string $fingerprint): Order
|
||||||
{
|
{
|
||||||
@@ -121,4 +131,118 @@ class CheckoutService
|
|||||||
|
|
||||||
return $order;
|
return $order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every payment type currently offered to the storefront — every key
|
||||||
|
* in config('lunar.payments.types') that is BOTH administratively
|
||||||
|
* enabled (Modules\Core\Payment\Models\PaymentMethod::enabled) AND
|
||||||
|
* whose registered PaymentDriver reports itself usable right now
|
||||||
|
* (PaymentDriver::isConfigured() — e.g. Stripe with no API key set is
|
||||||
|
* never offered, regardless of the enabled toggle). A type with no
|
||||||
|
* PaymentMethod row at all (never seeded) is treated as not offered,
|
||||||
|
* same as disabled — nothing here creates one; see
|
||||||
|
* InstallLunarCommand::seedPaymentMethods().
|
||||||
|
*
|
||||||
|
* @return array<string>
|
||||||
|
*/
|
||||||
|
public function getPaymentMethods(): array
|
||||||
|
{
|
||||||
|
return PaymentMethod::where('enabled', true)
|
||||||
|
->pluck('type')
|
||||||
|
->filter(fn (string $type) => $this->resolvePaymentDriver($type)?->isConfigured() ?? false)
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records which payment type the shopper picked (Cart::meta
|
||||||
|
* ['payment_method']) — read by e.g. Modules\Core\Payment\Pipelines\
|
||||||
|
* Cart\ApplyCashOnDeliveryFee to add that type's own cart-total
|
||||||
|
* adjustments before recalculation.
|
||||||
|
*
|
||||||
|
* Also snapshots Cart::fingerprint() into meta, *after* saving the
|
||||||
|
* chosen type — the fingerprint has to reflect the final total
|
||||||
|
* including any payment-type-specific adjustment (e.g. a COD
|
||||||
|
* surcharge), which only exists once payment_method is set and the
|
||||||
|
* cart recalculates. Captured here, server-side, rather than asked of
|
||||||
|
* the storefront: this is the last moment before confirmPayment() that
|
||||||
|
* the shopper's reviewed total is known, and confirmPayment() reads it
|
||||||
|
* back internally instead of taking a fingerprint parameter — a
|
||||||
|
* storefront should never need to know Cart::fingerprint() exists.
|
||||||
|
*
|
||||||
|
* Does not itself call a PaymentDriver — selecting a method and
|
||||||
|
* confirming payment against it are deliberately separate steps, same
|
||||||
|
* as selecting a shipping option happens before placing the order.
|
||||||
|
*
|
||||||
|
* @throws UnknownPaymentTypeException if $type isn't currently offered
|
||||||
|
* — see getPaymentMethods() for what that means (registered,
|
||||||
|
* administratively enabled, and its driver reports itself usable)
|
||||||
|
*/
|
||||||
|
public function selectPaymentMethod(string $type): Cart
|
||||||
|
{
|
||||||
|
if (! in_array($type, $this->getPaymentMethods(), true)) {
|
||||||
|
throw new UnknownPaymentTypeException($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cart = $this->cart->currentOrCreate();
|
||||||
|
$cart->meta = [...$cart->meta->toArray(), 'payment_method' => $type];
|
||||||
|
$cart->save();
|
||||||
|
|
||||||
|
$cart = $cart->calculate();
|
||||||
|
$cart->meta = [...$cart->meta->toArray(), 'checkout_fingerprint' => $cart->fingerprint()];
|
||||||
|
$cart->save();
|
||||||
|
|
||||||
|
Event::dispatch(new PaymentMethodSelected($cart, $type));
|
||||||
|
|
||||||
|
return $cart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves $type's registered PaymentDriver and calls confirm() —
|
||||||
|
* the driver decides whether/when the order actually gets placed (see
|
||||||
|
* Modules\Core\Checkout\Contracts\PaymentDriver's docblock). $data
|
||||||
|
* carries whatever that driver needs (Stripe's payment_intent id, a
|
||||||
|
* future redirect-based provider's callback payload).
|
||||||
|
*
|
||||||
|
* The fingerprint passed to the driver is the one captured by
|
||||||
|
* selectPaymentMethod(), not supplied by the caller — see that
|
||||||
|
* method's docblock. Throws the same FingerprintMismatchException a
|
||||||
|
* caller-supplied one would if the cart's total has since changed;
|
||||||
|
* missing entirely (selectPaymentMethod() was never called for this
|
||||||
|
* cart) is treated the same as a mismatch, not a different error.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
*
|
||||||
|
* @throws UnknownPaymentTypeException if $type isn't currently offered
|
||||||
|
* (see getPaymentMethods()) — re-checked here, not just in
|
||||||
|
* selectPaymentMethod(), since a type could be disabled between
|
||||||
|
* selection and confirmation
|
||||||
|
* @throws \Lunar\Exceptions\FingerprintMismatchException
|
||||||
|
* @throws \Lunar\Exceptions\Carts\CartException
|
||||||
|
*/
|
||||||
|
public function confirmPayment(string $type, array $data = []): Order
|
||||||
|
{
|
||||||
|
if (! in_array($type, $this->getPaymentMethods(), true)) {
|
||||||
|
throw new UnknownPaymentTypeException($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cart = $this->cart->currentOrCreate();
|
||||||
|
$fingerprint = $cart->meta['checkout_fingerprint'] ?? '';
|
||||||
|
|
||||||
|
return $this->resolvePaymentDriver($type)->confirm($cart, $type, $fingerprint, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves $type's registered PaymentDriver, or null if $type has no
|
||||||
|
* 'payment_driver' registered in config('lunar.payments.types.<type>')
|
||||||
|
* at all — deliberately non-throwing so getPaymentMethods() can filter
|
||||||
|
* unresolvable types silently rather than treating "not registered"
|
||||||
|
* as an error condition when just checking availability.
|
||||||
|
*/
|
||||||
|
private function resolvePaymentDriver(string $type): ?PaymentDriver
|
||||||
|
{
|
||||||
|
$driverClass = config("lunar.payments.types.{$type}.payment_driver");
|
||||||
|
|
||||||
|
return $driverClass ? app($driverClass) : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Command;
|
namespace Modules\Core\Command;
|
||||||
|
|
||||||
|
use Lunar\Admin\Models\Staff;
|
||||||
use Lunar\Admin\Console\Commands\MakeLunarAdminCommand;
|
use Lunar\Admin\Console\Commands\MakeLunarAdminCommand;
|
||||||
|
|
||||||
use function Laravel\Prompts\text;
|
use function Laravel\Prompts\text;
|
||||||
@@ -31,7 +32,7 @@ class CreateAdminCommand extends MakeLunarAdminCommand
|
|||||||
required: true,
|
required: true,
|
||||||
validate: fn (string $email): ?string => match (true) {
|
validate: fn (string $email): ?string => match (true) {
|
||||||
! filter_var($email, FILTER_VALIDATE_EMAIL) => 'The email address must be valid.',
|
! filter_var($email, FILTER_VALIDATE_EMAIL) => 'The email address must be valid.',
|
||||||
\Lunar\Admin\Models\Staff::where('email', $email)->exists() => 'A user with this email address already exists',
|
Staff::where('email', $email)->exists() => 'A user with this email address already exists',
|
||||||
default => null,
|
default => null,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Command;
|
namespace Modules\Core\Command;
|
||||||
|
|
||||||
|
use RecursiveIteratorIterator;
|
||||||
|
use RecursiveDirectoryIterator;
|
||||||
|
use FilesystemIterator;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Modules\Core\ResultType\Error;
|
use Modules\Core\ResultType\Error;
|
||||||
@@ -88,10 +91,10 @@ class ExportCommand extends Command
|
|||||||
$zip->addFile($sqlFile, basename($sqlFile));
|
$zip->addFile($sqlFile, basename($sqlFile));
|
||||||
|
|
||||||
if (is_dir($filesDir)) {
|
if (is_dir($filesDir)) {
|
||||||
$iterator = new \RecursiveIteratorIterator(
|
$iterator = new RecursiveIteratorIterator(
|
||||||
new \RecursiveDirectoryIterator(
|
new RecursiveDirectoryIterator(
|
||||||
$filesDir,
|
$filesDir,
|
||||||
\FilesystemIterator::SKIP_DOTS,
|
FilesystemIterator::SKIP_DOTS,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
foreach ($iterator as $file) {
|
foreach ($iterator as $file) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use Lunar\Models\TaxZone;
|
|||||||
use Modules\Core\Localization\Models\LanguageLine;
|
use Modules\Core\Localization\Models\LanguageLine;
|
||||||
use Modules\Core\Localization\Services\StorefrontLabels;
|
use Modules\Core\Localization\Services\StorefrontLabels;
|
||||||
use Modules\Core\Localization\Services\TranslationService;
|
use Modules\Core\Localization\Services\TranslationService;
|
||||||
|
use Modules\Core\Payment\Models\PaymentMethod;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overrides Lunar's own lunar:install to skip the interactive prompts (migrate
|
* Overrides Lunar's own lunar:install to skip the interactive prompts (migrate
|
||||||
@@ -247,6 +248,9 @@ class InstallLunarCommand extends Command
|
|||||||
$this->components->info('Seeding storefront label translations');
|
$this->components->info('Seeding storefront label translations');
|
||||||
$this->seedStorefrontLabels($translations);
|
$this->seedStorefrontLabels($translations);
|
||||||
|
|
||||||
|
$this->components->info('Seeding payment method settings');
|
||||||
|
$this->seedPaymentMethods();
|
||||||
|
|
||||||
$this->components->info('Publishing Filament assets');
|
$this->components->info('Publishing Filament assets');
|
||||||
$this->call('filament:assets');
|
$this->call('filament:assets');
|
||||||
|
|
||||||
@@ -278,4 +282,37 @@ class InstallLunarCommand extends Command
|
|||||||
$translations->create('storefront', $key, $text);
|
$translations->create('storefront', $key, $text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-type skip-if-exists, same idempotent convention as
|
||||||
|
* seedStorefrontLabels() — a type already present (including one an
|
||||||
|
* admin has since edited via the Filament Payment Methods resource) is
|
||||||
|
* left untouched. Safe to re-run after a new payment type is added to
|
||||||
|
* config('lunar.payments.types') (e.g. installing a Stripe/Nexi
|
||||||
|
* package), which is the whole reason this isn't a one-time-only seed.
|
||||||
|
*
|
||||||
|
* Seeded disabled — a newly-seeded row (whether from this store's
|
||||||
|
* initial install, or a payment provider package installed later)
|
||||||
|
* shouldn't go live for shoppers before staff have actually reviewed
|
||||||
|
* it (real credentials configured, a fee set, etc.) and turned it on
|
||||||
|
* via the Payment Methods resource. See CheckoutService::
|
||||||
|
* getPaymentMethods(), which only offers a type once both 'enabled'
|
||||||
|
* here and its driver's own isConfigured() check pass.
|
||||||
|
*/
|
||||||
|
private function seedPaymentMethods(): void
|
||||||
|
{
|
||||||
|
$existingTypes = PaymentMethod::pluck('type');
|
||||||
|
|
||||||
|
foreach (array_keys(config('lunar.payments.types', [])) as $type) {
|
||||||
|
if ($existingTypes->contains($type)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentMethod::create([
|
||||||
|
'type' => $type,
|
||||||
|
'enabled' => false,
|
||||||
|
'data' => [],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\Core;
|
namespace Modules\Core;
|
||||||
|
|
||||||
|
use Lunar\Admin\Filament\Resources\OrderResource\Pages\ManageOrder;
|
||||||
use Filament\Contracts\Plugin;
|
use Filament\Contracts\Plugin;
|
||||||
use Filament\Panel;
|
use Filament\Panel;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
@@ -24,6 +25,7 @@ use Modules\Core\Cart\Filament\Resources\CartResource;
|
|||||||
use Modules\Core\Catalog\Filament\Extensions\ProductOptionResourceExtension;
|
use Modules\Core\Catalog\Filament\Extensions\ProductOptionResourceExtension;
|
||||||
use Modules\Core\Catalog\Filament\Extensions\ValuesRelationManagerExtension;
|
use Modules\Core\Catalog\Filament\Extensions\ValuesRelationManagerExtension;
|
||||||
use Modules\Core\Localization\Filament\Resources\LanguageLineResource;
|
use Modules\Core\Localization\Filament\Resources\LanguageLineResource;
|
||||||
|
use Modules\Core\Payment\Filament\Resources\PaymentMethodResource;
|
||||||
use Modules\Core\Review\Filament\Extensions\ProductResourceExtension;
|
use Modules\Core\Review\Filament\Extensions\ProductResourceExtension;
|
||||||
use Modules\Core\Review\Models\ProductReview;
|
use Modules\Core\Review\Models\ProductReview;
|
||||||
use Modules\Core\Shipping\Extensions\OrderViewExtension;
|
use Modules\Core\Shipping\Extensions\OrderViewExtension;
|
||||||
@@ -48,6 +50,7 @@ class CorePlugin implements Plugin
|
|||||||
->resources([
|
->resources([
|
||||||
LanguageLineResource::class,
|
LanguageLineResource::class,
|
||||||
CartResource::class,
|
CartResource::class,
|
||||||
|
PaymentMethodResource::class,
|
||||||
])
|
])
|
||||||
->plugin(ShippingPlugin::make())
|
->plugin(ShippingPlugin::make())
|
||||||
->pages([ManagePickupManifests::class]);
|
->pages([ManagePickupManifests::class]);
|
||||||
@@ -59,7 +62,7 @@ class CorePlugin implements Plugin
|
|||||||
ValuesRelationManager::class => ValuesRelationManagerExtension::class,
|
ValuesRelationManager::class => ValuesRelationManagerExtension::class,
|
||||||
ShippingMethodResource::class => ShippingMethodResourceExtension::class,
|
ShippingMethodResource::class => ShippingMethodResourceExtension::class,
|
||||||
ListShippingMethod::class => ShippingMethodListExtension::class,
|
ListShippingMethod::class => ShippingMethodListExtension::class,
|
||||||
OrderResource\Pages\ManageOrder::class => OrderViewExtension::class,
|
ManageOrder::class => OrderViewExtension::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Product::macro('reviews', function (): HasMany {
|
Product::macro('reviews', function (): HasMany {
|
||||||
@@ -73,9 +76,8 @@ class CorePlugin implements Plugin
|
|||||||
'password',
|
'password',
|
||||||
'remember_token',
|
'remember_token',
|
||||||
'email_verified_at',
|
'email_verified_at',
|
||||||
'two_factor_secret',
|
'app_authentication_secret',
|
||||||
'two_factor_recovery_codes',
|
'app_authentication_recovery_codes',
|
||||||
'two_factor_confirmed_at',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
LunarStaff::created(function (LunarStaff $staff) {
|
LunarStaff::created(function (LunarStaff $staff) {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Customer\RelationManagers;
|
namespace Modules\Core\Customer\RelationManagers;
|
||||||
|
|
||||||
use Filament\Forms\Components\Group;
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Actions\EditAction;
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Schemas\Components\Group;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Tables\Actions\CreateAction;
|
|
||||||
use Filament\Tables\Actions\DeleteAction;
|
|
||||||
use Filament\Tables\Actions\EditAction;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@@ -38,9 +38,9 @@ class AddressRelationManager extends BaseAddressRelationManager
|
|||||||
),
|
),
|
||||||
])
|
])
|
||||||
->headerActions([
|
->headerActions([
|
||||||
CreateAction::make()->form($this->addressForm()),
|
CreateAction::make()->schema($this->addressForm()),
|
||||||
])
|
])
|
||||||
->actions([
|
->recordActions([
|
||||||
EditAction::make('editAddress')
|
EditAction::make('editAddress')
|
||||||
->fillForm(fn (AddressContract $record): array => [
|
->fillForm(fn (AddressContract $record): array => [
|
||||||
'line_one' => $record->line_one,
|
'line_one' => $record->line_one,
|
||||||
@@ -51,7 +51,7 @@ class AddressRelationManager extends BaseAddressRelationManager
|
|||||||
'contact_email' => $record->contact_email,
|
'contact_email' => $record->contact_email,
|
||||||
'contact_phone' => $record->contact_phone,
|
'contact_phone' => $record->contact_phone,
|
||||||
])
|
])
|
||||||
->form($this->addressForm()),
|
->schema($this->addressForm()),
|
||||||
DeleteAction::make('deleteAddress'),
|
DeleteAction::make('deleteAddress'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Customer\RelationManagers;
|
namespace Modules\Core\Customer\RelationManagers;
|
||||||
|
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Actions\EditAction;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
@@ -14,16 +16,16 @@ class UserRelationManager extends BaseUserRelationManager
|
|||||||
public function getDefaultTable(Table $table): Table
|
public function getDefaultTable(Table $table): Table
|
||||||
{
|
{
|
||||||
return $table->columns([
|
return $table->columns([
|
||||||
Tables\Columns\TextColumn::make('name')
|
TextColumn::make('name')
|
||||||
->label(__('lunarpanel::user.table.name.label')),
|
->label(__('lunarpanel::user.table.name.label')),
|
||||||
Tables\Columns\TextColumn::make('email')
|
TextColumn::make('email')
|
||||||
->label(__('lunarpanel::user.table.email.label')),
|
->label(__('lunarpanel::user.table.email.label')),
|
||||||
])->actions([
|
])->recordActions([
|
||||||
Tables\Actions\EditAction::make('edit')
|
EditAction::make('edit')
|
||||||
->after(
|
->after(
|
||||||
fn (Model $record) => CustomerUserEdited::dispatch($record)
|
fn (Model $record) => CustomerUserEdited::dispatch($record)
|
||||||
)
|
)
|
||||||
->form([
|
->schema([
|
||||||
TextInput::make('email')
|
TextInput::make('email')
|
||||||
->label(__('lunarpanel::user.form.email.label'))
|
->label(__('lunarpanel::user.form.email.label'))
|
||||||
->required()
|
->required()
|
||||||
|
|||||||
@@ -2,8 +2,17 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Localization\Filament\Resources;
|
namespace Modules\Core\Localization\Filament\Resources;
|
||||||
|
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Schemas\Components\Fieldset;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
|
use Modules\Core\Localization\Filament\Resources\LanguageLineResource\Pages\ListLanguageLines;
|
||||||
|
use Modules\Core\Localization\Filament\Resources\LanguageLineResource\Pages\CreateLanguageLine;
|
||||||
|
use Modules\Core\Localization\Filament\Resources\LanguageLineResource\Pages\EditLanguageLine;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Filament\Forms;
|
use Filament\Forms;
|
||||||
use Filament\Forms\Form;
|
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
@@ -15,29 +24,29 @@ class LanguageLineResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = LanguageLine::class;
|
protected static ?string $model = LanguageLine::class;
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'heroicon-o-language';
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-language';
|
||||||
|
|
||||||
protected static ?string $navigationGroup = 'Settings';
|
protected static string | \UnitEnum | null $navigationGroup = 'Settings';
|
||||||
|
|
||||||
protected static ?string $modelLabel = 'Translation';
|
protected static ?string $modelLabel = 'Translation';
|
||||||
|
|
||||||
protected static ?string $pluralModelLabel = 'Translations';
|
protected static ?string $pluralModelLabel = 'Translations';
|
||||||
|
|
||||||
public static function form(Form $form): Form
|
public static function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $form->schema([
|
return $schema->components([
|
||||||
Forms\Components\TextInput::make('group')
|
TextInput::make('group')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255)
|
->maxLength(255)
|
||||||
->default('storefront')
|
->default('storefront')
|
||||||
->helperText('Namespace for this label, e.g. "storefront" for e-shop UI text.'),
|
->helperText('Namespace for this label, e.g. "storefront" for e-shop UI text.'),
|
||||||
|
|
||||||
Forms\Components\TextInput::make('key')
|
TextInput::make('key')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255)
|
->maxLength(255)
|
||||||
->helperText('Dot-notation key, e.g. "nav.cart".'),
|
->helperText('Dot-notation key, e.g. "nav.cart".'),
|
||||||
|
|
||||||
Forms\Components\Fieldset::make('Translations')
|
Fieldset::make('Translations')
|
||||||
->schema(static::localeInputs()),
|
->schema(static::localeInputs()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -46,16 +55,16 @@ class LanguageLineResource extends Resource
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
Tables\Columns\TextColumn::make('group')
|
TextColumn::make('group')
|
||||||
->badge()
|
->badge()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
Tables\Columns\TextColumn::make('key')
|
TextColumn::make('key')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
...static::localeColumns(),
|
...static::localeColumns(),
|
||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
Tables\Filters\SelectFilter::make('group')
|
SelectFilter::make('group')
|
||||||
->options(fn () => LanguageLine::query()->distinct()->pluck('group', 'group')),
|
->options(fn () => LanguageLine::query()->distinct()->pluck('group', 'group')),
|
||||||
])
|
])
|
||||||
->defaultSort('key');
|
->defaultSort('key');
|
||||||
@@ -69,38 +78,38 @@ class LanguageLineResource extends Resource
|
|||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => Pages\ListLanguageLines::route('/'),
|
'index' => ListLanguageLines::route('/'),
|
||||||
'create' => Pages\CreateLanguageLine::route('/create'),
|
'create' => CreateLanguageLine::route('/create'),
|
||||||
'edit' => Pages\EditLanguageLine::route('/{record}/edit'),
|
'edit' => EditLanguageLine::route('/{record}/edit'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<Forms\Components\Textarea>
|
* @return array<Textarea>
|
||||||
*/
|
*/
|
||||||
private static function localeInputs(): array
|
private static function localeInputs(): array
|
||||||
{
|
{
|
||||||
return static::localeCodes()
|
return static::localeCodes()
|
||||||
->map(fn (string $code) => Forms\Components\Textarea::make("text.{$code}")
|
->map(fn (string $code) => Textarea::make("text.{$code}")
|
||||||
->label(strtoupper($code))
|
->label(strtoupper($code))
|
||||||
->rows(2))
|
->rows(2))
|
||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<Tables\Columns\TextColumn>
|
* @return array<TextColumn>
|
||||||
*/
|
*/
|
||||||
private static function localeColumns(): array
|
private static function localeColumns(): array
|
||||||
{
|
{
|
||||||
return static::localeCodes()
|
return static::localeCodes()
|
||||||
->map(fn (string $code) => Tables\Columns\TextColumn::make("text.{$code}")
|
->map(fn (string $code) => TextColumn::make("text.{$code}")
|
||||||
->label(strtoupper($code))
|
->label(strtoupper($code))
|
||||||
->limit(40)
|
->limit(40)
|
||||||
->toggleable())
|
->toggleable())
|
||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function localeCodes(): \Illuminate\Support\Collection
|
private static function localeCodes(): Collection
|
||||||
{
|
{
|
||||||
return Language::query()->pluck('code');
|
return Language::query()->pluck('code');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Localization\Filament\Resources\LanguageLineResource\Pages;
|
namespace Modules\Core\Localization\Filament\Resources\LanguageLineResource\Pages;
|
||||||
|
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
@@ -17,7 +18,7 @@ class EditLanguageLine extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\DeleteAction::make()
|
DeleteAction::make()
|
||||||
->action(function (LanguageLine $record) {
|
->action(function (LanguageLine $record) {
|
||||||
app(TranslationService::class)->delete($record);
|
app(TranslationService::class)->delete($record);
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Localization\Filament\Resources\LanguageLineResource\Pages;
|
namespace Modules\Core\Localization\Filament\Resources\LanguageLineResource\Pages;
|
||||||
|
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
use Modules\Core\Localization\Filament\Resources\LanguageLineResource;
|
use Modules\Core\Localization\Filament\Resources\LanguageLineResource;
|
||||||
@@ -13,7 +14,7 @@ class ListLanguageLines extends ListRecords
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\CreateAction::make(),
|
CreateAction::make(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\Core\MigrateImport;
|
namespace Modules\Core\MigrateImport;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
use Modules\Core\MigrateImport\JudgeMe\JudgeMeExportImporter;
|
use Modules\Core\MigrateImport\JudgeMe\JudgeMeExportImporter;
|
||||||
use Modules\Core\MigrateImport\Shopify\ShopifyExportImporter;
|
use Modules\Core\MigrateImport\Shopify\ShopifyExportImporter;
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ class ImporterFactory
|
|||||||
['judgeme', 'export'] => new JudgeMeExportImporter,
|
['judgeme', 'export'] => new JudgeMeExportImporter,
|
||||||
// ["woocommerce", "export"] => new WooCommerceExportImporter(),
|
// ["woocommerce", "export"] => new WooCommerceExportImporter(),
|
||||||
// ["woocommerce", "api"] => new WooCommerceApiImporter(),
|
// ["woocommerce", "api"] => new WooCommerceApiImporter(),
|
||||||
default => throw new \InvalidArgumentException(
|
default => throw new InvalidArgumentException(
|
||||||
"No importer available for source \"{$spec->source}\" with type \"{$spec->type}\".",
|
"No importer available for source \"{$spec->source}\" with type \"{$spec->type}\".",
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace Modules\Core\MigrateImport\JudgeMe;
|
namespace Modules\Core\MigrateImport\JudgeMe;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
class JudgeMeCsvReader
|
class JudgeMeCsvReader
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@@ -12,7 +14,7 @@ class JudgeMeCsvReader
|
|||||||
$handle = fopen($csvPath, 'r');
|
$handle = fopen($csvPath, 'r');
|
||||||
|
|
||||||
if ($handle === false) {
|
if ($handle === false) {
|
||||||
throw new \RuntimeException("Could not open CSV file: {$csvPath}");
|
throw new RuntimeException("Could not open CSV file: {$csvPath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
$headers = fgetcsv($handle);
|
$headers = fgetcsv($handle);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\Core\MigrateImport\JudgeMe;
|
namespace Modules\Core\MigrateImport\JudgeMe;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Modules\Core\MigrateImport\Importer;
|
use Modules\Core\MigrateImport\Importer;
|
||||||
@@ -75,7 +76,7 @@ class JudgeMeExportImporter implements Importer
|
|||||||
foreach ($urls as $url) {
|
foreach ($urls as $url) {
|
||||||
try {
|
try {
|
||||||
$review->addMediaFromUrl($url)->toMediaCollection(ProductReview::IMAGES_COLLECTION);
|
$review->addMediaFromUrl($url)->toMediaCollection(ProductReview::IMAGES_COLLECTION);
|
||||||
} catch (\Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
Log::warning('JudgeMe import: failed to download review image', [
|
Log::warning('JudgeMe import: failed to download review image', [
|
||||||
'review_id' => $review->id,
|
'review_id' => $review->id,
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace Modules\Core\MigrateImport\Shopify;
|
namespace Modules\Core\MigrateImport\Shopify;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
class ShopifyCsvReader
|
class ShopifyCsvReader
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@@ -12,7 +14,7 @@ class ShopifyCsvReader
|
|||||||
$handle = fopen($csvPath, 'r');
|
$handle = fopen($csvPath, 'r');
|
||||||
|
|
||||||
if ($handle === false) {
|
if ($handle === false) {
|
||||||
throw new \RuntimeException("Could not open CSV file: {$csvPath}");
|
throw new RuntimeException("Could not open CSV file: {$csvPath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
$headers = fgetcsv($handle);
|
$headers = fgetcsv($handle);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace Modules\Core\MigrateImport\Shopify;
|
namespace Modules\Core\MigrateImport\Shopify;
|
||||||
|
|
||||||
|
use Lunar\Models\TaxClass;
|
||||||
|
use Lunar\Models\ProductOption;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Lunar\Models\Collection;
|
use Lunar\Models\Collection;
|
||||||
use Lunar\Models\CollectionGroup;
|
use Lunar\Models\CollectionGroup;
|
||||||
@@ -120,7 +122,7 @@ class ShopifyExportImporter implements Importer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<int, \Lunar\Models\ProductOption>
|
* @return array<int, ProductOption>
|
||||||
*/
|
*/
|
||||||
private function attachOptions(Product $product, array $row): array
|
private function attachOptions(Product $product, array $row): array
|
||||||
{
|
{
|
||||||
@@ -146,7 +148,7 @@ class ShopifyExportImporter implements Importer
|
|||||||
string $handle,
|
string $handle,
|
||||||
int $index,
|
int $index,
|
||||||
array $row,
|
array $row,
|
||||||
\Lunar\Models\TaxClass $taxClass,
|
TaxClass $taxClass,
|
||||||
Currency $currency,
|
Currency $currency,
|
||||||
array $options,
|
array $options,
|
||||||
): void {
|
): void {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Notification;
|
namespace Modules\Core\Notification;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
|
|
||||||
class NotificationRegistry
|
class NotificationRegistry
|
||||||
@@ -44,7 +45,7 @@ class NotificationRegistry
|
|||||||
$notification->delay($event->delaySeconds);
|
$notification->delay($event->delaySeconds);
|
||||||
}
|
}
|
||||||
$notification->notifiable()->notify($notification);
|
$notification->notifiable()->notify($notification);
|
||||||
} catch (\Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
report($e);
|
report($e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Option;
|
namespace Modules\Core\Option;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Exception;
|
||||||
|
use RuntimeException;
|
||||||
use Traversable;
|
use Traversable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,7 +42,7 @@ final class LazyOption extends Option
|
|||||||
public function __construct($callback, array $arguments = [])
|
public function __construct($callback, array $arguments = [])
|
||||||
{
|
{
|
||||||
if (!is_callable($callback)) {
|
if (!is_callable($callback)) {
|
||||||
throw new \InvalidArgumentException("Invalid callback given");
|
throw new InvalidArgumentException("Invalid callback given");
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->callback = $callback;
|
$this->callback = $callback;
|
||||||
@@ -71,7 +74,7 @@ final class LazyOption extends Option
|
|||||||
return $this->option()->getOrCall($callable);
|
return $this->option()->getOrCall($callable);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getOrThrow(\Exception $ex)
|
public function getOrThrow(Exception $ex)
|
||||||
{
|
{
|
||||||
return $this->option()->getOrThrow($ex);
|
return $this->option()->getOrThrow($ex);
|
||||||
}
|
}
|
||||||
@@ -146,7 +149,7 @@ final class LazyOption extends Option
|
|||||||
if ($option instanceof Option) {
|
if ($option instanceof Option) {
|
||||||
$this->option = $option;
|
$this->option = $option;
|
||||||
} else {
|
} else {
|
||||||
throw new \RuntimeException(
|
throw new RuntimeException(
|
||||||
sprintf("Expected instance of %s", Option::class),
|
sprintf("Expected instance of %s", Option::class),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Option;
|
namespace Modules\Core\Option;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use Exception;
|
||||||
use EmptyIterator;
|
use EmptyIterator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +26,7 @@ final class None extends Option
|
|||||||
|
|
||||||
public function get()
|
public function get()
|
||||||
{
|
{
|
||||||
throw new \RuntimeException("None has no value.");
|
throw new RuntimeException("None has no value.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getOrCall($callable)
|
public function getOrCall($callable)
|
||||||
@@ -37,7 +39,7 @@ final class None extends Option
|
|||||||
return $default;
|
return $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getOrThrow(\Exception $ex)
|
public function getOrThrow(Exception $ex)
|
||||||
{
|
{
|
||||||
throw $ex;
|
throw $ex;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Option;
|
namespace Modules\Core\Option;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
use ArrayAccess;
|
use ArrayAccess;
|
||||||
use IteratorAggregate;
|
use IteratorAggregate;
|
||||||
|
|
||||||
@@ -164,7 +165,7 @@ abstract class Option implements IteratorAggregate
|
|||||||
abstract public function getOrCall($callable);
|
abstract public function getOrCall($callable);
|
||||||
|
|
||||||
/** @return T */
|
/** @return T */
|
||||||
abstract public function getOrThrow(\Exception $ex);
|
abstract public function getOrThrow(Exception $ex);
|
||||||
|
|
||||||
abstract public function isEmpty(): bool;
|
abstract public function isEmpty(): bool;
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Option;
|
namespace Modules\Core\Option;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
use ArrayIterator;
|
use ArrayIterator;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ final class Some extends Option
|
|||||||
return $this->value;
|
return $this->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getOrThrow(\Exception $ex)
|
public function getOrThrow(Exception $ex)
|
||||||
{
|
{
|
||||||
return $this->value;
|
return $this->value;
|
||||||
}
|
}
|
||||||
@@ -88,7 +89,7 @@ final class Some extends Option
|
|||||||
/** @var mixed */
|
/** @var mixed */
|
||||||
$rs = $callable($this->value);
|
$rs = $callable($this->value);
|
||||||
if (!$rs instanceof Option) {
|
if (!$rs instanceof Option) {
|
||||||
throw new \RuntimeException(
|
throw new RuntimeException(
|
||||||
"Callables passed to flatMap() must return an Option. Maybe you should use map() instead?",
|
"Callables passed to flatMap() must return an Option. Maybe you should use map() instead?",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Payment\Drivers;
|
||||||
|
|
||||||
|
use Lunar\Exceptions\Carts\CartException;
|
||||||
|
use Lunar\Exceptions\DisallowMultipleCartOrdersException;
|
||||||
|
use Lunar\Exceptions\FingerprintMismatchException;
|
||||||
|
use Lunar\Models\Cart;
|
||||||
|
use Lunar\Models\Order;
|
||||||
|
use Modules\Core\Checkout\Contracts\PaymentDriver;
|
||||||
|
use Modules\Core\Checkout\Services\CheckoutService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared by every payment type with no real gateway to confirm against —
|
||||||
|
* cash-in-hand, cash-on-delivery — where the shopper pays at pickup/on
|
||||||
|
* delivery, not at checkout. confirm() has nothing to wait on, so it places
|
||||||
|
* the order immediately, same as Lunar's own OfflinePayment would, but
|
||||||
|
* through CheckoutService::placeOrder() so it goes through the same
|
||||||
|
* fingerprint check every other driver does. $data is unused: nothing about
|
||||||
|
* this confirmation depends on gateway-specific payload.
|
||||||
|
*
|
||||||
|
* Sets the order status to config("lunar.payments.types.{$type}.authorized")
|
||||||
|
* afterward, using the type actually confirmed — not a hardcoded key —
|
||||||
|
* since this one driver is shared across multiple types.
|
||||||
|
* placeOrder() itself leaves the order at Lunar's configured draft_status,
|
||||||
|
* same as every driver is responsible for moving it on from.
|
||||||
|
*/
|
||||||
|
class OfflinePaymentDriver implements PaymentDriver
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly CheckoutService $checkout,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always true — no external dependency to be missing.
|
||||||
|
*/
|
||||||
|
public function isConfigured(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws FingerprintMismatchException
|
||||||
|
* @throws CartException
|
||||||
|
* @throws DisallowMultipleCartOrdersException
|
||||||
|
*/
|
||||||
|
public function confirm(Cart $cart, string $type, string $fingerprint, array $data): Order
|
||||||
|
{
|
||||||
|
$order = $this->checkout->placeOrder($fingerprint);
|
||||||
|
|
||||||
|
$order->update([
|
||||||
|
'status' => config("lunar.payments.types.{$type}.authorized", $order->status),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $order->refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Payment\Drivers;
|
||||||
|
|
||||||
|
use Lunar\Exceptions\FingerprintMismatchException;
|
||||||
|
use Lunar\Exceptions\Carts\CartException;
|
||||||
|
use Lunar\Exceptions\DisallowMultipleCartOrdersException;
|
||||||
|
use Lunar\Models\Cart;
|
||||||
|
use Lunar\Models\Order;
|
||||||
|
use Lunar\Stripe\Actions\UpdateOrderFromIntent;
|
||||||
|
use Lunar\Stripe\Facades\Stripe;
|
||||||
|
use Lunar\Stripe\Models\StripePaymentIntent;
|
||||||
|
use Modules\Core\Checkout\Contracts\PaymentDriver;
|
||||||
|
use Modules\Core\Checkout\Services\CheckoutService;
|
||||||
|
use Modules\Core\Payment\Exceptions\PaymentNotConfirmedException;
|
||||||
|
use Stripe\PaymentIntent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps Lunar\Stripe\StripePaymentType::authorize() to satisfy
|
||||||
|
* Modules\Core\Checkout\Contracts\PaymentDriver — calls
|
||||||
|
* CheckoutService::placeOrder($fingerprint) at the moment Stripe confirms
|
||||||
|
* payment, instead of the vendor's own Cart::createOrder() call.
|
||||||
|
*
|
||||||
|
* This is a fork, not a decoration: StripePaymentType::authorize() is
|
||||||
|
* `final` and calls Cart::createOrder() directly with no seam to redirect
|
||||||
|
* that one call — so this class reimplements authorize()'s logic (intent
|
||||||
|
* retrieval, capture-on-policy, status mapping via UpdateOrderFromIntent)
|
||||||
|
* rather than wrapping the vendor method. Kept deliberately close to the
|
||||||
|
* original so a lunarphp/stripe upgrade is easy to diff against. See
|
||||||
|
* docs/payments.md.
|
||||||
|
*/
|
||||||
|
class StripePaymentDriver implements PaymentDriver
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly CheckoutService $checkout,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same key lunarphp/stripe's own StripeManager reads its API key from
|
||||||
|
* (Stripe::setApiKey(config('services.stripe.key')) in
|
||||||
|
* StripeManager::__construct()) — no key, no usable driver.
|
||||||
|
*/
|
||||||
|
public function isConfigured(): bool
|
||||||
|
{
|
||||||
|
return filled(config('services.stripe.key'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws PaymentNotConfirmedException if Stripe hasn't confirmed the
|
||||||
|
* payment intent (wrong intent id, already processed, order already
|
||||||
|
* placed, or the gateway call itself fails) — nothing here should be
|
||||||
|
* treated as "place the order anyway."
|
||||||
|
* @throws FingerprintMismatchException
|
||||||
|
* @throws CartException
|
||||||
|
*/
|
||||||
|
public function confirm(Cart $cart, string $type, string $fingerprint, array $data): Order
|
||||||
|
{
|
||||||
|
$paymentIntentId = $data['payment_intent'];
|
||||||
|
|
||||||
|
$paymentIntentModel = StripePaymentIntent::where('intent_id', $paymentIntentId)->first();
|
||||||
|
|
||||||
|
if ($paymentIntentModel && ! $paymentIntentModel->isActive()) {
|
||||||
|
throw new PaymentNotConfirmedException('Payment intent already processed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $paymentIntentModel) {
|
||||||
|
$paymentIntentModel = StripePaymentIntent::create([
|
||||||
|
'intent_id' => $paymentIntentId,
|
||||||
|
'cart_id' => $cart->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$paymentIntentModel->update(['processing_at' => now()]);
|
||||||
|
|
||||||
|
$stripe = Stripe::getClient();
|
||||||
|
$paymentIntent = $stripe->paymentIntents->retrieve($paymentIntentId);
|
||||||
|
|
||||||
|
if (! $paymentIntent) {
|
||||||
|
throw new PaymentNotConfirmedException('Unable to locate payment intent.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$policy = config('lunar.stripe.policy', 'automatic');
|
||||||
|
|
||||||
|
if ($paymentIntent->status === PaymentIntent::STATUS_REQUIRES_CAPTURE && $policy === 'automatic') {
|
||||||
|
$paymentIntent = $stripe->paymentIntents->capture($paymentIntentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($paymentIntent->status !== PaymentIntent::STATUS_SUCCEEDED) {
|
||||||
|
$paymentIntentModel->update(['status' => $paymentIntent->status]);
|
||||||
|
|
||||||
|
throw new PaymentNotConfirmedException(
|
||||||
|
$paymentIntent->last_payment_error->message ?? "Payment intent status: {$paymentIntent->status}."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$order = $this->checkout->placeOrder($fingerprint);
|
||||||
|
} catch (DisallowMultipleCartOrdersException|CartException $e) {
|
||||||
|
throw new PaymentNotConfirmedException($e->getMessage(), previous: $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
$paymentIntentModel->order_id = $order->id;
|
||||||
|
$paymentIntentModel->status = $paymentIntent->status;
|
||||||
|
$paymentIntentModel->processed_at = now();
|
||||||
|
$paymentIntentModel->save();
|
||||||
|
|
||||||
|
UpdateOrderFromIntent::execute($order, $paymentIntent);
|
||||||
|
|
||||||
|
return $order->refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Payment\Exceptions;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown by a Modules\Core\Checkout\Contracts\PaymentDriver when the
|
||||||
|
* gateway has not confirmed payment — wrong/expired intent, already
|
||||||
|
* processed, or the gateway itself rejects the confirmation. A driver
|
||||||
|
* throws this instead of silently placing the order: CheckoutService::
|
||||||
|
* placeOrder() must only ever be called once a driver has positively
|
||||||
|
* confirmed payment, never as a fallback.
|
||||||
|
*/
|
||||||
|
class PaymentNotConfirmedException extends RuntimeException
|
||||||
|
{
|
||||||
|
public function __construct(string $message, ?Throwable $previous = null)
|
||||||
|
{
|
||||||
|
parent::__construct($message, previous: $previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Payment\Filament\Resources;
|
||||||
|
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Columns\ToggleColumn;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Modules\Core\Payment\Filament\Resources\PaymentMethodResource\Pages\ListPaymentMethods;
|
||||||
|
use Modules\Core\Payment\Models\PaymentMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One row per payment type key (config('lunar.payments.types')), seeded by
|
||||||
|
* InstallLunarCommand — never created/deleted here, only edited. `enabled`
|
||||||
|
* toggles inline; `data.fee` (currently the only type-specific setting, for
|
||||||
|
* cash-on-delivery's flat surcharge — see ApplyCashOnDeliveryFee) is edited
|
||||||
|
* via a modal action rather than a dedicated form field, since not every
|
||||||
|
* type has the same data keys.
|
||||||
|
*/
|
||||||
|
class PaymentMethodResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = PaymentMethod::class;
|
||||||
|
|
||||||
|
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-credit-card';
|
||||||
|
|
||||||
|
protected static string|\UnitEnum|null $navigationGroup = 'Settings';
|
||||||
|
|
||||||
|
protected static ?string $modelLabel = 'Payment Method';
|
||||||
|
|
||||||
|
protected static ?string $pluralModelLabel = 'Payment Methods';
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('type')
|
||||||
|
->label('Type'),
|
||||||
|
ToggleColumn::make('enabled')
|
||||||
|
->label('Enabled'),
|
||||||
|
TextColumn::make('data.fee')
|
||||||
|
->label('Fee')
|
||||||
|
->formatStateUsing(fn (?int $state) => $state
|
||||||
|
? number_format($state / 100, 2)
|
||||||
|
: '—'),
|
||||||
|
TextColumn::make('updated_at')
|
||||||
|
->label('Last updated')
|
||||||
|
->dateTime(),
|
||||||
|
])
|
||||||
|
->recordActions([
|
||||||
|
static::editFeeAction(),
|
||||||
|
])
|
||||||
|
->defaultSort('type');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* $data['fee'] is stored as an integer minor unit (cents), matching
|
||||||
|
* Lunar's own Price convention everywhere else in this codebase — the
|
||||||
|
* form collects/displays a decimal and converts at the boundary.
|
||||||
|
*/
|
||||||
|
private static function editFeeAction(): Action
|
||||||
|
{
|
||||||
|
return Action::make('edit_fee')
|
||||||
|
->label('Edit fee')
|
||||||
|
->icon('heroicon-o-pencil')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('fee')
|
||||||
|
->label('Fee')
|
||||||
|
->numeric()
|
||||||
|
->minValue(0)
|
||||||
|
->step(0.01)
|
||||||
|
->helperText('Flat surcharge added when this payment method is selected.'),
|
||||||
|
])
|
||||||
|
->fillForm(fn (PaymentMethod $record) => [
|
||||||
|
'fee' => filled($record->data['fee'] ?? null) ? $record->data['fee'] / 100 : null,
|
||||||
|
])
|
||||||
|
->action(function (PaymentMethod $record, array $data) {
|
||||||
|
$record->update([
|
||||||
|
'data' => [
|
||||||
|
...$record->data->toArray(),
|
||||||
|
'fee' => filled($data['fee']) ? (int) round($data['fee'] * 100) : null,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => ListPaymentMethods::route('/'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function canCreate(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function canDelete($record = null): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Payment\Filament\Resources\PaymentMethodResource\Pages;
|
||||||
|
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
use Modules\Core\Payment\Filament\Resources\PaymentMethodResource;
|
||||||
|
|
||||||
|
class ListPaymentMethods extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = PaymentMethodResource::class;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Payment\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin-editable settings for one payment type key (matching a key in
|
||||||
|
* config('lunar.payments.types')) — enabled/disabled, and whatever type-
|
||||||
|
* specific data it needs (starts with 'fee' for cash-on-delivery's flat
|
||||||
|
* surcharge). Mirrors Lunar's own Discount model: a single jsonb 'data'
|
||||||
|
* column holding keyed settings, rather than a fixed column per setting or
|
||||||
|
* a separate conditions table — new settings are a code change (a new key
|
||||||
|
* read from data), not a migration.
|
||||||
|
*
|
||||||
|
* Seeded once per type by InstallLunarCommand (skip-if-exists, same
|
||||||
|
* idempotent convention as seedStorefrontLabels()) — never auto-created on
|
||||||
|
* read, so a read path stays a pure read.
|
||||||
|
*/
|
||||||
|
class PaymentMethod extends Model
|
||||||
|
{
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'enabled' => 'boolean',
|
||||||
|
'data' => AsArrayObject::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Payment\Pipelines\Cart;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Lunar\DataTypes\Price;
|
||||||
|
use Lunar\Models\Contracts\Cart as CartContract;
|
||||||
|
use Modules\Core\Payment\Models\PaymentMethod;
|
||||||
|
|
||||||
|
final class ApplyCashOnDeliveryFee
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Called just before cart totals are calculated.
|
||||||
|
*
|
||||||
|
* @param Closure(CartContract): mixed $next
|
||||||
|
*/
|
||||||
|
public function handle(CartContract $cart, Closure $next): mixed
|
||||||
|
{
|
||||||
|
if (($cart->meta['payment_method'] ?? null) === 'cash-on-delivery') {
|
||||||
|
$fee = (int) (PaymentMethod::where('type', 'cash-on-delivery')->value('data->fee') ?? 0);
|
||||||
|
|
||||||
|
$cart->shippingTotal = new Price(
|
||||||
|
($cart->shippingTotal?->value ?? 0) + $fee,
|
||||||
|
$cart->currency,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($cart);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Lunar\Pipelines\Cart\ApplyShipping;
|
||||||
|
|
||||||
|
class PaymentServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
$this->mergeConfigFrom(__DIR__ . '/../../config/payment.php', 'payment');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
config([
|
||||||
|
'lunar.payments.types' => array_merge(
|
||||||
|
config('lunar.payments.types', []),
|
||||||
|
config('payment.types', [])
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cartPipeline = config('lunar.cart.pipelines.cart', []);
|
||||||
|
$insertAfter = array_search(ApplyShipping::class, $cartPipeline, true);
|
||||||
|
|
||||||
|
foreach (config('payment.cart_pipeline', []) as $pipe) {
|
||||||
|
if (in_array($pipe, $cartPipeline, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($insertAfter === false) {
|
||||||
|
$cartPipeline[] = $pipe;
|
||||||
|
} else {
|
||||||
|
array_splice($cartPipeline, $insertAfter + 1, 0, [$pipe]);
|
||||||
|
$insertAfter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config(['lunar.cart.pipelines.cart' => $cartPipeline]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Modules\Core\ResultType;
|
namespace Modules\Core\ResultType;
|
||||||
|
|
||||||
|
use Modules\Core\Option\Option;
|
||||||
use Modules\Core\Option\None;
|
use Modules\Core\Option\None;
|
||||||
use Modules\Core\Option\Some;
|
use Modules\Core\Option\Some;
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ use Modules\Core\Option\Some;
|
|||||||
* @template T
|
* @template T
|
||||||
* @template E
|
* @template E
|
||||||
*
|
*
|
||||||
* @extends \Modules\Core\ResultType\Result<T,E>
|
* @extends Result<T, E>
|
||||||
*/
|
*/
|
||||||
final class Error extends Result
|
final class Error extends Result
|
||||||
{
|
{
|
||||||
@@ -39,7 +40,7 @@ final class Error extends Result
|
|||||||
*
|
*
|
||||||
* @param F $value
|
* @param F $value
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\ResultType\Result<T,F>
|
* @return Result<T, F>
|
||||||
*/
|
*/
|
||||||
public static function create($value): Error
|
public static function create($value): Error
|
||||||
{
|
{
|
||||||
@@ -49,7 +50,7 @@ final class Error extends Result
|
|||||||
/**
|
/**
|
||||||
* Get the success option value.
|
* Get the success option value.
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\Option\Option<T>
|
* @return Option<T>
|
||||||
*/
|
*/
|
||||||
public function success()
|
public function success()
|
||||||
{
|
{
|
||||||
@@ -63,7 +64,7 @@ final class Error extends Result
|
|||||||
*
|
*
|
||||||
* @param callable(T):S $f
|
* @param callable(T):S $f
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\ResultType\Result<S,E>
|
* @return Result<S, E>
|
||||||
*/
|
*/
|
||||||
public function map(callable $f): Result
|
public function map(callable $f): Result
|
||||||
{
|
{
|
||||||
@@ -76,20 +77,20 @@ final class Error extends Result
|
|||||||
* @template S
|
* @template S
|
||||||
* @template F
|
* @template F
|
||||||
*
|
*
|
||||||
* @param callable(T):\Modules\Core\ResultType\Result<S,F> $f
|
* @param callable(T):Result<S, F> $f
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\ResultType\Result<S,F>
|
* @return Result<S, F>
|
||||||
*/
|
*/
|
||||||
public function flatMap(callable $f): Result
|
public function flatMap(callable $f): Result
|
||||||
{
|
{
|
||||||
/** @var \Modules\Core\ResultType\Result<S,F> */
|
/** @var Result<S, F> */
|
||||||
return self::create($this->value);
|
return self::create($this->value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the error option value.
|
* Get the error option value.
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\Option\Option<E>
|
* @return Option<E>
|
||||||
*/
|
*/
|
||||||
public function error(): Some
|
public function error(): Some
|
||||||
{
|
{
|
||||||
@@ -103,7 +104,7 @@ final class Error extends Result
|
|||||||
*
|
*
|
||||||
* @param callable(E):F $f
|
* @param callable(E):F $f
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\ResultType\Result<T,F>
|
* @return Result<T, F>
|
||||||
*/
|
*/
|
||||||
public function mapError(callable $f): Result
|
public function mapError(callable $f): Result
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Modules\Core\ResultType;
|
namespace Modules\Core\ResultType;
|
||||||
|
|
||||||
|
use Modules\Core\Option\Option;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @template T
|
||||||
* @template E
|
* @template E
|
||||||
@@ -13,7 +15,7 @@ abstract class Result
|
|||||||
/**
|
/**
|
||||||
* Get the success option value.
|
* Get the success option value.
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\Option\Option<T>
|
* @return Option<T>
|
||||||
*/
|
*/
|
||||||
abstract public function success();
|
abstract public function success();
|
||||||
|
|
||||||
@@ -24,7 +26,7 @@ abstract class Result
|
|||||||
*
|
*
|
||||||
* @param callable(T):S $f
|
* @param callable(T):S $f
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\ResultType\Result<S,E>
|
* @return Result<S, E>
|
||||||
*/
|
*/
|
||||||
abstract public function map(callable $f);
|
abstract public function map(callable $f);
|
||||||
|
|
||||||
@@ -34,16 +36,16 @@ abstract class Result
|
|||||||
* @template S
|
* @template S
|
||||||
* @template F
|
* @template F
|
||||||
*
|
*
|
||||||
* @param callable(T):\Modules\Core\ResultType\Result<S,F> $f
|
* @param callable(T):Result<S, F> $f
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\ResultType\Result<S,F>
|
* @return Result<S, F>
|
||||||
*/
|
*/
|
||||||
abstract public function flatMap(callable $f);
|
abstract public function flatMap(callable $f);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the error option value.
|
* Get the error option value.
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\Option\Option<E>
|
* @return Option<E>
|
||||||
*/
|
*/
|
||||||
abstract public function error();
|
abstract public function error();
|
||||||
|
|
||||||
@@ -54,7 +56,7 @@ abstract class Result
|
|||||||
*
|
*
|
||||||
* @param callable(E):F $f
|
* @param callable(E):F $f
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\ResultType\Result<T,F>
|
* @return Result<T, F>
|
||||||
*/
|
*/
|
||||||
abstract public function mapError(callable $f);
|
abstract public function mapError(callable $f);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Modules\Core\ResultType;
|
namespace Modules\Core\ResultType;
|
||||||
|
|
||||||
|
use Modules\Core\Option\Option;
|
||||||
use Modules\Core\Option\None;
|
use Modules\Core\Option\None;
|
||||||
use Modules\Core\Option\Some;
|
use Modules\Core\Option\Some;
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ use Modules\Core\Option\Some;
|
|||||||
* @template T
|
* @template T
|
||||||
* @template E
|
* @template E
|
||||||
*
|
*
|
||||||
* @extends \Modules\Core\ResultType\Result<T,E>
|
* @extends Result<T, E>
|
||||||
*/
|
*/
|
||||||
final class Success extends Result
|
final class Success extends Result
|
||||||
{
|
{
|
||||||
@@ -39,7 +40,7 @@ final class Success extends Result
|
|||||||
*
|
*
|
||||||
* @param S $value
|
* @param S $value
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\ResultType\Result<S,E>
|
* @return Result<S, E>
|
||||||
*/
|
*/
|
||||||
public static function create($value): Success
|
public static function create($value): Success
|
||||||
{
|
{
|
||||||
@@ -49,7 +50,7 @@ final class Success extends Result
|
|||||||
/**
|
/**
|
||||||
* Get the success option value.
|
* Get the success option value.
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\Option\Option<T>
|
* @return Option<T>
|
||||||
*/
|
*/
|
||||||
public function success(): Some
|
public function success(): Some
|
||||||
{
|
{
|
||||||
@@ -63,7 +64,7 @@ final class Success extends Result
|
|||||||
*
|
*
|
||||||
* @param callable(T):S $f
|
* @param callable(T):S $f
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\ResultType\Result<S,E>
|
* @return Result<S, E>
|
||||||
*/
|
*/
|
||||||
public function map(callable $f): Result
|
public function map(callable $f): Result
|
||||||
{
|
{
|
||||||
@@ -76,9 +77,9 @@ final class Success extends Result
|
|||||||
* @template S
|
* @template S
|
||||||
* @template F
|
* @template F
|
||||||
*
|
*
|
||||||
* @param callable(T):\Modules\Core\ResultType\Result<S,F> $f
|
* @param callable(T):Result<S, F> $f
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\ResultType\Result<S,F>
|
* @return Result<S, F>
|
||||||
*/
|
*/
|
||||||
public function flatMap(callable $f)
|
public function flatMap(callable $f)
|
||||||
{
|
{
|
||||||
@@ -88,7 +89,7 @@ final class Success extends Result
|
|||||||
/**
|
/**
|
||||||
* Get the error option value.
|
* Get the error option value.
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\Option\Option<E>
|
* @return Option<E>
|
||||||
*/
|
*/
|
||||||
public function error()
|
public function error()
|
||||||
{
|
{
|
||||||
@@ -102,7 +103,7 @@ final class Success extends Result
|
|||||||
*
|
*
|
||||||
* @param callable(E):F $f
|
* @param callable(E):F $f
|
||||||
*
|
*
|
||||||
* @return \Modules\Core\ResultType\Result<T,F>
|
* @return Result<T, F>
|
||||||
*/
|
*/
|
||||||
public function mapError(callable $f): Result
|
public function mapError(callable $f): Result
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Review\Filament\Pages;
|
namespace Modules\Core\Review\Filament\Pages;
|
||||||
|
|
||||||
use Filament\Forms\Components\Group;
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Schemas\Components\Group;
|
||||||
|
use Filament\Actions\ViewAction;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Actions\DeleteBulkAction;
|
||||||
use Filament\Forms\Components\Placeholder;
|
use Filament\Forms\Components\Placeholder;
|
||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Form;
|
|
||||||
use Filament\Tables\Actions\Action;
|
|
||||||
use Filament\Tables\Actions\DeleteAction;
|
|
||||||
use Filament\Tables\Actions\DeleteBulkAction;
|
|
||||||
use Filament\Tables\Actions\ViewAction;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
@@ -40,10 +40,10 @@ class ManageProductReviews extends BaseManageRelatedRecords
|
|||||||
return 'Reviews';
|
return 'Reviews';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function form(Form $form): Form
|
public function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $form
|
return $schema
|
||||||
->schema([
|
->components([
|
||||||
TextInput::make('rating')
|
TextInput::make('rating')
|
||||||
->label('Rating')
|
->label('Rating')
|
||||||
->disabled(),
|
->disabled(),
|
||||||
@@ -127,12 +127,12 @@ class ManageProductReviews extends BaseManageRelatedRecords
|
|||||||
->filters([
|
->filters([
|
||||||
//
|
//
|
||||||
])
|
])
|
||||||
->actions([
|
->recordActions([
|
||||||
ViewAction::make(),
|
ViewAction::make(),
|
||||||
Action::make('reply')
|
Action::make('reply')
|
||||||
->label(fn (ProductReview $record) => $record->reply ? 'Edit reply' : 'Reply')
|
->label(fn (ProductReview $record) => $record->reply ? 'Edit reply' : 'Reply')
|
||||||
->icon('heroicon-o-chat-bubble-left-right')
|
->icon('heroicon-o-chat-bubble-left-right')
|
||||||
->form([
|
->schema([
|
||||||
Textarea::make('reply')
|
Textarea::make('reply')
|
||||||
->label('Reply')
|
->label('Reply')
|
||||||
->required(),
|
->required(),
|
||||||
@@ -146,7 +146,7 @@ class ManageProductReviews extends BaseManageRelatedRecords
|
|||||||
}),
|
}),
|
||||||
DeleteAction::make(),
|
DeleteAction::make(),
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->toolbarActions([
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Shipping\Carriers\Acs;
|
namespace Modules\Core\Shipping\Carriers\Acs;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Lunar\Models\Order;
|
use Lunar\Models\Order;
|
||||||
@@ -88,7 +89,7 @@ class AcsFulfillmentService implements CarrierFulfillmentInterface, SupportsMani
|
|||||||
public function cancelShipment(Shipment $shipment): void
|
public function cancelShipment(Shipment $shipment): void
|
||||||
{
|
{
|
||||||
if ($shipment->manifest_reference) {
|
if ($shipment->manifest_reference) {
|
||||||
throw new \RuntimeException('Cannot cancel a shipment already included in an issued manifest.');
|
throw new RuntimeException('Cannot cancel a shipment already included in an issued manifest.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->client->call('ACS_Delete_Voucher', [
|
$this->client->call('ACS_Delete_Voucher', [
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Shipping\Concerns;
|
namespace Modules\Core\Shipping\Concerns;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Lunar\DataTypes\ShippingOption;
|
use Lunar\DataTypes\ShippingOption;
|
||||||
use Lunar\Models\Cart;
|
use Lunar\Models\Cart;
|
||||||
@@ -27,7 +28,7 @@ use Lunar\Shipping\Models\ShippingRate;
|
|||||||
*/
|
*/
|
||||||
trait CachesLivePricing
|
trait CachesLivePricing
|
||||||
{
|
{
|
||||||
private function cached(ShippingRate $shippingRate, Cart $cart, \Closure $resolve): ?ShippingOption
|
private function cached(ShippingRate $shippingRate, Cart $cart, Closure $resolve): ?ShippingOption
|
||||||
{
|
{
|
||||||
return Cache::remember(
|
return Cache::remember(
|
||||||
"shipping.live_price.{$shippingRate->id}.{$cart->id}",
|
"shipping.live_price.{$shippingRate->id}.{$cart->id}",
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Shipping\Extensions;
|
namespace Modules\Core\Shipping\Extensions;
|
||||||
|
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Closure;
|
||||||
|
use Throwable;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Forms;
|
use Filament\Forms;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
@@ -20,28 +25,28 @@ class OrderViewExtension extends ViewPageExtension
|
|||||||
return $actions;
|
return $actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createShipmentAction(): Actions\Action
|
private function createShipmentAction(): Action
|
||||||
{
|
{
|
||||||
return Actions\Action::make('create_shipment')
|
return Action::make('create_shipment')
|
||||||
->label('Create Shipment')
|
->label('Create Shipment')
|
||||||
->icon('heroicon-o-truck')
|
->icon('heroicon-o-truck')
|
||||||
->modalSubmitActionLabel('Create Shipment')
|
->modalSubmitActionLabel('Create Shipment')
|
||||||
->form([
|
->schema([
|
||||||
Forms\Components\TextInput::make('weight')
|
TextInput::make('weight')
|
||||||
->label('Package weight (kg)')
|
->label('Package weight (kg)')
|
||||||
->numeric()
|
->numeric()
|
||||||
->minValue(0)
|
->minValue(0)
|
||||||
->helperText('Leave blank to use the carrier\'s default.'),
|
->helperText('Leave blank to use the carrier\'s default.'),
|
||||||
Forms\Components\TextInput::make('destination_location_id')
|
TextInput::make('destination_location_id')
|
||||||
->label('Box Now locker ID')
|
->label('Box Now locker ID')
|
||||||
->helperText('Only required for Box Now shipments.')
|
->helperText('Only required for Box Now shipments.')
|
||||||
->default(fn (Order $record) => $record->shippingAddress?->meta['box_now_locker']['locationId'] ?? null),
|
->default(fn (Order $record) => $record->shippingAddress?->meta['box_now_locker']['locationId'] ?? null),
|
||||||
Forms\Components\Toggle::make('confirm')
|
Toggle::make('confirm')
|
||||||
->label('Confirm')
|
->label('Confirm')
|
||||||
->helperText('This will create a real shipment with the carrier.')
|
->helperText('This will create a real shipment with the carrier.')
|
||||||
->rules([
|
->rules([
|
||||||
function () {
|
function () {
|
||||||
return function (string $attribute, $value, \Closure $fail) {
|
return function (string $attribute, $value, Closure $fail) {
|
||||||
if ($value !== true) {
|
if ($value !== true) {
|
||||||
$fail('Please confirm before creating the shipment.');
|
$fail('Please confirm before creating the shipment.');
|
||||||
}
|
}
|
||||||
@@ -49,7 +54,7 @@ class OrderViewExtension extends ViewPageExtension
|
|||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->action(function (Order $record, array $data, Actions\Action $action) {
|
->action(function (Order $record, array $data, Action $action) {
|
||||||
$service = $this->resolveFulfillmentService($record);
|
$service = $this->resolveFulfillmentService($record);
|
||||||
|
|
||||||
if (! $service) {
|
if (! $service) {
|
||||||
@@ -70,7 +75,7 @@ class OrderViewExtension extends ViewPageExtension
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$service->createShipment($record, $request);
|
$service->createShipment($record, $request);
|
||||||
} catch (\Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
report($e);
|
report($e);
|
||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Shipping\Extensions;
|
namespace Modules\Core\Shipping\Extensions;
|
||||||
|
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Schemas\Components\Group;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Forms\Components\Group;
|
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Lunar\Admin\Support\Extending\BaseExtension;
|
use Lunar\Admin\Support\Extending\BaseExtension;
|
||||||
use Lunar\Shipping\Facades\Shipping;
|
use Lunar\Shipping\Facades\Shipping;
|
||||||
@@ -22,8 +23,8 @@ class ShippingMethodListExtension extends BaseExtension
|
|||||||
public function headerActions(array $actions): array
|
public function headerActions(array $actions): array
|
||||||
{
|
{
|
||||||
foreach ($actions as $action) {
|
foreach ($actions as $action) {
|
||||||
if ($action instanceof Actions\CreateAction) {
|
if ($action instanceof CreateAction) {
|
||||||
$action->form([
|
$action->schema([
|
||||||
ShippingMethodResource::getNameFormComponent(),
|
ShippingMethodResource::getNameFormComponent(),
|
||||||
Group::make([
|
Group::make([
|
||||||
ShippingMethodResource::getCodeFormComponent(),
|
ShippingMethodResource::getCodeFormComponent(),
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Shipping\Extensions;
|
namespace Modules\Core\Shipping\Extensions;
|
||||||
|
|
||||||
use Filament\Forms\Components\Component;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Forms\Components\Concerns\HasChildComponents;
|
use Filament\Schemas\Components\Component;
|
||||||
|
use Filament\Schemas\Components\Concerns\HasChildComponents;
|
||||||
|
use Filament\Schemas\Components\Utilities\Get;
|
||||||
|
use InvalidArgumentException;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Form;
|
|
||||||
use Filament\Forms\Get;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Lunar\Admin\Support\Extending\ResourceExtension;
|
use Lunar\Admin\Support\Extending\ResourceExtension;
|
||||||
@@ -15,11 +16,11 @@ use Modules\Core\Shipping\Contracts\SupportsLivePricing;
|
|||||||
|
|
||||||
class ShippingMethodResourceExtension extends ResourceExtension
|
class ShippingMethodResourceExtension extends ResourceExtension
|
||||||
{
|
{
|
||||||
public function extendForm(Form $form): Form
|
public function extendForm(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $form->schema(
|
return $schema->components(
|
||||||
$this->replaceChargeByField(
|
$this->replaceChargeByField(
|
||||||
$this->replaceDriverField($form->getComponents())
|
$this->replaceDriverField($schema->getComponents())
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -83,7 +84,7 @@ class ShippingMethodResourceExtension extends ResourceExtension
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return Shipping::driver($driver) instanceof SupportsLivePricing;
|
return Shipping::driver($driver) instanceof SupportsLivePricing;
|
||||||
} catch (\InvalidArgumentException) {
|
} catch (InvalidArgumentException) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Shipping\Filament\Pages;
|
namespace Modules\Core\Shipping\Filament\Pages;
|
||||||
|
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Actions\BulkAction;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Filament\Tables\Actions\Action;
|
|
||||||
use Filament\Tables\Actions\BulkAction;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Concerns\InteractsWithTable;
|
use Filament\Tables\Concerns\InteractsWithTable;
|
||||||
use Filament\Tables\Contracts\HasTable;
|
use Filament\Tables\Contracts\HasTable;
|
||||||
@@ -21,7 +21,7 @@ class ManagePickupManifests extends Page implements HasTable
|
|||||||
{
|
{
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
|
|
||||||
protected static ?string $navigationIcon = 'heroicon-o-truck';
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-truck';
|
||||||
|
|
||||||
protected static ?string $navigationLabel = 'Pickup Manifests';
|
protected static ?string $navigationLabel = 'Pickup Manifests';
|
||||||
|
|
||||||
@@ -38,11 +38,11 @@ class ManagePickupManifests extends Page implements HasTable
|
|||||||
* OrderResource uses 1) so this page never competes to be first even as
|
* OrderResource uses 1) so this page never competes to be first even as
|
||||||
* more Sales-group items are added later.
|
* more Sales-group items are added later.
|
||||||
*/
|
*/
|
||||||
protected static ?string $navigationGroup = 'Sales';
|
protected static string | \UnitEnum | null $navigationGroup = 'Sales';
|
||||||
|
|
||||||
protected static ?int $navigationSort = 100;
|
protected static ?int $navigationSort = 100;
|
||||||
|
|
||||||
protected static string $view = 'core::shipping.filament.pages.manage-pickup-manifests';
|
protected string $view = 'core::shipping.filament.pages.manage-pickup-manifests';
|
||||||
|
|
||||||
public function table(Table $table): Table
|
public function table(Table $table): Table
|
||||||
{
|
{
|
||||||
@@ -54,13 +54,13 @@ class ManagePickupManifests extends Page implements HasTable
|
|||||||
TextColumn::make('order.reference')->label('Order'),
|
TextColumn::make('order.reference')->label('Order'),
|
||||||
TextColumn::make('label_printed_at')->label('Printed')->dateTime()->placeholder('Not printed'),
|
TextColumn::make('label_printed_at')->label('Printed')->dateTime()->placeholder('Not printed'),
|
||||||
])
|
])
|
||||||
->actions([
|
->recordActions([
|
||||||
Action::make('print')
|
Action::make('print')
|
||||||
->label('Print')
|
->label('Print')
|
||||||
->icon('heroicon-o-printer')
|
->icon('heroicon-o-printer')
|
||||||
->action(fn (Shipment $record) => $this->printShipment($record)),
|
->action(fn (Shipment $record) => $this->printShipment($record)),
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->toolbarActions([
|
||||||
BulkAction::make('print_selected')
|
BulkAction::make('print_selected')
|
||||||
->label('Print selected')
|
->label('Print selected')
|
||||||
->icon('heroicon-o-printer')
|
->icon('heroicon-o-printer')
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
namespace Modules\Core\Shipping\Filament\Pages;
|
namespace Modules\Core\Shipping\Filament\Pages;
|
||||||
|
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Schemas\Components\Utilities\Get;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Form;
|
|
||||||
use Filament\Forms\Get;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@@ -36,12 +36,12 @@ use Lunar\Shipping\Models\ShippingRate;
|
|||||||
*/
|
*/
|
||||||
class ManageShippingRates extends BaseManageShippingRates
|
class ManageShippingRates extends BaseManageShippingRates
|
||||||
{
|
{
|
||||||
public function form(Form $form): Form
|
public function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
$form = parent::form($form);
|
$schema = parent::form($schema);
|
||||||
|
|
||||||
return $form->schema(
|
return $schema->components(
|
||||||
$this->labelPriceFieldsAsFallbackWhenLive($form->getComponents())
|
$this->labelPriceFieldsAsFallbackWhenLive($schema->getComponents())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user