Skip to content

feat: convert Minishop to Composer package with Filament v5 admin (Phase 1 + 2)#55

Merged
willard merged 10 commits into
mainfrom
feature/convert-to-package
May 23, 2026
Merged

feat: convert Minishop to Composer package with Filament v5 admin (Phase 1 + 2)#55
willard merged 10 commits into
mainfrom
feature/convert-to-package

Conversation

@willard

@willard willard commented May 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • Phases 1–3: Extract ecommerce engine, Filament v5 admin, HTTP layer, routes, tests into packages/minishop/
  • Drop-in package: Eliminate all App\ namespace dependencies so minishop/minishop can be installed in any Laravel app
  • Move CreateNewUser, LoginResponse, RegisterResponse into the package; register as default Fortify bindings
  • Add package base Controller with AuthorizesRequests trait
  • Auto-exclude cart_token from cookie encryption via EncryptCookies::except() in ServiceProvider
  • Gate storefront routes behind MINISHOP_STOREFRONT env var (default off; Admin + API only)
  • Add php artisan minishop:install command for drop-in setup
  • Fix Orchestra Testbench v11 package test suite: providers, middleware aliases, fixtures

Test plan

  • php artisan test --compact → 42 passed (main app)
  • php artisan test --compact --testsuite=Package → 251 passed
  • php artisan minishop:install runs without errors in a fresh Laravel app
  • Visit /dashboard as super-admin → Filament panel loads
  • Visit /dashboard as customer → 403 forbidden
  • POST /api/v1/auth/register → creates user, assigns customer role, returns token
  • Storefront routes not registered when MINISHOP_STOREFRONT is unset

🤖 Generated with Claude Code

willard-wpu and others added 6 commits May 20, 2026 08:03
Creates packages/minishop/ as a path-repository Composer package with
the Minishop namespace. Moves all 23 models, 7 enums, 8 actions,
6 services, 8 observers, 9 policies, mail, notifications, AI agent,
factories, domain seeders, and migrations into the package. Updates all
App\Models\*, App\Enums\*, etc. imports across controllers, tests, and
config to Minishop\* namespace. Registers custom Factory name/model
resolvers so Eloquent factories resolve correctly for the new namespace.
The main app retains only Fortify auth, settings controllers, and the
Laravel bootstrap layer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Install filament/filament:^5.0 in the package
- Create MinishopPanelProvider extending PanelProvider at /dashboard
- Add FilamentUser contract and canAccessPanel() to User model
- Build 11 Filament resources: Category, Tag, Product, ShippingMethod,
  TaxZone (with RatesRelationManager), Coupon, User, Customer, Order
  (with ItemsRelationManager), OrderReturn, ActivityLog
- Remove 25 legacy admin Inertia controllers, 20 admin Form Requests,
  23 admin test files, 12 admin Vue page directories
- Strip admin route group from routes/web.php
- Move OrderStatusChangedMail dispatch from deleted controller into
  OrderObserver so status-change emails still fire from any trigger
- Fix LowStockSubject to use url() instead of route('admin.products.show')
- Update auth/account tests to reference /dashboard path directly
- All 294 tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move storefront, account, API v1, and webhook controllers into Minishop\Http\Controllers\*
- Move form requests (storefront, account, API) and Eloquent API resources into the package
- Move mail and invoice PDF Blade views into packages/minishop/resources/views/
- Create packages/minishop/routes/web.php and api.php; strip main app routes to settings only
- Move all storefront, account, API, webhook, and unit tests into packages/minishop/tests/
- Fix phpunit.xml to reference package test suite; remove deleted main-app test directories
- Move OrderStatusChangedMail dispatch from deleted admin controller into OrderObserver
- Fix LowStockSubject URL generation to use url() helper (no Filament panel context at checkout)
- Use Route::middleware('web')->group() for implicit route model binding in package routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Update storefront Vue pages and composables: replace
  App/Http/Controllers/Storefront/* imports with Minishop/Http/Controllers/Storefront/*
  (generated by php artisan wayfinder:generate after Phase 3 controller move)
- Replace deleted admin Wayfinder imports in AppSidebar.vue with hardcoded
  Filament panel paths (/dashboard/products, /dashboard/orders, etc.)
- Remove Admin/ProductController import from AppSidebarHeader.vue;
  point low-stock badge to Filament products page
- Delete Dashboard.vue (no Inertia route; /dashboard now served by Filament)
- Delete resources/js/tests/pages/admin/ and Dashboard.test.ts
  (tested deleted Inertia admin pages replaced by Filament)
- Update AppSidebar.test.ts to remove dead admin controller mocks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Eliminate all App\ namespace dependencies from the package so it can
  be dropped into any Laravel app without modifications
- Move CreateNewUser, LoginResponse, and RegisterResponse into the
  package (Minishop\Actions\Fortify, Minishop\Http\Responses) and
  register them as default Fortify bindings in MinishopServiceProvider
- Create package base Controller with AuthorizesRequests trait
- Add `EncryptCookies::except('cart_token')` in ServiceProvider boot
  so the cart session cookie always bypasses encryption
- Gate storefront routes behind `minishop.load_storefront_routes`
  (MINISHOP_STOREFRONT env var, default false)
- Add minishop:install Artisan command for drop-in setup
- Fix Orchestra Testbench v11 package test suite: add AI, Sanctum,
  Fortify, Inertia, Filament, Livewire, Spatie providers; register
  role/permission middleware aliases; configure app key, auth guards,
  inertia page existence check, Fortify features; create app.blade.php
  fixture for Inertia test rendering
- Move OrderEmailTest to package test suite
- Update main app tests to remove stale route('home') references

Package test suite: 251 passed; main app tests: 42 passed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@willard

willard commented May 23, 2026

Copy link
Copy Markdown
Owner Author

Code Review

6 issues found (scored ≥ 80 confidence). Ordered by severity.


🔴 Critical — paymongoCallback trusts URL param for payment status

packages/minishop/src/Http/Controllers/Storefront/PaymentController.php

public function paymongoCallback(Order $order, Request $request): RedirectResponse
{
    if ($request->input('status') === 'success') {
        $order->update(['payment_status' => 'paid', 'status' => OrderStatus::Processing, ...]);
    }
}

Any visitor who knows an order number can hit /checkout/payment/{order}/callback?status=success and mark the order as paid without actually paying. PayMongo sends a signed webhook — the callback URL should only redirect the user, not update payment state. Payment state should be set exclusively in the WebhookController after verifying the PayMongo signature.


🔴 Critical — PaymentController routes have no ownership check

packages/minishop/src/Http/Controllers/Storefront/PaymentController.phpshow(), stripeIntent(), paymongoCheckout()

All three methods accept any Order via route model binding with no check that the order belongs to the current session or user. An authenticated user can access another customer's payment page or create a Stripe/PayMongo intent for their order.

Add an ownership guard (e.g. compare $order->customer->user_id against the authenticated user, or check the cart session token).


🔴 High — Order confirmation page exposes PII without ownership check

packages/minishop/src/Http/Controllers/Storefront/CheckoutController.phpconfirmation()

public function confirmation(Order $order): Response
{
    $order->load(['items', 'customer.user']);
    return Inertia::render('storefront/OrderConfirmation', ['order' => $order]);
}

Any visitor who knows or guesses an order number can view the full confirmation page including customer name, address, and line items. No auth or ownership check is applied. At minimum, compare $order->customer->user_id against auth()->id(), or verify a signed URL / session token.


🟠 High — Missing rate limiting on API auth routes

packages/minishop/routes/api.php

POST /api/v1/auth/register and POST /api/v1/auth/login have no throttle: middleware. The original app may have had it, and it appears to have been dropped during package extraction. Without it these endpoints are open to brute-force and credential-stuffing attacks.

Add ->middleware('throttle:6,1') (or a named rate limiter) to both routes.


🟡 Medium — CreateOrderAction increments coupon used_count on invalid coupons

packages/minishop/src/Actions/CreateOrderAction.php

$coupon?->increment('used_count') fires whenever a coupon code is present in the order data, even when $coupon->isValid() already returned false (expired, usage-limit-exceeded, minimum-order-failed). This silently burns usage slots on coupons that should not have been applied.

Guard the increment: only call it when the coupon was actually valid and a discount was calculated.


🟡 Medium — ProcessReturnAction refund comparison uses wrong total

packages/minishop/src/Actions/ProcessReturnAction.php

$totalRefunded >= $order->total_amount compares the sum of refunded line-item subtotals against total_amount, which includes shipping and tax. The order can never be promoted to Refunded status even after every item has been refunded, because the refunded items subtotal will always be less than the order total.

Compare against $order->subtotal (items only), or subtract shipping/tax from total_amount before comparing.


Review generated by Claude Code

willard-wpu and others added 4 commits May 23, 2026 19:11
- StorefrontRendererContract: swappable inertia/blade/custom renderer
  bound in ServiceProvider; 12 controllers + route closure migrated
- PaymentManager (extends Manager): driver-based gateway plugin system
  with StripeGateway, NullGateway (COD); host apps extend via Payment::extend()
- Remove PayMongo gateway, webhook controller, routes, migration columns,
  model fields, and all tests
- publishesMigrations() replaces loadMigrationsFrom() so host apps control
  which migrations run

Fix 6 code-review findings:
- Stripe webhook: catch UnexpectedValueException (bad JSON → 400 not 500)
- Stripe webhook: wrap order update in DB::transaction() + lockForUpdate()
  to prevent duplicate confirmation emails on concurrent webhook delivery
- Stripe webhook: guard $order->customer->user null-deref with eager load
  and null check before queuing mail
- StripeGateway::initiate(): check intent status before reusing; recreate
  intent when canceled or succeeded (client_secret would be unusable)
- BladeRenderer: convert PascalCase segments to kebab-case so
  'storefront/OrderConfirmation' → 'storefront.order-confirmation'
  (was broken strtolower producing 'storefront.orderconfirmation')
- Webhook routes: add throttle:60,1 to generic {gateway} route

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CI was failing because storefront routes (MINISHOP_STOREFRONT=true) were not
registered in .env.example, so Wayfinder never generated the TypeScript action
files for the package's storefront controllers. Added the env var to .env.example
and an explicit wayfinder:generate step before npm run build so CI always has
the action files before Vite processes the Vue imports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ongo

- Replace `dashboard()` Wayfinder import with hardcoded `/dashboard` in
  AppHeader, AppSidebar, and Welcome — the Filament panel doesn't register
  a Wayfinder-compatible named route, so Wayfinder never generates `dashboard`
- Remove PayMongo imports and template block from Payment.vue (`paymongoCheckout`,
  `initPayMongo`, gateway === 'paymongo' onMounted branch, PayMongo UI section)
- Add MINISHOP_STOREFRONT=true to .env.example so CI registers storefront
  routes before wayfinder:generate runs, ensuring all storefront action files
  are generated before Vite processes Vue imports

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…table

publishesMigrations() alone does not load migrations — it only registers them
for publishing. The host app's php artisan migrate never ran the package
permission/schema tables, causing every test that seeds RoleAndPermissionSeeder
to fail with "no such table: permissions".

Added loadMigrationsFrom() alongside publishesMigrations() so migrations run
automatically in any host app (including CI) while still allowing host apps to
publish and customise them when needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@willard willard merged commit 74b0a91 into main May 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants