feat: shop customer accounts (register/login + order history)
Schema: - shop_customers (company_id, name, phone unique-per-tenant, email, password, client_id auto-linked, last_login_at) - online_orders.shop_customer_id nullable FK Auth: - New 'shop' guard (session driver, shop_customers provider) in config/auth.php - ShopCustomer Authenticatable with hashed password cast and BelongsToTenant global scope — login attempts naturally scoped to current tenant subdomain Flow: - ShopAuthController: register / login / logout / account - Register auto-links to existing Client by phone match - /shop/account: order history (only the logged customer's orders) + profile - Checkout prefills name/phone/email from logged customer + sets shop_customer_id (and client_id from auto-link) on the placed order - Layout nav switches between Login/Register and "👤 Name + Ieșire" Tests (8 new): - register creates customer + auto-login - register auto-links existing Client by phone - duplicate phone rejected - login validates credentials - /account requires auth (redirects to /shop/login) - /account lists only the logged customer's orders - checkout attaches shop_customer_id - customers tenant-isolated Full suite: 117 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Tenant\Client;
|
||||
use App\Models\Tenant\ShopCustomer;
|
||||
use App\Tenancy\TenantManager;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ShopAuthController extends Controller
|
||||
{
|
||||
private function tenantOrFail()
|
||||
{
|
||||
$tenant = app(TenantManager::class)->current();
|
||||
if (! $tenant || ! data_get($tenant->settings, 'shop.enabled')) {
|
||||
throw new NotFoundHttpException('Magazinul online nu este activ.');
|
||||
}
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
public function showRegister()
|
||||
{
|
||||
$tenant = $this->tenantOrFail();
|
||||
if (Auth::guard('shop')->check()) return redirect('/shop/account');
|
||||
return view('shop.auth.register', ['tenant' => $tenant, 'cartCount' => $this->cartCount()]);
|
||||
}
|
||||
|
||||
public function register(Request $request)
|
||||
{
|
||||
$tenant = $this->tenantOrFail();
|
||||
$data = $request->validate([
|
||||
'name' => 'required|string|max:160',
|
||||
'phone' => 'required|string|max:40',
|
||||
'email' => 'nullable|email|max:160',
|
||||
'password' => 'required|string|min:6|confirmed',
|
||||
]);
|
||||
|
||||
// Unique per tenant (handled by composite index, but check for nicer error).
|
||||
if (ShopCustomer::where('phone', $data['phone'])->exists()) {
|
||||
return back()->withErrors(['phone' => 'Există deja un cont cu acest telefon.'])->withInput();
|
||||
}
|
||||
|
||||
// Auto-link to existing Client by phone if present.
|
||||
$client = Client::where('phone', $data['phone'])->first();
|
||||
|
||||
$customer = ShopCustomer::create([
|
||||
'client_id' => $client?->id,
|
||||
'name' => $data['name'],
|
||||
'phone' => $data['phone'],
|
||||
'email' => $data['email'] ?? null,
|
||||
'password' => $data['password'], // hashed by cast
|
||||
]);
|
||||
|
||||
event(new Registered($customer));
|
||||
Auth::guard('shop')->login($customer, remember: true);
|
||||
$customer->forceFill(['last_login_at' => now()])->save();
|
||||
|
||||
return redirect('/shop/account');
|
||||
}
|
||||
|
||||
public function showLogin()
|
||||
{
|
||||
$tenant = $this->tenantOrFail();
|
||||
if (Auth::guard('shop')->check()) return redirect('/shop/account');
|
||||
return view('shop.auth.login', ['tenant' => $tenant, 'cartCount' => $this->cartCount()]);
|
||||
}
|
||||
|
||||
public function login(Request $request)
|
||||
{
|
||||
$tenant = $this->tenantOrFail();
|
||||
$data = $request->validate([
|
||||
'phone' => 'required|string|max:40',
|
||||
'password' => 'required|string',
|
||||
]);
|
||||
|
||||
$ok = Auth::guard('shop')->attempt(
|
||||
['phone' => $data['phone'], 'password' => $data['password']],
|
||||
remember: true
|
||||
);
|
||||
if (! $ok) {
|
||||
return back()->withErrors(['phone' => 'Telefon sau parolă incorecte.'])->withInput();
|
||||
}
|
||||
|
||||
$request->session()->regenerate();
|
||||
Auth::guard('shop')->user()?->forceFill(['last_login_at' => now()])->save();
|
||||
return redirect()->intended('/shop/account');
|
||||
}
|
||||
|
||||
public function logout(Request $request)
|
||||
{
|
||||
Auth::guard('shop')->logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
return redirect('/shop');
|
||||
}
|
||||
|
||||
public function account()
|
||||
{
|
||||
$tenant = $this->tenantOrFail();
|
||||
$customer = Auth::guard('shop')->user();
|
||||
if (! $customer) return redirect('/shop/login');
|
||||
|
||||
$orders = $customer->orders()
|
||||
->latest('created_at')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
return view('shop.account', [
|
||||
'tenant' => $tenant,
|
||||
'customer' => $customer,
|
||||
'orders' => $orders,
|
||||
'cartCount' => $this->cartCount(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function cartCount(): int
|
||||
{
|
||||
$tenant = app(TenantManager::class)->current();
|
||||
$cart = (array) session('shop_cart_' . ($tenant?->id ?? '0'), []);
|
||||
return (int) collect($cart)->sum('qty');
|
||||
}
|
||||
}
|
||||
@@ -155,11 +155,13 @@ class ShopController extends Controller
|
||||
if (empty($cart)) return redirect('/shop');
|
||||
|
||||
$subtotal = collect($cart)->sum(fn ($i) => $i['price'] * $i['qty']);
|
||||
$customer = \Illuminate\Support\Facades\Auth::guard('shop')->user();
|
||||
|
||||
return view('shop.checkout', [
|
||||
'tenant' => $tenant,
|
||||
'cart' => $cart,
|
||||
'subtotal' => $subtotal,
|
||||
'customer' => $customer,
|
||||
'deliveryOptions' => (array) data_get($tenant->settings, 'shop.delivery_methods', ['pickup']),
|
||||
'cartCount' => $this->cartCount(),
|
||||
]);
|
||||
@@ -188,9 +190,13 @@ class ShopController extends Controller
|
||||
$deliveryFee = ($freeOver > 0 && $subtotal >= $freeOver) ? 0.0 : $fee;
|
||||
}
|
||||
|
||||
$order = DB::transaction(function () use ($tenant, $cart, $data, $deliveryFee) {
|
||||
$shopCustomer = \Illuminate\Support\Facades\Auth::guard('shop')->user();
|
||||
|
||||
$order = DB::transaction(function () use ($tenant, $cart, $data, $deliveryFee, $shopCustomer) {
|
||||
$order = OnlineOrder::create([
|
||||
'number' => OnlineOrder::generateNumber($tenant->id),
|
||||
'shop_customer_id' => $shopCustomer?->id,
|
||||
'client_id' => $shopCustomer?->client_id,
|
||||
'customer_name' => $data['customer_name'],
|
||||
'customer_phone' => $data['customer_phone'],
|
||||
'customer_email' => $data['customer_email'] ?? null,
|
||||
|
||||
@@ -29,7 +29,7 @@ class OnlineOrder extends Model
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'company_id', 'number', 'tracking_token', 'client_id',
|
||||
'company_id', 'number', 'tracking_token', 'client_id', 'shop_customer_id',
|
||||
'customer_name', 'customer_phone', 'customer_email',
|
||||
'delivery_method', 'address', 'status',
|
||||
'subtotal', 'delivery_fee', 'total', 'notes',
|
||||
@@ -51,6 +51,11 @@ class OnlineOrder extends Model
|
||||
return $this->belongsTo(Client::class);
|
||||
}
|
||||
|
||||
public function shopCustomer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ShopCustomer::class);
|
||||
}
|
||||
|
||||
public function trackingUrl(): string
|
||||
{
|
||||
return url('/shop/order/' . $this->tracking_token);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenant;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class ShopCustomer extends Authenticatable
|
||||
{
|
||||
use BelongsToTenant, Notifiable, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id', 'client_id', 'name', 'phone', 'email', 'password', 'last_login_at',
|
||||
];
|
||||
|
||||
protected $hidden = ['password', 'remember_token'];
|
||||
|
||||
protected $casts = [
|
||||
'last_login_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Client::class);
|
||||
}
|
||||
|
||||
public function orders(): HasMany
|
||||
{
|
||||
return $this->hasMany(OnlineOrder::class);
|
||||
}
|
||||
|
||||
/** Auth column for Laravel's session guard. */
|
||||
public function getAuthIdentifierName()
|
||||
{
|
||||
return 'id';
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,12 @@ return [
|
||||
'driver' => 'session',
|
||||
'provider' => 'super_admins',
|
||||
],
|
||||
|
||||
// Public storefront customer auth (per-tenant).
|
||||
'shop' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'shop_customers',
|
||||
],
|
||||
],
|
||||
|
||||
'providers' => [
|
||||
@@ -33,6 +39,10 @@ return [
|
||||
'driver' => 'eloquent',
|
||||
'model' => SuperAdmin::class,
|
||||
],
|
||||
'shop_customers' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => \App\Models\Tenant\ShopCustomer::class,
|
||||
],
|
||||
],
|
||||
|
||||
'passwords' => [
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?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('shop_customers', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->foreignId('company_id')->constrained()->cascadeOnDelete();
|
||||
$t->foreignId('client_id')->nullable()->constrained()->nullOnDelete();
|
||||
$t->string('name', 160);
|
||||
$t->string('phone', 40);
|
||||
$t->string('email', 160)->nullable();
|
||||
$t->string('password');
|
||||
$t->dateTime('last_login_at')->nullable();
|
||||
$t->rememberToken();
|
||||
$t->timestamps();
|
||||
$t->softDeletes();
|
||||
|
||||
$t->unique(['company_id', 'phone'], 'shop_customers_company_phone_unique');
|
||||
$t->index(['company_id', 'email']);
|
||||
});
|
||||
|
||||
Schema::table('online_orders', function (Blueprint $t) {
|
||||
$t->foreignId('shop_customer_id')->nullable()->after('client_id')
|
||||
->constrained()->nullOnDelete();
|
||||
$t->index(['company_id', 'shop_customer_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('online_orders', function (Blueprint $t) {
|
||||
$t->dropForeign(['shop_customer_id']);
|
||||
$t->dropColumn('shop_customer_id');
|
||||
});
|
||||
Schema::dropIfExists('shop_customers');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
@extends('shop.layout')
|
||||
@section('title', 'Contul meu')
|
||||
@section('content')
|
||||
@php
|
||||
$currency = $tenant->settings['currency'] ?? 'MDL';
|
||||
$statuses = \App\Models\Tenant\OnlineOrder::STATUSES;
|
||||
@endphp
|
||||
|
||||
<h1 style="font-size:22px;margin-bottom:16px;">Salut, {{ $customer->name }}!</h1>
|
||||
|
||||
<div class="card" style="margin-bottom:16px;">
|
||||
<h3 style="font-size:15px;margin-bottom:10px;">Date contact</h3>
|
||||
<p class="muted">📞 {{ $customer->phone }}</p>
|
||||
@if ($customer->email)<p class="muted">✉️ {{ $customer->email }}</p>@endif
|
||||
</div>
|
||||
|
||||
<h2 style="font-size:18px;margin-bottom:12px;">Comenzile mele ({{ $orders->count() }})</h2>
|
||||
|
||||
@if ($orders->isEmpty())
|
||||
<div class="card" style="text-align:center;padding:32px;">
|
||||
<p class="muted">Nu ai nicio comandă încă.</p>
|
||||
<a class="btn" href="/shop" style="margin-top:12px;">Vezi catalogul</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="card">
|
||||
<table class="cart">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nr.</th><th>Data</th><th>Articole</th><th class="r">Total</th><th>Status</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($orders as $order)
|
||||
<tr>
|
||||
<td><strong>#{{ $order->number }}</strong></td>
|
||||
<td>{{ $order->created_at->format('d.m.Y') }}</td>
|
||||
<td>{{ $order->items()->count() }}</td>
|
||||
<td class="r">{{ number_format((float) $order->total, 2) }} {{ $currency }}</td>
|
||||
<td><span class="status-pill" style="font-size:11px;">{{ $statuses[$order->status] ?? $order->status }}</span></td>
|
||||
<td><a href="{{ $order->trackingUrl() }}" class="muted" style="text-decoration:underline;">Detalii →</a></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
@@ -0,0 +1,31 @@
|
||||
@extends('shop.layout')
|
||||
@section('title', 'Login')
|
||||
@section('content')
|
||||
|
||||
<div style="max-width:380px;margin:0 auto;">
|
||||
<h1 style="font-size:22px;margin-bottom:16px;">Intră în cont</h1>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="card" style="border-color:#fca5a5;background:#fef2f2;margin-bottom:14px;">
|
||||
<ul style="margin:0;padding-left:18px;color:#991b1b;font-size:14px;">
|
||||
@foreach ($errors->all() as $e)<li>{{ $e }}</li>@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="/shop/login" class="card">
|
||||
@csrf
|
||||
<div class="field"><label>Telefon *</label>
|
||||
<input type="text" name="phone" value="{{ old('phone') }}" required placeholder="+373…">
|
||||
</div>
|
||||
<div class="field"><label>Parolă *</label>
|
||||
<input type="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn block">Intră</button>
|
||||
</form>
|
||||
|
||||
<p class="muted" style="text-align:center;margin-top:12px;">
|
||||
Nu ai cont? <a href="/shop/register" style="color:inherit;text-decoration:underline;">Înregistrare</a>
|
||||
</p>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,40 @@
|
||||
@extends('shop.layout')
|
||||
@section('title', 'Înregistrare')
|
||||
@section('content')
|
||||
|
||||
<div style="max-width:420px;margin:0 auto;">
|
||||
<h1 style="font-size:22px;margin-bottom:16px;">Înregistrare cont</h1>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="card" style="border-color:#fca5a5;background:#fef2f2;margin-bottom:14px;">
|
||||
<ul style="margin:0;padding-left:18px;color:#991b1b;font-size:14px;">
|
||||
@foreach ($errors->all() as $e)<li>{{ $e }}</li>@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="/shop/register" class="card">
|
||||
@csrf
|
||||
<div class="field"><label>Nume *</label>
|
||||
<input type="text" name="name" value="{{ old('name') }}" required>
|
||||
</div>
|
||||
<div class="field"><label>Telefon *</label>
|
||||
<input type="text" name="phone" value="{{ old('phone') }}" required placeholder="+373…">
|
||||
</div>
|
||||
<div class="field"><label>Email</label>
|
||||
<input type="email" name="email" value="{{ old('email') }}">
|
||||
</div>
|
||||
<div class="field"><label>Parolă *</label>
|
||||
<input type="password" name="password" required minlength="6">
|
||||
</div>
|
||||
<div class="field"><label>Confirmă parola *</label>
|
||||
<input type="password" name="password_confirmation" required minlength="6">
|
||||
</div>
|
||||
<button type="submit" class="btn block">Creează cont</button>
|
||||
</form>
|
||||
|
||||
<p class="muted" style="text-align:center;margin-top:12px;">
|
||||
Ai deja cont? <a href="/shop/login" style="color:inherit;text-decoration:underline;">Login</a>
|
||||
</p>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -21,15 +21,15 @@
|
||||
@csrf
|
||||
<div class="field">
|
||||
<label>Nume complet *</label>
|
||||
<input type="text" name="customer_name" value="{{ old('customer_name') }}" required>
|
||||
<input type="text" name="customer_name" value="{{ old('customer_name', ($customer ?? null)?->name) }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Telefon *</label>
|
||||
<input type="text" name="customer_phone" value="{{ old('customer_phone') }}" required placeholder="+373…">
|
||||
<input type="text" name="customer_phone" value="{{ old('customer_phone', ($customer ?? null)?->phone) }}" required placeholder="+373…">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Email</label>
|
||||
<input type="email" name="customer_email" value="{{ old('customer_email') }}">
|
||||
<input type="email" name="customer_email" value="{{ old('customer_email', ($customer ?? null)?->email) }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Livrare *</label>
|
||||
|
||||
@@ -72,6 +72,15 @@
|
||||
<a href="/shop/cart">🛒 Coș
|
||||
@if (($cartCount ?? 0) > 0)<span class="cart-badge">{{ $cartCount }}</span>@endif
|
||||
</a>
|
||||
@auth('shop')
|
||||
<a href="/shop/account">👤 {{ Auth::guard('shop')->user()->name }}</a>
|
||||
<form method="POST" action="/shop/logout" style="display:inline;">@csrf
|
||||
<button type="submit" style="background:transparent;border:0;color:inherit;cursor:pointer;font:inherit;">Ieșire</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="/shop/login">Login</a>
|
||||
<a href="/shop/register" style="background:rgba(255,255,255,.2);border-radius:6px;padding:4px 10px;">Înregistrare</a>
|
||||
@endauth
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -93,6 +93,16 @@ Route::controller(\App\Http\Controllers\ShopController::class)->prefix('shop')->
|
||||
Route::post('/part/{id}/add', 'addToCart')->where('id', '\d+')->name('shop.cart.add');
|
||||
});
|
||||
|
||||
// ─── Shop customer auth ────────────────────────────────────────────
|
||||
Route::controller(\App\Http\Controllers\ShopAuthController::class)->prefix('shop')->group(function () {
|
||||
Route::get('/register', 'showRegister')->name('shop.register');
|
||||
Route::post('/register', 'register');
|
||||
Route::get('/login', 'showLogin')->name('shop.login');
|
||||
Route::post('/login', 'login');
|
||||
Route::post('/logout', 'logout')->name('shop.logout');
|
||||
Route::get('/account', 'account')->name('shop.account');
|
||||
});
|
||||
|
||||
// ─── Public WO tracking (no auth, tenant-scoped via subdomain) ──────
|
||||
Route::get('/t/{token}', [\App\Http\Controllers\TrackingController::class, 'show'])
|
||||
->where('token', '[A-Za-z0-9]{16,32}')
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Central\Company;
|
||||
use App\Models\Central\Plan;
|
||||
use App\Models\Tenant\Client;
|
||||
use App\Models\Tenant\OnlineOrder;
|
||||
use App\Models\Tenant\Part;
|
||||
use App\Models\Tenant\ShopCustomer;
|
||||
use App\Tenancy\TenantManager;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ShopAccountTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_register_creates_customer_and_logs_in(): void
|
||||
{
|
||||
$this->makeShopCompany('reg');
|
||||
|
||||
$resp = $this->post('http://reg.service.mir.md/shop/register', [
|
||||
'name' => 'Ion Pop',
|
||||
'phone' => '+37369123456',
|
||||
'email' => 'ion@example.com',
|
||||
'password' => 'secret123',
|
||||
'password_confirmation' => 'secret123',
|
||||
]);
|
||||
|
||||
$resp->assertRedirect('/shop/account');
|
||||
|
||||
$customer = ShopCustomer::where('phone', '+37369123456')->first();
|
||||
$this->assertNotNull($customer);
|
||||
$this->assertEquals('Ion Pop', $customer->name);
|
||||
$this->assertTrue(Hash::check('secret123', $customer->password));
|
||||
$this->assertTrue(Auth::guard('shop')->check());
|
||||
}
|
||||
|
||||
public function test_register_auto_links_existing_client_by_phone(): void
|
||||
{
|
||||
$this->makeShopCompany('link');
|
||||
$client = Client::create([
|
||||
'name' => 'Existing', 'phone' => '+37369999999',
|
||||
'type' => 'individual', 'status' => 'active',
|
||||
]);
|
||||
|
||||
$this->post('http://link.service.mir.md/shop/register', [
|
||||
'name' => 'Existing', 'phone' => '+37369999999',
|
||||
'password' => 'pw1234', 'password_confirmation' => 'pw1234',
|
||||
])->assertRedirect();
|
||||
|
||||
$cust = ShopCustomer::first();
|
||||
$this->assertEquals($client->id, $cust->client_id);
|
||||
}
|
||||
|
||||
public function test_register_rejects_duplicate_phone(): void
|
||||
{
|
||||
$this->makeShopCompany('dup');
|
||||
ShopCustomer::create([
|
||||
'name' => 'X', 'phone' => '+37377777777', 'password' => Hash::make('pw'),
|
||||
]);
|
||||
|
||||
$this->post('http://dup.service.mir.md/shop/register', [
|
||||
'name' => 'Y', 'phone' => '+37377777777',
|
||||
'password' => 'pw1234', 'password_confirmation' => 'pw1234',
|
||||
])->assertSessionHasErrors('phone');
|
||||
|
||||
$this->assertEquals(1, ShopCustomer::count());
|
||||
}
|
||||
|
||||
public function test_login_validates_credentials(): void
|
||||
{
|
||||
$this->makeShopCompany('lg');
|
||||
ShopCustomer::create([
|
||||
'name' => 'L', 'phone' => '+37360123456',
|
||||
'password' => Hash::make('correct'),
|
||||
]);
|
||||
|
||||
$this->post('http://lg.service.mir.md/shop/login', [
|
||||
'phone' => '+37360123456', 'password' => 'wrong',
|
||||
])->assertSessionHasErrors();
|
||||
$this->assertFalse(Auth::guard('shop')->check());
|
||||
|
||||
$this->post('http://lg.service.mir.md/shop/login', [
|
||||
'phone' => '+37360123456', 'password' => 'correct',
|
||||
])->assertRedirect();
|
||||
$this->assertTrue(Auth::guard('shop')->check());
|
||||
}
|
||||
|
||||
public function test_account_page_requires_auth(): void
|
||||
{
|
||||
$this->makeShopCompany('acc');
|
||||
$this->get('http://acc.service.mir.md/shop/account')->assertRedirect('/shop/login');
|
||||
}
|
||||
|
||||
public function test_account_page_lists_only_my_orders(): void
|
||||
{
|
||||
$ctx = $this->makeShopCompany('myorders');
|
||||
$part = Part::create(['name' => 'X', 'sell_price' => 100, 'qty' => 5, 'unit' => 'buc',
|
||||
'is_active' => true, 'is_published' => true]);
|
||||
|
||||
$me = ShopCustomer::create([
|
||||
'name' => 'Me', 'phone' => '+37300000001',
|
||||
'password' => Hash::make('pw'),
|
||||
]);
|
||||
$other = ShopCustomer::create([
|
||||
'name' => 'Other', 'phone' => '+37300000002',
|
||||
'password' => Hash::make('pw'),
|
||||
]);
|
||||
|
||||
$myOrder = OnlineOrder::create([
|
||||
'number' => OnlineOrder::generateNumber($ctx['company']->id),
|
||||
'shop_customer_id' => $me->id,
|
||||
'customer_name' => 'Me', 'customer_phone' => '+37300000001',
|
||||
'delivery_method' => 'pickup', 'status' => 'new',
|
||||
]);
|
||||
OnlineOrder::create([
|
||||
'number' => OnlineOrder::generateNumber($ctx['company']->id),
|
||||
'shop_customer_id' => $other->id,
|
||||
'customer_name' => 'Other', 'customer_phone' => '+37300000002',
|
||||
'delivery_method' => 'pickup', 'status' => 'new',
|
||||
]);
|
||||
|
||||
Auth::guard('shop')->login($me);
|
||||
$resp = $this->get('http://myorders.service.mir.md/shop/account');
|
||||
$resp->assertOk();
|
||||
$resp->assertSee('#' . $myOrder->number);
|
||||
$resp->assertSee('Comenzile mele (1)');
|
||||
}
|
||||
|
||||
public function test_checkout_attaches_shop_customer_id_when_logged_in(): void
|
||||
{
|
||||
$ctx = $this->makeShopCompany('co');
|
||||
$part = Part::create(['name' => 'Plăcuțe', 'sell_price' => 250, 'qty' => 5,
|
||||
'unit' => 'buc', 'is_active' => true, 'is_published' => true]);
|
||||
|
||||
$customer = ShopCustomer::create([
|
||||
'name' => 'CO User', 'phone' => '+37345555555',
|
||||
'password' => Hash::make('pw'),
|
||||
]);
|
||||
Auth::guard('shop')->login($customer);
|
||||
|
||||
$base = 'http://co.service.mir.md/shop';
|
||||
$this->post("$base/part/{$part->id}/add", ['qty' => 1])->assertRedirect();
|
||||
|
||||
$this->post("$base/checkout", [
|
||||
'customer_name' => $customer->name,
|
||||
'customer_phone' => $customer->phone,
|
||||
'delivery_method' => 'pickup',
|
||||
])->assertRedirect();
|
||||
|
||||
$order = OnlineOrder::first();
|
||||
$this->assertEquals($customer->id, $order->shop_customer_id);
|
||||
}
|
||||
|
||||
public function test_customers_isolated_per_tenant(): void
|
||||
{
|
||||
$this->makeShopCompany('a');
|
||||
ShopCustomer::create([
|
||||
'name' => 'A Cust', 'phone' => '+37311111111',
|
||||
'password' => Hash::make('pw'),
|
||||
]);
|
||||
|
||||
$this->makeShopCompany('b');
|
||||
$this->assertEquals(0, ShopCustomer::count());
|
||||
}
|
||||
|
||||
private function makeShopCompany(string $slug): array
|
||||
{
|
||||
$plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]);
|
||||
$company = Company::create([
|
||||
'plan_id' => $plan->id, 'slug' => $slug,
|
||||
'name' => ucfirst($slug), 'status' => 'active',
|
||||
'settings' => ['shop' => ['enabled' => true, 'delivery_methods' => ['pickup']]],
|
||||
]);
|
||||
app(TenantManager::class)->setCurrent($company);
|
||||
return compact('company');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user