diff --git a/.github/workflows/check-pr-maintainer-access.yml b/.github/workflows/check-pr-maintainer-access.yml new file mode 100644 index 0000000000..1083fad081 --- /dev/null +++ b/.github/workflows/check-pr-maintainer-access.yml @@ -0,0 +1,68 @@ +# Copied from https://raw.githubusercontent.com/filamentphp/filament/3.x/.github/workflows/check-pr-maintainer-access.yml +name: check-pr-maintainer-access + +on: + pull_request_target: + types: + - opened + +permissions: + pull-requests: write + +jobs: + notify-when-maintainers-cannot-edit: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v3 + with: + script: | + const query = ` + query($number: Int!) { + repository(owner: "lunarphp", name: "lunar") { + pullRequest(number: $number) { + headRepositoryOwner { + login + } + maintainerCanModify + } + } + } + ` + + const pullNumber = context.issue.number + const variables = { number: pullNumber } + + try { + console.log(`Check #${pullNumber} for maintainer edit access...`) + const result = await github.graphql(query, variables) + + console.log(JSON.stringify(result, null, 2)) + + const pullRequest = result.repository.pullRequest + + if (pullRequest.headRepositoryOwner.login === 'lunarphp') { + console.log('PR owned by lunarphp') + + return + } + + if (! pullRequest.maintainerCanModify) { + console.log('PR not owned by lunarphp and does not have maintainer edits enabled') + + await github.issues.createComment({ + issue_number: pullNumber, + owner: 'lunarphp', + repo: 'lunar', + body: 'Thanks for submitting a PR!\n\nIn order to review and merge PRs most efficiently, we require that all PRs grant maintainer edit access before we review them. If your fork belongs to a GitHub organization, please move the repository to your personal account and try again. If you\'re already using a personal fork, you can learn how to enable maintainer access [in the GitHub documentation](https://docs.github.com/en/github/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork).' + }) + + await github.issues.update({ + issue_number: pullNumber, + owner: 'lunarphp', + repo: context.repo.repo, + state: 'closed' + }) + } + } catch(error) { + console.log(error) + } diff --git a/docs/core/extending/discounts.md b/docs/core/extending/discounts.md index f23987d199..3e98b70239 100644 --- a/docs/core/extending/discounts.md +++ b/docs/core/extending/discounts.md @@ -1,3 +1,4 @@ + # Discounts ## Overview @@ -45,3 +46,51 @@ class MyCustomDiscountType extends AbstractDiscountType } } ``` + + +## Adding form fields for your discount in the admin panel + +If you require fields in the Lunar admin for your discount type, ensure your discount implements `Lunar\Admin\Base\LunarPanelDiscountInterface`. You will need to provide the `lunarPanelSchema`, `lunarPanelOnFill` and `lunarPanelOnSave` methods. + +```php +label('My label') + ->required(), + ]; + } + + /** + * Mutate the model data before displaying it in the admin form. + */ + public function lunarPanelOnFill(array $data): array + { + // optionally do something with $data + return $data; + } + + /** + * Mutate the form data before saving it to the discount model. + */ + public function lunarPanelOnSave(array $data): array + { + // optionally do something with $data + return $data; + } +} +``` diff --git a/docs/core/installation.md b/docs/core/installation.md index 4dd1862e15..9633b8b4dc 100644 --- a/docs/core/installation.md +++ b/docs/core/installation.md @@ -29,13 +29,14 @@ You may need to update your app's `composer.json` to set `"minimum-stability": " ### Add the LunarUser Trait -Some parts of the core rely on the `User` model having certain relationships set up. We've bundled these into a trait which you must add to any models that represent users in your database. +Some parts of the core rely on the User model having certain relationships set up. We have bundled these into a trait and an interface, which you must add to any models that represent users in your database. ```php use Lunar\Base\Traits\LunarUser; +use Lunar\Base\LunarUser as LunarUserInterface; // ... -class User extends Authenticatable +class User extends Authenticatable implements LunarUserInterface { use LunarUser; // ... diff --git a/docs/core/reference/carts.md b/docs/core/reference/carts.md index 1aa386b648..5a0a8c948e 100644 --- a/docs/core/reference/carts.md +++ b/docs/core/reference/carts.md @@ -69,6 +69,20 @@ $cartLine = new \Lunar\Models\CartLine([ $cart->lines()->create([/* .. */]); ``` +### Validation + +When adding items to a cart there are a series of validation actions which are run, which are defined in the `config/lunar/cart.php` config file. + +These actions will throw a `Lunar\Exceptions\Carts\CartException`. + +```php +try { + $cart->add($purchasable, 500); +} catch (\Lunar\Exceptions\Carts\CartException $e) { + $error = $e->getMessage(); +} +``` + Now you have a basic Cart up and running, it's time to show you how you would use the cart to get all the calculated totals and tax. diff --git a/docs/core/reference/products.md b/docs/core/reference/products.md index 995de86372..667fff4fe2 100644 --- a/docs/core/reference/products.md +++ b/docs/core/reference/products.md @@ -82,7 +82,7 @@ You can associate attributes to a product type like so (it's just a straight forward [Polymorphic relationship](https://laravel.com/docs/8.x/eloquent-relationships#many-to-many-polymorphic-relations)). ```php -$productType->mappedAttributes()->associate([ /* attribute ids ... */ ]); +$productType->mappedAttributes()->attach([ /* attribute ids ... */ ]); ``` You can associate both `Product` and `ProductVariant` attributes to a product type which will then display on either the diff --git a/docs/core/reference/taxation.md b/docs/core/reference/taxation.md index fa84669548..e93596149c 100644 --- a/docs/core/reference/taxation.md +++ b/docs/core/reference/taxation.md @@ -34,16 +34,16 @@ These specify a geographic zone for tax rates to be applied. Tax Zones can be ba Lunar\Models\TaxZone ``` -|Field|Description| -|:-|:-| -|id|| -|name|e.g. `UK`| -|zone_type|`country`, `state`, or `postcode`| -|price_display|`tax_inclusive` or `tax_exclusive`| -|active|true/false| -|default|true/false| -|created_at|| -|updated_at|| +|Field| Description | +|:-|:------------------------------------| +|id| | +|name| e.g. `UK` | +|zone_type| `country`, `states`, or `postcodes` | +|price_display| `tax_inclusive` or `tax_exclusive` | +|active| true/false | +|default| true/false | +|created_at| | +|updated_at| | ```php $taxZone = TaxZone::create([ diff --git a/docs/core/starter-kits.md b/docs/core/starter-kits.md index eb014636b7..ae47161065 100644 --- a/docs/core/starter-kits.md +++ b/docs/core/starter-kits.md @@ -22,8 +22,7 @@ If you would prefer to install Lunar into your own Laravel application, please f ## Installation ::: tip -We assume you have a suitable local development environment in which to run Lunar. We would highly suggest Laravel Herd -for this purpose. +We assume you have a suitable local development environment in which to run Lunar. We would highly suggest [Laravel Herd](https://herd.laravel.com) for this purpose. ::: ### Create a New Project diff --git a/packages/admin/resources/lang/en/fieldtypes.php b/packages/admin/resources/lang/en/fieldtypes.php index 46329780fc..e0b034d825 100644 --- a/packages/admin/resources/lang/en/fieldtypes.php +++ b/packages/admin/resources/lang/en/fieldtypes.php @@ -53,5 +53,20 @@ ], 'file' => [ 'label' => 'File', + 'form' => [ + 'file_types' => [ + 'label' => 'Allowed File Types', + 'placeholder' => 'New MIME', + ], + 'multiple' => [ + 'label' => 'Allow Multiple Files', + ], + 'min_files' => [ + 'label' => 'Min. Files', + ], + 'max_files' => [ + 'label' => 'Max. Files', + ], + ], ], ]; diff --git a/packages/admin/resources/views/resources/product-resource/widgets/product-options.blade.php b/packages/admin/resources/views/resources/product-resource/widgets/product-options.blade.php index c5e1c1db15..4d33260c27 100644 --- a/packages/admin/resources/views/resources/product-resource/widgets/product-options.blade.php +++ b/packages/admin/resources/views/resources/product-resource/widgets/product-options.blade.php @@ -122,7 +122,7 @@ @endif
- + @foreach($permutation['values'] as $option => $value) {{ $option }}: {{ $value }} @endforeach diff --git a/packages/admin/src/Base/LunarPanelDiscountInterface.php b/packages/admin/src/Base/LunarPanelDiscountInterface.php new file mode 100644 index 0000000000..936950cdee --- /dev/null +++ b/packages/admin/src/Base/LunarPanelDiscountInterface.php @@ -0,0 +1,21 @@ +helperText( __('lunarpanel::attribute.form.description.helper') ) + ->afterStateHydrated(fn ($state, $component) => $state ?: $component->state([Language::getDefault()->code => null])) ->maxLength(255), Forms\Components\TextInput::make('handle') ->label( @@ -133,6 +134,7 @@ public function table(Table $table): Table ]) ->headerActions([ Tables\Actions\CreateAction::make()->mutateFormDataUsing(function (array $data, RelationManager $livewire) { + $data['configuration'] = $data['configuration'] ?? []; $data['system'] = false; $data['attribute_type'] = $livewire->ownerRecord->attributable_type; $data['position'] = $livewire->ownerRecord->attributes()->count() + 1; diff --git a/packages/admin/src/Filament/Resources/CollectionResource.php b/packages/admin/src/Filament/Resources/CollectionResource.php index 6abb3cd209..64bf446e6b 100644 --- a/packages/admin/src/Filament/Resources/CollectionResource.php +++ b/packages/admin/src/Filament/Resources/CollectionResource.php @@ -4,7 +4,6 @@ use Filament\Forms; use Filament\Forms\Components\Component; -use Filament\Pages\Page; use Filament\Pages\SubNavigationPosition; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Database\Eloquent\Builder; @@ -89,16 +88,16 @@ protected static function getDefaultRelations(): array return []; } - public static function getRecordSubNavigation(Page $page): array + public static function getDefaultSubNavigation(): array { - return $page->generateNavigationItems([ + return [ Pages\EditCollection::class, Pages\ManageCollectionChildren::class, Pages\ManageCollectionProducts::class, Pages\ManageCollectionAvailability::class, Pages\ManageCollectionMedia::class, Pages\ManageCollectionUrls::class, - ]); + ]; } public static function getDefaultPages(): array diff --git a/packages/admin/src/Filament/Resources/CustomerResource.php b/packages/admin/src/Filament/Resources/CustomerResource.php index aa97eea5ae..c84b571cba 100644 --- a/packages/admin/src/Filament/Resources/CustomerResource.php +++ b/packages/admin/src/Filament/Resources/CustomerResource.php @@ -159,7 +159,13 @@ protected static function getCustomerGroupsFormComponent(): Component { return Forms\Components\CheckboxList::make('customerGroups') ->label(__('lunarpanel::customer.form.customer_groups.label')) - ->relationship(name: 'customerGroups', titleAttribute: 'name'); + ->relationship( + name: 'customerGroups', + titleAttribute: 'name', + modifyQueryUsing: fn (Builder $query) => $query->distinct( + ['id', 'name', 'handle', 'default'] + ) + ); } protected static function getDefaultTable(Table $table): Table diff --git a/packages/admin/src/Filament/Resources/DiscountResource.php b/packages/admin/src/Filament/Resources/DiscountResource.php index a7d053ecd4..90f11278d3 100644 --- a/packages/admin/src/Filament/Resources/DiscountResource.php +++ b/packages/admin/src/Filament/Resources/DiscountResource.php @@ -11,6 +11,7 @@ use Filament\Tables; use Filament\Tables\Table; use Illuminate\Support\Str; +use Lunar\Admin\Base\LunarPanelDiscountInterface; use Lunar\Admin\Filament\Resources\DiscountResource\Pages; use Lunar\Admin\Filament\Resources\DiscountResource\RelationManagers\BrandLimitationRelationManager; use Lunar\Admin\Filament\Resources\DiscountResource\RelationManagers\CollectionLimitationRelationManager; @@ -57,6 +58,18 @@ public static function getNavigationGroup(): ?string public static function getDefaultForm(Form $form): Form { + $discountSchemas = Discounts::getTypes()->map(function ($discount) { + if (! $discount instanceof LunarPanelDiscountInterface) { + return; + } + + return Forms\Components\Section::make(Str::slug(get_class($discount))) + ->heading($discount->getName()) + ->visible( + fn (Forms\Get $get) => $get('type') == get_class($discount) + )->schema($discount->lunarPanelSchema()); + })->filter(); + return $form->schema([ Forms\Components\Section::make('')->schema( static::getMainFormComponents() @@ -84,6 +97,7 @@ public static function getDefaultForm(Form $form): Form )->schema( static::getAmountOffFormComponents() ), + ...$discountSchemas, ]); } diff --git a/packages/admin/src/Filament/Resources/DiscountResource/Pages/EditDiscount.php b/packages/admin/src/Filament/Resources/DiscountResource/Pages/EditDiscount.php index 754c8fe12b..6859259c9d 100644 --- a/packages/admin/src/Filament/Resources/DiscountResource/Pages/EditDiscount.php +++ b/packages/admin/src/Filament/Resources/DiscountResource/Pages/EditDiscount.php @@ -3,6 +3,7 @@ namespace Lunar\Admin\Filament\Resources\DiscountResource\Pages; use Filament\Actions; +use Lunar\Admin\Base\LunarPanelDiscountInterface; use Lunar\Admin\Filament\Resources\DiscountResource; use Lunar\Admin\Support\Pages\BaseEditRecord; use Lunar\DiscountTypes\BuyXGetY; @@ -19,8 +20,29 @@ protected function getDefaultHeaderActions(): array ]; } + protected function mutateFormDataBeforeFill(array $data): array + { + if (class_exists($data['type'])) { + $type = new $data['type']; + + if ($type instanceof LunarPanelDiscountInterface) { + return $type->lunarPanelOnFill($data); + } + } + + return $data; + } + protected function mutateFormDataBeforeSave(array $data): array { + if (class_exists($data['type'])) { + $type = new $data['type']; + + if ($type instanceof LunarPanelDiscountInterface) { + return $type->lunarPanelOnSave($data); + } + } + $minPrices = $data['data']['min_prices'] ?? []; $fixedPrices = $data['data']['fixed_values'] ?? []; $currencies = Currency::enabled()->get(); diff --git a/packages/admin/src/Filament/Resources/ProductResource/Widgets/ProductOptionsWidget.php b/packages/admin/src/Filament/Resources/ProductResource/Widgets/ProductOptionsWidget.php index 088a96ee1f..a082be186d 100644 --- a/packages/admin/src/Filament/Resources/ProductResource/Widgets/ProductOptionsWidget.php +++ b/packages/admin/src/Filament/Resources/ProductResource/Widgets/ProductOptionsWidget.php @@ -105,7 +105,7 @@ public function configureBaseOptions(): void $options = []; foreach ($productOptions as $productOption) { - $values = $productOption->values->map(function ($value) { + $values = $productOption->values->count() ? $productOption->values->map(function ($value) { return $this->mapOptionValue($value, true); })->merge( $disabledSharedOptionValues->filter( @@ -113,7 +113,7 @@ public function configureBaseOptions(): void )->map( fn ($value) => $this->mapOptionValue($value, false) ) - )->sortBy('position')->values()->toArray(); + )->sortBy('position')->values()->toArray() : []; $options[] = $this->mapOption($productOption, $values); } diff --git a/packages/admin/src/Support/FieldTypes/File.php b/packages/admin/src/Support/FieldTypes/File.php index 19f4d83881..95abf266b8 100644 --- a/packages/admin/src/Support/FieldTypes/File.php +++ b/packages/admin/src/Support/FieldTypes/File.php @@ -13,9 +13,70 @@ class File extends BaseFieldType public static function getFilamentComponent(Attribute $attribute): Component { - return FileUpload::make($attribute->handle) + $file_types = $attribute->configuration->get('file_types'); + $multiple = (bool) $attribute->configuration->get('multiple'); + $min_files = $attribute->configuration->get('min_files'); + $max_files = $attribute->configuration->get('max_files'); + + $input = FileUpload::make($attribute->handle) ->when(filled($attribute->validation_rules), fn (FileUpload $component) => $component->rules($attribute->validation_rules)) ->required((bool) $attribute->required) ->helperText($attribute->translate('description')); + + if (! blank($file_types) && is_array($file_types)) { + $input->acceptedFileTypes($file_types); + } + + if ($multiple) { + $input->multiple(); + } + + if ($min_files) { + $input->minFiles($min_files); + } + + if ($max_files) { + $input->maxFiles($max_files); + } + + return $input; + } + + public static function getConfigurationFields(): array + { + return [ + \Filament\Forms\Components\TagsInput::make('file_types') + ->label( + __('lunarpanel::fieldtypes.file.form.file_types.label') + )->suggestions([ + 'image/*', + 'image/jpeg', + 'image/png', + 'image/gif', + 'audio/*', + 'audio/mpeg', + 'audio/aac', + 'audio/wav', + 'video/*', + 'video/mp4', + 'video/mpeg', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/rtf', + 'application/pdf', + ]) + ->placeholder(__('lunarpanel::fieldtypes.file.form.file_types.placeholder')) + ->reorderable(), + \Filament\Forms\Components\Toggle::make('multiple')->label( + __('lunarpanel::fieldtypes.file.form.multiple.label') + ), + \Filament\Forms\Components\TextInput::make('min_files') + ->label( + __('lunarpanel::fieldtypes.file.form.min_files.label') + )->nullable()->numeric(), + \Filament\Forms\Components\TextInput::make('max_files')->label( + __('lunarpanel::fieldtypes.file.form.max_files.label') + )->nullable()->numeric(), + ]; } } diff --git a/packages/admin/src/Support/Forms/AttributeData.php b/packages/admin/src/Support/Forms/AttributeData.php index ba21380bd4..3d03ebef03 100644 --- a/packages/admin/src/Support/Forms/AttributeData.php +++ b/packages/admin/src/Support/Forms/AttributeData.php @@ -62,6 +62,13 @@ public function getFilamentComponent(Attribute $attribute): Component return $state; }) ->mutateDehydratedStateUsing(function ($state) use ($attribute) { + if ($attribute->type == FileFieldType::class) { + $instance = new $attribute->type; + $instance->setValue($state); + + return $instance; + } + if ( ! $state || (get_class($state) != $attribute->type) diff --git a/packages/admin/src/Support/Forms/Components/Attributes.php b/packages/admin/src/Support/Forms/Components/Attributes.php index 1ea605743d..ba2fd4beee 100644 --- a/packages/admin/src/Support/Forms/Components/Attributes.php +++ b/packages/admin/src/Support/Forms/Components/Attributes.php @@ -40,7 +40,7 @@ protected function setUp(): void } if ($modelClass == ProductVariant::class) { - $productTypeId = $record->product?->product_type_id ?: ProductType::first()->id; + $productTypeId = $record?->product?->product_type_id ?: ProductType::first()->id; // If we have a product type, the attributes should be based off that. if ($productTypeId) { diff --git a/packages/admin/src/Support/Pages/BaseListRecords.php b/packages/admin/src/Support/Pages/BaseListRecords.php index dadf83c846..7c7432543b 100644 --- a/packages/admin/src/Support/Pages/BaseListRecords.php +++ b/packages/admin/src/Support/Pages/BaseListRecords.php @@ -45,7 +45,7 @@ protected function applySearchToTableQuery(Builder $query): Builder $query->when( ! $ids->isEmpty(), - fn ($query) => $query->orderByRaw("field(id, {$placeholders})", $ids->toArray()) // TODO: Only supports MySQL + fn ($query) => $query->orderBySequence($ids->toArray()) ); } diff --git a/packages/admin/src/Support/Resources/BaseResource.php b/packages/admin/src/Support/Resources/BaseResource.php index 68657d7b71..845642e2b8 100644 --- a/packages/admin/src/Support/Resources/BaseResource.php +++ b/packages/admin/src/Support/Resources/BaseResource.php @@ -76,7 +76,7 @@ protected static function applyGlobalSearchAttributeConstraints(Builder $query, $query->when( ! $ids->isEmpty(), - fn ($query) => $query->orderByRaw("field(id, {$placeholders})", $ids->toArray()) + fn ($query) => $query->orderBySequence($ids->toArray()) ); } else { diff --git a/packages/admin/src/Support/Synthesizers/FileSynth.php b/packages/admin/src/Support/Synthesizers/FileSynth.php index ac812d6a74..cbd95c87a3 100644 --- a/packages/admin/src/Support/Synthesizers/FileSynth.php +++ b/packages/admin/src/Support/Synthesizers/FileSynth.php @@ -2,6 +2,7 @@ namespace Lunar\Admin\Support\Synthesizers; +use Illuminate\Support\Arr; use Lunar\FieldTypes\File; class FileSynth extends AbstractFieldSynth @@ -21,7 +22,7 @@ public function hydrate($value) $instance->setValue($value); - return $instance; + return Arr::wrap($value); } public function get(&$target, $key) diff --git a/packages/core/config/cart.php b/packages/core/config/cart.php index 371ab00698..f9f77b8f93 100644 --- a/packages/core/config/cart.php +++ b/packages/core/config/cart.php @@ -89,10 +89,12 @@ 'add_to_cart' => [ Lunar\Validation\CartLine\CartLineQuantity::class, + Lunar\Validation\CartLine\CartLineStock::class, ], 'update_cart_line' => [ Lunar\Validation\CartLine\CartLineQuantity::class, + Lunar\Validation\CartLine\CartLineStock::class, ], 'remove_from_cart' => [], diff --git a/packages/core/config/orders.php b/packages/core/config/orders.php index ba9d9b5d07..7e522cc9f8 100644 --- a/packages/core/config/orders.php +++ b/packages/core/config/orders.php @@ -67,7 +67,7 @@ | Order Pipelines |-------------------------------------------------------------------------- | - | Define which pipelines should be run throughout an orders lifecycle. + | Define which pipelines should be run throughout an order's lifecycle. | The default ones provided should suit most needs, however you are | free to add your own as you see fit. | diff --git a/packages/core/database/migrations/2024_01_11_100000_add_description_to_attributes_table.php b/packages/core/database/migrations/2024_01_11_100000_add_description_to_attributes_table.php index 4c9cad4fb7..1ac6417209 100644 --- a/packages/core/database/migrations/2024_01_11_100000_add_description_to_attributes_table.php +++ b/packages/core/database/migrations/2024_01_11_100000_add_description_to_attributes_table.php @@ -9,7 +9,7 @@ public function up(): void { Schema::table($this->prefix.'attributes', function (Blueprint $table) { - $table->json('description')->after('name'); + $table->json('description')->after('name')->nullable(); }); } diff --git a/packages/core/database/state/PopulateProductOptionLabelWithName.php b/packages/core/database/state/PopulateProductOptionLabelWithName.php index 19d7cb1b8f..3fe46468b5 100644 --- a/packages/core/database/state/PopulateProductOptionLabelWithName.php +++ b/packages/core/database/state/PopulateProductOptionLabelWithName.php @@ -20,8 +20,7 @@ public function run() } DB::transaction(function () { - ProductOption::where('label', '') - ->orWhereNull('label') + ProductOption::whereNull('label') ->update([ 'label' => DB::raw('name'), ]); @@ -37,6 +36,6 @@ protected function canRun() protected function shouldRun() { - return ProductOption::whereJsonLength('label', 0)->count() > 0; + return ProductOption::whereNull('label')->count() > 0; } } diff --git a/packages/core/src/Console/InstallLunar.php b/packages/core/src/Console/InstallLunar.php index 4bd3cc7070..cc7ec431b7 100644 --- a/packages/core/src/Console/InstallLunar.php +++ b/packages/core/src/Console/InstallLunar.php @@ -300,7 +300,6 @@ private function shouldOverwriteConfig(): bool private function publishConfiguration(bool $forcePublish = false): void { $params = [ - '--provider' => "Lunar\LunarServiceProvider", '--tag' => 'lunar', ]; diff --git a/packages/core/src/FieldTypes/File.php b/packages/core/src/FieldTypes/File.php index 123e2807b4..cd8c2b623f 100644 --- a/packages/core/src/FieldTypes/File.php +++ b/packages/core/src/FieldTypes/File.php @@ -8,7 +8,7 @@ class File implements FieldType, JsonSerializable { /** - * @var string + * @var string|array|null */ protected $value; @@ -30,9 +30,9 @@ public function jsonSerialize(): mixed /** * Create a new instance of File field type. * - * @param string $value + * @param string|array|null $value */ - public function __construct($value = '') + public function __construct($value = null) { $this->setValue($value); } @@ -44,13 +44,17 @@ public function __construct($value = '') */ public function __toString() { - return $this->getValue(); + if (is_array($this->getValue())) { + return implode(', ', $this->getValue()); + } + + return $this->getValue() ?? ''; } /** * Return the value of this field. * - * @return string + * @return string|array|null */ public function getValue() { @@ -60,10 +64,14 @@ public function getValue() /** * Set the value of this field. * - * @param string $value + * @param string|array|null $value */ public function setValue($value) { + if (blank($value)) { + $value = null; + } + $this->value = $value; } @@ -74,7 +82,10 @@ public function getConfig(): array { return [ 'options' => [ + 'file_types' => 'array', + 'multiple' => 'boolean', 'max_files' => 'numeric', + 'min_files' => 'numeric', ], ]; } diff --git a/packages/core/src/LunarServiceProvider.php b/packages/core/src/LunarServiceProvider.php index ced1c4b454..aa593e2d22 100644 --- a/packages/core/src/LunarServiceProvider.php +++ b/packages/core/src/LunarServiceProvider.php @@ -6,6 +6,7 @@ use Illuminate\Auth\Events\Login; use Illuminate\Auth\Events\Logout; use Illuminate\Console\Scheduling\Schedule; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Events\MigrationsEnded; use Illuminate\Database\Events\MigrationsStarted; use Illuminate\Database\Events\NoPendingMigrations; @@ -190,6 +191,7 @@ public function boot(): void } $this->registerObservers(); + $this->registerBuilderMacros(); $this->registerBlueprintMacros(); $this->registerStateListeners(); @@ -309,6 +311,35 @@ protected function registerObservers(): void } } + protected function registerBuilderMacros(): void + { + Builder::macro('orderBySequence', function (array $ids) { + /** @var Builder $this */ + $driver = $this->getConnection()->getDriverName(); + + if (empty($ids)) { + return $this; + } + + if ($driver === 'mysql') { + $placeholders = implode(',', array_fill(0, count($ids), '?')); + + return $this->orderByRaw("FIELD(id, {$placeholders})", $ids); + } + + if ($driver === 'pgsql') { + $orderCases = ''; + foreach ($ids as $index => $id) { + $orderCases .= "WHEN id = $id THEN $index "; + } + + return $this->orderByRaw("CASE $orderCases ELSE ".count($ids).' END'); + } + + return $this; + }); + } + /** * Register the blueprint macros. */ diff --git a/packages/core/src/Models/Cart.php b/packages/core/src/Models/Cart.php index e8ebd35e46..b700ecce2a 100644 --- a/packages/core/src/Models/Cart.php +++ b/packages/core/src/Models/Cart.php @@ -572,7 +572,7 @@ public function setShippingOption(ShippingOption $option, bool $refresh = true): */ public function getShippingOption(): ?ShippingOption { - return ShippingManifest::getShippingOption($this); + return ShippingManifest::getShippingOption($this->calculate()); } /** @@ -592,18 +592,20 @@ public function createOrder( bool $allowMultipleOrders = false, ?int $orderIdToUpdate = null ): Order { + $cart = $this->refresh()->recalculate(); + foreach (config('lunar.cart.validators.order_create', [ ValidateCartForOrderCreation::class, ]) as $action) { app($action)->using( - cart: $this, + cart: $cart, )->validate(); } return app( config('lunar.cart.actions.order_create', CreateOrder::class) )->execute( - $this->refresh()->recalculate(), + $cart, $allowMultipleOrders, $orderIdToUpdate )->then(fn ($order) => $order->refresh()); diff --git a/packages/stripe/src/StripePaymentType.php b/packages/stripe/src/StripePaymentType.php index 0e873e2205..b882a320fc 100644 --- a/packages/stripe/src/StripePaymentType.php +++ b/packages/stripe/src/StripePaymentType.php @@ -8,6 +8,7 @@ use Lunar\Base\DataTransferObjects\PaymentChecks; use Lunar\Base\DataTransferObjects\PaymentRefund; use Lunar\Events\PaymentAttemptEvent; +use Lunar\Exceptions\Carts\CartException; use Lunar\Exceptions\DisallowMultipleCartOrdersException; use Lunar\Models\Transaction; use Lunar\PaymentTypes\AbstractPayment; @@ -61,7 +62,7 @@ final public function authorize(): ?PaymentAuthorize if (! $this->order) { try { $this->order = $this->cart->createOrder(); - } catch (DisallowMultipleCartOrdersException $e) { + } catch (DisallowMultipleCartOrdersException|CartException $e) { $failure = new PaymentAuthorize( success: false, message: $e->getMessage(), diff --git a/packages/table-rate-shipping/src/ShippingPlugin.php b/packages/table-rate-shipping/src/ShippingPlugin.php index 6ffd06719a..671672eeef 100644 --- a/packages/table-rate-shipping/src/ShippingPlugin.php +++ b/packages/table-rate-shipping/src/ShippingPlugin.php @@ -24,6 +24,10 @@ public function boot(Panel $panel): void public function register(Panel $panel): void { + if (! config('lunar.shipping-tables.enabled')) { + return; + } + $panel->navigationGroups([ NavigationGroup::make('shipping') ->label( diff --git a/packages/table-rate-shipping/src/ShippingServiceProvider.php b/packages/table-rate-shipping/src/ShippingServiceProvider.php index 384e3ac33c..246c9c663b 100644 --- a/packages/table-rate-shipping/src/ShippingServiceProvider.php +++ b/packages/table-rate-shipping/src/ShippingServiceProvider.php @@ -14,10 +14,13 @@ class ShippingServiceProvider extends ServiceProvider { - public function boot(ShippingModifiers $shippingModifiers) + public function register() { $this->mergeConfigFrom(__DIR__.'/../config/shipping-tables.php', 'lunar.shipping-tables'); + } + public function boot(ShippingModifiers $shippingModifiers) + { if (! config('lunar.shipping-tables.enabled')) { return; } diff --git a/tests/admin/Feature/Filament/Resources/ProductResource/Widgets/CollectionTreeViewTest.php b/tests/admin/Feature/Filament/Resources/ProductResource/Widgets/CollectionTreeViewTest.php index e85a76ddf2..f90d523b99 100644 --- a/tests/admin/Feature/Filament/Resources/ProductResource/Widgets/CollectionTreeViewTest.php +++ b/tests/admin/Feature/Filament/Resources/ProductResource/Widgets/CollectionTreeViewTest.php @@ -111,8 +111,7 @@ ], ['id' => $collection->id]) ->assertCount('nodes', 1) ->assertSet('nodes.0.children.0.id', $child->id) - ->mountAction('makeRoot', ['id' => $child->id]) - ->callAction('makeRoot') + ->callAction('makeRoot', arguments: ['id' => $child->id]) ->assertCount('nodes.0.children', 0) ->assertCount('nodes', 2); }); diff --git a/tests/admin/Feature/Filament/Resources/StaffResource/Pages/EditStaffTest.php b/tests/admin/Feature/Filament/Resources/StaffResource/Pages/EditStaffTest.php index f212ee5526..59c5c0df3d 100644 --- a/tests/admin/Feature/Filament/Resources/StaffResource/Pages/EditStaffTest.php +++ b/tests/admin/Feature/Filament/Resources/StaffResource/Pages/EditStaffTest.php @@ -75,13 +75,10 @@ ->assertHasNoFormErrors(); expect($staff->hasExactRoles($roles)) - ->toBeTrue(); - - // check assigned permissions does not include role's permissions - expect($permissions->reject(fn ($val, $handle) => $handle == $rolePermission)->toArray()) - ->toEqualCanonicalizing($staff->getDirectPermissions()->pluck('name')->toArray()); - - // check role's permission - expect($rolePermission) + ->toBeTrue() + ->and( + $permissions->reject(fn ($val, $handle) => $handle == $rolePermission)->keys()->toArray() + )->toEqualCanonicalizing($staff->getDirectPermissions()->pluck('name')->toArray()) + ->and($rolePermission) ->toEqualCanonicalizing($staff->getPermissionsViaRoles()->pluck('name')->toArray()); }); diff --git a/tests/core/Unit/Actions/Carts/RemovePurchasableTest.php b/tests/core/Unit/Actions/Carts/RemovePurchasableTest.php index e21518130a..2f86030031 100644 --- a/tests/core/Unit/Actions/Carts/RemovePurchasableTest.php +++ b/tests/core/Unit/Actions/Carts/RemovePurchasableTest.php @@ -17,7 +17,9 @@ 'currency_id' => $currency->id, ]); - $purchasable = ProductVariant::factory()->create(); + $purchasable = ProductVariant::factory()->create([ + 'stock' => 1, + ]); Price::factory()->create([ 'price' => 100, diff --git a/tests/core/Unit/Actions/Carts/UpdateCartLineTest.php b/tests/core/Unit/Actions/Carts/UpdateCartLineTest.php index 8c219cfe05..279756221f 100644 --- a/tests/core/Unit/Actions/Carts/UpdateCartLineTest.php +++ b/tests/core/Unit/Actions/Carts/UpdateCartLineTest.php @@ -18,7 +18,9 @@ 'currency_id' => $currency->id, ]); - $purchasable = ProductVariant::factory()->create(); + $purchasable = ProductVariant::factory()->create([ + 'stock' => 1, + ]); Price::factory()->create([ 'price' => 100, diff --git a/tests/core/Unit/Models/CartTest.php b/tests/core/Unit/Models/CartTest.php index 1efe360c9b..1ce8c12855 100644 --- a/tests/core/Unit/Models/CartTest.php +++ b/tests/core/Unit/Models/CartTest.php @@ -538,7 +538,9 @@ 'currency_id' => $currency->id, ]); - $purchasable = ProductVariant::factory()->create(); + $purchasable = ProductVariant::factory()->create([ + 'stock' => 1, + ]); Price::factory()->create([ 'price' => 100, @@ -562,7 +564,9 @@ 'currency_id' => $currency->id, ]); - $purchasable = ProductVariant::factory()->create(); + $purchasable = ProductVariant::factory()->create([ + 'stock' => 1, + ]); Price::factory()->create([ 'price' => 100, @@ -614,7 +618,9 @@ 'currency_id' => $currency->id, ]); - $purchasable = ProductVariant::factory()->create(); + $purchasable = ProductVariant::factory()->create([ + 'stock' => 1, + ]); Price::factory()->create([ 'price' => 100, @@ -691,7 +697,9 @@ 'currency_id' => $currency->id, ]); - $purchasable = ProductVariant::factory()->create(); + $purchasable = ProductVariant::factory()->create([ + 'stock' => 1, + ]); Price::factory()->create([ 'price' => 100, diff --git a/tests/core/Unit/Validation/Cart/ValidateCartForOrderCreationTest.php b/tests/core/Unit/Validation/Cart/ValidateCartForOrderCreationTest.php index 5c0fb842ca..233430db2c 100644 --- a/tests/core/Unit/Validation/Cart/ValidateCartForOrderCreationTest.php +++ b/tests/core/Unit/Validation/Cart/ValidateCartForOrderCreationTest.php @@ -98,6 +98,13 @@ 'shippable' => true, ]); + \Lunar\Models\Price::factory()->create([ + 'currency_id' => $currency->id, + 'priceable_id' => $purchasable->id, + 'priceable_type' => get_class($purchasable), + 'price' => 500, + ]); + $cart->lines()->create([ 'purchasable_type' => get_class($purchasable), 'purchasable_id' => $purchasable->id, @@ -113,10 +120,8 @@ 'cart_id' => $cart->id, ]); - $this->expectException(CartException::class); - $this->expectExceptionMessage(__('lunar::exceptions.carts.shipping_option_missing')); + expect(fn () => $validator->validate())->toThrow(CartException::class); - $validator->validate(); }); test('can validate collection with partial shipping address', function () { @@ -131,6 +136,13 @@ 'shippable' => true, ]); + \Lunar\Models\Price::factory()->create([ + 'currency_id' => $currency->id, + 'priceable_id' => $purchasable->id, + 'priceable_type' => get_class($purchasable), + 'price' => 500, + ]); + $cart->lines()->create([ 'purchasable_type' => get_class($purchasable), 'purchasable_id' => $purchasable->id, @@ -183,6 +195,13 @@ 'shippable' => true, ]); + \Lunar\Models\Price::factory()->create([ + 'currency_id' => $currency->id, + 'priceable_id' => $purchasable->id, + 'priceable_type' => get_class($purchasable), + 'price' => 500, + ]); + $cart->lines()->create([ 'purchasable_type' => get_class($purchasable), 'purchasable_id' => $purchasable->id, @@ -246,6 +265,13 @@ 'shippable' => true, ]); + \Lunar\Models\Price::factory()->create([ + 'currency_id' => $currency->id, + 'priceable_id' => $purchasable->id, + 'priceable_type' => get_class($purchasable), + 'price' => 500, + ]); + $cart->lines()->create([ 'purchasable_type' => get_class($purchasable), 'purchasable_id' => $purchasable->id, diff --git a/tests/shipping/TestUtils.php b/tests/shipping/TestUtils.php index 663a9f33ad..9a2d82a721 100644 --- a/tests/shipping/TestUtils.php +++ b/tests/shipping/TestUtils.php @@ -9,7 +9,7 @@ trait TestUtils { - public function createCart($currency = null, $price = 100, $quantity = 1) + public function createCart($currency = null, $price = 100, $quantity = 1, $calculate = true) { if (! $currency) { $currency = Currency::factory()->create([ @@ -38,10 +38,10 @@ public function createCart($currency = null, $price = 100, $quantity = 1) 'quantity' => $quantity, ]); - $this->assertNull($cart->total); - $this->assertNull($cart->taxTotal); - $this->assertNull($cart->subTotal); + expect($cart->total)->toBeNull() + ->and($cart->taxTotal)->toBeNull() + ->and($cart->subTotal)->toBeNull(); - return $cart->calculate(); + return $calculate ? $cart->calculate() : $cart; } } diff --git a/tests/shipping/Unit/ShippingModifierTest.php b/tests/shipping/Unit/ShippingModifierTest.php new file mode 100644 index 0000000000..d1ed8e8b2c --- /dev/null +++ b/tests/shipping/Unit/ShippingModifierTest.php @@ -0,0 +1,72 @@ +create([ + 'default' => true, + ]); + + $country = Country::factory()->create(); + + TaxClass::factory()->create([ + 'default' => true, + ]); + + $shippingZone = ShippingZone::factory()->create([ + 'type' => 'countries', + ]); + + $shippingZone->countries()->attach($country); + + $shippingMethod = ShippingMethod::factory()->create([ + 'driver' => 'ship-by', + 'code' => 'BASEDEL', + 'data' => [ + 'minimum_spend' => [ + "{$currency->code}" => 200, + ], + ], + ]); + + $shippingRate = \Lunar\Shipping\Models\ShippingRate::factory()->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + + $shippingRate->prices()->createMany([ + [ + 'price' => 1000, + 'min_quantity' => 1, + 'currency_id' => $currency->id, + ], + [ + 'price' => 0, + 'min_quantity' => 500, + 'currency_id' => $currency->id, + ], + ]); + + $cart = $this->createCart($currency, 6000, calculate: false); + + $cart->shippingAddress()->create( + \Lunar\Models\CartAddress::factory()->make([ + 'country_id' => $country->id, + 'shipping_option' => 'BASEDEL', + 'state' => null, + 'type' => 'shipping', + ])->toArray() + ); + + $option = $cart->refresh()->getShippingOption(); + + expect($option->price->value)->toBe(0); +})->group('shipping-modifier'); diff --git a/tests/stripe/Unit/StripePaymentTypeTest.php b/tests/stripe/Unit/StripePaymentTypeTest.php index b33e66aa64..a4c8e6af43 100644 --- a/tests/stripe/Unit/StripePaymentTypeTest.php +++ b/tests/stripe/Unit/StripePaymentTypeTest.php @@ -65,6 +65,7 @@ it('will fail if cart already has an order', function () { $cart = CartBuilder::build(); $order = $cart->createOrder(); + $order->update([ 'placed_at' => now(), ]); @@ -75,9 +76,12 @@ 'payment_intent' => 'PI_CAPTURE', ])->authorize(); - expect($response)->toBeInstanceOf(PaymentAuthorize::class); - expect($response->success)->toBeFalse(); - expect($response->message)->toBe('Carts can only have one order associated to them.'); + expect($response)->toBeInstanceOf(PaymentAuthorize::class) + ->and($response->success)->toBeFalse() + ->and($response->message)->toBeIn([ + 'Carts can only have one order associated to them.', + __('lunar::exceptions.carts.order_exists'), + ]); }); it('will fail if payment intent status is requires_payment_method', function () {