Files
Vasyka 75386c354a 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>
2026-06-02 19:43:39 +00:00

90 lines
2.3 KiB
PHP

<?php
namespace App\Models\Tenant;
use App\Models\Concerns\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
class OnlineOrder extends Model
{
use BelongsToTenant, SoftDeletes;
public const STATUSES = [
'new' => 'Nouă',
'confirmed' => 'Confirmată',
'packed' => 'Pregătită',
'shipped' => 'Expediată',
'delivered' => 'Livrată',
'cancelled' => 'Anulată',
];
public const DELIVERY = [
'pickup' => 'Ridicare din service',
'courier' => 'Curier',
'post' => 'Poștă',
];
protected $fillable = [
'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',
];
protected $casts = [
'subtotal' => 'decimal:2',
'delivery_fee' => 'decimal:2',
'total' => 'decimal:2',
];
public function items(): HasMany
{
return $this->hasMany(OnlineOrderItem::class);
}
public function client(): BelongsTo
{
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);
}
public function recalcTotal(): void
{
$this->subtotal = (float) $this->items()->sum('total');
$this->total = round((float) $this->subtotal + (float) $this->delivery_fee, 2);
$this->save();
}
public static function generateNumber(int $companyId): string
{
$year = date('y');
$count = static::withoutGlobalScopes()
->where('company_id', $companyId)
->whereYear('created_at', date('Y'))
->count();
return sprintf('SO-%s-%04d', $year, $count + 1);
}
protected static function booted(): void
{
static::creating(function (self $o) {
if (empty($o->tracking_token)) {
$o->tracking_token = Str::random(24);
}
});
}
}