Plugin-first ecommerce for TN CMS: product catalog, cart, checkout, and orders with a provider/contract architecture for payments and shipping. Blade-rendered and SEO/cache friendly; VueJS is reserved for optional, isolated interaction islands in later phases.
Status: Phase 2 — guest shopping flow. Product catalog (Phase 1) plus a session-backed cart, guest checkout, order creation, and a Filament order admin. No payment/shipping/tax/coupons/accounts yet (later phases). Activation-safe: with the plugin active but migrations un-run, storefront pages render empty states, never 500s.
Run migrations after activating:
php artisan migrate. Optional demo data:php artisan db:seed --class="Plugins\Ecommerce\Database\Seeders\EcommerceDemoSeeder".
plugins/ecommerce/
├── plugin.json # TN CMS manifest (name, slug, provider, filament)
├── composer.json # PSR-4: Plugins\Ecommerce\ => src/
├── config/ # ecommerce, cart, order, payment, shipping, tax
├── routes/
│ ├── web.php # auto-loaded; storefront + loads api.php/admin.php
│ ├── api.php # same-origin storefront API (health endpoint only)
│ └── admin.php # placeholder (Filament is the admin surface)
├── resources/views/frontend/ # products, cart, checkout placeholders
├── database/migrations/ # empty in Phase 0
└── src/
├── EcommercePluginServiceProvider.php
└── Http/Controllers/StorefrontController.php
The TN CMS ExtensionManager auto-wires an active plugin's:
- PSR-4 autoload (
Plugins\Ecommerce\→src/) ecommerce::Blade view namespacedatabase/migrations/routes/web.phponly — wrapped in thewebmiddleware group and registered before the CMS frontend catch-all.
Everything else the plugin wires itself in EcommercePluginServiceProvider:
- Merges the six config files under their own top-level keys.
- Registers the theme view-override layer (see below).
Because a plugin provider's boot() runs after the CMS catch-all route,
the API and admin route groups are loaded from routes/web.php (the only
pre-catch-all window) rather than from the provider. See the header comment in
routes/web.php.
| Name | Method | Default URL |
|---|---|---|
ecommerce.products.index |
GET | /products |
ecommerce.products.show |
GET | /products/{slug} |
ecommerce.categories.show |
GET | /product-category/{slug} |
ecommerce.brands.show |
GET | /brand/{slug} |
ecommerce.cart.index |
GET | /cart |
ecommerce.cart.add |
POST | /cart/add |
ecommerce.cart.update |
POST | /cart/update |
ecommerce.cart.remove |
POST | /cart/remove |
ecommerce.checkout.index |
GET | /checkout |
ecommerce.checkout.store |
POST | /checkout |
ecommerce.orders.thank-you |
GET | /order/{order}/thank-you |
ecommerce.api.health |
GET | /api/ecommerce/health |
Fired via the core hook system (do_action) and Laravel events (plugin-local,
no Core listeners):
| Hook | Event | When |
|---|---|---|
ecommerce.cart.updated |
CartUpdated |
any cart mutation |
ecommerce.order.created |
OrderCreated |
order placed at checkout |
ecommerce.order.status_changed |
OrderStatusChanged |
admin status change |
ecommerce.order.completed |
— | order marked completed |
Set ECOMMERCE_ROUTE_PREFIX (config ecommerce.route_prefix) to namespace the
storefront (e.g. shop → /shop/products) and avoid CMS page-slug collisions.
Any storefront view can be overridden by the active theme without touching the plugin. Copy a view into the theme's vendor path:
themes/{active-theme}/views/vendor/ecommerce/frontend/products.blade.php
themes/{active-theme}/views/vendor/ecommerce/frontend/cart.blade.php
themes/{active-theme}/views/vendor/ecommerce/frontend/checkout.blade.php
The provider prepends themes/{theme}/views/vendor/ecommerce (active theme,
then default theme) ahead of the plugin's own views, so a theme copy wins.
Merged config keys: ecommerce, cart, order, payment, shipping, tax.
Relevant environment variables:
ECOMMERCE_ENABLED(defaulttrue)ECOMMERCE_ROUTE_PREFIX(default empty)ECOMMERCE_CURRENCY(defaultUSD)ECOMMERCE_PAYMENT_PROVIDER(defaultoffline)ECOMMERCE_SHIPPING_PROVIDER(defaultflat_rate)
php artisan optimize:clear
php artisan plugin:list
php artisan plugin:activate ecommerce # if this command exists in core
php artisan route:list | findstr ecommercePer the multi-task safety rules, Core was not modified. One gap is worth flagging for a future, separately-approved change:
- No migration auto-run on plugin activation. The
ExtensionManagermakes a plugin's migrations discoverable (adds the path to the migrator) but does not run them on activate/deactivate. Until a core activation hook exists, ecommerce tables (Phase 1+) must be applied withphp artisan migrate. This plugin is written to fail gracefully when its tables are absent, so this gap does not cause 500s — it only means an operator runsmigratemanually.