From 73ec6f94483fc77fa4d29ceadf4b33070ef14c39 Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Wed, 22 May 2024 10:06:44 +0200 Subject: [PATCH 01/17] Feature - Add support of extendsInfolist hook (#1531) This PR add support of extendsInfolist hook and fix headerWidget / FooterWidget not triggered on abstract BaseViewRecord --------- Co-authored-by: Glenn Jacobs --- docs/admin/extending/pages.md | 35 ++++++++++- .../OrderResource/Pages/ManageOrder.php | 2 +- .../src/Support/Pages/BaseViewRecord.php | 3 + .../Pages/Concerns/ExtendsInfolist.php | 18 ++++++ .../Support/Extending/ViewPageExtension.php | 59 +++++++++++++++++++ 5 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 packages/admin/src/Support/Pages/Concerns/ExtendsInfolist.php create mode 100644 tests/admin/Feature/Support/Extending/ViewPageExtension.php diff --git a/docs/admin/extending/pages.md b/docs/admin/extending/pages.md index 7cbf415194..d4ca2b7b4c 100644 --- a/docs/admin/extending/pages.md +++ b/docs/admin/extending/pages.md @@ -268,10 +268,26 @@ An example of extending a view page. ```php use Filament\Actions; + +use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Table; +use Filament\Infolists\Infolist; +use Filament\Infolists\Components\TextEntry; use Lunar\Admin\Support\Extending\ViewPageExtension; +use Lunar\Admin\Filament\Widgets; class MyViewExtension extends ViewPageExtension { + public function headerWidgets(array $widgets): array + { + $widgets = [ + ...$widgets, + Widgets\Dashboard\Orders\OrderStatsOverview::make(), + ]; + + return $widgets; + } + public function heading($title): string { return $title . ' - Example'; @@ -293,7 +309,24 @@ class MyViewExtension extends ViewPageExtension return $actions; } - + + public function extendsInfolist(Infolist $infolist): Infolist + { + return $infolist->schema([ + ...$infolist->getComponents(true), + TextEntry::make('custom_title'), + ]); + } + + public function footerWidgets(array $widgets): array + { + $widgets = [ + ...$widgets, + Widgets\Dashboard\Orders\LatestOrdersTable::make(), + ]; + + return $widgets; + } } // Typically placed in your AppServiceProvider file... diff --git a/packages/admin/src/Filament/Resources/OrderResource/Pages/ManageOrder.php b/packages/admin/src/Filament/Resources/OrderResource/Pages/ManageOrder.php index 71a05cd7b7..842ed18475 100644 --- a/packages/admin/src/Filament/Resources/OrderResource/Pages/ManageOrder.php +++ b/packages/admin/src/Filament/Resources/OrderResource/Pages/ManageOrder.php @@ -284,7 +284,7 @@ public static function getOrderSummaryInfolist(): Infolists\Components\Component ]); } - public function infolist(Infolist $infolist): Infolist + public function getDefaultInfolist(Infolist $infolist): Infolist { return $infolist ->schema([ diff --git a/packages/admin/src/Support/Pages/BaseViewRecord.php b/packages/admin/src/Support/Pages/BaseViewRecord.php index da805130aa..725ad6bbe8 100644 --- a/packages/admin/src/Support/Pages/BaseViewRecord.php +++ b/packages/admin/src/Support/Pages/BaseViewRecord.php @@ -6,8 +6,11 @@ abstract class BaseViewRecord extends ViewRecord { + use Concerns\ExtendsFooterWidgets; use Concerns\ExtendsHeaderActions; use Concerns\ExtendsHeaderWidgets; use Concerns\ExtendsHeadings; + use Concerns\ExtendsInfolist; + use \Lunar\Admin\Support\Concerns\CallsHooks; use \Lunar\Admin\Support\Concerns\CallsHooks; } diff --git a/packages/admin/src/Support/Pages/Concerns/ExtendsInfolist.php b/packages/admin/src/Support/Pages/Concerns/ExtendsInfolist.php new file mode 100644 index 0000000000..25d91e971e --- /dev/null +++ b/packages/admin/src/Support/Pages/Concerns/ExtendsInfolist.php @@ -0,0 +1,18 @@ +getDefaultInfolist($infolist)); + } + + protected function getDefaultInfolist(Infolist $infolist): Infolist + { + return $infolist; + } +} diff --git a/tests/admin/Feature/Support/Extending/ViewPageExtension.php b/tests/admin/Feature/Support/Extending/ViewPageExtension.php new file mode 100644 index 0000000000..4a531d9037 --- /dev/null +++ b/tests/admin/Feature/Support/Extending/ViewPageExtension.php @@ -0,0 +1,59 @@ +group('extending.view'); + +beforeEach(function () { + $this->asStaff(); + + $currency = \Lunar\Models\Currency::factory()->create([ + 'default' => true, + ]); + + $country = \Lunar\Models\Country::factory()->create(); + + $this->order = \Lunar\Models\Order::factory() + ->for(\Lunar\Models\Customer::factory()) + ->has(\Lunar\Models\OrderAddress::factory()->state([ + 'type' => 'shipping', + 'country_id' => $country->id, + ]), 'shippingAddress') + ->has(\Lunar\Models\OrderAddress::factory()->state([ + 'type' => 'billing', + 'country_id' => $country->id, + ]), 'billingAddress') + ->create([ + 'currency_code' => $currency->code, + 'meta' => [ + 'additional_info' => Str::random(), + ], + ]); + +}); + +it('can extend Infolist', function () { + $class = new class extends \Lunar\Admin\Support\Extending\ViewPageExtension + { + public function extendsInfolist(Infolist $infolist): Infolist + { + return $infolist->schema([ + ...$infolist->getComponents(true), + \Filament\Infolists\Components\TextEntry::make('custom_title') + ->label('custom_title'), + ]); + } + }; + + LunarPanel::registerExtension($class, ManageOrder::class); + + \Livewire\Livewire::test(ManageOrder::class, [ + 'record' => $this->order->getRouteKey(), + ]) + ->assertSee($this->order->reference) + ->assertSee('custom_title'); +}); From d84dc4da40140a0bd765ad506f1bd8e59c922376 Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Wed, 22 May 2024 11:19:56 +0200 Subject: [PATCH 02/17] New config on StandardMediaConversions (#1565) --- packages/admin/src/Filament/Resources/BrandResource.php | 2 +- .../CollectionResource/Pages/ManageCollectionProducts.php | 2 +- .../RelationManagers/ProductConditionRelationManager.php | 2 +- .../RelationManagers/ProductLimitationRelationManager.php | 2 +- .../RelationManagers/ProductRewardRelationManager.php | 2 +- packages/admin/src/Filament/Resources/ProductResource.php | 2 +- .../src/Support/RelationManagers/MediaRelationManager.php | 6 +++--- packages/core/config/media.php | 2 ++ .../database/state/EnsureMediaCollectionsAreRenamed.php | 2 +- packages/core/src/Base/StandardMediaDefinitions.php | 4 +++- packages/core/src/Models/Product.php | 2 +- .../RelationManagers/ShippingExclusionRelationManager.php | 2 +- tests/admin/Stubs/TestMediaDefinition.php | 6 +++--- tests/core/TestCase.php | 1 + tests/core/Unit/Traits/HasMediaTraitTest.php | 2 +- 15 files changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/admin/src/Filament/Resources/BrandResource.php b/packages/admin/src/Filament/Resources/BrandResource.php index d97ea81405..4e4294477d 100644 --- a/packages/admin/src/Filament/Resources/BrandResource.php +++ b/packages/admin/src/Filament/Resources/BrandResource.php @@ -116,7 +116,7 @@ protected static function getTableColumns(): array { return [ SpatieMediaLibraryImageColumn::make('thumbnail') - ->collection('images') + ->collection(config('lunar.media.collection')) ->conversion('small') ->limit(1) ->square() diff --git a/packages/admin/src/Filament/Resources/CollectionResource/Pages/ManageCollectionProducts.php b/packages/admin/src/Filament/Resources/CollectionResource/Pages/ManageCollectionProducts.php index bf04cfaa8d..33a8e21c06 100644 --- a/packages/admin/src/Filament/Resources/CollectionResource/Pages/ManageCollectionProducts.php +++ b/packages/admin/src/Filament/Resources/CollectionResource/Pages/ManageCollectionProducts.php @@ -60,7 +60,7 @@ public function table(Table $table): Table return $table->columns([ Tables\Columns\SpatieMediaLibraryImageColumn::make('thumbnail') - ->collection('images') + ->collection(config('lunar.media.collection')) ->conversion('small') ->limit(1) ->square() diff --git a/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductConditionRelationManager.php b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductConditionRelationManager.php index d642946cc4..5ad289b5c5 100644 --- a/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductConditionRelationManager.php +++ b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductConditionRelationManager.php @@ -64,7 +64,7 @@ public function table(Table $table): Table }), ])->columns([ Tables\Columns\SpatieMediaLibraryImageColumn::make('purchasable.thumbnail') - ->collection('images') + ->collection(config('lunar.media.collection')) ->conversion('small') ->limit(1) ->square() diff --git a/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductLimitationRelationManager.php b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductLimitationRelationManager.php index f7430351b9..005fefaa7d 100644 --- a/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductLimitationRelationManager.php +++ b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductLimitationRelationManager.php @@ -59,7 +59,7 @@ public function table(Table $table): Table }), ])->columns([ Tables\Columns\SpatieMediaLibraryImageColumn::make('purchasable.thumbnail') - ->collection('images') + ->collection(config('lunar.media.collection')) ->conversion('small') ->limit(1) ->square() diff --git a/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductRewardRelationManager.php b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductRewardRelationManager.php index e9d0de67aa..6dc13d2354 100644 --- a/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductRewardRelationManager.php +++ b/packages/admin/src/Filament/Resources/DiscountResource/RelationManagers/ProductRewardRelationManager.php @@ -64,7 +64,7 @@ public function table(Table $table): Table }), ])->columns([ Tables\Columns\SpatieMediaLibraryImageColumn::make('purchasable.thumbnail') - ->collection('images') + ->collection(config('lunar.media.collection')) ->conversion('small') ->limit(1) ->square() diff --git a/packages/admin/src/Filament/Resources/ProductResource.php b/packages/admin/src/Filament/Resources/ProductResource.php index 4f72d9c1c2..88132a8727 100644 --- a/packages/admin/src/Filament/Resources/ProductResource.php +++ b/packages/admin/src/Filament/Resources/ProductResource.php @@ -243,7 +243,7 @@ public static function getTableColumns(): array 'published' => 'success', }), SpatieMediaLibraryImageColumn::make('thumbnail') - ->collection('images') + ->collection(config('lunar.media.collection')) ->conversion('small') ->limit(1) ->square() diff --git a/packages/admin/src/Support/RelationManagers/MediaRelationManager.php b/packages/admin/src/Support/RelationManagers/MediaRelationManager.php index 7891b5c248..7ff506dc0b 100644 --- a/packages/admin/src/Support/RelationManagers/MediaRelationManager.php +++ b/packages/admin/src/Support/RelationManagers/MediaRelationManager.php @@ -56,12 +56,12 @@ public function table(Table $table): Table ->heading(function () { $product = $this->getOwnerRecord(); - return $product->getMediaCollectionTitle($this->mediaCollection) ?? Str::ucfirst($this->mediaCollection); + return $product->getMediaCollectionTitle(config('lunar.media.collection')) ?? Str::ucfirst($this->mediaCollection); }) ->description(function () { $product = $this->getOwnerRecord(); - - return $product->getMediaCollectionDescription($this->mediaCollection) ?? ''; + + return $product->getMediaCollectionDescription(config('lunar.media.collection')) ?? ''; }) ->recordTitleAttribute('name') ->modifyQueryUsing(fn (Builder $query) => $query->where('collection_name', $this->mediaCollection)->orderBy('order_column')) diff --git a/packages/core/config/media.php b/packages/core/config/media.php index f9b3fed363..d2d0814963 100644 --- a/packages/core/config/media.php +++ b/packages/core/config/media.php @@ -13,6 +13,8 @@ Lunar\Models\ProductOptionValue::class => StandardMediaDefinitions::class, ], + 'collection' => 'images', + 'fallback' => [ 'url' => env('FALLBACK_IMAGE_URL', null), 'path' => env('FALLBACK_IMAGE_PATH', null), diff --git a/packages/core/database/state/EnsureMediaCollectionsAreRenamed.php b/packages/core/database/state/EnsureMediaCollectionsAreRenamed.php index 70b2fde256..9701b71553 100644 --- a/packages/core/database/state/EnsureMediaCollectionsAreRenamed.php +++ b/packages/core/database/state/EnsureMediaCollectionsAreRenamed.php @@ -21,7 +21,7 @@ public function run() return; } - $this->getOutdatedMediaQuery()->update(['collection_name' => 'images']); + $this->getOutdatedMediaQuery()->update(['collection_name' => config('lunar.media.collection')]); } protected function shouldRun() diff --git a/packages/core/src/Base/StandardMediaDefinitions.php b/packages/core/src/Base/StandardMediaDefinitions.php index b123338e4c..7c838d829a 100644 --- a/packages/core/src/Base/StandardMediaDefinitions.php +++ b/packages/core/src/Base/StandardMediaDefinitions.php @@ -29,7 +29,9 @@ public function registerMediaCollections(HasMedia $model): void // Reset to avoid duplication $model->mediaCollections = []; - $collection = $model->addMediaCollection('images'); + $collection = $model->addMediaCollection( + config('lunar.media.collection') + ); if ($fallbackUrl) { $collection = $collection->useFallbackUrl($fallbackUrl); diff --git a/packages/core/src/Models/Product.php b/packages/core/src/Models/Product.php index f313c3a152..6af7e30750 100644 --- a/packages/core/src/Models/Product.php +++ b/packages/core/src/Models/Product.php @@ -113,7 +113,7 @@ public function productType(): BelongsTo */ public function images(): MorphMany { - return $this->media()->where('collection_name', 'images'); + return $this->media()->where('collection_name', config('lunar.media.collection')); } /** diff --git a/packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource/RelationManagers/ShippingExclusionRelationManager.php b/packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource/RelationManagers/ShippingExclusionRelationManager.php index f400d6260d..0aba85d4ca 100644 --- a/packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource/RelationManagers/ShippingExclusionRelationManager.php +++ b/packages/table-rate-shipping/src/Filament/Resources/ShippingExclusionListResource/RelationManagers/ShippingExclusionRelationManager.php @@ -52,7 +52,7 @@ public function table(Table $table): Table return $table ->columns([ Tables\Columns\SpatieMediaLibraryImageColumn::make('purchasable.thumbnail') - ->collection('images') + ->collection(config('lunar.media.collection')) ->conversion('small') ->limit(1) ->square() diff --git a/tests/admin/Stubs/TestMediaDefinition.php b/tests/admin/Stubs/TestMediaDefinition.php index 6eaff58431..e73853420b 100644 --- a/tests/admin/Stubs/TestMediaDefinition.php +++ b/tests/admin/Stubs/TestMediaDefinition.php @@ -14,14 +14,14 @@ public function registerMediaConversions(HasMedia $model, Media $media = null): public function registerMediaCollections(HasMedia $model): void { - $model->addMediaCollection('images'); + $model->addMediaCollection(config('lunar.media.collection')); $model->addMediaCollection('videos'); } public function getMediaCollectionTitles(): array { return [ - 'images' => 'Images', + config('lunar.media.collection') => 'Images', 'videos' => 'Videos', ]; } @@ -29,7 +29,7 @@ public function getMediaCollectionTitles(): array public function getMediaCollectionDescriptions(): array { return [ - 'images' => 'Images', + config('lunar.media.collection') => 'Images', 'videos' => 'Videos', ]; } diff --git a/tests/core/TestCase.php b/tests/core/TestCase.php index 879b5d3175..95fc201534 100644 --- a/tests/core/TestCase.php +++ b/tests/core/TestCase.php @@ -26,6 +26,7 @@ protected function setUp(): void Config::set('providers.users.model', User::class); Config::set('lunar.urls.generator', TestUrlGenerator::class); Config::set('lunar.taxes.driver', 'test'); + Config::set('lunar.media.collection', 'images'); Taxes::extend('test', function ($app) { return $app->make(TestTaxDriver::class); diff --git a/tests/core/Unit/Traits/HasMediaTraitTest.php b/tests/core/Unit/Traits/HasMediaTraitTest.php index ca00065fb2..1603bb8a54 100644 --- a/tests/core/Unit/Traits/HasMediaTraitTest.php +++ b/tests/core/Unit/Traits/HasMediaTraitTest.php @@ -18,7 +18,7 @@ $product = Product::factory()->create(); - $product->addMedia($file)->toMediaCollection('images'); + $product->addMedia($file)->toMediaCollection(config('lunar.media.collection')); $image = $product->images->first(); From f1dc5670b2b20ffb846cb92d9427ffd01a09bd60 Mon Sep 17 00:00:00 2001 From: alecritson Date: Wed, 22 May 2024 09:22:34 +0000 Subject: [PATCH 03/17] chore: fix code style --- .../admin/src/Support/RelationManagers/MediaRelationManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/admin/src/Support/RelationManagers/MediaRelationManager.php b/packages/admin/src/Support/RelationManagers/MediaRelationManager.php index 7ff506dc0b..eb9f7555b2 100644 --- a/packages/admin/src/Support/RelationManagers/MediaRelationManager.php +++ b/packages/admin/src/Support/RelationManagers/MediaRelationManager.php @@ -60,7 +60,7 @@ public function table(Table $table): Table }) ->description(function () { $product = $this->getOwnerRecord(); - + return $product->getMediaCollectionDescription(config('lunar.media.collection')) ?? ''; }) ->recordTitleAttribute('name') From 9dba10ee4cef8c7fcdf2f4ddb67a4cc782c166d2 Mon Sep 17 00:00:00 2001 From: Eugene van der Merwe Date: Wed, 22 May 2024 14:39:58 +0200 Subject: [PATCH 04/17] Fix transaction timestamp format (#1771) --- .../resources/views/infolists/components/transaction.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/admin/resources/views/infolists/components/transaction.blade.php b/packages/admin/resources/views/infolists/components/transaction.blade.php index fc8b895700..51cdc4a977 100644 --- a/packages/admin/resources/views/infolists/components/transaction.blade.php +++ b/packages/admin/resources/views/infolists/components/transaction.blade.php @@ -76,7 +76,7 @@ class="w-4" /> - {{ $transaction->created_at->format('jS F Y h:ma') }} + {{ $transaction->created_at->format('jS F Y h:ia') }} @if($threeD = $transaction->meta['threedSecure'] ?? null) From bc38f2786b71a9217bdea8351ec8ca6eb060506d Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Wed, 22 May 2024 14:53:12 +0100 Subject: [PATCH 05/17] Allow Stripe to be faked at a facade level (#1757) --- docs/core/upgrading.md | 15 ++ .../stripe/resources/responses/charge.json | 84 +++++++ .../stripe/resources/responses/charges.json | 91 +++++++ .../responses/payment_intent_created.json | 47 ++++ .../responses/payment_intent_fetched.json | 47 ++++ .../responses/payment_intent_paid.json | 236 ++++++++++++++++++ ...ayment_intent_requires_payment_method.json | 47 ++++ .../src/Actions/UpdateOrderFromIntent.php | 4 +- .../stripe/src/Components/PaymentForm.php | 8 +- packages/stripe/src/Facades/Stripe.php | 32 +++ packages/stripe/src/Facades/StripeFacade.php | 16 -- .../stripe/src/Managers/StripeManager.php | 24 +- packages/stripe/src/MockClient.php | 134 ++++++++++ packages/stripe/src/StripePaymentType.php | 11 +- .../src/StripePaymentsServiceProvider.php | 2 +- tests/stripe/TestCase.php | 6 +- .../stripe/Unit/Actions/StoreChargesTest.php | 4 +- .../Actions/UpdateOrderFromIntentTest.php | 2 +- .../Unit/Managers/StripeManagerTest.php | 4 +- tests/stripe/Unit/StripePaymentTypeTest.php | 4 +- 20 files changed, 762 insertions(+), 56 deletions(-) create mode 100644 packages/stripe/resources/responses/charge.json create mode 100644 packages/stripe/resources/responses/charges.json create mode 100644 packages/stripe/resources/responses/payment_intent_created.json create mode 100644 packages/stripe/resources/responses/payment_intent_fetched.json create mode 100644 packages/stripe/resources/responses/payment_intent_paid.json create mode 100644 packages/stripe/resources/responses/payment_intent_requires_payment_method.json create mode 100644 packages/stripe/src/Facades/Stripe.php delete mode 100644 packages/stripe/src/Facades/StripeFacade.php create mode 100644 packages/stripe/src/MockClient.php diff --git a/docs/core/upgrading.md b/docs/core/upgrading.md index cf6cb05d71..0b1d0f6e59 100644 --- a/docs/core/upgrading.md +++ b/docs/core/upgrading.md @@ -18,6 +18,21 @@ php artisan migrate Lunar currently provides bug fixes and security updates for only the latest minor release, e.g. `0.7`. +## [Unreleased] + +### High Impact + +#### Stripe addon facade change + +If you are using the Stripe addon, you need to update the facade as the name has changed. + +```php +// Old +\Lunar\Stripe\Facades\StripeFacade; + +// New +\Lunar\Stripe\Facades\Stripe; +``` ## 1.0 diff --git a/packages/stripe/resources/responses/charge.json b/packages/stripe/resources/responses/charge.json new file mode 100644 index 0000000000..86291f26a7 --- /dev/null +++ b/packages/stripe/resources/responses/charge.json @@ -0,0 +1,84 @@ +{ + "id": "{id}", + "object": "charge", + "amount": 1099, + "amount_captured": 1099, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_3MmlLrLkdIwHu7ix0uke3Ezy", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "calculated_statement_descriptor": "Stripe", + "captured": true, + "created": 1679090539, + "currency": "usd", + "customer": null, + "description": null, + "disputed": false, + "failure_balance_transaction": null, + "failure_code": "{failure_code}", + "failure_message": null, + "fraud_details": {}, + "invoice": null, + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 32, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": null, + "payment_method": "card_1MmlLrLkdIwHu7ixIJwEWSNR", + "payment_method_details": { + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": null + }, + "country": "US", + "exp_month": 3, + "exp_year": 2024, + "fingerprint": "mToisGZ01V71BCos", + "funding": "credit", + "installments": null, + "last4": "4242", + "mandate": null, + "network": "visa", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xTTJKVGtMa2RJd0h1N2l4KOvG06AGMgZfBXyr1aw6LBa9vaaSRWU96d8qBwz9z2J_CObiV_H2-e8RezSK_sw0KISesp4czsOUlVKY", + "refunded": false, + "review": null, + "shipping": null, + "source_transfer": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "{status}", + "transfer_data": null, + "transfer_group": null +} \ No newline at end of file diff --git a/packages/stripe/resources/responses/charges.json b/packages/stripe/resources/responses/charges.json new file mode 100644 index 0000000000..a317bf62ff --- /dev/null +++ b/packages/stripe/resources/responses/charges.json @@ -0,0 +1,91 @@ +{ + "object": "list", + "url": "/v1/charges", + "has_more": false, + "data": [ + { + "id": "ch_3MmlLrLkdIwHu7ix0snN0B15", + "object": "charge", + "amount": 1099, + "amount_captured": 1099, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_3MmlLrLkdIwHu7ix0uke3Ezy", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "calculated_statement_descriptor": "Stripe", + "captured": true, + "created": 1679090539, + "currency": "usd", + "customer": null, + "description": null, + "disputed": false, + "failure_balance_transaction": null, + "failure_code": "{failure_code}", + "failure_message": null, + "fraud_details": {}, + "invoice": null, + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 32, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": null, + "payment_method": "card_1MmlLrLkdIwHu7ixIJwEWSNR", + "payment_method_details": { + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": null + }, + "country": "US", + "exp_month": 3, + "exp_year": 2024, + "fingerprint": "mToisGZ01V71BCos", + "funding": "credit", + "installments": null, + "last4": "4242", + "mandate": null, + "network": "visa", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xTTJKVGtMa2RJd0h1N2l4KOvG06AGMgZfBXyr1aw6LBa9vaaSRWU96d8qBwz9z2J_CObiV_H2-e8RezSK_sw0KISesp4czsOUlVKY", + "refunded": false, + "review": null, + "shipping": null, + "source_transfer": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "{status}", + "transfer_data": null, + "transfer_group": null + } + ] +} \ No newline at end of file diff --git a/packages/stripe/resources/responses/payment_intent_created.json b/packages/stripe/resources/responses/payment_intent_created.json new file mode 100644 index 0000000000..14c3edca62 --- /dev/null +++ b/packages/stripe/resources/responses/payment_intent_created.json @@ -0,0 +1,47 @@ +{ + "id": "pi_1DqH152eZvKYlo2CFHYZuxkP", + "object": "payment_intent", + "amount": 2000, + "amount_capturable": 0, + "amount_received": 0, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "object": "list", + "data": [], + "has_more": false, + "url": "/v1/charges?payment_intent=pi_1DqH152eZvKYlo2CFHYZuxkP" + }, + "client_secret": "pi_1DqH152eZvKYlo2CFHYZuxkP_secret_XNCxrfxMZshhdt1VmraRVGMKY", + "confirmation_method": "automatic", + "created": 1546940219, + "currency": "usd", + "customer": null, + "description": null, + "invoice": null, + "last_payment_error": null, + "livemode": false, + "metadata": {}, + "next_action": null, + "on_behalf_of": null, + "payment_method": null, + "payment_method_options": {}, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": null, + "redaction": null, + "review": null, + "setup_future_usage": null, + "shipping": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "requires_payment_method", + "transfer_data": null, + "transfer_group": null +} \ No newline at end of file diff --git a/packages/stripe/resources/responses/payment_intent_fetched.json b/packages/stripe/resources/responses/payment_intent_fetched.json new file mode 100644 index 0000000000..d274260124 --- /dev/null +++ b/packages/stripe/resources/responses/payment_intent_fetched.json @@ -0,0 +1,47 @@ +{ + "id": "{id}", + "object": "payment_intent", + "amount": 2000, + "amount_capturable": 0, + "amount_received": 0, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "object": "list", + "data": [], + "has_more": false, + "url": "/v1/charges?payment_intent=pi_1DqH152eZvKYlo2CFHYZuxkP" + }, + "client_secret": "pi_1DqH152eZvKYlo2CFHYZuxkP_secret_XNCxrfxMZshhdt1VmraRVGMKY", + "confirmation_method": "automatic", + "created": 1546940219, + "currency": "usd", + "customer": null, + "description": null, + "invoice": null, + "last_payment_error": null, + "livemode": false, + "metadata": {}, + "next_action": null, + "on_behalf_of": null, + "payment_method": null, + "payment_method_options": {}, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": null, + "redaction": null, + "review": null, + "setup_future_usage": null, + "shipping": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "requires_payment_method", + "transfer_data": null, + "transfer_group": null +} \ No newline at end of file diff --git a/packages/stripe/resources/responses/payment_intent_paid.json b/packages/stripe/resources/responses/payment_intent_paid.json new file mode 100644 index 0000000000..904c57b851 --- /dev/null +++ b/packages/stripe/resources/responses/payment_intent_paid.json @@ -0,0 +1,236 @@ +{ + "id": "{id}", + "object": "payment_intent", + "amount": 1099, + "amount_capturable": 0, + "amount_received": 0, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "{capture_method}", + "latest_charge": { + "id": "ch_3Kj1O52eZvKYlo2C1uoaNKty", + "object": "charge", + "amount": 100, + "amount_captured": 0, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": { + "id": "txn_1MiN3gLkdIwHu7ixxapQrznl", + "object": "balance_transaction", + "amount": -400, + "available_on": 1678043844, + "created": 1678043844, + "currency": "usd", + "description": null, + "exchange_rate": null, + "fee": 100, + "fee_details": [], + "net": -400, + "reporting_category": "transfer", + "source": "tr_1MiN3gLkdIwHu7ixNCZvFdgA", + "status": "available", + "type": "transfer" + }, + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "calculated_statement_descriptor": null, + "captured": "{captured}", + "created": 1648646197, + "currency": "usd", + "customer": null, + "description": "My First Test Charge (created for API docs)", + "disputed": false, + "failure_balance_transaction": null, + "failure_code": "{failure_code}", + "failure_message": null, + "fraud_details": {}, + "invoice": null, + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "order": null, + "outcome": null, + "paid": true, + "payment_intent": null, + "payment_method": "card_1Kj1O22eZvKYlo2C6wOeM223", + "payment_method_details": { + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "exp_month": 8, + "exp_year": 2023, + "fingerprint": "Xt5EWLLDS7FJjR1c", + "funding": "credit", + "installments": null, + "last4": "4242", + "mandate": null, + "moto": null, + "network": "visa", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/acct_1032D82eZvKYlo2C/ch_3Kj1O52eZvKYlo2C1uoaNKty/rcpt_LPr6IoEA84tnVv2KpRx7eHrhytjjkHn", + "redaction": null, + "refunded": false, + "refunds": { + "object": "list", + "data": [], + "has_more": false, + "url": "/v1/charges/ch_3Kj1O52eZvKYlo2C1uoaNKty/refunds" + }, + "review": null, + "shipping": null, + "source_transfer": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "{payment_status}", + "transfer_data": null, + "transfer_group": null + }, + "charges": { + "object": "list", + "data": [ + { + "id": "ch_3Kj1O52eZvKYlo2C1uoaNKty", + "object": "charge", + "amount": 100, + "amount_captured": 0, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_1032HU2eZvKYlo2CEPtcnUvl", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "calculated_statement_descriptor": null, + "captured": "{captured}", + "created": 1648646197, + "currency": "usd", + "customer": null, + "description": "My First Test Charge (created for API docs)", + "disputed": false, + "failure_balance_transaction": null, + "failure_code": "{failure_code}", + "failure_message": null, + "fraud_details": {}, + "invoice": null, + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "order": null, + "outcome": null, + "paid": true, + "payment_intent": null, + "payment_method": "card_1Kj1O22eZvKYlo2C6wOeM223", + "payment_method_details": { + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "exp_month": 8, + "exp_year": 2023, + "fingerprint": "Xt5EWLLDS7FJjR1c", + "funding": "credit", + "installments": null, + "last4": "4242", + "mandate": null, + "moto": null, + "network": "visa", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/acct_1032D82eZvKYlo2C/ch_3Kj1O52eZvKYlo2C1uoaNKty/rcpt_LPr6IoEA84tnVv2KpRx7eHrhytjjkHn", + "redaction": null, + "refunded": false, + "refunds": { + "object": "list", + "data": [], + "has_more": false, + "url": "/v1/charges/ch_3Kj1O52eZvKYlo2C1uoaNKty/refunds" + }, + "review": null, + "shipping": null, + "source_transfer": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "{payment_status}", + "transfer_data": null, + "transfer_group": null + } + ], + "has_more": false, + "url": "/v1/charges?payment_intent=pi_success" + }, + "client_secret": "pi_1DqH152eZvKYlo2CFHYZuxkP_secret_XNCxrfxMZshhdt1VmraRVGMKY", + "confirmation_method": "automatic", + "created": 1546940219, + "currency": "eur", + "customer": null, + "description": null, + "invoice": null, + "last_payment_error": "{payment_error}", + "livemode": false, + "metadata": {}, + "next_action": null, + "on_behalf_of": null, + "payment_method": null, + "payment_method_options": {}, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": null, + "redaction": null, + "review": null, + "setup_future_usage": null, + "shipping": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "{status}", + "transfer_data": null, + "transfer_group": null +} diff --git a/packages/stripe/resources/responses/payment_intent_requires_payment_method.json b/packages/stripe/resources/responses/payment_intent_requires_payment_method.json new file mode 100644 index 0000000000..14c3edca62 --- /dev/null +++ b/packages/stripe/resources/responses/payment_intent_requires_payment_method.json @@ -0,0 +1,47 @@ +{ + "id": "pi_1DqH152eZvKYlo2CFHYZuxkP", + "object": "payment_intent", + "amount": 2000, + "amount_capturable": 0, + "amount_received": 0, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "object": "list", + "data": [], + "has_more": false, + "url": "/v1/charges?payment_intent=pi_1DqH152eZvKYlo2CFHYZuxkP" + }, + "client_secret": "pi_1DqH152eZvKYlo2CFHYZuxkP_secret_XNCxrfxMZshhdt1VmraRVGMKY", + "confirmation_method": "automatic", + "created": 1546940219, + "currency": "usd", + "customer": null, + "description": null, + "invoice": null, + "last_payment_error": null, + "livemode": false, + "metadata": {}, + "next_action": null, + "on_behalf_of": null, + "payment_method": null, + "payment_method_options": {}, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": null, + "redaction": null, + "review": null, + "setup_future_usage": null, + "shipping": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "requires_payment_method", + "transfer_data": null, + "transfer_group": null +} \ No newline at end of file diff --git a/packages/stripe/src/Actions/UpdateOrderFromIntent.php b/packages/stripe/src/Actions/UpdateOrderFromIntent.php index 6e705985e1..0a1acd6e5e 100644 --- a/packages/stripe/src/Actions/UpdateOrderFromIntent.php +++ b/packages/stripe/src/Actions/UpdateOrderFromIntent.php @@ -4,7 +4,7 @@ use Illuminate\Support\Facades\DB; use Lunar\Models\Order; -use Lunar\Stripe\Facades\StripeFacade; +use Lunar\Stripe\Facades\Stripe; use Stripe\PaymentIntent; class UpdateOrderFromIntent @@ -17,7 +17,7 @@ final public static function execute( ): Order { return DB::transaction(function () use ($order, $paymentIntent) { - $charges = StripeFacade::getCharges($paymentIntent->id); + $charges = Stripe::getCharges($paymentIntent->id); $order = app(StoreCharges::class)->store($order, $charges); $requiresCapture = $paymentIntent->status === PaymentIntent::STATUS_REQUIRES_CAPTURE; diff --git a/packages/stripe/src/Components/PaymentForm.php b/packages/stripe/src/Components/PaymentForm.php index 3dac3bd60a..c0c91feddc 100644 --- a/packages/stripe/src/Components/PaymentForm.php +++ b/packages/stripe/src/Components/PaymentForm.php @@ -4,8 +4,8 @@ use Livewire\Component; use Lunar\Models\Cart; -use Lunar\Stripe\Facades\StripeFacade; -use Stripe\Stripe; +use Lunar\Stripe\Facades\Stripe; +use Stripe\Stripe as StripeClient; class PaymentForm extends Component { @@ -42,7 +42,7 @@ class PaymentForm extends Component */ public function mount() { - Stripe::setApiKey(config('services.stripe.key')); + StripeClient::setApiKey(config('services.stripe.key')); $this->policy = config('stripe.policy', 'capture'); } @@ -53,7 +53,7 @@ public function mount() */ public function getClientSecretProperty() { - $intent = StripeFacade::createIntent($this->cart); + $intent = Stripe::createIntent($this->cart); return $intent->client_secret; } diff --git a/packages/stripe/src/Facades/Stripe.php b/packages/stripe/src/Facades/Stripe.php new file mode 100644 index 0000000000..ff6930e4fc --- /dev/null +++ b/packages/stripe/src/Facades/Stripe.php @@ -0,0 +1,32 @@ +shippingAddress; @@ -68,7 +68,7 @@ public function createIntent(Cart $cart) return $paymentIntent; } - public function syncIntent(Cart $cart) + public function syncIntent(Cart $cart): void { $meta = (array) $cart->meta; @@ -86,14 +86,11 @@ public function syncIntent(Cart $cart) /** * Fetch an intent from the Stripe API. - * - * @param string $intentId - * @return null|\Stripe\PaymentIntent */ - public function fetchIntent($intentId) + public function fetchIntent(string $intentId, $options = null): ?PaymentIntent { try { - $intent = PaymentIntent::retrieve($intentId); + $intent = PaymentIntent::retrieve($intentId, $options); } catch (InvalidRequestException $e) { return null; } @@ -116,20 +113,15 @@ public function getCharges(string $paymentIntentId): Collection return collect(); } - public function getCharge($chargeId) + public function getCharge(string $chargeId): Charge { return $this->getClient()->charges->retrieve($chargeId); } /** * Build the intent - * - * @param int $value - * @param string $currencyCode - * @param \Lunar\Models\CartAddress $shipping - * @return \Stripe\PaymentIntent */ - protected function buildIntent($value, $currencyCode, $shipping) + protected function buildIntent(int $value, string $currencyCode, CartAddress $shipping): PaymentIntent { return PaymentIntent::create([ 'amount' => $value, diff --git a/packages/stripe/src/MockClient.php b/packages/stripe/src/MockClient.php new file mode 100644 index 0000000000..c4235aee36 --- /dev/null +++ b/packages/stripe/src/MockClient.php @@ -0,0 +1,134 @@ +url = 'https://checkout.stripe.com/pay/cs_test_'.Str::random(32); + } + + public function request($method, $absUrl, $headers, $params, $hasFile) + { + $id = array_slice(explode('/', $absUrl), -1)[0]; + + $policy = config('lunar.stripe.policy'); + + if ($method == 'get' && str_contains($absUrl, 'charges')) { + + $status = 'succeeded'; + $failureCode = null; + + if (($params['payment_intent'] ?? null) == 'PI_FAIL') { + $status = 'failed'; + $failureCode = 'FAILED'; + } + + $this->rBody = $this->getResponse('charges', [ + 'status' => $status, + 'failure_code' => $failureCode, + ]); + + return [$this->rBody, $this->rcode, $this->rheaders]; + } + + if ($method == 'get' && str_contains($absUrl, 'payment_intents')) { + if (str_contains($absUrl, 'PI_CAPTURE')) { + $this->rBody = $this->getResponse('payment_intent_paid', [ + 'id' => $id, + 'status' => 'succeeded', + 'capture_method' => 'automatic', + 'payment_status' => 'succeeded', + 'payment_error' => null, + 'failure_code' => null, + 'captured' => true, + ]); + + return [$this->rBody, $this->rcode, $this->rheaders]; + } + + if (str_contains($absUrl, 'PI_FAIL')) { + $this->rBody = $this->getResponse('payment_intent_paid', [ + 'id' => $id, + 'status' => 'requires_payment_method', + 'capture_method' => 'automatic', + 'payment_status' => 'failed', + 'payment_error' => 'foo', + 'failure_code' => 1234, + 'captured' => false, + ]); + + return [$this->rBody, $this->rcode, $this->rheaders]; + } + + if (str_contains($absUrl, 'PI_REQUIRES_PAYMENT_METHOD')) { + $this->rBody = $this->getResponse('payment_intent_requires_payment_method'); + + return [$this->rBody, $this->rcode, $this->rheaders]; + } + + if (str_contains($absUrl, 'PI_REQUIRES_ACTION')) { + $this->rBody = $this->getResponse('payment_intent_paid', [ + 'id' => $id, + 'status' => PaymentIntent::STATUS_REQUIRES_ACTION, + 'capture_method' => 'automatic', + 'payment_status' => 'failed', + 'payment_error' => 'foo', + 'failure_code' => 1234, + 'captured' => false, + ]); + + return [$this->rBody, $this->rcode, $this->rheaders]; + } + + } + + if ($method == 'post' && str_contains($absUrl, 'payment_intents')) { + $this->rBody = $this->getResponse('payment_intent_created'); + + return [$this->rBody, $this->rcode, $this->rheaders]; + } + + if ($method == 'get' && str_contains($absUrl, 'payment_intents')) { + $this->rBody = $this->getResponse('payment_intent_created', [ + 'id' => $id, + ]); + + return [$this->rBody, $this->rcode, $this->rheaders]; + } + + return [$this->rBody, $this->rcode, $this->rheaders]; + } + + /** + * Fetches a response for the mock + * + * @param string $filename + * @param array $replace + * @return string + */ + protected function getResponse($filename, $replace = []) + { + $response = File::get(__DIR__.'/../resources/responses/'.$filename.'.json'); + + foreach ($replace as $token => $value) { + $response = str_replace('{'.$token.'}', $value, $response); + } + + return $response; + } +} diff --git a/packages/stripe/src/StripePaymentType.php b/packages/stripe/src/StripePaymentType.php index 6af674f274..fa8608b342 100644 --- a/packages/stripe/src/StripePaymentType.php +++ b/packages/stripe/src/StripePaymentType.php @@ -10,10 +10,9 @@ use Lunar\Models\Transaction; use Lunar\PaymentTypes\AbstractPayment; use Lunar\Stripe\Actions\UpdateOrderFromIntent; -use Lunar\Stripe\Facades\StripeFacade; +use Lunar\Stripe\Facades\Stripe; use Stripe\Exception\InvalidRequestException; use Stripe\PaymentIntent; -use Stripe\Stripe; class StripePaymentType extends AbstractPayment { @@ -41,7 +40,7 @@ class StripePaymentType extends AbstractPayment */ public function __construct() { - $this->stripe = StripeFacade::getClient(); + $this->stripe = Stripe::getClient(); $this->policy = config('lunar.stripe.policy', 'automatic'); } @@ -137,9 +136,9 @@ public function capture(Transaction $transaction, $amount = 0): PaymentCapture $payload['amount_to_capture'] = $amount; } - $charge = StripeFacade::getCharge($transaction->reference); + $charge = Stripe::getCharge($transaction->reference); - $paymentIntent = StripeFacade::fetchIntent($charge->payment_intent); + $paymentIntent = Stripe::fetchIntent($charge->payment_intent); try { $response = $this->stripe->paymentIntents->capture( @@ -165,7 +164,7 @@ public function capture(Transaction $transaction, $amount = 0): PaymentCapture */ public function refund(Transaction $transaction, int $amount = 0, $notes = null): PaymentRefund { - $charge = StripeFacade::getCharge($transaction->reference); + $charge = Stripe::getCharge($transaction->reference); try { $refund = $this->stripe->refunds->create( diff --git a/packages/stripe/src/StripePaymentsServiceProvider.php b/packages/stripe/src/StripePaymentsServiceProvider.php index 153ea6fe7c..2d1d469d62 100644 --- a/packages/stripe/src/StripePaymentsServiceProvider.php +++ b/packages/stripe/src/StripePaymentsServiceProvider.php @@ -29,7 +29,7 @@ public function boot() return $app->make(ConstructWebhookEvent::class); }); - $this->app->singleton('gc:stripe', function ($app) { + $this->app->singleton('lunar:stripe', function ($app) { return $app->make(StripeManager::class); }); diff --git a/tests/stripe/TestCase.php b/tests/stripe/TestCase.php index 4d0566ecbb..bb931625c6 100644 --- a/tests/stripe/TestCase.php +++ b/tests/stripe/TestCase.php @@ -7,13 +7,12 @@ use Kalnoy\Nestedset\NestedSetServiceProvider; use Livewire\LivewireServiceProvider; use Lunar\LunarServiceProvider; +use Lunar\Stripe\Facades\Stripe; use Lunar\Stripe\StripePaymentsServiceProvider; -use Lunar\Tests\Stripe\Stripe\MockClient; use Lunar\Tests\Stubs\User; use Spatie\Activitylog\ActivitylogServiceProvider; use Spatie\LaravelBlink\BlinkServiceProvider; use Spatie\MediaLibrary\MediaLibraryServiceProvider; -use Stripe\ApiRequestor; class TestCase extends \Orchestra\Testbench\TestCase { @@ -27,8 +26,7 @@ protected function setUp(): void activity()->disableLogging(); - $mockClient = new MockClient(); - ApiRequestor::setHttpClient($mockClient); + Stripe::fake(); } protected function getPackageProviders($app) diff --git a/tests/stripe/Unit/Actions/StoreChargesTest.php b/tests/stripe/Unit/Actions/StoreChargesTest.php index 5f8be979e7..eb9f5a60ba 100644 --- a/tests/stripe/Unit/Actions/StoreChargesTest.php +++ b/tests/stripe/Unit/Actions/StoreChargesTest.php @@ -7,7 +7,7 @@ $order = $cart->createOrder(); - $paymentIntent = \Lunar\Stripe\Facades\StripeFacade::getClient() + $paymentIntent = \Lunar\Stripe\Facades\Stripe::getClient() ->paymentIntents ->retrieve('PI_CAPTURE'); @@ -30,7 +30,7 @@ $order = $cart->createOrder(); - $paymentIntent = \Lunar\Stripe\Facades\StripeFacade::getClient() + $paymentIntent = \Lunar\Stripe\Facades\Stripe::getClient() ->paymentIntents ->retrieve('PI_CAPTURE'); diff --git a/tests/stripe/Unit/Actions/UpdateOrderFromIntentTest.php b/tests/stripe/Unit/Actions/UpdateOrderFromIntentTest.php index 62f40852d4..891dd79535 100644 --- a/tests/stripe/Unit/Actions/UpdateOrderFromIntentTest.php +++ b/tests/stripe/Unit/Actions/UpdateOrderFromIntentTest.php @@ -8,7 +8,7 @@ $order = $cart->createOrder(); - $paymentIntent = \Lunar\Stripe\Facades\StripeFacade::getClient() + $paymentIntent = \Lunar\Stripe\Facades\Stripe::getClient() ->paymentIntents ->retrieve('PI_REQUIRES_ACTION'); diff --git a/tests/stripe/Unit/Managers/StripeManagerTest.php b/tests/stripe/Unit/Managers/StripeManagerTest.php index 7aa5e77e9c..6f3d733bb5 100644 --- a/tests/stripe/Unit/Managers/StripeManagerTest.php +++ b/tests/stripe/Unit/Managers/StripeManagerTest.php @@ -1,6 +1,6 @@ calculate()); + Stripe::createIntent($cart->calculate()); expect($cart->refresh()->meta['payment_intent'])->toBe('pi_1DqH152eZvKYlo2CFHYZuxkP'); }); diff --git a/tests/stripe/Unit/StripePaymentTypeTest.php b/tests/stripe/Unit/StripePaymentTypeTest.php index 0ed2667230..e9ff0cbc84 100644 --- a/tests/stripe/Unit/StripePaymentTypeTest.php +++ b/tests/stripe/Unit/StripePaymentTypeTest.php @@ -2,7 +2,7 @@ use Lunar\Base\DataTransferObjects\PaymentAuthorize; use Lunar\Models\Transaction; -use Lunar\Stripe\Facades\StripeFacade; +use Lunar\Stripe\Facades\Stripe; use Lunar\Stripe\StripePaymentType; use Lunar\Tests\Stripe\Utils\CartBuilder; @@ -59,7 +59,7 @@ ], ]); - StripeFacade::createIntent($cart->calculate()); + Stripe::createIntent($cart->calculate()); expect($cart->refresh()->meta['payment_intent'])->toBe('PI_FOOBAR'); }); From 5a2de4c40c637780b39821bf78dbbc7d2c9e1efb Mon Sep 17 00:00:00 2001 From: Thor Nissen Date: Wed, 22 May 2024 16:24:37 +0200 Subject: [PATCH 06/17] Fixed hardcoded navigation group on DiscountResource (#1727) --- packages/admin/src/Filament/Resources/DiscountResource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/admin/src/Filament/Resources/DiscountResource.php b/packages/admin/src/Filament/Resources/DiscountResource.php index e537486909..3f636604dc 100644 --- a/packages/admin/src/Filament/Resources/DiscountResource.php +++ b/packages/admin/src/Filament/Resources/DiscountResource.php @@ -52,7 +52,7 @@ public static function getNavigationIcon(): ?string public static function getNavigationGroup(): ?string { - return 'Sales'; + return __('lunarpanel::global.sections.sales'); } public static function getDefaultForm(Form $form): Form From d76cb796bdb47f01193546cf49c9585ace9b2169 Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Wed, 22 May 2024 15:25:54 +0100 Subject: [PATCH 07/17] Update upgrading.md --- docs/core/upgrading.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/upgrading.md b/docs/core/upgrading.md index 0b1d0f6e59..d031716950 100644 --- a/docs/core/upgrading.md +++ b/docs/core/upgrading.md @@ -18,7 +18,7 @@ php artisan migrate Lunar currently provides bug fixes and security updates for only the latest minor release, e.g. `0.7`. -## [Unreleased] +## 1.0.0-alpha.20 ### High Impact From 764da69c0097c1db0a3f6d6b313d6ae1814f6c88 Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Wed, 22 May 2024 18:03:45 +0200 Subject: [PATCH 08/17] Feature - Support extend tabs (#1725) This PR add support of extends tabs on BaseListRecords Co-authored-by: Glenn Jacobs --- docs/admin/extending/pages.md | 9 +++++++++ .../Resources/OrderResource/Pages/ListOrders.php | 2 +- .../ProductResource/Pages/ListProducts.php | 2 +- .../Pages/ListProductVariants.php | 2 +- .../src/Support/Extending/ListPageExtension.php | 5 +++++ .../admin/src/Support/Pages/BaseListRecords.php | 1 + .../src/Support/Pages/Concerns/ExtendsTabs.php | 16 ++++++++++++++++ 7 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 packages/admin/src/Support/Pages/Concerns/ExtendsTabs.php diff --git a/docs/admin/extending/pages.md b/docs/admin/extending/pages.md index d4ca2b7b4c..8ba953278f 100644 --- a/docs/admin/extending/pages.md +++ b/docs/admin/extending/pages.md @@ -29,6 +29,15 @@ class MyCreateExtension extends CreatePageExtension { return $title . ' - Example'; } + + public function getTabs(array $tabs): array + { + return [ + ...$tabs, + 'review' => Tab::make('Review') + ->modifyQueryUsing(fn (Builder $query) => $query->where('status', 'review')), + ]; + } public function headerWidgets(array $widgets): array { diff --git a/packages/admin/src/Filament/Resources/OrderResource/Pages/ListOrders.php b/packages/admin/src/Filament/Resources/OrderResource/Pages/ListOrders.php index 4d1139bfe4..5da0b626da 100644 --- a/packages/admin/src/Filament/Resources/OrderResource/Pages/ListOrders.php +++ b/packages/admin/src/Filament/Resources/OrderResource/Pages/ListOrders.php @@ -19,7 +19,7 @@ protected function getDefaultHeaderActions(): array ]; } - public function getTabs(): array + public function getDefaultTabs(): array { $statuses = collect( config('lunar.orders.statuses', []) diff --git a/packages/admin/src/Filament/Resources/ProductResource/Pages/ListProducts.php b/packages/admin/src/Filament/Resources/ProductResource/Pages/ListProducts.php index cfc147b0b4..3a979ce033 100644 --- a/packages/admin/src/Filament/Resources/ProductResource/Pages/ListProducts.php +++ b/packages/admin/src/Filament/Resources/ProductResource/Pages/ListProducts.php @@ -78,7 +78,7 @@ public static function createRecord(array $data, string $model): Model return $product; } - public function getTabs(): array + public function getDefaultTabs(): array { return [ 'all' => Tab::make('All'), diff --git a/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ListProductVariants.php b/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ListProductVariants.php index 85b7384c77..333ca30898 100644 --- a/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ListProductVariants.php +++ b/packages/admin/src/Filament/Resources/ProductVariantResource/Pages/ListProductVariants.php @@ -19,7 +19,7 @@ public static function createActionFormInputs(): array return []; } - public function getTabs(): array + public function getDefaultTabs(): array { return []; } diff --git a/packages/admin/src/Support/Extending/ListPageExtension.php b/packages/admin/src/Support/Extending/ListPageExtension.php index 9c3481036e..d882973a1e 100644 --- a/packages/admin/src/Support/Extending/ListPageExtension.php +++ b/packages/admin/src/Support/Extending/ListPageExtension.php @@ -14,6 +14,11 @@ public function subheading($title): ?string return $title; } + public function getTabs(array $tabs): array + { + return $tabs; + } + public function relationManagers(array $managers): array { return $managers; diff --git a/packages/admin/src/Support/Pages/BaseListRecords.php b/packages/admin/src/Support/Pages/BaseListRecords.php index 8601382f3b..dadf83c846 100644 --- a/packages/admin/src/Support/Pages/BaseListRecords.php +++ b/packages/admin/src/Support/Pages/BaseListRecords.php @@ -13,6 +13,7 @@ abstract class BaseListRecords extends ListRecords use Concerns\ExtendsHeaderWidgets; use Concerns\ExtendsHeadings; use Concerns\ExtendsTablePagination; + use Concerns\ExtendsTabs; use \Lunar\Admin\Support\Concerns\CallsHooks; protected function applySearchToTableQuery(Builder $query): Builder diff --git a/packages/admin/src/Support/Pages/Concerns/ExtendsTabs.php b/packages/admin/src/Support/Pages/Concerns/ExtendsTabs.php new file mode 100644 index 0000000000..d7760c2e0c --- /dev/null +++ b/packages/admin/src/Support/Pages/Concerns/ExtendsTabs.php @@ -0,0 +1,16 @@ +callLunarHook('getTabs', $this->getDefaultTabs()); + } +} From 5e7bd8a6ac228084cd167362fcdaaf82b5086213 Mon Sep 17 00:00:00 2001 From: Glenn Jacobs Date: Wed, 22 May 2024 21:08:33 +0100 Subject: [PATCH 09/17] Merge 0.8 (#1743) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merges changes from the `0.8` branch into `1.x`. --------- Co-authored-by: Dominik Nguyen Co-authored-by: Jakub Theimer <5587309+theimerj@users.noreply.github.com> Co-authored-by: Lionel Guichard Co-authored-by: wychoong <67364036+wychoong@users.noreply.github.com> Co-authored-by: Alec Ritson Co-authored-by: Andrea De Luca <53230888+se09deluca@users.noreply.github.com> Co-authored-by: Andréas Lundgren <1066486+adevade@users.noreply.github.com> Co-authored-by: Ryan Mitchell Co-authored-by: Tuvshu Co-authored-by: Bert Brunekreeft Co-authored-by: Stéphane Bour Co-authored-by: dewebdesigns Co-authored-by: Dinis Esteves <35270727+DinisEsteves@users.noreply.github.com> Co-authored-by: Ryan McAllen Co-authored-by: mattdfloyd Co-authored-by: ItsJustPW <40368729+ItsJustPW@users.noreply.github.com> Co-authored-by: Finn <71390226+FinnPaes@users.noreply.github.com> Co-authored-by: Sascha Deus Co-authored-by: glennjacobs --- docs/core/reference/customers.md | 4 +- docs/core/reference/orders.md | 5 +-- docs/core/reference/products.md | 4 +- docs/core/reference/search.md | 2 +- docs/core/reference/tags.md | 4 +- packages/admin/.gitattributes | 6 +++ packages/admin/.github/FUNDING.yml | 3 ++ .../.github/workflows/close-pull-request.yml | 13 ++++++ packages/admin/src/Models/Staff.php | 17 ++++++++ packages/core/.gitattributes | 6 +++ .../.github/workflows/close-pull-request.yml | 2 +- packages/core/config/pricing.php | 2 +- .../2021_08_10_101547_create_media_table.php | 2 +- .../database/state/ConvertTaxbreakdown.php | 1 - .../core/resources/lang/de/exceptions.php | 18 ++++++++ .../core/resources/lang/en/exceptions.php | 1 + packages/core/src/Base/ModelManifest.php | 2 +- packages/core/src/Base/ShippingModifiers.php | 2 +- .../CartLineIdMismatchException.php | 5 +-- .../BillingAddressIncompleteException.php | 1 + .../Carts/BillingAddressMissingException.php | 1 + .../src/Exceptions/Carts/CartException.php | 23 +++++----- .../Exceptions/Carts/OrderExistsException.php | 1 + .../ShippingAddressIncompleteException.php | 1 + .../Carts/ShippingAddressMissingException.php | 1 + .../Carts/ShippingOptionMissingException.php | 1 + .../CustomerNotBelongsToUserException.php | 5 +-- .../DisallowMultipleCartOrdersException.php | 4 +- .../src/Exceptions/FieldTypeException.php | 5 +-- .../FieldTypes/FieldTypeMissingException.php | 6 +-- .../FieldTypes/InvalidFieldTypeException.php | 6 +-- .../FingerprintMismatchException.php | 5 +-- .../InvalidCartLineQuantityException.php | 5 +-- .../InvalidDataTypeValueException.php | 5 +-- .../InvalidPaymentTypeException.php | 5 +-- .../core/src/Exceptions/LunarException.php | 8 ++++ .../MaximumCartLineQuantityException.php | 5 +-- .../MissingCurrencyPriceException.php | 5 +-- .../NonPurchasableItemException.php | 4 +- .../src/Exceptions/SchedulingException.php | 5 +-- packages/core/src/Models/ProductOption.php | 19 ++++++++ .../src/Pipelines/CartLine/GetUnitPrice.php | 2 +- packages/meilisearch/.github/FUNDING.yml | 3 ++ .../.github/workflows/close-pull-request.yml | 13 ++++++ packages/opayo/.github/FUNDING.yml | 3 ++ .../.github/workflows/close-pull-request.yml | 13 ++++++ packages/paypal/.github/FUNDING.yml | 3 ++ .../.github/workflows/close-pull-request.yml | 13 ++++++ packages/stripe/.github/FUNDING.yml | 3 ++ .../.github/workflows/close-pull-request.yml | 13 ++++++ .../table-rate-shipping/.github/FUNDING.yml | 3 ++ .../.github/workflows/close-pull-request.yml | 13 ++++++ .../Extending/ResourceExtensionTest.php | 3 -- tests/core/Unit/Base/MacroableModelTest.php | 2 - .../core/Unit/Base/ShippingModifiersTest.php | 43 +++++++++++++++++++ tests/core/Unit/Console/CartPruneTest.php | 35 +++++++++++++++ .../Unit/Search/ProductOptionIndexerTest.php | 5 ++- 57 files changed, 314 insertions(+), 76 deletions(-) create mode 100644 packages/admin/.gitattributes create mode 100644 packages/admin/.github/FUNDING.yml create mode 100644 packages/admin/.github/workflows/close-pull-request.yml create mode 100644 packages/core/.gitattributes create mode 100644 packages/core/resources/lang/de/exceptions.php create mode 100644 packages/core/src/Exceptions/LunarException.php create mode 100644 packages/meilisearch/.github/FUNDING.yml create mode 100644 packages/meilisearch/.github/workflows/close-pull-request.yml create mode 100644 packages/opayo/.github/FUNDING.yml create mode 100644 packages/opayo/.github/workflows/close-pull-request.yml create mode 100644 packages/paypal/.github/FUNDING.yml create mode 100644 packages/paypal/.github/workflows/close-pull-request.yml create mode 100644 packages/stripe/.github/FUNDING.yml create mode 100644 packages/stripe/.github/workflows/close-pull-request.yml create mode 100644 packages/table-rate-shipping/.github/FUNDING.yml create mode 100644 packages/table-rate-shipping/.github/workflows/close-pull-request.yml create mode 100644 tests/core/Unit/Base/ShippingModifiersTest.php create mode 100644 tests/core/Unit/Console/CartPruneTest.php diff --git a/docs/core/reference/customers.md b/docs/core/reference/customers.md index ad781d9016..2e4463f6c2 100644 --- a/docs/core/reference/customers.md +++ b/docs/core/reference/customers.md @@ -183,7 +183,7 @@ $myModel->scheduleCustomerGroup( // Schedule the product to be enabled straight away $myModel->scheduleCustomerGroup($customerGroup); -// The schedule method will accept and array or collection of customer groups. +// The schedule method will accept an array or collection of customer groups. $myModel->scheduleCustomerGroup(CustomerGroup::get()); ``` @@ -217,7 +217,7 @@ You can override any of these yourself as they are merged behind the scenes. The `HasCustomerGroup` trait adds a `customerGroup` scope to the model. This lets you query based on availability for a specific or multiple customer groups. -The scope will accept either a single ID or instance of `CustomerGroup` and will accept accept an array. +The scope will accept either a single ID or instance of `CustomerGroup` and will accept an array. ```php $results = MyModel::customerGroup(1, $startDate, $endDate)->paginate(); diff --git a/docs/core/reference/orders.md b/docs/core/reference/orders.md index 8a0d0c6884..60e78c3f28 100644 --- a/docs/core/reference/orders.md +++ b/docs/core/reference/orders.md @@ -97,7 +97,7 @@ $cart->canCreateOrder(); Under the hood this will use the `ValidateCartForOrderCreation` class which lunar provides and throw any validation exceptions with helpful messages if the cart isn't ready to create an order. -You can specific you're own class to handle this in `config/cart.php`. +You can specify you're own class to handle this in `config/cart.php`. ```php return [ @@ -365,8 +365,7 @@ $order->refunds; // Get all transactions that are refunds. We will be looking to add support for the most popular payment providers, so keep an eye out here as we will list them all out. -In the meantime, you can absolutely still get a storefront working, at the end of the day Lunar doesn't really mind if -you what payment provider you use or plan to use. +In the meantime, you can absolutely still get a storefront working, at the end of the day Lunar doesn't really mind what payment provider you use or plan to use. In terms of an order, all it's worried about is whether or not the `placed_at` column is populated on the orders table, the rest is completely up to you how you want to handle that. We have some helper utilities to make such things easier diff --git a/docs/core/reference/products.md b/docs/core/reference/products.md index ddfe23812e..995de86372 100644 --- a/docs/core/reference/products.md +++ b/docs/core/reference/products.md @@ -40,7 +40,7 @@ $product->schedule($customerGroup, now()->addDays(14)); // Schedule the product to be enabled straight away $product->schedule($customerGroup); -// The schedule method will accept and array or collection of customer groups. +// The schedule method will accept an array or collection of customer groups. $product->schedule(CustomerGroup::get()); ``` @@ -105,7 +105,7 @@ $product->load(['productType']); ## Product Identifiers You can choose to add product identifiers to each product variant. These are fields which, as the name suggests, allow -you to identify a product and it's variants for use in your internal systems. You can choose whether these are required +you to identify a product and its variants for use in your internal systems. You can choose whether these are required and unique in the hub whilst editing. ### Available fields diff --git a/docs/core/reference/search.md b/docs/core/reference/search.md index b2a2cb9152..705ccf870e 100644 --- a/docs/core/reference/search.md +++ b/docs/core/reference/search.md @@ -36,7 +36,7 @@ If you installed the Lunar package in an existing project and you would like to php artisan lunar:search:index ``` -The command will import the records of the models listed in the `lunar/indexer.php` configuration file. Type `--help` to see the available options. +The command will import the records of the models listed in the `lunar/search.php` configuration file. Type `--help` to see the available options. ## Meilisearch diff --git a/docs/core/reference/tags.md b/docs/core/reference/tags.md index 8f5605a639..bcdc1dd22c 100644 --- a/docs/core/reference/tags.md +++ b/docs/core/reference/tags.md @@ -36,7 +36,9 @@ You can then attach tags like so: ```php $tags = collect(['Tag One', 'Tag Two', 'Tag Three']); -$model = SomethingWithTags::syncTags($tags); +$model = SomethingWithTags::first(); + +$model->syncTags($tags); $model->tags; diff --git a/packages/admin/.gitattributes b/packages/admin/.gitattributes new file mode 100644 index 0000000000..d52b86c31f --- /dev/null +++ b/packages/admin/.gitattributes @@ -0,0 +1,6 @@ +* text=auto eol=lf + +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/phpunit.xml export-ignore diff --git a/packages/admin/.github/FUNDING.yml b/packages/admin/.github/FUNDING.yml new file mode 100644 index 0000000000..8c14ff3364 --- /dev/null +++ b/packages/admin/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [lunar] diff --git a/packages/admin/.github/workflows/close-pull-request.yml b/packages/admin/.github/workflows/close-pull-request.yml new file mode 100644 index 0000000000..38d66b6a15 --- /dev/null +++ b/packages/admin/.github/workflows/close-pull-request.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on the Lunar Admin Repo which is a read-only sub split of `lunarphp/lunar`. Please submit your PR on the https://github.com/lunarphp/lunar repository.

Thanks!" \ No newline at end of file diff --git a/packages/admin/src/Models/Staff.php b/packages/admin/src/Models/Staff.php index e7e5cff2c4..b9e5622060 100644 --- a/packages/admin/src/Models/Staff.php +++ b/packages/admin/src/Models/Staff.php @@ -62,6 +62,13 @@ protected static function newFactory(): StaffFactory 'password' => 'hashed', ]; + /** + * Append attributes to the model. + * + * @var array + */ + protected $appends = ['fullName', 'gravatar']; + /** * Create a new instance of the Model. */ @@ -117,6 +124,16 @@ public function getFullNameAttribute(): string return $this->firstname.' '.$this->lastname; } + /** + * Get staff member's Gravatar URLs. + */ + public function getGravatarAttribute(): string + { + $hash = md5(strtolower(trim($this->attributes['email']))); + + return "https://www.gravatar.com/avatar/{$hash}?d=mp"; + } + public function canAccessPanel(Panel $panel): bool { return true; diff --git a/packages/core/.gitattributes b/packages/core/.gitattributes new file mode 100644 index 0000000000..d52b86c31f --- /dev/null +++ b/packages/core/.gitattributes @@ -0,0 +1,6 @@ +* text=auto eol=lf + +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/phpunit.xml export-ignore diff --git a/packages/core/.github/workflows/close-pull-request.yml b/packages/core/.github/workflows/close-pull-request.yml index c369e3887b..f854c79c8b 100644 --- a/packages/core/.github/workflows/close-pull-request.yml +++ b/packages/core/.github/workflows/close-pull-request.yml @@ -10,4 +10,4 @@ jobs: steps: - uses: superbrothers/close-pull-request@v3 with: - comment: "Thank you for your pull request. However, you have submitted this PR on the Lunar Core Repo which is a read-only sub split of `lunar/lunar`. Please submit your PR on the https://github.com/lunarphp/lunar repository.

Thanks!" \ No newline at end of file + comment: "Thank you for your pull request. However, you have submitted this PR on the Lunar Core Repo which is a read-only sub split of `lunarphp/lunar`. Please submit your PR on the https://github.com/lunarphp/lunar repository.

Thanks!" \ No newline at end of file diff --git a/packages/core/config/pricing.php b/packages/core/config/pricing.php index 6a2a99c09a..3283cc51d6 100644 --- a/packages/core/config/pricing.php +++ b/packages/core/config/pricing.php @@ -12,7 +12,7 @@ | Specify whether the prices entered into the system include tax or not. | */ - 'stored_inclusive_of_tax' => false, + 'stored_inclusive_of_tax' => env('LUNAR_STORE_INCLUSIVE_OF_TAX', false), /* |-------------------------------------------------------------------------- diff --git a/packages/core/database/migrations/2021_08_10_101547_create_media_table.php b/packages/core/database/migrations/2021_08_10_101547_create_media_table.php index 54cdc8ef5e..fb6cb1264c 100644 --- a/packages/core/database/migrations/2021_08_10_101547_create_media_table.php +++ b/packages/core/database/migrations/2021_08_10_101547_create_media_table.php @@ -1,8 +1,8 @@ 'Das Model ":class" implementiert nicht das "Bestellbar (purchasable)" Interface.', + 'cart_line_id_mismatch' => 'Die Position gehört nicht zu diesem Warenkorb.', + 'invalid_cart_line_quantity' => 'Die Bestellmenge muss mindestens "1" sein, angegeben wurden ":quantity".', + 'maximum_cart_line_quantity' => 'Die Bestellmenge darf nicht mehr als ":quantity" betragen.', + 'carts.shipping_missing' => 'Eine Lieferadresse ist erforderlich', + 'carts.billing_missing' => 'Eine Rechnungsadresse ist erforderlich', + 'carts.billing_incomplete' => 'Die Rechnungsadresse ist unvollständig', + 'carts.order_exists' => 'Für diesen Warenkorb existiert bereits eine Bestellung', + 'carts.shipping_option_missing' => 'Eine gültige Versandart fehlt', + 'missing_currency_price' => 'Es existiert kein Preis für die Währung ":currency"', + 'fieldtype_missing' => 'Der FeldType ":class" existiert nicht', + 'invalid_fieldtype' => 'Die Klasse ":class" implementiert nicht das Feldtyp-Interface.', + 'discounts.invalid_type' => 'Die Liste der Rabatte darf nur ":expected" enthalten, gefunden wurde ":actual"', + 'disallow_multiple_cart_orders' => 'Ein Warenkorb kann nur zu einer Bestellung gehören.', +]; diff --git a/packages/core/resources/lang/en/exceptions.php b/packages/core/resources/lang/en/exceptions.php index a9ae83cdc9..b9386e57fd 100644 --- a/packages/core/resources/lang/en/exceptions.php +++ b/packages/core/resources/lang/en/exceptions.php @@ -5,6 +5,7 @@ 'cart_line_id_mismatch' => 'This cart line does not belong to this cart', 'invalid_cart_line_quantity' => 'Expected quantity to be at least "1", ":quantity" found.', 'maximum_cart_line_quantity' => 'Quantity cannot exceed :quantity.', + 'carts.invalid_action' => 'The cart action was invalid', 'carts.shipping_missing' => 'A shipping address is required', 'carts.billing_missing' => 'A billing address is required', 'carts.billing_incomplete' => 'The billing address is incomplete', diff --git a/packages/core/src/Base/ModelManifest.php b/packages/core/src/Base/ModelManifest.php index 71b4f16a0b..82619dd5aa 100644 --- a/packages/core/src/Base/ModelManifest.php +++ b/packages/core/src/Base/ModelManifest.php @@ -47,7 +47,7 @@ public function getRegisteredModel(string $baseModelClass): Model */ public function removeModel(string $baseModelClass): void { - $this->models = $this->models->flip()->forget($baseModelClass); + $this->models = $this->models->flip()->forget($baseModelClass)->flip(); } /** diff --git a/packages/core/src/Base/ShippingModifiers.php b/packages/core/src/Base/ShippingModifiers.php index 60ab1da31a..4809976099 100644 --- a/packages/core/src/Base/ShippingModifiers.php +++ b/packages/core/src/Base/ShippingModifiers.php @@ -48,6 +48,6 @@ public function add($modifier) */ public function remove($modifier) { - $this->modifiers->forget($modifier); + $this->modifiers = $this->modifiers->reject(fn ($value) => $value == $modifier); } } diff --git a/packages/core/src/Exceptions/CartLineIdMismatchException.php b/packages/core/src/Exceptions/CartLineIdMismatchException.php index 05f10febd1..7cd72b4c18 100644 --- a/packages/core/src/Exceptions/CartLineIdMismatchException.php +++ b/packages/core/src/Exceptions/CartLineIdMismatchException.php @@ -2,8 +2,7 @@ namespace Lunar\Exceptions; -use Exception; - -class CartLineIdMismatchException extends Exception +class CartLineIdMismatchException extends LunarException { + // } diff --git a/packages/core/src/Exceptions/Carts/BillingAddressIncompleteException.php b/packages/core/src/Exceptions/Carts/BillingAddressIncompleteException.php index 35e276fd2d..6f9ce3365f 100644 --- a/packages/core/src/Exceptions/Carts/BillingAddressIncompleteException.php +++ b/packages/core/src/Exceptions/Carts/BillingAddressIncompleteException.php @@ -4,4 +4,5 @@ class BillingAddressIncompleteException extends CartException { + // } diff --git a/packages/core/src/Exceptions/Carts/BillingAddressMissingException.php b/packages/core/src/Exceptions/Carts/BillingAddressMissingException.php index bddec1b4be..33af232e21 100644 --- a/packages/core/src/Exceptions/Carts/BillingAddressMissingException.php +++ b/packages/core/src/Exceptions/Carts/BillingAddressMissingException.php @@ -4,4 +4,5 @@ class BillingAddressMissingException extends CartException { + // } diff --git a/packages/core/src/Exceptions/Carts/CartException.php b/packages/core/src/Exceptions/Carts/CartException.php index e97074527d..a7b6aec847 100644 --- a/packages/core/src/Exceptions/Carts/CartException.php +++ b/packages/core/src/Exceptions/Carts/CartException.php @@ -2,34 +2,32 @@ namespace Lunar\Exceptions\Carts; -use Exception; use Illuminate\Contracts\Support\MessageBag; +use Lunar\Exceptions\LunarException; -class CartException extends Exception +class CartException extends LunarException { /** - * The cart exception message bag + * The cart exception message bag. */ protected MessageBag $messageBag; - public function __construct(MessageBag $messageBag = null) + public function __construct(MessageBag $messageBag) { parent::__construct(static::summarize($messageBag)); + $this->messageBag = $messageBag; } /** - * Create an error message summary from the validation errors. - * - * @param \Illuminate\Contracts\Validation\Validator $validator - * @return string + * Create a summary from the error messages. */ - protected static function summarize($messageBag) + protected static function summarize(MessageBag $messageBag): string { $messages = $messageBag->all(); if (! count($messages) || ! is_string($messages[0])) { - return 'The cart action was invalid'; + return __('lunar::exceptions.carts.invalid_action'); } $message = array_shift($messages); @@ -43,7 +41,10 @@ protected static function summarize($messageBag) return $message; } - public function errors() + /** + * Get the error message bag. + */ + public function errors(): MessageBag { return $this->messageBag; } diff --git a/packages/core/src/Exceptions/Carts/OrderExistsException.php b/packages/core/src/Exceptions/Carts/OrderExistsException.php index 94c2cae3fb..d898005a86 100644 --- a/packages/core/src/Exceptions/Carts/OrderExistsException.php +++ b/packages/core/src/Exceptions/Carts/OrderExistsException.php @@ -4,4 +4,5 @@ class OrderExistsException extends CartException { + // } diff --git a/packages/core/src/Exceptions/Carts/ShippingAddressIncompleteException.php b/packages/core/src/Exceptions/Carts/ShippingAddressIncompleteException.php index 83e8de6351..3787535571 100644 --- a/packages/core/src/Exceptions/Carts/ShippingAddressIncompleteException.php +++ b/packages/core/src/Exceptions/Carts/ShippingAddressIncompleteException.php @@ -4,4 +4,5 @@ class ShippingAddressIncompleteException extends CartException { + // } diff --git a/packages/core/src/Exceptions/Carts/ShippingAddressMissingException.php b/packages/core/src/Exceptions/Carts/ShippingAddressMissingException.php index 29eef467df..4fc84926f0 100644 --- a/packages/core/src/Exceptions/Carts/ShippingAddressMissingException.php +++ b/packages/core/src/Exceptions/Carts/ShippingAddressMissingException.php @@ -4,4 +4,5 @@ class ShippingAddressMissingException extends CartException { + // } diff --git a/packages/core/src/Exceptions/Carts/ShippingOptionMissingException.php b/packages/core/src/Exceptions/Carts/ShippingOptionMissingException.php index 8a0cf58971..b3b310622a 100644 --- a/packages/core/src/Exceptions/Carts/ShippingOptionMissingException.php +++ b/packages/core/src/Exceptions/Carts/ShippingOptionMissingException.php @@ -4,4 +4,5 @@ class ShippingOptionMissingException extends CartException { + // } diff --git a/packages/core/src/Exceptions/CustomerNotBelongsToUserException.php b/packages/core/src/Exceptions/CustomerNotBelongsToUserException.php index 19127e3444..4668975eec 100644 --- a/packages/core/src/Exceptions/CustomerNotBelongsToUserException.php +++ b/packages/core/src/Exceptions/CustomerNotBelongsToUserException.php @@ -2,8 +2,7 @@ namespace Lunar\Exceptions; -use Exception; - -class CustomerNotBelongsToUserException extends Exception +class CustomerNotBelongsToUserException extends LunarException { + // } diff --git a/packages/core/src/Exceptions/DisallowMultipleCartOrdersException.php b/packages/core/src/Exceptions/DisallowMultipleCartOrdersException.php index 41ac76c750..eb29e42f4c 100644 --- a/packages/core/src/Exceptions/DisallowMultipleCartOrdersException.php +++ b/packages/core/src/Exceptions/DisallowMultipleCartOrdersException.php @@ -2,9 +2,7 @@ namespace Lunar\Exceptions; -use Exception; - -class DisallowMultipleCartOrdersException extends Exception +class DisallowMultipleCartOrdersException extends LunarException { public function __construct() { diff --git a/packages/core/src/Exceptions/FieldTypeException.php b/packages/core/src/Exceptions/FieldTypeException.php index a1a3362e0d..c336c66754 100644 --- a/packages/core/src/Exceptions/FieldTypeException.php +++ b/packages/core/src/Exceptions/FieldTypeException.php @@ -2,8 +2,7 @@ namespace Lunar\Exceptions; -use Exception; - -class FieldTypeException extends Exception +class FieldTypeException extends LunarException { + // } diff --git a/packages/core/src/Exceptions/FieldTypes/FieldTypeMissingException.php b/packages/core/src/Exceptions/FieldTypes/FieldTypeMissingException.php index 24d9ff0733..29afbf201e 100644 --- a/packages/core/src/Exceptions/FieldTypes/FieldTypeMissingException.php +++ b/packages/core/src/Exceptions/FieldTypes/FieldTypeMissingException.php @@ -2,11 +2,11 @@ namespace Lunar\Exceptions\FieldTypes; -use Exception; +use Lunar\Exceptions\LunarException; -class FieldTypeMissingException extends Exception +class FieldTypeMissingException extends LunarException { - public function __construct($classname) + public function __construct(string $classname) { $this->message = __('lunar::exceptions.fieldtype_missing', [ 'class' => $classname, diff --git a/packages/core/src/Exceptions/FieldTypes/InvalidFieldTypeException.php b/packages/core/src/Exceptions/FieldTypes/InvalidFieldTypeException.php index 8d2c364445..b1a7b7782d 100644 --- a/packages/core/src/Exceptions/FieldTypes/InvalidFieldTypeException.php +++ b/packages/core/src/Exceptions/FieldTypes/InvalidFieldTypeException.php @@ -2,11 +2,11 @@ namespace Lunar\Exceptions\FieldTypes; -use Exception; +use Lunar\Exceptions\LunarException; -class InvalidFieldTypeException extends Exception +class InvalidFieldTypeException extends LunarException { - public function __construct($classname) + public function __construct(string $classname) { $this->message = __('lunar::exceptions.invalid_fieldtype', [ 'class' => $classname, diff --git a/packages/core/src/Exceptions/FingerprintMismatchException.php b/packages/core/src/Exceptions/FingerprintMismatchException.php index d9e815d6be..f4fd3e0a93 100644 --- a/packages/core/src/Exceptions/FingerprintMismatchException.php +++ b/packages/core/src/Exceptions/FingerprintMismatchException.php @@ -2,8 +2,7 @@ namespace Lunar\Exceptions; -use Exception; - -class FingerprintMismatchException extends Exception +class FingerprintMismatchException extends LunarException { + // } diff --git a/packages/core/src/Exceptions/InvalidCartLineQuantityException.php b/packages/core/src/Exceptions/InvalidCartLineQuantityException.php index f708c9fb1d..41ce4ceec5 100644 --- a/packages/core/src/Exceptions/InvalidCartLineQuantityException.php +++ b/packages/core/src/Exceptions/InvalidCartLineQuantityException.php @@ -2,8 +2,7 @@ namespace Lunar\Exceptions; -use Exception; - -class InvalidCartLineQuantityException extends Exception +class InvalidCartLineQuantityException extends LunarException { + // } diff --git a/packages/core/src/Exceptions/InvalidDataTypeValueException.php b/packages/core/src/Exceptions/InvalidDataTypeValueException.php index a0e75e6fd3..b26298a443 100644 --- a/packages/core/src/Exceptions/InvalidDataTypeValueException.php +++ b/packages/core/src/Exceptions/InvalidDataTypeValueException.php @@ -2,8 +2,7 @@ namespace Lunar\Exceptions; -use Exception; - -class InvalidDataTypeValueException extends Exception +class InvalidDataTypeValueException extends LunarException { + // } diff --git a/packages/core/src/Exceptions/InvalidPaymentTypeException.php b/packages/core/src/Exceptions/InvalidPaymentTypeException.php index 49b9a53695..bba66c1fab 100644 --- a/packages/core/src/Exceptions/InvalidPaymentTypeException.php +++ b/packages/core/src/Exceptions/InvalidPaymentTypeException.php @@ -2,8 +2,7 @@ namespace Lunar\Exceptions; -use Exception; - -class InvalidPaymentTypeException extends Exception +class InvalidPaymentTypeException extends LunarException { + // } diff --git a/packages/core/src/Exceptions/LunarException.php b/packages/core/src/Exceptions/LunarException.php new file mode 100644 index 0000000000..3b7bb8ee7b --- /dev/null +++ b/packages/core/src/Exceptions/LunarException.php @@ -0,0 +1,8 @@ +attributes['name'] = json_encode($value); + } + + protected function label(): Attribute + { + return Attribute::make( + get: fn (string $value) => json_decode($value), + set: fn ($value) => json_encode($value), + ); + } + /** * Define which attributes should be * protected from mass assignment. diff --git a/packages/core/src/Pipelines/CartLine/GetUnitPrice.php b/packages/core/src/Pipelines/CartLine/GetUnitPrice.php index d179965896..e5cc64b0a0 100644 --- a/packages/core/src/Pipelines/CartLine/GetUnitPrice.php +++ b/packages/core/src/Pipelines/CartLine/GetUnitPrice.php @@ -13,7 +13,7 @@ class GetUnitPrice /** * Called just before cart totals are calculated. * - * @return void + * @return mixed */ public function handle(CartLine $cartLine, Closure $next) { diff --git a/packages/meilisearch/.github/FUNDING.yml b/packages/meilisearch/.github/FUNDING.yml new file mode 100644 index 0000000000..8c14ff3364 --- /dev/null +++ b/packages/meilisearch/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [lunar] diff --git a/packages/meilisearch/.github/workflows/close-pull-request.yml b/packages/meilisearch/.github/workflows/close-pull-request.yml new file mode 100644 index 0000000000..177299fd8a --- /dev/null +++ b/packages/meilisearch/.github/workflows/close-pull-request.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on the Lunar Meilisearch Repo which is a read-only sub split of `lunarphp/lunar`. Please submit your PR on the https://github.com/lunarphp/lunar repository.

Thanks!" diff --git a/packages/opayo/.github/FUNDING.yml b/packages/opayo/.github/FUNDING.yml new file mode 100644 index 0000000000..8c14ff3364 --- /dev/null +++ b/packages/opayo/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [lunar] diff --git a/packages/opayo/.github/workflows/close-pull-request.yml b/packages/opayo/.github/workflows/close-pull-request.yml new file mode 100644 index 0000000000..e67bd32fe9 --- /dev/null +++ b/packages/opayo/.github/workflows/close-pull-request.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on the Lunar Opayo Repo which is a read-only sub split of `lunarphp/lunar`. Please submit your PR on the https://github.com/lunarphp/lunar repository.

Thanks!" diff --git a/packages/paypal/.github/FUNDING.yml b/packages/paypal/.github/FUNDING.yml new file mode 100644 index 0000000000..8c14ff3364 --- /dev/null +++ b/packages/paypal/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [lunar] diff --git a/packages/paypal/.github/workflows/close-pull-request.yml b/packages/paypal/.github/workflows/close-pull-request.yml new file mode 100644 index 0000000000..43460e490e --- /dev/null +++ b/packages/paypal/.github/workflows/close-pull-request.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on the Lunar PayPal Repo which is a read-only sub split of `lunarphp/lunar`. Please submit your PR on the https://github.com/lunarphp/lunar repository.

Thanks!" diff --git a/packages/stripe/.github/FUNDING.yml b/packages/stripe/.github/FUNDING.yml new file mode 100644 index 0000000000..8c14ff3364 --- /dev/null +++ b/packages/stripe/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [lunar] diff --git a/packages/stripe/.github/workflows/close-pull-request.yml b/packages/stripe/.github/workflows/close-pull-request.yml new file mode 100644 index 0000000000..d360b0ae8f --- /dev/null +++ b/packages/stripe/.github/workflows/close-pull-request.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on the Lunar Stripe Repo which is a read-only sub split of `lunarphp/lunar`. Please submit your PR on the https://github.com/lunarphp/lunar repository.

Thanks!" diff --git a/packages/table-rate-shipping/.github/FUNDING.yml b/packages/table-rate-shipping/.github/FUNDING.yml new file mode 100644 index 0000000000..8c14ff3364 --- /dev/null +++ b/packages/table-rate-shipping/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [lunar] diff --git a/packages/table-rate-shipping/.github/workflows/close-pull-request.yml b/packages/table-rate-shipping/.github/workflows/close-pull-request.yml new file mode 100644 index 0000000000..b6c1ca7f29 --- /dev/null +++ b/packages/table-rate-shipping/.github/workflows/close-pull-request.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on the Lunar Table Rate Shipping Repo which is a read-only sub split of `lunarphp/lunar`. Please submit your PR on the https://github.com/lunarphp/lunar repository.

Thanks!" diff --git a/tests/admin/Unit/Support/Extending/ResourceExtensionTest.php b/tests/admin/Unit/Support/Extending/ResourceExtensionTest.php index 7155263ef9..79cd54ee2e 100644 --- a/tests/admin/Unit/Support/Extending/ResourceExtensionTest.php +++ b/tests/admin/Unit/Support/Extending/ResourceExtensionTest.php @@ -1,8 +1,5 @@ create([ + 'decimal_places' => 2, + ]); + + $this->cart = Cart::factory()->create([ + 'currency_id' => $currency->id, + ]); + + $this->class = new class extends ShippingModifier + { + public function handle(Cart $cart) + { + // + } + }; + + $this->shippingModifiers = new ShippingModifiers(); +}); + +test('can add modifier', function () { + $this->shippingModifiers->add($this->class::class); + + expect($this->shippingModifiers->getModifiers())->toHaveCount(1); +}); + +function can_remove_modifier() +{ + $this->shippingModifiers->remove($this->class::class); + + expect($this->shippingModifiers->getModifiers())->toHaveCount(0); +} diff --git a/tests/core/Unit/Console/CartPruneTest.php b/tests/core/Unit/Console/CartPruneTest.php new file mode 100644 index 0000000000..8e667f9bd4 --- /dev/null +++ b/tests/core/Unit/Console/CartPruneTest.php @@ -0,0 +1,35 @@ +create(); + $channel = Channel::factory()->create(); + + $cart = Cart::create([ + 'currency_id' => $currency->id, + 'channel_id' => $channel->id, + 'meta' => ['foo' => 'bar'], + 'updated_at' => Carbon::now()->subDay(120), + ]); + + $cart = Cart::create([ + 'currency_id' => $currency->id, + 'channel_id' => $channel->id, + 'meta' => ['foo' => 'bar'], + 'updated_at' => Carbon::now()->subDay(20), + ]); + + expect(Cart::query()->get())->toHaveCount(2); + + $this->artisan('lunar:prune:carts'); + + expect(Cart::query()->get())->toHaveCount(1); +}); diff --git a/tests/core/Unit/Search/ProductOptionIndexerTest.php b/tests/core/Unit/Search/ProductOptionIndexerTest.php index 088acc1ffe..39aae45fb7 100644 --- a/tests/core/Unit/Search/ProductOptionIndexerTest.php +++ b/tests/core/Unit/Search/ProductOptionIndexerTest.php @@ -1,6 +1,7 @@ toSearchableArray($productOption); - expect($data['name_en'])->toEqual($productOption->name['en']); - expect($data['label_en'])->toEqual($productOption->label['en']); + expect($data['name_en'])->toEqual($productOption->name->en) + ->and($data['label_en'])->toEqual($productOption->label->en); }); From 320eb52cf099216475940bb6f1041e46d80ac07d Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Wed, 22 May 2024 22:11:04 +0200 Subject: [PATCH 10/17] Display shared product options (#1648) The shared product options list didn't display shared options. Now the user can translate and manage all product options. --------- Co-authored-by: Glenn Jacobs --- packages/admin/resources/lang/en/productoption.php | 3 +++ .../src/Filament/Resources/ProductOptionResource.php | 12 +++++++----- .../Pages/EditProductOption.php | 8 ++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/admin/resources/lang/en/productoption.php b/packages/admin/resources/lang/en/productoption.php index 7d4d9c799e..2711643461 100644 --- a/packages/admin/resources/lang/en/productoption.php +++ b/packages/admin/resources/lang/en/productoption.php @@ -16,6 +16,9 @@ 'handle' => [ 'label' => 'Handle', ], + 'shared' => [ + 'label' => 'Shared', + ], ], 'form' => [ diff --git a/packages/admin/src/Filament/Resources/ProductOptionResource.php b/packages/admin/src/Filament/Resources/ProductOptionResource.php index deeb7eaa6b..d6ac058dbb 100644 --- a/packages/admin/src/Filament/Resources/ProductOptionResource.php +++ b/packages/admin/src/Filament/Resources/ProductOptionResource.php @@ -7,6 +7,7 @@ use Filament\Support\Facades\FilamentIcon; use Filament\Tables; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Str; use Lunar\Admin\Filament\Resources\ProductOptionResource\Pages; use Lunar\Admin\Filament\Resources\ProductOptionResource\RelationManagers; @@ -83,7 +84,8 @@ protected static function getHandleFormComponent(): Component return Forms\Components\TextInput::make('handle') ->label(__('lunarpanel::productoption.form.handle.label')) ->required() - ->maxLength(255); + ->maxLength(255) + ->disabled(fn ($record) => ! $record->shared); } public static function getDefaultTable(Table $table): Table @@ -96,9 +98,12 @@ public static function getDefaultTable(Table $table): Table ->label(__('lunarpanel::productoption.table.label.label')), Tables\Columns\TextColumn::make('handle') ->label(__('lunarpanel::productoption.table.handle.label')), + Tables\Columns\BooleanColumn::make('shared') + ->label(__('lunarpanel::productoption.table.shared.label')), ]) ->filters([ - // + Tables\Filters\Filter::make('shared') + ->query(fn (Builder $query): Builder => $query->where('shared', true)), ]) ->actions([ Tables\Actions\EditAction::make(), @@ -108,9 +113,6 @@ public static function getDefaultTable(Table $table): Table Tables\Actions\DeleteBulkAction::make(), ]), ]) - ->modifyQueryUsing( - fn ($query) => $query->shared() - ) ->searchable(); } diff --git a/packages/admin/src/Filament/Resources/ProductOptionResource/Pages/EditProductOption.php b/packages/admin/src/Filament/Resources/ProductOptionResource/Pages/EditProductOption.php index 28da330b34..64541e4506 100644 --- a/packages/admin/src/Filament/Resources/ProductOptionResource/Pages/EditProductOption.php +++ b/packages/admin/src/Filament/Resources/ProductOptionResource/Pages/EditProductOption.php @@ -4,6 +4,7 @@ use Filament\Actions; use Lunar\Admin\Filament\Resources\ProductOptionResource; +use Lunar\Admin\Filament\Resources\ProductOptionResource\RelationManagers; use Lunar\Admin\Support\Pages\BaseEditRecord; class EditProductOption extends BaseEditRecord @@ -16,4 +17,11 @@ protected function getDefaultHeaderActions(): array Actions\DeleteAction::make(), ]; } + + public function getRelationManagers(): array + { + return $this->record->shared ? [ + RelationManagers\ValuesRelationManager::class, + ] : []; + } } From a140cac105968f8618887246371b65f4f0b005b4 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Wed, 22 May 2024 21:15:40 +0100 Subject: [PATCH 11/17] Support attributes on customer groups (#1726) This PR adds support for attributes on customer groups using the pre-existing conventions and approaches in the codebase. Replaces https://github.com/lunarphp/lunar/pull/1443 Co-authored-by: Glenn Jacobs --- .../Resources/CustomerGroupResource.php | 8 ++++++ ...dd_attributes_to_customer_groups_table.php | 22 +++++++++++++++ packages/core/src/Base/AttributeManifest.php | 2 ++ packages/core/src/Models/CustomerGroup.php | 27 +++++++++++++++++++ 4 files changed, 59 insertions(+) create mode 100644 packages/core/database/migrations/2024_01_04_100013_add_attributes_to_customer_groups_table.php diff --git a/packages/admin/src/Filament/Resources/CustomerGroupResource.php b/packages/admin/src/Filament/Resources/CustomerGroupResource.php index cd3ca277b2..97ef213bfe 100644 --- a/packages/admin/src/Filament/Resources/CustomerGroupResource.php +++ b/packages/admin/src/Filament/Resources/CustomerGroupResource.php @@ -4,10 +4,12 @@ use Filament\Forms; use Filament\Forms\Components\Component; +use Filament\Forms\Form; use Filament\Support\Facades\FilamentIcon; use Filament\Tables; use Filament\Tables\Table; use Lunar\Admin\Filament\Resources\CustomerGroupResource\Pages; +use Lunar\Admin\Support\Forms\Components\Attributes; use Lunar\Admin\Support\Resources\BaseResource; use Lunar\Models\CustomerGroup; @@ -45,6 +47,7 @@ protected static function getMainFormComponents(): array static::getNameFormComponent(), static::getHandleFormComponent(), static::getDefaultFormComponent(), + static::getAttributeDataFormComponent(), ]; } @@ -73,6 +76,11 @@ protected static function getDefaultFormComponent(): Component ->label(__('lunarpanel::customergroup.form.default.label')); } + protected static function getAttributeDataFormComponent(): Component + { + return Attributes::make()->statePath('attribute_data'); + } + public static function getDefaultTable(Table $table): Table { return $table diff --git a/packages/core/database/migrations/2024_01_04_100013_add_attributes_to_customer_groups_table.php b/packages/core/database/migrations/2024_01_04_100013_add_attributes_to_customer_groups_table.php new file mode 100644 index 0000000000..196aff0e69 --- /dev/null +++ b/packages/core/database/migrations/2024_01_04_100013_add_attributes_to_customer_groups_table.php @@ -0,0 +1,22 @@ +prefix.'customer_groups', function (Blueprint $table) { + $table->json('attribute_data')->after('default')->nullable(); + }); + } + + public function down() + { + Schema::table($this->prefix.'customer_groups', function ($table) { + $table->dropColumn('attribute_data'); + }); + } +} diff --git a/packages/core/src/Base/AttributeManifest.php b/packages/core/src/Base/AttributeManifest.php index c52d0faada..fb2630d02d 100644 --- a/packages/core/src/Base/AttributeManifest.php +++ b/packages/core/src/Base/AttributeManifest.php @@ -8,6 +8,7 @@ use Lunar\Models\Brand; use Lunar\Models\Collection as ModelsCollection; use Lunar\Models\Customer; +use Lunar\Models\CustomerGroup; use Lunar\Models\Product; use Lunar\Models\ProductVariant; @@ -26,6 +27,7 @@ class AttributeManifest ModelsCollection::class, Customer::class, Brand::class, + CustomerGroup::class, // Order::class, ]; diff --git a/packages/core/src/Models/CustomerGroup.php b/packages/core/src/Models/CustomerGroup.php index 6e81d16b37..c1c8e98b73 100644 --- a/packages/core/src/Models/CustomerGroup.php +++ b/packages/core/src/Models/CustomerGroup.php @@ -5,6 +5,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Lunar\Base\BaseModel; +use Lunar\Base\Casts\AsAttributeData; +use Lunar\Base\Traits\HasAttributes; use Lunar\Base\Traits\HasDefaultRecord; use Lunar\Base\Traits\HasMacros; use Lunar\Database\Factories\CustomerGroupFactory; @@ -14,15 +16,24 @@ * @property string $name * @property string $handle * @property bool $default + * @property ?array $attribute_data * @property ?\Illuminate\Support\Carbon $created_at * @property ?\Illuminate\Support\Carbon $updated_at */ class CustomerGroup extends BaseModel { + use HasAttributes; use HasDefaultRecord; use HasFactory; use HasMacros; + /** + * {@inheritDoc} + */ + protected $casts = [ + 'attribute_data' => AsAttributeData::class, + ]; + /** * {@inheritDoc} */ @@ -87,4 +98,20 @@ public function collections(): BelongsToMany "{$prefix}collection_customer_group" )->withTimestamps(); } + + /** + * Get the mapped attributes relation. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphToMany + */ + public function mappedAttributes() + { + $prefix = config('lunar.database.table_prefix'); + + return $this->morphToMany( + Attribute::class, + 'attributable', + "{$prefix}attributables" + )->withTimestamps(); + } } From a494f5037954f9830f951126d5b3fe5cc99c1457 Mon Sep 17 00:00:00 2001 From: glennjacobs Date: Wed, 22 May 2024 20:18:09 +0000 Subject: [PATCH 12/17] chore: fix code style --- packages/admin/src/Filament/Resources/CustomerGroupResource.php | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/admin/src/Filament/Resources/CustomerGroupResource.php b/packages/admin/src/Filament/Resources/CustomerGroupResource.php index 97ef213bfe..d72d8ea95b 100644 --- a/packages/admin/src/Filament/Resources/CustomerGroupResource.php +++ b/packages/admin/src/Filament/Resources/CustomerGroupResource.php @@ -4,7 +4,6 @@ use Filament\Forms; use Filament\Forms\Components\Component; -use Filament\Forms\Form; use Filament\Support\Facades\FilamentIcon; use Filament\Tables; use Filament\Tables\Table; From 35687ad6dbba48033a0646373f7f55e2920b23be Mon Sep 17 00:00:00 2001 From: tvlokven <113194867+tvlokven@users.noreply.github.com> Date: Thu, 23 May 2024 00:06:57 +0200 Subject: [PATCH 13/17] Allow order creation without a shipping address when a collection shipping option has been selected (#1745) Currently, a shipping address is always required if a Cart is "shippable", i.e. when it contains physical products. However, when a customer chooses to pick up an order, rather than having it delivered, a shipping address should not be required. --------- Co-authored-by: tim Co-authored-by: Glenn Jacobs --- .../Cart/ValidateCartForOrderCreation.php | 28 +-- .../Cart/ValidateCartForOrderCreationTest.php | 165 +++++++++++++++++- 2 files changed, 179 insertions(+), 14 deletions(-) diff --git a/packages/core/src/Validation/Cart/ValidateCartForOrderCreation.php b/packages/core/src/Validation/Cart/ValidateCartForOrderCreation.php index 33936e6dca..37a1733340 100644 --- a/packages/core/src/Validation/Cart/ValidateCartForOrderCreation.php +++ b/packages/core/src/Validation/Cart/ValidateCartForOrderCreation.php @@ -33,24 +33,26 @@ public function validate(): bool return $this->fail('cart', $billingValidator->errors()->getMessages()); } - // Is this cart shippable and if so, does it have a shipping address. if ($cart->isShippable()) { - if (! $cart->shippingAddress) { - return $this->fail('cart', __('lunar::exceptions.carts.shipping_missing')); + // Do we have a shipping option applied? + if (! $shippingOption = $cart->getShippingOption()) { + return $this->fail('cart', __('lunar::exceptions.carts.shipping_option_missing')); } - $shippingValidator = Validator::make( - $cart->shippingAddress->toArray(), - $this->getAddressRules() - ); + // Is this cart going to be shipped and if so, does it have a shipping address? + if (! $shippingOption->collect) { + if (! $cart->shippingAddress) { + return $this->fail('cart', __('lunar::exceptions.carts.shipping_missing')); + } - if ($shippingValidator->fails()) { - return $this->fail('cart', $shippingValidator->errors()->getMessages()); - } + $shippingValidator = Validator::make( + $cart->shippingAddress->toArray(), + $this->getAddressRules() + ); - // Do we have a shipping option applied? - if (! $cart->getShippingOption()) { - return $this->fail('cart', __('lunar::exceptions.carts.shipping_option_missing')); + if ($shippingValidator->fails()) { + return $this->fail('cart', $shippingValidator->errors()->getMessages()); + } } } diff --git a/tests/core/Unit/Validation/Cart/ValidateCartForOrderCreationTest.php b/tests/core/Unit/Validation/Cart/ValidateCartForOrderCreationTest.php index 24c23b0750..5c0fb842ca 100644 --- a/tests/core/Unit/Validation/Cart/ValidateCartForOrderCreationTest.php +++ b/tests/core/Unit/Validation/Cart/ValidateCartForOrderCreationTest.php @@ -1,10 +1,16 @@ create(); $cart = Cart::factory()->create([ 'currency_id' => $currency->id, ]); + $purchasable = ProductVariant::factory()->create([ + 'shippable' => true, + ]); + + $cart->lines()->create([ + 'purchasable_type' => get_class($purchasable), + 'purchasable_id' => $purchasable->id, + 'quantity' => 1, + ]); + $validator = (new ValidateCartForOrderCreation)->using( cart: $cart ); @@ -95,13 +111,114 @@ CartAddress::factory()->create([ 'type' => 'billing', 'cart_id' => $cart->id, + ]); + + $this->expectException(CartException::class); + $this->expectExceptionMessage(__('lunar::exceptions.carts.shipping_option_missing')); + + $validator->validate(); +}); + +test('can validate collection with partial shipping address', function () { + $currency = Currency::factory()->create(); + $taxClass = TaxClass::factory()->create(); + + $cart = Cart::factory()->create([ + 'currency_id' => $currency->id, + ]); + + $purchasable = ProductVariant::factory()->create([ + 'shippable' => true, + ]); + + $cart->lines()->create([ + 'purchasable_type' => get_class($purchasable), + 'purchasable_id' => $purchasable->id, + 'quantity' => 1, + ]); + + $shippingOption = new ShippingOption( + name: 'Collection', + description: 'Collection', + identifier: 'COLLECT', + price: new Price(0, $cart->currency, 1), + taxClass: $taxClass, + collect: true + ); + + ShippingManifest::addOption($shippingOption); + + CartAddress::factory()->create([ + 'type' => 'shipping', + 'cart_id' => $cart->id, 'first_name' => null, 'line_one' => null, 'city' => null, 'postcode' => null, 'country_id' => null, + 'shipping_option' => $shippingOption->getIdentifier(), + ]); + + CartAddress::factory()->create([ + 'type' => 'billing', + 'cart_id' => $cart->id, ]); + $validator = (new ValidateCartForOrderCreation)->using( + cart: $cart + ); + + expect($validator->validate())->toBeTrue(); +}); + +test('can validate delivery with partial shipping address', function () { + $currency = Currency::factory()->create(); + $taxClass = TaxClass::factory()->create(); + + $cart = Cart::factory()->create([ + 'currency_id' => $currency->id, + ]); + + $purchasable = ProductVariant::factory()->create([ + 'shippable' => true, + ]); + + $cart->lines()->create([ + 'purchasable_type' => get_class($purchasable), + 'purchasable_id' => $purchasable->id, + 'quantity' => 1, + ]); + + $shippingOption = new ShippingOption( + name: 'Basic Delivery', + description: 'Basic Delivery', + identifier: 'BASDEL', + price: new Price(500, $cart->currency, 1), + taxClass: $taxClass + ); + + ShippingManifest::addOption($shippingOption); + + CartAddress::factory()->create([ + 'type' => 'shipping', + 'cart_id' => $cart->id, + 'first_name' => null, + 'line_one' => null, + 'city' => null, + 'postcode' => null, + 'country_id' => null, + 'shipping_option' => $shippingOption->getIdentifier(), + ]); + + CartAddress::factory()->create([ + 'type' => 'billing', + 'cart_id' => $cart->id, + ]); + + $validator = (new ValidateCartForOrderCreation)->using( + cart: $cart + ); + try { $validator->validate(); } catch (CartException $e) { @@ -116,3 +233,49 @@ ]))->toBeTrue(); } }); + +test('can validate delivery with populated shipping address', function () { + $currency = Currency::factory()->create(); + $taxClass = TaxClass::factory()->create(); + + $cart = Cart::factory()->create([ + 'currency_id' => $currency->id, + ]); + + $purchasable = ProductVariant::factory()->create([ + 'shippable' => true, + ]); + + $cart->lines()->create([ + 'purchasable_type' => get_class($purchasable), + 'purchasable_id' => $purchasable->id, + 'quantity' => 1, + ]); + + $shippingOption = new ShippingOption( + name: 'Basic Delivery', + description: 'Basic Delivery', + identifier: 'BASDEL', + price: new Price(500, $cart->currency, 1), + taxClass: $taxClass + ); + + ShippingManifest::addOption($shippingOption); + + CartAddress::factory()->create([ + 'type' => 'shipping', + 'cart_id' => $cart->id, + 'shipping_option' => $shippingOption->getIdentifier(), + ]); + + CartAddress::factory()->create([ + 'type' => 'billing', + 'cart_id' => $cart->id, + ]); + + $validator = (new ValidateCartForOrderCreation)->using( + cart: $cart + ); + + expect($validator->validate())->toBeTrue(); +}); From 60ab0bf66c96898cae4162ad6ee2a043af421803 Mon Sep 17 00:00:00 2001 From: Lionel Guichard Date: Thu, 23 May 2024 09:29:52 +0200 Subject: [PATCH 14/17] Add tooltip and character limit to Order additional info (#1714) --- .../Resources/OrderResource/Pages/ManageOrder.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/admin/src/Filament/Resources/OrderResource/Pages/ManageOrder.php b/packages/admin/src/Filament/Resources/OrderResource/Pages/ManageOrder.php index 842ed18475..6381ca3906 100644 --- a/packages/admin/src/Filament/Resources/OrderResource/Pages/ManageOrder.php +++ b/packages/admin/src/Filament/Resources/OrderResource/Pages/ManageOrder.php @@ -352,13 +352,20 @@ public function getDefaultInfolist(Infolist $infolist): Infolist ->schema(fn ($state) => blank($state) ? [ Infolists\Components\TextEntry::make('no_additional_info') ->hiddenLabel() - // ->weight(FontWeight::SemiBold) ->getStateUsing(fn () => __('lunarpanel::order.infolist.no_additional_info.label')), ] : collect($state) ->map(fn ($value, $key) => Infolists\Components\TextEntry::make('meta_'.$key) ->state($value) ->label($key) - ->inlineLabel()) + ->copyable() + ->limit(50)->tooltip(function (Infolists\Components\TextEntry $component): ?string { + $state = $component->getState(); + if (strlen($state) <= $component->getCharacterLimit()) { + return null; + } + + return $state; + })) ->toArray()), ]) From 1d707d8bb1c0d4ac1a072f1acae35d134abb7155 Mon Sep 17 00:00:00 2001 From: Suman Shrestha Date: Thu, 23 May 2024 13:48:04 +0545 Subject: [PATCH 15/17] =?UTF-8?q?Move=20default=20to=20same=20name=20colum?= =?UTF-8?q?n,=20and=20remove=20deprecated=20BooleanColumn=E2=80=A6=20(#163?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The column default can me merged with name like this as the column only has valid value for one row. Also replaced BooleanColumn with IconColumn. ![image](https://github.com/lunarphp/lunar/assets/8534680/76bac6d2-8833-4a00-8aeb-62bc629cbb0c) --------- Co-authored-by: Glenn Jacobs --- composer.json | 4 +++- packages/admin/composer.json | 3 ++- packages/admin/resources/dist/lunar-panel.css | 2 +- packages/admin/resources/dist/lunar-panel.js | 1 - .../src/Filament/Resources/ChannelResource.php | 14 +++++++++++--- .../src/Filament/Resources/CurrencyResource.php | 17 +++++++++++++---- .../Resources/CustomerGroupResource.php | 14 +++++++++++--- .../src/Filament/Resources/LanguageResource.php | 14 +++++++++++--- .../src/Filament/Resources/TaxClassResource.php | 14 +++++++++++--- .../src/Filament/Resources/TaxZoneResource.php | 16 ++++++++++++---- packages/admin/tailwind.config.js | 3 ++- 11 files changed, 77 insertions(+), 25 deletions(-) diff --git a/composer.json b/composer.json index 12d384272e..3bc7ce3efd 100644 --- a/composer.json +++ b/composer.json @@ -9,11 +9,12 @@ } ], "require": { + "awcodes/filament-badgeable-column": "^2.3.2", "awcodes/shout": "^2.0.2", "barryvdh/laravel-dompdf": "^2.0", - "dompdf/dompdf": "^2.0.7", "cartalyst/converter": "^8.0|^9.0", "doctrine/dbal": "^3.6", + "dompdf/dompdf": "^2.0.7", "ext-bcmath": "*", "ext-exif": "*", "ext-intl": "*", @@ -40,6 +41,7 @@ "larastan/larastan": "^2.9", "laravel/pint": "1.13.1", "mockery/mockery": "^1.4.4", + "nunomaduro/larastan": "^2.0", "orchestra/testbench": "^8.0|^9.0", "pestphp/pest": "^2.0", "pestphp/pest-plugin-laravel": "^2.0", diff --git a/packages/admin/composer.json b/packages/admin/composer.json index ccf7e8eb58..7302928564 100644 --- a/packages/admin/composer.json +++ b/packages/admin/composer.json @@ -22,7 +22,8 @@ "technikermathe/blade-lucide-icons": "^v3.0", "marvinosswald/filament-input-select-affix": "^0.2.0", "leandrocfe/filament-apex-charts": "^3.1.3", - "awcodes/shout": "^2.0.2" + "awcodes/shout": "^2.0.2", + "awcodes/filament-badgeable-column": "^2.3.2" }, "extra": { "laravel": { diff --git a/packages/admin/resources/dist/lunar-panel.css b/packages/admin/resources/dist/lunar-panel.css index c25bd444aa..2753bbbe4b 100644 --- a/packages/admin/resources/dist/lunar-panel.css +++ b/packages/admin/resources/dist/lunar-panel.css @@ -1 +1 @@ -/*! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border-width:0;border-style:solid;border-color:rgba(var(--gray-200),1)}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family),ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:rgba(var(--gray-400),1)}input::placeholder,textarea::placeholder{opacity:1;color:rgba(var(--gray-400),1)}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],input:where(:not([type])),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:rgba(var(--gray-500),var(--tw-border-opacity,1));border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,input:where(:not([type])):focus,select:focus,textarea:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:rgba(var(--gray-500),var(--tw-text-opacity,1));opacity:1}input::placeholder,textarea::placeholder{color:rgba(var(--gray-500),var(--tw-text-opacity,1));opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='rgba(var(--gray-500), var(--tw-stroke-opacity, 1))' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple],[size]:where(select:not([size="1"])){background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:rgba(var(--gray-500),var(--tw-border-opacity,1));border-width:1px;--tw-shadow:0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}[type=checkbox]:checked,[type=radio]:checked{border-color:#0000;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}@media (forced-colors:active){[type=checkbox]:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}@media (forced-colors:active){[type=radio]:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=checkbox]:indeterminate,[type=radio]:checked:focus,[type=radio]:checked:hover{border-color:#0000;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-size:100% 100%;background-position:50%;background-repeat:no-repeat}@media (forced-colors:active){[type=checkbox]:indeterminate{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:#0000;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}:root.dark{color-scheme:dark}[data-field-wrapper]{scroll-margin-top:8rem}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.-left-\[calc\(0\.5rem_-_1px\)\]{left:calc(-.5rem - -1px)}.-left-\[calc\(0\.75rem_-_1px\)\]{left:calc(-.75rem - -1px)}.left-0{left:0}.left-5{left:1.25rem}.right-0{right:0}.top-\[2px\]{top:2px}.-my-8{margin-top:-2rem;margin-bottom:-2rem}.-ml-\[5px\]{margin-left:-5px}.-mt-3\.5{margin-top:-.875rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.ml-5{margin-left:1.25rem}.ml-7{margin-left:1.75rem}.ml-8{margin-left:2rem}.mt-4{margin-top:1rem}.flow-root{display:flow-root}.w-1\/3{width:33.333333%}.w-\[2px\]{width:2px}.min-w-\[50vw\]{min-width:50vw}.min-w-full{min-width:100%}.flex-shrink-0,.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-y-px{--tw-translate-y:-1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.\!cursor-default{cursor:default!important}.cursor-grab{cursor:grab}.scroll-mt-32{scroll-margin-top:8rem}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.place-content-center{place-content:center}.gap-0{gap:0}.gap-0\.5{gap:.125rem}.gap-2\.5{gap:.625rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.divide-y-2>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(2px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(2px*var(--tw-divide-y-reverse))}.divide-gray-950\/10>:not([hidden])~:not([hidden]){border-color:rgba(var(--gray-950),.1)}.\!overflow-auto{overflow:auto!important}.rounded-b-lg{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.\!border-red-300{--tw-border-opacity:1!important;border-color:rgb(252 165 165/var(--tw-border-opacity))!important}.\!border-red-500{--tw-border-opacity:1!important;border-color:rgb(239 68 68/var(--tw-border-opacity))!important}.border-green-300{--tw-border-opacity:1;border-color:rgb(134 239 172/var(--tw-border-opacity))}.border-orange-300{--tw-border-opacity:1;border-color:rgb(253 186 116/var(--tw-border-opacity))}.border-sky-300{--tw-border-opacity:1;border-color:rgb(125 211 252/var(--tw-border-opacity))}.border-white\/10{border-color:#ffffff1a}.\!bg-red-50{--tw-bg-opacity:1!important;background-color:rgb(254 242 242/var(--tw-bg-opacity))!important}.bg-gray-300\/20{background-color:rgba(var(--gray-300),.2)}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.bg-orange-50{--tw-bg-opacity:1;background-color:rgb(255 247 237/var(--tw-bg-opacity))}.bg-purple-500{--tw-bg-opacity:1;background-color:rgb(168 85 247/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity))}.bg-sky-50{--tw-bg-opacity:1;background-color:rgb(240 249 255/var(--tw-bg-opacity))}.bg-sky-500{--tw-bg-opacity:1;background-color:rgb(14 165 233/var(--tw-bg-opacity))}.bg-teal-500{--tw-bg-opacity:1;background-color:rgb(20 184 166/var(--tw-bg-opacity))}.bg-white\/70{background-color:#ffffffb3}.\!p-0{padding:0!important}.\!p-3{padding:.75rem!important}.\!ps-6{padding-inline-start:1.5rem!important}.pl-2{padding-left:.5rem}.pl-8{padding-left:2rem}.pt-8{padding-top:2rem}.pt-\[1px\]{padding-top:1px}.pt-\[5px\]{padding-top:5px}.uppercase{text-transform:uppercase}.\!text-red-400\/60{color:#f8717199!important}.\!text-red-400\/80{color:#f87171cc!important}.\!text-red-600{--tw-text-opacity:1!important;color:rgb(220 38 38/var(--tw-text-opacity))!important}.text-gray-900{--tw-text-opacity:1;color:rgba(var(--gray-900),var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.text-orange-500{--tw-text-opacity:1;color:rgb(249 115 22/var(--tw-text-opacity))}.text-orange-600{--tw-text-opacity:1;color:rgb(234 88 12/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-sky-600{--tw-text-opacity:1;color:rgb(2 132 199/var(--tw-text-opacity))}.shadow,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.ring-1,.ring-4{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-gray-100{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--gray-100),var(--tw-ring-opacity))}.ring-purple-100{--tw-ring-opacity:1;--tw-ring-color:rgb(243 232 255/var(--tw-ring-opacity))}.ring-sky-100{--tw-ring-opacity:1;--tw-ring-color:rgb(224 242 254/var(--tw-ring-opacity))}.ring-teal-100{--tw-ring-opacity:1;--tw-ring-color:rgb(204 251 241/var(--tw-ring-opacity))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:cursor-pointer:hover{cursor:pointer}.hover\:bg-danger-100\/80:hover{background-color:rgba(var(--danger-100),.8)}.hover\:bg-primary-50:hover{--tw-bg-opacity:1;background-color:rgba(var(--primary-50),var(--tw-bg-opacity))}.hover\:bg-primary-50\/50:hover{background-color:rgba(var(--primary-50),.5)}.hover\:underline:hover{text-decoration-line:underline}.group:hover .group-hover\:flex{display:flex}.group:hover .group-hover\:scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/button:hover .group-hover\/button\:text-gray-500{--tw-text-opacity:1;color:rgba(var(--gray-500),var(--tw-text-opacity))}.group:hover .group-hover\:text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.group:hover .group-hover\:text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}:is(.dark .dark\:divide-gray-600)>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgba(var(--gray-600),var(--tw-divide-opacity))}:is(.dark .dark\:divide-white\/10)>:not([hidden])~:not([hidden]){border-color:#ffffff1a}:is(.dark .dark\:divide-white\/5)>:not([hidden])~:not([hidden]){border-color:#ffffff0d}:is(.dark .dark\:border-b){border-bottom-width:1px}:is(.dark .dark\:border-gray-600){--tw-border-opacity:1;border-color:rgba(var(--gray-600),var(--tw-border-opacity))}:is(.dark .dark\:border-white\/10){border-color:#ffffff1a}:is(.dark .dark\:border-t-white\/10){border-top-color:#ffffff1a}:is(.dark .dark\:bg-gray-600){--tw-bg-opacity:1;background-color:rgba(var(--gray-600),var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800){--tw-bg-opacity:1;background-color:rgba(var(--gray-800),var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-900){--tw-bg-opacity:1;background-color:rgba(var(--gray-900),var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-400\/10){background-color:#4ade801a}:is(.dark .dark\:bg-orange-400\/10){background-color:#fb923c1a}:is(.dark .dark\:bg-sky-400\/10){background-color:#38bdf81a}:is(.dark .dark\:bg-white\/10){background-color:#ffffff1a}:is(.dark .dark\:bg-white\/5){background-color:#ffffff0d}:is(.dark .dark\:\!text-red-400\/60){color:#f8717199!important}:is(.dark .dark\:text-gray-100){--tw-text-opacity:1;color:rgba(var(--gray-100),var(--tw-text-opacity))}:is(.dark .dark\:text-gray-200){--tw-text-opacity:1;color:rgba(var(--gray-200),var(--tw-text-opacity))}:is(.dark .dark\:text-gray-300){--tw-text-opacity:1;color:rgba(var(--gray-300),var(--tw-text-opacity))}:is(.dark .dark\:text-gray-400){--tw-text-opacity:1;color:rgba(var(--gray-400),var(--tw-text-opacity))}:is(.dark .dark\:text-gray-500){--tw-text-opacity:1;color:rgba(var(--gray-500),var(--tw-text-opacity))}:is(.dark .dark\:text-green-400){--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity))}:is(.dark .dark\:text-green-400\/80){color:#4ade80cc}:is(.dark .dark\:text-orange-400){--tw-text-opacity:1;color:rgb(251 146 60/var(--tw-text-opacity))}:is(.dark .dark\:text-primary-400){--tw-text-opacity:1;color:rgba(var(--primary-400),var(--tw-text-opacity))}:is(.dark .dark\:text-primary-400\/80){color:rgba(var(--primary-400),.8)}:is(.dark .dark\:text-red-400\/80){color:#f87171cc}:is(.dark .dark\:text-sky-400){--tw-text-opacity:1;color:rgb(56 189 248/var(--tw-text-opacity))}:is(.dark .dark\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:ring-gray-600){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--gray-600),var(--tw-ring-opacity))}:is(.dark .dark\:ring-gray-700){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--gray-700),var(--tw-ring-opacity))}:is(.dark .dark\:ring-purple-800){--tw-ring-opacity:1;--tw-ring-color:rgb(107 33 168/var(--tw-ring-opacity))}:is(.dark .dark\:ring-sky-800){--tw-ring-opacity:1;--tw-ring-color:rgb(7 89 133/var(--tw-ring-opacity))}:is(.dark .dark\:ring-teal-800){--tw-ring-opacity:1;--tw-ring-color:rgb(17 94 89/var(--tw-ring-opacity))}:is(.dark .dark\:ring-white\/10){--tw-ring-color:#ffffff1a}:is(.dark .dark\:ring-white\/20){--tw-ring-color:#fff3}:is(.dark .dark\:hover\:bg-danger-300\/20:hover){background-color:rgba(var(--danger-300),.2)}:is(.dark .dark\:hover\:bg-white\/5:hover){background-color:#ffffff0d}:is(.dark .dark\:focus-visible\:ring-primary-500:focus-visible){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--primary-500),var(--tw-ring-opacity))}:is(.dark .group\/button:hover .dark\:group-hover\/button\:text-gray-400){--tw-text-opacity:1;color:rgba(var(--gray-400),var(--tw-text-opacity))}:is(.dark .group:hover .dark\:group-hover\:text-green-400){--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity))}:is(.dark .group:hover .dark\:group-hover\:text-red-400){--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}@media (min-width:768px){.md\:min-w-\[32rem\]{min-width:32rem}}.ltr\:rotate-90:where([dir=ltr],[dir=ltr] *){--tw-rotate:90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rtl\:\!rotate-90:where([dir=rtl],[dir=rtl] *){--tw-rotate:90deg!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.rtl\:space-x-reverse:where([dir=rtl],[dir=rtl] *)>:not([hidden])~:not([hidden]){--tw-space-x-reverse:1}.rtl\:text-right:where([dir=rtl],[dir=rtl] *){text-align:right}.\[\&_table\]\:h-\[1px\] table{height:1px} \ No newline at end of file +/*! tailwindcss v3.4.0 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border-width:0;border-style:solid;border-color:rgba(var(--gray-200),1)}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family),ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:rgba(var(--gray-400),1)}input::placeholder,textarea::placeholder{opacity:1;color:rgba(var(--gray-400),1)}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],input:where(:not([type])),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:rgba(var(--gray-500),var(--tw-border-opacity,1));border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,input:where(:not([type])):focus,select:focus,textarea:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:rgba(var(--gray-500),var(--tw-text-opacity,1));opacity:1}input::placeholder,textarea::placeholder{color:rgba(var(--gray-500),var(--tw-text-opacity,1));opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='rgba(var(--gray-500), var(--tw-stroke-opacity, 1))' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple],[size]:where(select:not([size="1"])){background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:rgba(var(--gray-500),var(--tw-border-opacity,1));border-width:1px;--tw-shadow:0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}[type=checkbox]:checked,[type=radio]:checked{border-color:#0000;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}@media (forced-colors:active){[type=checkbox]:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}@media (forced-colors:active){[type=radio]:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=checkbox]:indeterminate,[type=radio]:checked:focus,[type=radio]:checked:hover{border-color:#0000;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-size:100% 100%;background-position:50%;background-repeat:no-repeat}@media (forced-colors:active){[type=checkbox]:indeterminate{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:#0000;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}:root.dark{color-scheme:dark}[data-field-wrapper]{scroll-margin-top:8rem}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.-left-\[calc\(0\.5rem_-_1px\)\]{left:calc(-.5rem - -1px)}.-left-\[calc\(0\.75rem_-_1px\)\]{left:calc(-.75rem - -1px)}.left-0{left:0}.left-5{left:1.25rem}.right-0{right:0}.top-\[2px\]{top:2px}.-my-8{margin-top:-2rem;margin-bottom:-2rem}.-ml-\[5px\]{margin-left:-5px}.-mt-3\.5{margin-top:-.875rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.ml-5{margin-left:1.25rem}.ml-7{margin-left:1.75rem}.ml-8{margin-left:2rem}.mt-4{margin-top:1rem}.flow-root{display:flow-root}.w-1\/3{width:33.333333%}.w-\[2px\]{width:2px}.min-w-\[50vw\]{min-width:50vw}.min-w-full{min-width:100%}.flex-shrink-0,.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-y-px{--tw-translate-y:-1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.\!cursor-default{cursor:default!important}.cursor-grab{cursor:grab}.scroll-mt-32{scroll-margin-top:8rem}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.place-content-center{place-content:center}.gap-0{gap:0}.gap-0\.5{gap:.125rem}.gap-2\.5{gap:.625rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.divide-y-2>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(2px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(2px*var(--tw-divide-y-reverse))}.divide-gray-950\/10>:not([hidden])~:not([hidden]){border-color:rgba(var(--gray-950),.1)}.\!overflow-auto{overflow:auto!important}.\!rounded-full{border-radius:9999px!important}.rounded-b-lg{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.\!border-red-300{--tw-border-opacity:1!important;border-color:rgb(252 165 165/var(--tw-border-opacity))!important}.\!border-red-500{--tw-border-opacity:1!important;border-color:rgb(239 68 68/var(--tw-border-opacity))!important}.border-green-300{--tw-border-opacity:1;border-color:rgb(134 239 172/var(--tw-border-opacity))}.border-orange-300{--tw-border-opacity:1;border-color:rgb(253 186 116/var(--tw-border-opacity))}.border-sky-300{--tw-border-opacity:1;border-color:rgb(125 211 252/var(--tw-border-opacity))}.border-white\/10{border-color:#ffffff1a}.\!bg-red-50{--tw-bg-opacity:1!important;background-color:rgb(254 242 242/var(--tw-bg-opacity))!important}.bg-gray-300\/20{background-color:rgba(var(--gray-300),.2)}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.bg-orange-50{--tw-bg-opacity:1;background-color:rgb(255 247 237/var(--tw-bg-opacity))}.bg-purple-500{--tw-bg-opacity:1;background-color:rgb(168 85 247/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity))}.bg-sky-50{--tw-bg-opacity:1;background-color:rgb(240 249 255/var(--tw-bg-opacity))}.bg-sky-500{--tw-bg-opacity:1;background-color:rgb(14 165 233/var(--tw-bg-opacity))}.bg-teal-500{--tw-bg-opacity:1;background-color:rgb(20 184 166/var(--tw-bg-opacity))}.bg-white\/70{background-color:#ffffffb3}.\!p-0{padding:0!important}.\!p-3{padding:.75rem!important}.\!ps-6{padding-inline-start:1.5rem!important}.pl-2{padding-left:.5rem}.pl-8{padding-left:2rem}.pt-8{padding-top:2rem}.pt-\[1px\]{padding-top:1px}.pt-\[5px\]{padding-top:5px}.uppercase{text-transform:uppercase}.\!text-red-400\/60{color:#f8717199!important}.\!text-red-400\/80{color:#f87171cc!important}.\!text-red-600{--tw-text-opacity:1!important;color:rgb(220 38 38/var(--tw-text-opacity))!important}.text-gray-900{--tw-text-opacity:1;color:rgba(var(--gray-900),var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.text-orange-500{--tw-text-opacity:1;color:rgb(249 115 22/var(--tw-text-opacity))}.text-orange-600{--tw-text-opacity:1;color:rgb(234 88 12/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-sky-600{--tw-text-opacity:1;color:rgb(2 132 199/var(--tw-text-opacity))}.shadow,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.ring-1,.ring-4{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-gray-100{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--gray-100),var(--tw-ring-opacity))}.ring-purple-100{--tw-ring-opacity:1;--tw-ring-color:rgb(243 232 255/var(--tw-ring-opacity))}.ring-sky-100{--tw-ring-opacity:1;--tw-ring-color:rgb(224 242 254/var(--tw-ring-opacity))}.ring-teal-100{--tw-ring-opacity:1;--tw-ring-color:rgb(204 251 241/var(--tw-ring-opacity))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:cursor-pointer:hover{cursor:pointer}.hover\:bg-danger-100\/80:hover{background-color:rgba(var(--danger-100),.8)}.hover\:bg-primary-50:hover{--tw-bg-opacity:1;background-color:rgba(var(--primary-50),var(--tw-bg-opacity))}.hover\:bg-primary-50\/50:hover{background-color:rgba(var(--primary-50),.5)}.hover\:underline:hover{text-decoration-line:underline}.group:hover .group-hover\:flex{display:flex}.group:hover .group-hover\:scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/button:hover .group-hover\/button\:text-gray-500{--tw-text-opacity:1;color:rgba(var(--gray-500),var(--tw-text-opacity))}.group:hover .group-hover\:text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.group:hover .group-hover\:text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}@media (min-width:768px){.md\:min-w-\[32rem\]{min-width:32rem}}:is(:where([dir=ltr]) .ltr\:rotate-90){--tw-rotate:90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is(:where([dir=rtl]) .rtl\:\!rotate-90){--tw-rotate:90deg!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}:is(:where([dir=rtl]) .rtl\:rotate-180){--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is(:where([dir=rtl]) .rtl\:space-x-reverse)>:not([hidden])~:not([hidden]){--tw-space-x-reverse:1}:is(:where([dir=rtl]) .rtl\:text-right){text-align:right}:is(:where(.dark) .dark\:divide-gray-600)>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgba(var(--gray-600),var(--tw-divide-opacity))}:is(:where(.dark) .dark\:divide-white\/10)>:not([hidden])~:not([hidden]){border-color:#ffffff1a}:is(:where(.dark) .dark\:divide-white\/5)>:not([hidden])~:not([hidden]){border-color:#ffffff0d}:is(:where(.dark) .dark\:border-b){border-bottom-width:1px}:is(:where(.dark) .dark\:border-gray-600){--tw-border-opacity:1;border-color:rgba(var(--gray-600),var(--tw-border-opacity))}:is(:where(.dark) .dark\:border-white\/10){border-color:#ffffff1a}:is(:where(.dark) .dark\:border-t-white\/10){border-top-color:#ffffff1a}:is(:where(.dark) .dark\:bg-gray-600){--tw-bg-opacity:1;background-color:rgba(var(--gray-600),var(--tw-bg-opacity))}:is(:where(.dark) .dark\:bg-gray-800){--tw-bg-opacity:1;background-color:rgba(var(--gray-800),var(--tw-bg-opacity))}:is(:where(.dark) .dark\:bg-gray-900){--tw-bg-opacity:1;background-color:rgba(var(--gray-900),var(--tw-bg-opacity))}:is(:where(.dark) .dark\:bg-green-400\/10){background-color:#4ade801a}:is(:where(.dark) .dark\:bg-orange-400\/10){background-color:#fb923c1a}:is(:where(.dark) .dark\:bg-sky-400\/10){background-color:#38bdf81a}:is(:where(.dark) .dark\:bg-white\/10){background-color:#ffffff1a}:is(:where(.dark) .dark\:bg-white\/5){background-color:#ffffff0d}:is(:where(.dark) .dark\:\!text-red-400\/60){color:#f8717199!important}:is(:where(.dark) .dark\:text-gray-100){--tw-text-opacity:1;color:rgba(var(--gray-100),var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-gray-200){--tw-text-opacity:1;color:rgba(var(--gray-200),var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-gray-300){--tw-text-opacity:1;color:rgba(var(--gray-300),var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-gray-400){--tw-text-opacity:1;color:rgba(var(--gray-400),var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-gray-500){--tw-text-opacity:1;color:rgba(var(--gray-500),var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-green-400){--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-green-400\/80){color:#4ade80cc}:is(:where(.dark) .dark\:text-orange-400){--tw-text-opacity:1;color:rgb(251 146 60/var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-primary-400){--tw-text-opacity:1;color:rgba(var(--primary-400),var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-primary-400\/80){color:rgba(var(--primary-400),.8)}:is(:where(.dark) .dark\:text-red-400\/80){color:#f87171cc}:is(:where(.dark) .dark\:text-sky-400){--tw-text-opacity:1;color:rgb(56 189 248/var(--tw-text-opacity))}:is(:where(.dark) .dark\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(:where(.dark) .dark\:ring-gray-600){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--gray-600),var(--tw-ring-opacity))}:is(:where(.dark) .dark\:ring-gray-700){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--gray-700),var(--tw-ring-opacity))}:is(:where(.dark) .dark\:ring-purple-800){--tw-ring-opacity:1;--tw-ring-color:rgb(107 33 168/var(--tw-ring-opacity))}:is(:where(.dark) .dark\:ring-sky-800){--tw-ring-opacity:1;--tw-ring-color:rgb(7 89 133/var(--tw-ring-opacity))}:is(:where(.dark) .dark\:ring-teal-800){--tw-ring-opacity:1;--tw-ring-color:rgb(17 94 89/var(--tw-ring-opacity))}:is(:where(.dark) .dark\:ring-white\/10){--tw-ring-color:#ffffff1a}:is(:where(.dark) .dark\:ring-white\/20){--tw-ring-color:#fff3}:is(:where(.dark) .dark\:hover\:bg-danger-300\/20:hover){background-color:rgba(var(--danger-300),.2)}:is(:where(.dark) .dark\:hover\:bg-white\/5:hover){background-color:#ffffff0d}:is(:where(.dark) .dark\:focus-visible\:ring-primary-500:focus-visible){--tw-ring-opacity:1;--tw-ring-color:rgba(var(--primary-500),var(--tw-ring-opacity))}:is(:where(.dark) .group\/button:hover .dark\:group-hover\/button\:text-gray-400){--tw-text-opacity:1;color:rgba(var(--gray-400),var(--tw-text-opacity))}:is(:where(.dark) .group:hover .dark\:group-hover\:text-green-400){--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity))}:is(:where(.dark) .group:hover .dark\:group-hover\:text-red-400){--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}.\[\&_table\]\:h-\[1px\] table{height:1px} \ No newline at end of file diff --git a/packages/admin/resources/dist/lunar-panel.js b/packages/admin/resources/dist/lunar-panel.js index 38d4cc9aab..e69de29bb2 100644 --- a/packages/admin/resources/dist/lunar-panel.js +++ b/packages/admin/resources/dist/lunar-panel.js @@ -1 +0,0 @@ -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFtdLAogICJzb3VyY2VzQ29udGVudCI6IFtdLAogICJtYXBwaW5ncyI6ICIiLAogICJuYW1lcyI6IFtdCn0K diff --git a/packages/admin/src/Filament/Resources/ChannelResource.php b/packages/admin/src/Filament/Resources/ChannelResource.php index b82458c04c..45140e7622 100644 --- a/packages/admin/src/Filament/Resources/ChannelResource.php +++ b/packages/admin/src/Filament/Resources/ChannelResource.php @@ -2,12 +2,15 @@ namespace Lunar\Admin\Filament\Resources; +use Awcodes\FilamentBadgeableColumn\Components\Badge; +use Awcodes\FilamentBadgeableColumn\Components\BadgeableColumn; use Filament\Forms; use Filament\Forms\Components\Component; use Filament\Support\Facades\FilamentIcon; use Filament\Tables; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletingScope; use Illuminate\Support\Str; use Lunar\Admin\Filament\Resources\ChannelResource\Pages; @@ -112,14 +115,19 @@ public static function getDefaultTable(Table $table): Table protected static function getTableColumns(): array { return [ - Tables\Columns\TextColumn::make('name') + BadgeableColumn::make('name') + ->separator('') + ->suffixBadges([ + Badge::make('default') + ->label(__('lunarpanel::channel.table.default.label')) + ->color('gray') + ->visible(fn(Model $record) => $record->default), + ]) ->label(__('lunarpanel::channel.table.name.label')), Tables\Columns\TextColumn::make('handle') ->label(__('lunarpanel::channel.table.handle.label')), Tables\Columns\TextColumn::make('url') ->label(__('lunarpanel::channel.table.url.label')), - Tables\Columns\BooleanColumn::make('default') - ->label(__('lunarpanel::channel.table.default.label')), ]; } diff --git a/packages/admin/src/Filament/Resources/CurrencyResource.php b/packages/admin/src/Filament/Resources/CurrencyResource.php index 45a1a6f22f..31886cf5d0 100644 --- a/packages/admin/src/Filament/Resources/CurrencyResource.php +++ b/packages/admin/src/Filament/Resources/CurrencyResource.php @@ -2,10 +2,13 @@ namespace Lunar\Admin\Filament\Resources; +use Awcodes\FilamentBadgeableColumn\Components\Badge; +use Awcodes\FilamentBadgeableColumn\Components\BadgeableColumn; use Filament\Forms; use Filament\Forms\Components\Component; use Filament\Support\Facades\FilamentIcon; use Filament\Tables; +use Illuminate\Database\Eloquent\Model; use Lunar\Admin\Filament\Resources\CurrencyResource\Pages; use Lunar\Admin\Support\Resources\BaseResource; use Lunar\Models\Currency; @@ -100,7 +103,14 @@ protected static function getDefaultFormComponent(): Component protected static function getDefaultTable(Tables\Table $table): Tables\Table { return $table->columns([ - Tables\Columns\TextColumn::make('name') + BadgeableColumn::make('name') + ->separator('') + ->suffixBadges([ + Badge::make('default') + ->label(__('lunarpanel::currency.table.default.label')) + ->color('gray') + ->visible(fn(Model $record) => $record->default), + ]) ->label(__('lunarpanel::currency.table.name.label')), Tables\Columns\TextColumn::make('code') ->label(__('lunarpanel::currency.table.code.label')), @@ -108,10 +118,9 @@ protected static function getDefaultTable(Tables\Table $table): Tables\Table ->label(__('lunarpanel::currency.table.exchange_rate.label')), Tables\Columns\TextColumn::make('decimal_places') ->label(__('lunarpanel::currency.table.decimal_places.label')), - Tables\Columns\BooleanColumn::make('enabled') + Tables\Columns\IconColumn::make('enabled') + ->boolean() ->label(__('lunarpanel::currency.table.enabled.label')), - Tables\Columns\BooleanColumn::make('default') - ->label(__('lunarpanel::currency.table.default.label')), ]); } diff --git a/packages/admin/src/Filament/Resources/CustomerGroupResource.php b/packages/admin/src/Filament/Resources/CustomerGroupResource.php index d72d8ea95b..31f42176af 100644 --- a/packages/admin/src/Filament/Resources/CustomerGroupResource.php +++ b/packages/admin/src/Filament/Resources/CustomerGroupResource.php @@ -2,11 +2,14 @@ namespace Lunar\Admin\Filament\Resources; +use Awcodes\FilamentBadgeableColumn\Components\Badge; +use Awcodes\FilamentBadgeableColumn\Components\BadgeableColumn; use Filament\Forms; use Filament\Forms\Components\Component; use Filament\Support\Facades\FilamentIcon; use Filament\Tables; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Model; use Lunar\Admin\Filament\Resources\CustomerGroupResource\Pages; use Lunar\Admin\Support\Forms\Components\Attributes; use Lunar\Admin\Support\Resources\BaseResource; @@ -100,12 +103,17 @@ public static function getDefaultTable(Table $table): Table protected static function getTableColumns(): array { return [ - Tables\Columns\TextColumn::make('name') + BadgeableColumn::make('name') + ->separator('') + ->suffixBadges([ + Badge::make('default') + ->label(__('lunarpanel::customergroup.table.default.label')) + ->color('gray') + ->visible(fn(Model $record) => $record->default), + ]) ->label(__('lunarpanel::customergroup.table.name.label')), Tables\Columns\TextColumn::make('handle') ->label(__('lunarpanel::customergroup.table.handle.label')), - Tables\Columns\BooleanColumn::make('default') - ->label(__('lunarpanel::customergroup.table.default.label')), ]; } diff --git a/packages/admin/src/Filament/Resources/LanguageResource.php b/packages/admin/src/Filament/Resources/LanguageResource.php index f43f5aaa9a..d3696dab4f 100644 --- a/packages/admin/src/Filament/Resources/LanguageResource.php +++ b/packages/admin/src/Filament/Resources/LanguageResource.php @@ -2,10 +2,13 @@ namespace Lunar\Admin\Filament\Resources; +use Awcodes\FilamentBadgeableColumn\Components\Badge; +use Awcodes\FilamentBadgeableColumn\Components\BadgeableColumn; use Filament\Forms; use Filament\Forms\Components\Component; use Filament\Support\Facades\FilamentIcon; use Filament\Tables; +use Illuminate\Database\Eloquent\Model; use Lunar\Admin\Filament\Resources\LanguageResource\Pages; use Lunar\Admin\Support\Resources\BaseResource; use Lunar\Models\Language; @@ -74,12 +77,17 @@ protected static function getDefaultFormComponent(): Component protected static function getDefaultTable(Tables\Table $table): Tables\Table { return $table->columns([ - Tables\Columns\TextColumn::make('name') + BadgeableColumn::make('name') + ->separator('') + ->suffixBadges([ + Badge::make('default') + ->label(__('lunarpanel::language.table.default.label')) + ->color('gray') + ->visible(fn(Model $record) => $record->default), + ]) ->label(__('lunarpanel::language.table.name.label')), Tables\Columns\TextColumn::make('code') ->label(__('lunarpanel::language.table.code.label')), - Tables\Columns\BooleanColumn::make('default') - ->label(__('lunarpanel::language.table.default.label')), ]); } diff --git a/packages/admin/src/Filament/Resources/TaxClassResource.php b/packages/admin/src/Filament/Resources/TaxClassResource.php index b4a8d75793..7f519757bb 100644 --- a/packages/admin/src/Filament/Resources/TaxClassResource.php +++ b/packages/admin/src/Filament/Resources/TaxClassResource.php @@ -2,11 +2,14 @@ namespace Lunar\Admin\Filament\Resources; +use Awcodes\FilamentBadgeableColumn\Components\Badge; +use Awcodes\FilamentBadgeableColumn\Components\BadgeableColumn; use Filament\Forms; use Filament\Forms\Components\Component; use Filament\Support\Facades\FilamentIcon; use Filament\Tables; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Model; use Lunar\Admin\Filament\Resources\TaxClassResource\Pages; use Lunar\Admin\Support\Resources\BaseResource; use Lunar\Models\TaxClass; @@ -82,9 +85,14 @@ public static function getDefaultTable(Table $table): Table protected static function getTableColumns(): array { return [ - Tables\Columns\BooleanColumn::make('default') - ->label(__('lunarpanel::taxzone.table.default.label')), - Tables\Columns\TextColumn::make('name') + BadgeableColumn::make('name') + ->separator('') + ->suffixBadges([ + Badge::make('default') + ->label(__('lunarpanel::taxclass.table.default.label')) + ->color('gray') + ->visible(fn(Model $record) => $record->default), + ]) ->label(__('lunarpanel::taxclass.table.name.label')), ]; } diff --git a/packages/admin/src/Filament/Resources/TaxZoneResource.php b/packages/admin/src/Filament/Resources/TaxZoneResource.php index 881747a922..4ff33e9d59 100644 --- a/packages/admin/src/Filament/Resources/TaxZoneResource.php +++ b/packages/admin/src/Filament/Resources/TaxZoneResource.php @@ -2,6 +2,8 @@ namespace Lunar\Admin\Filament\Resources; +use Awcodes\FilamentBadgeableColumn\Components\Badge; +use Awcodes\FilamentBadgeableColumn\Components\BadgeableColumn; use Filament\Forms; use Filament\Forms\Components\Component; use Filament\Support\Facades\FilamentIcon; @@ -318,13 +320,19 @@ public static function getDefaultTable(Table $table): Table protected static function getTableColumns(): array { return [ - Tables\Columns\BooleanColumn::make('default') - ->label(__('lunarpanel::taxzone.table.default.label')), - Tables\Columns\TextColumn::make('name') + BadgeableColumn::make('name') + ->separator('') + ->suffixBadges([ + Badge::make('default') + ->label(__('lunarpanel::taxzone.table.default.label')) + ->color('gray') + ->visible(fn(Model $record) => $record->default), + ]) ->label(__('lunarpanel::taxzone.table.name.label')), Tables\Columns\TextColumn::make('zone_type') ->label(__('lunarpanel::taxzone.table.zone_type.label')), - Tables\Columns\BooleanColumn::make('active') + Tables\Columns\IconColumn::make('active') + ->boolean() ->label(__('lunarpanel::taxzone.table.active.label')), ]; } diff --git a/packages/admin/tailwind.config.js b/packages/admin/tailwind.config.js index 557491e589..a6cb266e5e 100644 --- a/packages/admin/tailwind.config.js +++ b/packages/admin/tailwind.config.js @@ -4,5 +4,6 @@ module.exports = { presets: [preset], content: [ './resources/views/**/*.blade.php', + './vendor/awcodes/filament-badgeable-column/resources/**/*.blade.php', ], -} \ No newline at end of file +} From 631dedcf77255dcb36b38a509c1243e6540db89e Mon Sep 17 00:00:00 2001 From: glennjacobs Date: Thu, 23 May 2024 08:05:53 +0000 Subject: [PATCH 16/17] chore: fix code style --- packages/admin/src/Filament/Resources/ChannelResource.php | 2 +- packages/admin/src/Filament/Resources/CurrencyResource.php | 2 +- packages/admin/src/Filament/Resources/CustomerGroupResource.php | 2 +- packages/admin/src/Filament/Resources/LanguageResource.php | 2 +- packages/admin/src/Filament/Resources/TaxClassResource.php | 2 +- packages/admin/src/Filament/Resources/TaxZoneResource.php | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/admin/src/Filament/Resources/ChannelResource.php b/packages/admin/src/Filament/Resources/ChannelResource.php index 45140e7622..aa4410bd64 100644 --- a/packages/admin/src/Filament/Resources/ChannelResource.php +++ b/packages/admin/src/Filament/Resources/ChannelResource.php @@ -121,7 +121,7 @@ protected static function getTableColumns(): array Badge::make('default') ->label(__('lunarpanel::channel.table.default.label')) ->color('gray') - ->visible(fn(Model $record) => $record->default), + ->visible(fn (Model $record) => $record->default), ]) ->label(__('lunarpanel::channel.table.name.label')), Tables\Columns\TextColumn::make('handle') diff --git a/packages/admin/src/Filament/Resources/CurrencyResource.php b/packages/admin/src/Filament/Resources/CurrencyResource.php index 31886cf5d0..ba2b69e63e 100644 --- a/packages/admin/src/Filament/Resources/CurrencyResource.php +++ b/packages/admin/src/Filament/Resources/CurrencyResource.php @@ -109,7 +109,7 @@ protected static function getDefaultTable(Tables\Table $table): Tables\Table Badge::make('default') ->label(__('lunarpanel::currency.table.default.label')) ->color('gray') - ->visible(fn(Model $record) => $record->default), + ->visible(fn (Model $record) => $record->default), ]) ->label(__('lunarpanel::currency.table.name.label')), Tables\Columns\TextColumn::make('code') diff --git a/packages/admin/src/Filament/Resources/CustomerGroupResource.php b/packages/admin/src/Filament/Resources/CustomerGroupResource.php index 31f42176af..520b3ec583 100644 --- a/packages/admin/src/Filament/Resources/CustomerGroupResource.php +++ b/packages/admin/src/Filament/Resources/CustomerGroupResource.php @@ -109,7 +109,7 @@ protected static function getTableColumns(): array Badge::make('default') ->label(__('lunarpanel::customergroup.table.default.label')) ->color('gray') - ->visible(fn(Model $record) => $record->default), + ->visible(fn (Model $record) => $record->default), ]) ->label(__('lunarpanel::customergroup.table.name.label')), Tables\Columns\TextColumn::make('handle') diff --git a/packages/admin/src/Filament/Resources/LanguageResource.php b/packages/admin/src/Filament/Resources/LanguageResource.php index d3696dab4f..d34b41e745 100644 --- a/packages/admin/src/Filament/Resources/LanguageResource.php +++ b/packages/admin/src/Filament/Resources/LanguageResource.php @@ -83,7 +83,7 @@ protected static function getDefaultTable(Tables\Table $table): Tables\Table Badge::make('default') ->label(__('lunarpanel::language.table.default.label')) ->color('gray') - ->visible(fn(Model $record) => $record->default), + ->visible(fn (Model $record) => $record->default), ]) ->label(__('lunarpanel::language.table.name.label')), Tables\Columns\TextColumn::make('code') diff --git a/packages/admin/src/Filament/Resources/TaxClassResource.php b/packages/admin/src/Filament/Resources/TaxClassResource.php index 7f519757bb..345d339050 100644 --- a/packages/admin/src/Filament/Resources/TaxClassResource.php +++ b/packages/admin/src/Filament/Resources/TaxClassResource.php @@ -91,7 +91,7 @@ protected static function getTableColumns(): array Badge::make('default') ->label(__('lunarpanel::taxclass.table.default.label')) ->color('gray') - ->visible(fn(Model $record) => $record->default), + ->visible(fn (Model $record) => $record->default), ]) ->label(__('lunarpanel::taxclass.table.name.label')), ]; diff --git a/packages/admin/src/Filament/Resources/TaxZoneResource.php b/packages/admin/src/Filament/Resources/TaxZoneResource.php index 4ff33e9d59..caf63bc0b5 100644 --- a/packages/admin/src/Filament/Resources/TaxZoneResource.php +++ b/packages/admin/src/Filament/Resources/TaxZoneResource.php @@ -326,7 +326,7 @@ protected static function getTableColumns(): array Badge::make('default') ->label(__('lunarpanel::taxzone.table.default.label')) ->color('gray') - ->visible(fn(Model $record) => $record->default), + ->visible(fn (Model $record) => $record->default), ]) ->label(__('lunarpanel::taxzone.table.name.label')), Tables\Columns\TextColumn::make('zone_type') From fe6108a4bfb4b0fc35a35ee190c2e8e8afcafc61 Mon Sep 17 00:00:00 2001 From: Alec Ritson Date: Thu, 23 May 2024 09:19:51 +0100 Subject: [PATCH 17/17] Refactor Lunar transactions to use payment checks DTO (#1767) --- docs/core/reference/payments.md | 14 +++ .../components/transaction.blade.php | 33 +++--- .../Base/DataTransferObjects/PaymentCheck.php | 13 +++ .../DataTransferObjects/PaymentChecks.php | 69 ++++++++++++ packages/core/src/Models/Transaction.php | 5 + .../core/src/PaymentTypes/AbstractPayment.php | 7 ++ packages/opayo/src/OpayoPaymentType.php | 78 ++++++++++++++ packages/stripe/src/StripePaymentType.php | 41 +++++++ tests/opayo/Feature/OpayoPaymentTypeTest.php | 102 ++++++++++++++++++ tests/stripe/Unit/StripePaymentTypeTest.php | 100 ++++++++++++++++- 10 files changed, 441 insertions(+), 21 deletions(-) create mode 100644 packages/core/src/Base/DataTransferObjects/PaymentCheck.php create mode 100644 packages/core/src/Base/DataTransferObjects/PaymentChecks.php diff --git a/docs/core/reference/payments.md b/docs/core/reference/payments.md index c4e0479dca..ce56261e1a 100644 --- a/docs/core/reference/payments.md +++ b/docs/core/reference/payments.md @@ -330,6 +330,20 @@ $order->charges; // Get all transactions that are charges. $order->refunds; // Get all transactions that are refunds. ``` +### Payment Checks + +Some providers return information based on checks that can occur before payment is validated and completed. +This is usually related to 3DSecure but in some cases can relate to credit checks or anything the payment provider has deemed relevant to the transaction. + +You can get access to these checks via the `paymentChecks()` method on the `Transaction`. + +```php +foreach($transaction->paymentChecks() as $check) { + $check->successful; + $check->label; + $check->message; +} +``` ## Payments We will be looking to add support for the most popular payment providers, so keep an eye out here as we will list them all out. diff --git a/packages/admin/resources/views/infolists/components/transaction.blade.php b/packages/admin/resources/views/infolists/components/transaction.blade.php index 51cdc4a977..ef0ada5568 100644 --- a/packages/admin/resources/views/infolists/components/transaction.blade.php +++ b/packages/admin/resources/views/infolists/components/transaction.blade.php @@ -19,7 +19,7 @@ '!border-red-500 bg-red-50' => !$transaction->success, 'bg-gray-50' => $transaction->success, ]) -> +>
{{ $transaction->driver }} // @@ -53,7 +53,7 @@ @endif
- !$transaction->success, @@ -75,26 +75,19 @@ class="w-4" />
- {{ $transaction->created_at->format('jS F Y h:ia') }} - @if($threeD = $transaction->meta['threedSecure'] ?? null) -
- @foreach([ - 'address' => 'Address', - 'postalCode' => 'Postal Code', - 'securityCode' => 'Security Code', - ] as $metaKey => $metaLabel) - - {{ $metaLabel }} - - @endforeach -
- @endif +
+ @foreach($transaction->paymentChecks() as $check) + + {{ $check->label }}: {{ $check->message }} + + @endforeach +
@if($transaction->notes) @@ -111,7 +104,7 @@ class="w-4" @endif -
!$transaction->success, diff --git a/packages/core/src/Base/DataTransferObjects/PaymentCheck.php b/packages/core/src/Base/DataTransferObjects/PaymentCheck.php new file mode 100644 index 0000000000..a64f196841 --- /dev/null +++ b/packages/core/src/Base/DataTransferObjects/PaymentCheck.php @@ -0,0 +1,13 @@ +checks[] = $check; + } + + public function getChecks(): array + { + return $this->checks; + } + + public function current(): PaymentCheck + { + return $this->checks[$this->position]; + } + + public function next(): void + { + $this->position++; + } + + public function key(): mixed + { + return $this->position; + } + + public function valid(): bool + { + return isset($this->checks[$this->position]); + } + + public function rewind(): void + { + $this->position = 0; + } + + public function offsetExists(mixed $offset): bool + { + return isset($this->checks[$offset]); + } + + public function offsetGet(mixed $offset): mixed + { + return $this->checks[$offset]; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + if (is_null($offset)) { + $this->checks[] = $value; + } else { + $this->checks[$offset] = $value; + } + } + + public function offsetUnset(mixed $offset): void + { + unset($this->checks[$offset]); + } +} diff --git a/packages/core/src/Models/Transaction.php b/packages/core/src/Models/Transaction.php index 0386c5396e..cb672a18da 100644 --- a/packages/core/src/Models/Transaction.php +++ b/packages/core/src/Models/Transaction.php @@ -87,6 +87,11 @@ public function driver() return Payments::driver($this->driver); } + public function paymentChecks() + { + return $this->driver()->getPaymentChecks($this); + } + public function refund(int $amount, $notes = null) { return $this->driver()->refund($this, $amount, $notes); diff --git a/packages/core/src/PaymentTypes/AbstractPayment.php b/packages/core/src/PaymentTypes/AbstractPayment.php index 27d1c4f9e7..f3cfc7d59a 100644 --- a/packages/core/src/PaymentTypes/AbstractPayment.php +++ b/packages/core/src/PaymentTypes/AbstractPayment.php @@ -2,9 +2,11 @@ namespace Lunar\PaymentTypes; +use Lunar\Base\DataTransferObjects\PaymentChecks; use Lunar\Base\PaymentTypeInterface; use Lunar\Models\Cart; use Lunar\Models\Order; +use Lunar\Models\Transaction; abstract class AbstractPayment implements PaymentTypeInterface { @@ -71,4 +73,9 @@ public function setConfig(array $config): self return $this; } + + public function getPaymentChecks(Transaction $transaction): PaymentChecks + { + return new PaymentChecks; + } } diff --git a/packages/opayo/src/OpayoPaymentType.php b/packages/opayo/src/OpayoPaymentType.php index a1509f380b..ad300a7b3f 100644 --- a/packages/opayo/src/OpayoPaymentType.php +++ b/packages/opayo/src/OpayoPaymentType.php @@ -4,6 +4,8 @@ use Illuminate\Support\Str; use Lunar\Base\DataTransferObjects\PaymentCapture; +use Lunar\Base\DataTransferObjects\PaymentCheck; +use Lunar\Base\DataTransferObjects\PaymentChecks; use Lunar\Base\DataTransferObjects\PaymentRefund; use Lunar\Events\PaymentAttemptEvent; use Lunar\Models\Order; @@ -421,6 +423,82 @@ public function setPolicy(mixed $policy): void $this->policy = $policy; } + public function getPaymentChecks(Transaction $transaction): PaymentChecks + { + $meta = $transaction->meta['threedSecure'] ?? null; + + $checks = new PaymentChecks; + + if (! $meta) { + return $checks; + } + + if (isset($meta['address'])) { + $message = $meta['address']; + $successful = $meta['address'] == 'Matched'; + + if (is_bool($message) && ! $message) { + $message = 'NotMatched'; + $successful = false; + } + if (is_bool($message) && $message) { + $message = 'Matched'; + $successful = true; + } + $checks->addCheck( + new PaymentCheck( + successful: $successful, + label: 'Address', + message: $message, + ) + ); + } + + if (isset($meta['postalCode'])) { + $message = $meta['postalCode']; + $successful = $meta['postalCode'] == 'Matched'; + + if (is_bool($message) && ! $message) { + $message = 'NotMatched'; + $successful = false; + } + if (is_bool($message) && $message) { + $message = 'Matched'; + $successful = true; + } + $checks->addCheck( + new PaymentCheck( + successful: $successful, + label: 'Postal Code', + message: $message, + ) + ); + } + + if (isset($meta['securityCode'])) { + $message = $meta['securityCode']; + $successful = $meta['securityCode'] == 'Matched'; + + if (is_bool($message) && ! $message) { + $message = 'NotMatched'; + $successful = false; + } + if (is_bool($message) && $message) { + $message = 'Matched'; + $successful = true; + } + $checks->addCheck( + new PaymentCheck( + successful: $successful, + label: 'Security Code', + message: $message, + ) + ); + } + + return $checks; + } + private function saveCard(Order $order, object $details, string $authCode = null) { if (! $order->user_id) { diff --git a/packages/stripe/src/StripePaymentType.php b/packages/stripe/src/StripePaymentType.php index fa8608b342..c697b23c3b 100644 --- a/packages/stripe/src/StripePaymentType.php +++ b/packages/stripe/src/StripePaymentType.php @@ -4,6 +4,8 @@ use Lunar\Base\DataTransferObjects\PaymentAuthorize; use Lunar\Base\DataTransferObjects\PaymentCapture; +use Lunar\Base\DataTransferObjects\PaymentCheck; +use Lunar\Base\DataTransferObjects\PaymentChecks; use Lunar\Base\DataTransferObjects\PaymentRefund; use Lunar\Events\PaymentAttemptEvent; use Lunar\Exceptions\DisallowMultipleCartOrdersException; @@ -193,4 +195,43 @@ public function refund(Transaction $transaction, int $amount = 0, $notes = null) success: true ); } + + public function getPaymentChecks(Transaction $transaction): PaymentChecks + { + $meta = $transaction->meta; + + $checks = new PaymentChecks; + + if (isset($meta['address_line1_check'])) { + $checks->addCheck( + new PaymentCheck( + successful: $meta['address_line1_check'] == 'pass', + label: 'Address Line 1', + message: $meta['address_line1_check'], + ) + ); + } + + if (isset($meta['address_postal_code_check'])) { + $checks->addCheck( + new PaymentCheck( + successful: $meta['address_postal_code_check'] == 'pass', + label: 'Postal Code', + message: $meta['address_postal_code_check'], + ) + ); + } + + if (isset($meta['cvc_check'])) { + $checks->addCheck( + new PaymentCheck( + successful: $meta['cvc_check'] == 'pass', + label: 'CVC Check', + message: $meta['cvc_check'], + ) + ); + } + + return $checks; + } } diff --git a/tests/opayo/Feature/OpayoPaymentTypeTest.php b/tests/opayo/Feature/OpayoPaymentTypeTest.php index 75f280db76..c4049b3f39 100644 --- a/tests/opayo/Feature/OpayoPaymentTypeTest.php +++ b/tests/opayo/Feature/OpayoPaymentTypeTest.php @@ -134,3 +134,105 @@ 'last_four' => '1111', ]); }); + +it('can return correct payment checks', function () { + \Lunar\Models\Currency::factory()->create(); + + $cart = buildCart(); + + $order = $cart->createOrder(); + + $transactionA = \Lunar\Models\Transaction::factory()->create([ + 'order_id' => $order->id, + 'driver' => 'opayo', + 'meta' => [ + 'threedSecure' => [ + 'address' => 'Matched', + 'postalCode' => 'Matched', + 'securityCode' => 'Matched', + ], + ], + ]); + + $transactionB = \Lunar\Models\Transaction::factory()->create([ + 'order_id' => $order->id, + 'driver' => 'opayo', + 'meta' => [ + 'threedSecure' => [ + 'address' => true, + 'postalCode' => true, + 'securityCode' => true, + ], + ], + ]); + + $transactionC = \Lunar\Models\Transaction::factory()->create([ + 'order_id' => $order->id, + 'driver' => 'opayo', + 'meta' => [ + 'threedSecure' => [ + 'address' => 'NotMatched', + 'postalCode' => 'NotMatched', + 'securityCode' => 'NotMatched', + ], + ], + ]); + + $transactionD = \Lunar\Models\Transaction::factory()->create([ + 'order_id' => $order->id, + 'driver' => 'opayo', + 'meta' => [ + 'threedSecure' => [ + 'address' => false, + 'postalCode' => false, + 'securityCode' => false, + ], + ], + ]); + + $paymentAChecks = $transactionA->paymentChecks(); + + expect($paymentAChecks)->toHaveCount(3) + ->and($paymentAChecks[0]->successful) + ->toBe(true) + ->and($paymentAChecks[1]->successful) + ->toBe(true) + ->and($paymentAChecks[2]->successful) + ->toBe(true); + + $paymentBChecks = $transactionB->paymentChecks(); + + expect($paymentBChecks)->toHaveCount(3) + ->and($paymentBChecks[0]->successful) + ->toBe(true) + ->and($paymentBChecks[1]->successful) + ->toBe(true) + ->and($paymentBChecks[2]->successful) + ->toBe(true); + + $paymentCChecks = $transactionC->paymentChecks(); + + expect($paymentCChecks)->toHaveCount(3) + ->and($paymentCChecks[0]->successful) + ->not + ->toBe(true) + ->and($paymentCChecks[1]->successful) + ->not + ->toBe(true) + ->and($paymentCChecks[2]->successful) + ->not + ->toBe(true); + + $paymentDChecks = $transactionD->paymentChecks(); + + expect($paymentDChecks)->toHaveCount(3) + ->and($paymentCChecks[0]->successful) + ->not + ->toBe(true) + ->and($paymentDChecks[1]->successful) + ->not + ->toBe(true) + ->and($paymentDChecks[2]->successful) + ->not + ->toBe(true); +}); diff --git a/tests/stripe/Unit/StripePaymentTypeTest.php b/tests/stripe/Unit/StripePaymentTypeTest.php index e9ff0cbc84..0ad8f4e9f6 100644 --- a/tests/stripe/Unit/StripePaymentTypeTest.php +++ b/tests/stripe/Unit/StripePaymentTypeTest.php @@ -50,7 +50,7 @@ 'type' => 'capture', 'success' => false, ]); -})->group('foo'); +}); it('can retrieve existing payment intent', function () { $cart = CartBuilder::build([ @@ -111,3 +111,101 @@ expect($cart->refresh()->completedOrder)->toBeNull(); }); + +it('can return correct payment checks', function () { + \Lunar\Models\Currency::factory()->create(); + + $cart = buildCart(); + + $order = $cart->createOrder(); + + $transactionA = \Lunar\Models\Transaction::factory()->create([ + 'order_id' => $order->id, + 'driver' => 'stripe', + 'meta' => [ + 'address_line1_check' => 'pass', + 'address_postal_code_check' => 'pass', + 'cvc_check' => 'pass', + ], + ]); + + $transactionB = \Lunar\Models\Transaction::factory()->create([ + 'order_id' => $order->id, + 'driver' => 'stripe', + 'meta' => [ + 'address_line1_check' => 'fail', + 'address_postal_code_check' => 'fail', + 'cvc_check' => 'fail', + ], + ]); + + $transactionC = \Lunar\Models\Transaction::factory()->create([ + 'order_id' => $order->id, + 'driver' => 'stripe', + 'meta' => [ + 'address_line1_check' => 'unavailable', + 'address_postal_code_check' => 'unavailable', + 'cvc_check' => 'unavailable', + ], + ]); + + $transactionD = \Lunar\Models\Transaction::factory()->create([ + 'order_id' => $order->id, + 'driver' => 'stripe', + 'meta' => [ + 'address_line1_check' => 'unchecked', + 'address_postal_code_check' => 'unchecked', + 'cvc_check' => 'unchecked', + ], + ]); + + $paymentAChecks = $transactionA->paymentChecks(); + + expect($paymentAChecks)->toHaveCount(3) + ->and($paymentAChecks[0]->successful) + ->toBe(true) + ->and($paymentAChecks[1]->successful) + ->toBe(true) + ->and($paymentAChecks[2]->successful) + ->toBe(true); + + $paymentBChecks = $transactionB->paymentChecks(); + + expect($paymentBChecks)->toHaveCount(3) + ->and($paymentBChecks[0]->successful) + ->not + ->toBe(true) + ->and($paymentBChecks[1]->successful) + ->not + ->toBe(true) + ->and($paymentBChecks[2]->successful) + ->not + ->toBe(true); + + $paymentCChecks = $transactionC->paymentChecks(); + + expect($paymentCChecks)->toHaveCount(3) + ->and($paymentCChecks[0]->successful) + ->not + ->toBe(true) + ->and($paymentCChecks[1]->successful) + ->not + ->toBe(true) + ->and($paymentCChecks[2]->successful) + ->not + ->toBe(true); + + $paymentDChecks = $transactionD->paymentChecks(); + + expect($paymentDChecks)->toHaveCount(3) + ->and($paymentCChecks[0]->successful) + ->not + ->toBe(true) + ->and($paymentDChecks[1]->successful) + ->not + ->toBe(true) + ->and($paymentDChecks[2]->successful) + ->not + ->toBe(true); + +});