style updates
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'currency' => env('COMMERCE_CURRENCY', 'GBP'),
|
||||
'currency_symbol' => env('COMMERCE_CURRENCY_SYMBOL', '£'),
|
||||
|
||||
@@ -1,4 +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; }
|
||||
|
||||
interface InventoryProvider
|
||||
{
|
||||
public function adjust(Product $product, int $quantity, string $reason, ?int $userId = null): void;
|
||||
|
||||
public function available(Product $product): int;
|
||||
}
|
||||
|
||||
@@ -1,4 +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; }
|
||||
|
||||
interface PaymentProvider
|
||||
{
|
||||
public function charge(Order $order, array $data = []): array;
|
||||
|
||||
public function refund(Order $order, float $amount): array;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Commerce\Contracts;
|
||||
interface ShippingProvider { public function quote(array $address, float $subtotal): float; }
|
||||
|
||||
interface ShippingProvider
|
||||
{
|
||||
public function quote(array $address, float $subtotal): float;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Commerce\Contracts;
|
||||
interface TaxProvider { public function calculate(float $subtotal, array $address = []): float; }
|
||||
|
||||
interface TaxProvider
|
||||
{
|
||||
public function calculate(float $subtotal, array $address = []): float;
|
||||
}
|
||||
|
||||
+161
-13
@@ -1,24 +1,172 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
return new class extends Migration {
|
||||
|
||||
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(); });
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Commerce\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -9,15 +11,43 @@ 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 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.');
|
||||
$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.');
|
||||
}
|
||||
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.'); }
|
||||
}
|
||||
|
||||
@@ -1,10 +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()); }
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Commerce\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
final class Category extends Model
|
||||
{
|
||||
|
||||
protected $table = 'commerce_categories';
|
||||
|
||||
protected $guarded = [];
|
||||
protected function casts(): array { return ['is_active'=>'boolean']; }
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['is_active' => 'boolean'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Commerce\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
final class Customer extends Model
|
||||
{
|
||||
|
||||
protected $table = 'commerce_customers';
|
||||
|
||||
protected $guarded = [];
|
||||
protected function casts(): array { return ['billing_address'=>'array','shipping_address'=>'array']; }
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['billing_address' => 'array', 'shipping_address' => 'array'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Commerce\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
final class InventoryMovement extends Model
|
||||
{
|
||||
|
||||
protected $table = 'commerce_inventory_movements';
|
||||
|
||||
protected $guarded = [];
|
||||
protected function casts(): array { return []; }
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Commerce\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
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']; }
|
||||
|
||||
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'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Commerce\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
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']; }
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['unit_price' => 'decimal:2', 'tax_total' => 'decimal:2', 'line_total' => 'decimal:2', 'metadata' => 'array'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +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']; }
|
||||
|
||||
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'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Commerce\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
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']; }
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['price' => 'decimal:2', 'options' => 'array', 'is_active' => 'boolean'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Commerce\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
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']; }
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['value' => 'decimal:2', 'minimum_order' => 'decimal:2', 'starts_at' => 'datetime', 'ends_at' => 'datetime', 'is_active' => 'boolean', 'conditions' => 'array'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
<?php
|
||||
|
||||
// Relationship reference: Product belongsTo Category and hasMany ProductVariant; Order hasMany OrderItem and belongsTo Customer.
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<?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\DatabaseInventoryProvider;
|
||||
use Modules\Commerce\Services\ManualPaymentProvider;
|
||||
use Modules\Commerce\Services\FlatRateShippingProvider;
|
||||
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
|
||||
@@ -20,6 +23,7 @@ final class CommerceServiceProvider extends ServiceProvider
|
||||
$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'));
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<?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
|
||||
|
||||
@@ -1,5 +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']);
|
||||
|
||||
Route::get('/commerce/products', [ProductApiController::class, 'index']);
|
||||
Route::get('/commerce/products/{product}', [ProductApiController::class, 'show']);
|
||||
|
||||
@@ -1,10 +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');
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
@@ -1,4 +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); } }
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +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]); });
|
||||
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);
|
||||
}
|
||||
public function available(Product $product): int { return max(0,(int)$product->stock_quantity); }
|
||||
}
|
||||
|
||||
@@ -1,4 +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; } }
|
||||
|
||||
final class FlatRateShippingProvider implements ShippingProvider
|
||||
{
|
||||
public function quote(array $address, float $subtotal): float
|
||||
{
|
||||
return $subtotal >= 50 ? 0.0 : 4.95;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +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]; } }
|
||||
{
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +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(\Modules\Commerce\Contracts\InventoryProvider::class, app(\Modules\Commerce\Contracts\InventoryProvider::class)); }
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user