move into sonardesk

This commit is contained in:
Mark Gregory
2026-10-12 14:50:13 +01:00
parent 5f0d2ceb72
commit 38b4a7406f
879 changed files with 24021 additions and 1 deletions
+8
View File
@@ -0,0 +1,8 @@
<?php
return [
'currency' => env('COMMERCE_CURRENCY', 'GBP'),
'currency_symbol' => env('COMMERCE_CURRENCY_SYMBOL', '£'),
'prices_include_tax' => (bool) env('COMMERCE_PRICES_INCLUDE_TAX', true),
'low_stock_threshold' => (int) env('COMMERCE_LOW_STOCK_THRESHOLD', 5),
];
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace Modules\Commerce\Contracts;
use Modules\Commerce\Models\Product;
interface InventoryProvider
{
public function adjust(Product $product, int $quantity, string $reason, ?int $userId = null): void;
public function available(Product $product): int;
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace Modules\Commerce\Contracts;
use Modules\Commerce\Models\Order;
interface PaymentProvider
{
public function charge(Order $order, array $data = []): array;
public function refund(Order $order, float $amount): array;
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace Modules\Commerce\Contracts;
interface ShippingProvider
{
public function quote(array $address, float $subtotal): float;
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace Modules\Commerce\Contracts;
interface TaxProvider
{
public function calculate(float $subtotal, array $address = []): float;
}
@@ -0,0 +1,172 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('commerce_categories', function (Blueprint $table) {
$table->id();
$table->foreignId('parent_id')->nullable()->constrained('commerce_categories')->nullOnDelete();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->boolean('is_active')->default(true);
$table->unsignedInteger('sort_order')->default(0);
$table->timestamps();
});
Schema::create('commerce_products', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')->nullable()->constrained('commerce_categories')->nullOnDelete();
$table->string('type')->default('physical');
$table->string('name');
$table->string('slug')->unique();
$table->string('sku')->nullable()->unique();
$table->text('summary')->nullable();
$table->longText('description')->nullable();
$table->decimal('price', 12, 2)->default(0);
$table->decimal('compare_at_price', 12, 2)->nullable();
$table->decimal('cost_price', 12, 2)->nullable();
$table->string('currency', 3)->default('GBP');
$table->boolean('track_inventory')->default(true);
$table->integer('stock_quantity')->default(0);
$table->integer('low_stock_threshold')->default(5);
$table->boolean('is_featured')->default(false);
$table->string('status')->default('draft');
$table->timestamp('published_at')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['status', 'published_at']);
});
Schema::create('commerce_product_variants', function (Blueprint $table) {
$table->id();
$table->foreignId('product_id')->constrained('commerce_products')->cascadeOnDelete();
$table->string('name');
$table->string('sku')->unique();
$table->decimal('price', 12, 2)->nullable();
$table->integer('stock_quantity')->default(0);
$table->json('options')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('commerce_customers', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('email')->index();
$table->string('first_name');
$table->string('last_name');
$table->string('company')->nullable();
$table->string('phone')->nullable();
$table->string('tax_number')->nullable();
$table->json('billing_address')->nullable();
$table->json('shipping_address')->nullable();
$table->string('status')->default('active');
$table->timestamps();
});
Schema::create('commerce_orders', function (Blueprint $table) {
$table->id();
$table->string('number')->unique();
$table->foreignId('customer_id')->nullable()->constrained('commerce_customers')->nullOnDelete();
$table->string('email');
$table->string('status')->default('pending');
$table->string('payment_status')->default('unpaid');
$table->string('fulfilment_status')->default('unfulfilled');
$table->string('currency', 3)->default('GBP');
$table->decimal('subtotal', 12, 2)->default(0);
$table->decimal('discount_total', 12, 2)->default(0);
$table->decimal('shipping_total', 12, 2)->default(0);
$table->decimal('tax_total', 12, 2)->default(0);
$table->decimal('grand_total', 12, 2)->default(0);
$table->json('billing_address')->nullable();
$table->json('shipping_address')->nullable();
$table->text('notes')->nullable();
$table->timestamp('placed_at')->nullable();
$table->timestamps();
$table->index(['status', 'payment_status']);
});
Schema::create('commerce_order_items', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')->constrained('commerce_orders')->cascadeOnDelete();
$table->foreignId('product_id')->nullable()->constrained('commerce_products')->nullOnDelete();
$table->foreignId('variant_id')->nullable()->constrained('commerce_product_variants')->nullOnDelete();
$table->string('name');
$table->string('sku')->nullable();
$table->unsignedInteger('quantity');
$table->decimal('unit_price', 12, 2);
$table->decimal('tax_total', 12, 2)->default(0);
$table->decimal('line_total', 12, 2);
$table->json('metadata')->nullable();
$table->timestamps();
});
Schema::create('commerce_inventory_movements', function (Blueprint $table) {
$table->id();
$table->foreignId('product_id')->constrained('commerce_products')->cascadeOnDelete();
$table->foreignId('variant_id')->nullable()->constrained('commerce_product_variants')->cascadeOnDelete();
$table->integer('quantity');
$table->string('reason');
$table->string('reference_type')->nullable();
$table->unsignedBigInteger('reference_id')->nullable();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->text('notes')->nullable();
$table->timestamps();
$table->index(['reference_type', 'reference_id']);
});
Schema::create('commerce_promotions', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('code')->nullable()->unique();
$table->string('type')->default('percentage');
$table->decimal('value', 12, 2);
$table->decimal('minimum_order', 12, 2)->nullable();
$table->unsignedInteger('usage_limit')->nullable();
$table->unsignedInteger('times_used')->default(0);
$table->timestamp('starts_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->boolean('is_active')->default(true);
$table->json('conditions')->nullable();
$table->timestamps();
});
Schema::create('commerce_shipping_methods', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('code')->unique();
$table->decimal('price', 12, 2)->default(0);
$table->decimal('free_over', 12, 2)->nullable();
$table->boolean('is_active')->default(true);
$table->json('settings')->nullable();
$table->timestamps();
});
Schema::create('commerce_tax_rates', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('country_code', 2)->nullable();
$table->decimal('rate', 7, 4)->default(0);
$table->boolean('is_default')->default(false);
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('commerce_payment_transactions', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')->constrained('commerce_orders')->cascadeOnDelete();
$table->string('provider');
$table->string('provider_reference')->nullable()->index();
$table->string('type')->default('payment');
$table->string('status')->default('pending');
$table->decimal('amount', 12, 2);
$table->string('currency', 3);
$table->json('payload')->nullable();
$table->timestamps();
});
}
public function down(): void
{
foreach (['commerce_payment_transactions', 'commerce_tax_rates', 'commerce_shipping_methods', 'commerce_promotions', 'commerce_inventory_movements', 'commerce_order_items', 'commerce_orders', 'commerce_customers', 'commerce_product_variants', 'commerce_products', 'commerce_categories'] as $table) {
Schema::dropIfExists($table);
}
}
};
@@ -0,0 +1,53 @@
<?php
namespace Modules\Commerce\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\View\View;
use Modules\Commerce\Models\Customer;
use Modules\Commerce\Models\Order;
use Modules\Commerce\Models\Product;
use Modules\Commerce\Models\Promotion;
final class CommerceController extends Controller
{
public function index(): View
{
return view('commerce::admin.index', ['products' => Product::latest()->paginate(20), 'orders' => Order::latest()->limit(10)->get(), 'revenue' => Order::where('payment_status', 'paid')->sum('grand_total'), 'lowStock' => Product::where('track_inventory', true)->whereColumn('stock_quantity', '<=', 'low_stock_threshold')->count(), 'customers' => Customer::count(), 'promotions' => Promotion::where('is_active', true)->count()]);
}
public function storeProduct(Request $request): RedirectResponse
{
$data = $request->validate(['name' => ['required', 'string', 'max:255'], 'slug' => ['nullable', 'string', 'max:255', 'unique:commerce_products,slug'], 'sku' => ['nullable', 'string', 'max:120', 'unique:commerce_products,sku'], 'type' => ['required', 'in:physical,digital,service,bundle'], 'summary' => ['nullable', 'string'], 'description' => ['nullable', 'string'], 'price' => ['required', 'numeric', 'min:0'], 'stock_quantity' => ['required', 'integer'], 'status' => ['required', 'in:draft,active,archived']]);
$data['slug'] = $data['slug'] ?: Str::slug($data['name']);
$data['currency'] = config('commerce.currency', 'GBP');
$data['published_at'] = $data['status'] === 'active' ? now() : null;
Product::create($data);
return back()->with('status', 'Product created.');
}
public function updateProduct(Request $request, Product $product): RedirectResponse
{
$product->update($request->validate(['name' => ['required', 'string', 'max:255'], 'price' => ['required', 'numeric', 'min:0'], 'stock_quantity' => ['required', 'integer'], 'status' => ['required', 'in:draft,active,archived']]));
return back()->with('status', 'Product updated.');
}
public function destroyProduct(Product $product): RedirectResponse
{
$product->delete();
return back()->with('status', 'Product deleted.');
}
public function updateOrder(Request $request, Order $order): RedirectResponse
{
$order->update($request->validate(['status' => ['required', 'in:pending,processing,completed,cancelled,refunded'], 'payment_status' => ['required', 'in:unpaid,authorised,paid,partially_refunded,refunded,failed'], 'fulfilment_status' => ['required', 'in:unfulfilled,partial,fulfilled,returned']]));
return back()->with('status', 'Order updated.');
}
}
@@ -0,0 +1,20 @@
<?php
namespace Modules\Commerce\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Modules\Commerce\Models\Product;
final class ProductApiController extends Controller
{
public function index(): JsonResponse
{
return response()->json(Product::query()->where('status', 'active')->where(fn ($q) => $q->whereNull('published_at')->orWhere('published_at', '<=', now()))->latest()->paginate(24));
}
public function show(string $product): JsonResponse
{
return response()->json(Product::where('status', 'active')->where(fn ($q) => $q->where('slug', $product)->orWhere('id', $product))->firstOrFail());
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace Modules\Commerce\Models;
use Illuminate\Database\Eloquent\Model;
final class Category extends Model
{
protected $table = 'commerce_categories';
protected $guarded = [];
protected function casts(): array
{
return ['is_active' => 'boolean'];
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace Modules\Commerce\Models;
use Illuminate\Database\Eloquent\Model;
final class Customer extends Model
{
protected $table = 'commerce_customers';
protected $guarded = [];
protected function casts(): array
{
return ['billing_address' => 'array', 'shipping_address' => 'array'];
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace Modules\Commerce\Models;
use Illuminate\Database\Eloquent\Model;
final class InventoryMovement extends Model
{
protected $table = 'commerce_inventory_movements';
protected $guarded = [];
protected function casts(): array
{
return [];
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace Modules\Commerce\Models;
use Illuminate\Database\Eloquent\Model;
final class Order extends Model
{
protected $table = 'commerce_orders';
protected $guarded = [];
protected function casts(): array
{
return ['subtotal' => 'decimal:2', 'discount_total' => 'decimal:2', 'shipping_total' => 'decimal:2', 'tax_total' => 'decimal:2', 'grand_total' => 'decimal:2', 'billing_address' => 'array', 'shipping_address' => 'array', 'placed_at' => 'datetime'];
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace Modules\Commerce\Models;
use Illuminate\Database\Eloquent\Model;
final class OrderItem extends Model
{
protected $table = 'commerce_order_items';
protected $guarded = [];
protected function casts(): array
{
return ['unit_price' => 'decimal:2', 'tax_total' => 'decimal:2', 'line_total' => 'decimal:2', 'metadata' => 'array'];
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace Modules\Commerce\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
final class Product extends Model
{
use SoftDeletes;
protected $table = 'commerce_products';
protected $guarded = [];
protected function casts(): array
{
return ['price' => 'decimal:2', 'compare_at_price' => 'decimal:2', 'cost_price' => 'decimal:2', 'track_inventory' => 'boolean', 'is_featured' => 'boolean', 'published_at' => 'datetime', 'metadata' => 'array'];
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace Modules\Commerce\Models;
use Illuminate\Database\Eloquent\Model;
final class ProductVariant extends Model
{
protected $table = 'commerce_product_variants';
protected $guarded = [];
protected function casts(): array
{
return ['price' => 'decimal:2', 'options' => 'array', 'is_active' => 'boolean'];
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace Modules\Commerce\Models;
use Illuminate\Database\Eloquent\Model;
final class Promotion extends Model
{
protected $table = 'commerce_promotions';
protected $guarded = [];
protected function casts(): array
{
return ['value' => 'decimal:2', 'minimum_order' => 'decimal:2', 'starts_at' => 'datetime', 'ends_at' => 'datetime', 'is_active' => 'boolean', 'conditions' => 'array'];
}
}
+3
View File
@@ -0,0 +1,3 @@
<?php
// Relationship reference: Product belongsTo Category and hasMany ProductVariant; Order hasMany OrderItem and belongsTo Customer.
@@ -0,0 +1,32 @@
<?php
namespace Modules\Commerce\Providers;
use Illuminate\Support\ServiceProvider;
use Modules\Commerce\Contracts\InventoryProvider;
use Modules\Commerce\Contracts\PaymentProvider;
use Modules\Commerce\Contracts\ShippingProvider;
use Modules\Commerce\Contracts\TaxProvider;
use Modules\Commerce\Services\ConfiguredTaxProvider;
use Modules\Commerce\Services\DatabaseInventoryProvider;
use Modules\Commerce\Services\FlatRateShippingProvider;
use Modules\Commerce\Services\ManualPaymentProvider;
final class CommerceServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->register(RouteServiceProvider::class);
$this->mergeConfigFrom(module_path('Commerce', 'Config/config.php'), 'commerce');
$this->app->bind(InventoryProvider::class, DatabaseInventoryProvider::class);
$this->app->bind(PaymentProvider::class, ManualPaymentProvider::class);
$this->app->bind(ShippingProvider::class, FlatRateShippingProvider::class);
$this->app->bind(TaxProvider::class, ConfiguredTaxProvider::class);
}
public function boot(): void
{
$this->loadMigrationsFrom(module_path('Commerce', 'Database/Migrations'));
$this->loadViewsFrom(module_path('Commerce', 'Resources/views'), 'commerce');
}
}
@@ -0,0 +1,15 @@
<?php
namespace Modules\Commerce\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
final class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
Route::middleware('web')->group(module_path('Commerce', 'Routes/web.php'));
Route::prefix('api/v1')->middleware('api')->group(module_path('Commerce', 'Routes/api.php'));
}
}
@@ -0,0 +1,5 @@
<x-layouts.app :title="__('Commerce')"><div class="space-y-8"><div><h1 class="text-2xl font-semibold">Commerce</h1><p class="text-sm text-zinc-500">Catalogue, stock, customers, orders and promotions.</p></div>@if(session('status'))<div class="rounded border border-green-300 bg-green-50 p-3 text-green-800">{{ session('status') }}</div>@endif
<div class="grid gap-4 md:grid-cols-5"><div class="rounded-xl border p-4"><div class="text-sm text-zinc-500">Products</div><div class="text-2xl font-semibold">{{ $products->total() }}</div></div><div class="rounded-xl border p-4"><div class="text-sm text-zinc-500">Orders</div><div class="text-2xl font-semibold">{{ \Modules\Commerce\Models\Order::count() }}</div></div><div class="rounded-xl border p-4"><div class="text-sm text-zinc-500">Revenue</div><div class="text-2xl font-semibold">{{ config('commerce.currency_symbol','£') }}{{ number_format($revenue,2) }}</div></div><div class="rounded-xl border p-4"><div class="text-sm text-zinc-500">Customers</div><div class="text-2xl font-semibold">{{ $customers }}</div></div><div class="rounded-xl border p-4"><div class="text-sm text-zinc-500">Low stock</div><div class="text-2xl font-semibold">{{ $lowStock }}</div></div></div>
<div class="grid gap-6 lg:grid-cols-3"><form method="POST" action="{{ route('administration.commerce.products.store') }}" class="space-y-4 rounded-xl border p-5 lg:col-span-1">@csrf<h2 class="font-semibold">Add product</h2><flux:input name="name" label="Name" required/><div class="grid gap-4 md:grid-cols-2"><flux:input name="sku" label="SKU"/><flux:select name="type" label="Type"><option value="physical">Physical</option><option value="digital">Digital</option><option value="service">Service</option><option value="bundle">Bundle</option></flux:select><flux:input name="price" type="number" step="0.01" min="0" label="Price" required/><flux:input name="stock_quantity" type="number" value="0" label="Stock" required/></div><flux:textarea name="summary" label="Summary"/><flux:select name="status" label="Status"><option value="draft">Draft</option><option value="active">Active</option><option value="archived">Archived</option></flux:select><flux:button type="submit" variant="primary">Create product</flux:button></form>
<div class="overflow-hidden rounded-xl border lg:col-span-2"><table class="w-full text-sm"><thead class="bg-zinc-50"><tr><th class="p-3 text-left">Product</th><th class="p-3 text-left">SKU</th><th class="p-3 text-left">Price</th><th class="p-3 text-left">Stock</th><th class="p-3 text-left">Status</th><th class="p-3"></th></tr></thead><tbody>@forelse($products as $product)<tr class="border-t"><td class="p-3 font-medium">{{ $product->name }}</td><td class="p-3">{{ $product->sku ?: '—' }}</td><td class="p-3">{{ config('commerce.currency_symbol','£') }}{{ number_format($product->price,2) }}</td><td class="p-3">{{ $product->stock_quantity }}</td><td class="p-3">{{ ucfirst($product->status) }}</td><td class="p-3 text-right"><form method="POST" action="{{ route('administration.commerce.products.destroy',$product) }}">@csrf @method('DELETE')<flux:button size="sm" variant="danger" type="submit">Delete</flux:button></form></td></tr>@empty<tr><td colspan="6" class="p-6 text-center text-zinc-500">No products yet.</td></tr>@endforelse</tbody></table></div></div>{{ $products->links() }}
<div class="rounded-xl border p-5"><h2 class="mb-4 font-semibold">Recent orders</h2><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr><th class="p-2 text-left">Order</th><th class="p-2 text-left">Customer</th><th class="p-2 text-left">Total</th><th class="p-2 text-left">Status</th><th class="p-2 text-left">Payment</th></tr></thead><tbody>@forelse($orders as $order)<tr class="border-t"><td class="p-2 font-medium">{{ $order->number }}</td><td class="p-2">{{ $order->email }}</td><td class="p-2">{{ $order->currency }} {{ number_format($order->grand_total,2) }}</td><td class="p-2">{{ ucfirst($order->status) }}</td><td class="p-2">{{ ucfirst($order->payment_status) }}</td></tr>@empty<tr><td colspan="5" class="p-5 text-center text-zinc-500">No orders yet.</td></tr>@endforelse</tbody></table></div></div></div></x-layouts.app>
+7
View File
@@ -0,0 +1,7 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\Commerce\Http\Controllers\ProductApiController;
Route::get('/commerce/products', [ProductApiController::class, 'index']);
Route::get('/commerce/products/{product}', [ProductApiController::class, 'show']);
+12
View File
@@ -0,0 +1,12 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\Commerce\Http\Controllers\CommerceController;
Route::prefix('administration/commerce')->name('administration.commerce.')->middleware(['auth', 'permission:commerce.view'])->group(function () {
Route::get('/', [CommerceController::class, 'index'])->name('index');
Route::post('/products', [CommerceController::class, 'storeProduct'])->middleware('permission:commerce.manage')->name('products.store');
Route::put('/products/{product}', [CommerceController::class, 'updateProduct'])->middleware('permission:commerce.manage')->name('products.update');
Route::delete('/products/{product}', [CommerceController::class, 'destroyProduct'])->middleware('permission:commerce.manage')->name('products.destroy');
Route::put('/orders/{order}', [CommerceController::class, 'updateOrder'])->middleware('permission:commerce.manage')->name('orders.update');
});
@@ -0,0 +1,13 @@
<?php
namespace Modules\Commerce\Services;
use Modules\Commerce\Contracts\TaxProvider;
final class ConfiguredTaxProvider implements TaxProvider
{
public function calculate(float $subtotal, array $address = []): float
{
return config('commerce.prices_include_tax') ? 0.0 : round($subtotal * 0.2, 2);
}
}
@@ -0,0 +1,24 @@
<?php
namespace Modules\Commerce\Services;
use Illuminate\Support\Facades\DB;
use Modules\Commerce\Contracts\InventoryProvider;
use Modules\Commerce\Models\InventoryMovement;
use Modules\Commerce\Models\Product;
final class DatabaseInventoryProvider implements InventoryProvider
{
public function adjust(Product $product, int $quantity, string $reason, ?int $userId = null): void
{
DB::transaction(function () use ($product, $quantity, $reason, $userId) {
$product->increment('stock_quantity', $quantity);
InventoryMovement::create(['product_id' => $product->id, 'quantity' => $quantity, 'reason' => $reason, 'created_by' => $userId]);
});
}
public function available(Product $product): int
{
return max(0, (int) $product->stock_quantity);
}
}
@@ -0,0 +1,13 @@
<?php
namespace Modules\Commerce\Services;
use Modules\Commerce\Contracts\ShippingProvider;
final class FlatRateShippingProvider implements ShippingProvider
{
public function quote(array $address, float $subtotal): float
{
return $subtotal >= 50 ? 0.0 : 4.95;
}
}
@@ -0,0 +1,19 @@
<?php
namespace Modules\Commerce\Services;
use Modules\Commerce\Contracts\PaymentProvider;
use Modules\Commerce\Models\Order;
final class ManualPaymentProvider implements PaymentProvider
{
public function charge(Order $order, array $data = []): array
{
return ['status' => 'pending', 'provider' => 'manual', 'reference' => null];
}
public function refund(Order $order, float $amount): array
{
return ['status' => 'pending', 'provider' => 'manual', 'amount' => $amount];
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace Modules\Commerce\Tests\Feature;
use Modules\Commerce\Contracts\InventoryProvider;
use Tests\TestCase;
final class ModuleBootTest extends TestCase
{
public function test_commerce_routes_are_registered(): void
{
$this->assertTrue(app('router')->has('administration.commerce.index'));
}
public function test_commerce_contracts_are_bound(): void
{
$this->assertInstanceOf(InventoryProvider::class, app(InventoryProvider::class));
}
}
+1
View File
@@ -0,0 +1 @@
{"name":"Commerce","alias":"commerce","version":"1.0.0","description":"Products, inventory, customers, orders, promotions, tax, shipping and payments.","keywords":["commerce","catalog","inventory","orders","customers"],"priority":110,"providers":["Modules\\Commerce\\Providers\\CommerceServiceProvider"],"aliases":{},"files":[],"requires":["Core","Administration","Media","Sites"]}