Compare commits
12 Commits
docs/add-f
...
hotfix/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
905580ca1d | ||
|
|
39ab10759b | ||
|
|
5f99fba396 | ||
|
|
84f364de74 | ||
|
|
39c955cdc4 | ||
|
|
e02ca54187 | ||
|
|
ac46ee004b | ||
|
|
17a6eb260d | ||
|
|
5ea80366be | ||
|
|
99aa0cb980 | ||
|
|
3de53a76d0 | ||
|
|
7fa9b6aff8 |
157
CONTRIBUTING.md
157
CONTRIBUTING.md
@@ -239,6 +239,163 @@ git push origin feature/my-feature
|
||||
git push --no-verify
|
||||
```
|
||||
|
||||
### Keeping Your Feature Branch Up-to-Date
|
||||
|
||||
**Best practice for teams:** Sync your feature branch with `develop` regularly to avoid large merge conflicts.
|
||||
|
||||
#### Daily Start-of-Work Routine
|
||||
|
||||
```bash
|
||||
# 1. Get latest changes from develop
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
|
||||
# 2. Update your feature branch
|
||||
git checkout feature/my-feature
|
||||
git merge develop
|
||||
|
||||
# 3. If there are conflicts (see below), resolve them
|
||||
# 4. Continue working
|
||||
```
|
||||
|
||||
**How often?**
|
||||
- Minimum: Once per day (start of work)
|
||||
- Better: Multiple times per day if develop is active
|
||||
- Always: Before creating your Pull Request
|
||||
|
||||
#### Merge vs Rebase: Which to Use?
|
||||
|
||||
**For teams of 5+ developers, use `merge` (not `rebase`):**
|
||||
|
||||
```bash
|
||||
git checkout feature/my-feature
|
||||
git merge develop
|
||||
```
|
||||
|
||||
**Why merge over rebase?**
|
||||
- ✅ Safer: Preserves your commit history
|
||||
- ✅ Collaborative: Works when multiple people work on the same feature branch
|
||||
- ✅ Transparent: Shows when you integrated upstream changes
|
||||
- ✅ No force-push: Once you've pushed to origin, merge won't require `--force`
|
||||
|
||||
**When to use rebase:**
|
||||
- ⚠️ Only if you haven't pushed yet
|
||||
- ⚠️ Only if you're the sole developer on the branch
|
||||
- ⚠️ You want a cleaner, linear history
|
||||
|
||||
```bash
|
||||
# Only do this if you haven't pushed yet!
|
||||
git checkout feature/my-feature
|
||||
git rebase develop
|
||||
```
|
||||
|
||||
**Never rebase after pushing** - it rewrites history and breaks collaboration.
|
||||
|
||||
#### Handling Merge Conflicts
|
||||
|
||||
When you run `git merge develop` and see conflicts:
|
||||
|
||||
```bash
|
||||
$ git merge develop
|
||||
Auto-merging app/Http/Controllers/OrderController.php
|
||||
CONFLICT (content): Merge conflict in app/Http/Controllers/OrderController.php
|
||||
Automatic merge failed; fix conflicts and then commit the result.
|
||||
```
|
||||
|
||||
**Step-by-step resolution:**
|
||||
|
||||
1. **See which files have conflicts:**
|
||||
```bash
|
||||
git status
|
||||
# Look for "both modified:" files
|
||||
```
|
||||
|
||||
2. **Open conflicted files** - look for conflict markers:
|
||||
```php
|
||||
<<<<<<< HEAD
|
||||
// Your code
|
||||
=======
|
||||
// Code from develop
|
||||
>>>>>>> develop
|
||||
```
|
||||
|
||||
3. **Resolve conflicts** - edit the file to keep what you need:
|
||||
```php
|
||||
// Choose your code, their code, or combine both
|
||||
// Remove the <<<, ===, >>> markers
|
||||
```
|
||||
|
||||
4. **Mark as resolved:**
|
||||
```bash
|
||||
git add app/Http/Controllers/OrderController.php
|
||||
```
|
||||
|
||||
5. **Complete the merge:**
|
||||
```bash
|
||||
git commit -m "merge: resolve conflicts with develop"
|
||||
```
|
||||
|
||||
6. **Run tests to ensure nothing broke:**
|
||||
```bash
|
||||
./vendor/bin/sail artisan test
|
||||
```
|
||||
|
||||
7. **Push the merge commit:**
|
||||
```bash
|
||||
git push origin feature/my-feature
|
||||
```
|
||||
|
||||
#### When Conflicts Are Too Complex
|
||||
|
||||
If conflicts are extensive or you're unsure:
|
||||
|
||||
1. **Abort the merge:**
|
||||
```bash
|
||||
git merge --abort
|
||||
```
|
||||
|
||||
2. **Ask for help** in #engineering Slack:
|
||||
- "I'm merging develop into feature/X and have conflicts in OrderController"
|
||||
- Someone might have context on the upstream changes
|
||||
|
||||
3. **Pair program the resolution** - screen share with the person who made the conflicting changes
|
||||
|
||||
4. **Alternative: Start fresh** (last resort):
|
||||
```bash
|
||||
# Create new branch from latest develop
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
git checkout -b feature/my-feature-v2
|
||||
|
||||
# Cherry-pick your commits
|
||||
git cherry-pick <commit-hash>
|
||||
```
|
||||
|
||||
#### Example: Multi-Day Feature Work
|
||||
|
||||
```bash
|
||||
# Monday morning
|
||||
git checkout develop && git pull origin develop
|
||||
git checkout feature/payment-integration
|
||||
git merge develop # Get latest changes
|
||||
# Work all day, make commits
|
||||
|
||||
# Tuesday morning
|
||||
git checkout develop && git pull origin develop
|
||||
git checkout feature/payment-integration
|
||||
git merge develop # Sync again (someone added auth changes)
|
||||
# Continue working
|
||||
|
||||
# Wednesday
|
||||
git checkout develop && git pull origin develop
|
||||
git checkout feature/payment-integration
|
||||
git merge develop # Final sync before PR
|
||||
git push origin feature/payment-integration
|
||||
# Create Pull Request
|
||||
```
|
||||
|
||||
**Result:** Small, manageable syncs instead of one huge conflict on PR day.
|
||||
|
||||
### When to Test Locally
|
||||
|
||||
**Always run tests before pushing if you:**
|
||||
|
||||
@@ -24,7 +24,7 @@ class ProductLineController extends Controller
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.products.index1', $business->slug)
|
||||
->route('seller.business.products.index', $business->slug)
|
||||
->with('success', 'Product line created successfully.');
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class ProductLineController extends Controller
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.products.index1', $business->slug)
|
||||
->route('seller.business.products.index', $business->slug)
|
||||
->with('success', 'Product line updated successfully.');
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class ProductLineController extends Controller
|
||||
$productLine->delete();
|
||||
|
||||
return redirect()
|
||||
->route('seller.business.products.index1', $business->slug)
|
||||
->route('seller.business.products.index', $business->slug)
|
||||
->with('success', 'Product line deleted successfully.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Traits\FileStorageHelper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class StorageTestController extends Controller
|
||||
{
|
||||
use FileStorageHelper;
|
||||
|
||||
/**
|
||||
* Test storage configuration
|
||||
*/
|
||||
public function test(Request $request)
|
||||
{
|
||||
$results = [];
|
||||
$results['storage_info'] = $this->getStorageInfo();
|
||||
|
||||
// Test file upload if provided
|
||||
if ($request->hasFile('test_file')) {
|
||||
try {
|
||||
$file = $request->file('test_file');
|
||||
|
||||
// Store test file
|
||||
$path = $this->storeFile($file, 'tests');
|
||||
$results['upload'] = [
|
||||
'success' => true,
|
||||
'path' => $path,
|
||||
'url' => $this->getFileUrl($path),
|
||||
];
|
||||
|
||||
// Verify file exists
|
||||
$disk = Storage::disk($this->getStorageDisk());
|
||||
$results['verification'] = [
|
||||
'exists' => $disk->exists($path),
|
||||
'size' => $disk->size($path),
|
||||
];
|
||||
|
||||
// Delete test file
|
||||
$deleted = $this->deleteFile($path);
|
||||
$results['cleanup'] = [
|
||||
'deleted' => $deleted,
|
||||
'still_exists' => $disk->exists($path),
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
$results['error'] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json($results, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show test upload form
|
||||
*/
|
||||
public function form()
|
||||
{
|
||||
return view('storage-test');
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,14 @@ class ProductImage extends Model
|
||||
'path',
|
||||
'type',
|
||||
'order',
|
||||
'sort_order',
|
||||
'is_primary',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_primary' => 'boolean',
|
||||
'order' => 'integer',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
public function product(): BelongsTo
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const content = fs.readFileSync('resources/views/seller/products/edit11.blade.php', 'utf8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
let depth = 0;
|
||||
const stack = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const lineNum = i + 1;
|
||||
|
||||
// Skip lines that are Alpine.js @error handlers
|
||||
if (line.includes('@error') && line.includes('$event')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for @if (but not in @endif, @error, @enderror)
|
||||
if (/@if\s*\(/.test(line) && !/@endif/.test(line)) {
|
||||
depth++;
|
||||
stack.push({ line: lineNum, type: 'if', content: line.trim().substring(0, 80) });
|
||||
console.log(`${lineNum}: [depth +${depth}] @if`);
|
||||
}
|
||||
|
||||
// Check for @elseif
|
||||
if (/@elseif\s*\(/.test(line)) {
|
||||
console.log(`${lineNum}: [depth =${depth}] @elseif`);
|
||||
}
|
||||
|
||||
// Check for @else (but not @elseif, @endforelse, @enderror)
|
||||
if (/@else\b/.test(line) && !/@elseif/.test(line) && !/@endforelse/.test(line) && !/@enderror/.test(line)) {
|
||||
console.log(`${lineNum}: [depth =${depth}] @else`);
|
||||
}
|
||||
|
||||
// Check for @endif
|
||||
if (/@endif\b/.test(line)) {
|
||||
console.log(`${lineNum}: [depth -${depth}] @endif`);
|
||||
if (depth > 0) {
|
||||
depth--;
|
||||
stack.pop();
|
||||
} else {
|
||||
console.log(`ERROR: Extra @endif at line ${lineNum}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nFinal depth: ${depth}`);
|
||||
if (depth > 0) {
|
||||
console.log(`\nUNBALANCED: Missing ${depth} @endif statement(s)`);
|
||||
console.log('\nUnclosed @if statements:');
|
||||
stack.forEach(item => {
|
||||
console.log(` Line ${item.line}: ${item.content}`);
|
||||
});
|
||||
} else {
|
||||
console.log('\nAll @if/@endif pairs are balanced!');
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
$file = 'C:\Users\Boss Man\Documents\GitHub\hub\resources\views\seller\products\edit11.blade.php';
|
||||
$lines = file($file);
|
||||
|
||||
$stack = [];
|
||||
|
||||
foreach ($lines as $lineNum => $line) {
|
||||
$lineNum++; // 1-indexed
|
||||
|
||||
// Check for @if (but not @endif, @elseif, etc.)
|
||||
if (preg_match('/^\s*@if\(/', $line)) {
|
||||
$stack[] = ['type' => 'if', 'line' => $lineNum];
|
||||
echo "Line $lineNum: OPEN @if (stack depth: ".count($stack).")\n";
|
||||
}
|
||||
// Check for @elseif
|
||||
elseif (preg_match('/^\s*@elseif\(/', $line)) {
|
||||
echo "Line $lineNum: @elseif\n";
|
||||
}
|
||||
// Check for @else
|
||||
elseif (preg_match('/^\s*@else\s*$/', $line)) {
|
||||
echo "Line $lineNum: @else\n";
|
||||
}
|
||||
// Check for @endif
|
||||
elseif (preg_match('/^\s*@endif\s*$/', $line)) {
|
||||
if (empty($stack)) {
|
||||
echo "ERROR Line $lineNum: @endif without matching @if!\n";
|
||||
} else {
|
||||
$opened = array_pop($stack);
|
||||
echo "Line $lineNum: CLOSE @endif (opened at line {$opened['line']}, stack depth: ".count($stack).")\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($stack)) {
|
||||
echo "\nERROR: Unclosed @if directives:\n";
|
||||
foreach ($stack as $item) {
|
||||
echo " Line {$item['line']}: @if never closed\n";
|
||||
}
|
||||
} else {
|
||||
echo "\nAll @if/@endif directives are balanced!\n";
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
@section('content')
|
||||
<div class="container mx-auto px-4 py-6">
|
||||
{{-- Top Header Bar --}}
|
||||
<div class="bg-white border border-gray-300 rounded-md shadow-sm mb-6">
|
||||
<div class="px-6 py-4">
|
||||
<div class="card bg-base-100 shadow mb-6">
|
||||
<div class="card-body">
|
||||
<div class="flex items-start justify-between gap-6">
|
||||
{{-- Left: Product Image & Info --}}
|
||||
<div class="flex items-start gap-4">
|
||||
@@ -13,10 +13,10 @@
|
||||
@if($product->images->where('is_primary', true)->first())
|
||||
<img src="{{ asset('storage/' . $product->images->where('is_primary', true)->first()->path) }}"
|
||||
alt="{{ $product->name }}"
|
||||
class="w-16 h-16 object-cover rounded-md border border-gray-300">
|
||||
class="w-16 h-16 object-cover rounded-md border border-base-300">
|
||||
@else
|
||||
<div class="w-16 h-16 bg-gray-100 rounded-md border border-gray-300 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="w-16 h-16 bg-base-200 rounded-md border border-base-300 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-base-content/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -26,7 +26,7 @@
|
||||
{{-- Product Details --}}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<h1 class="text-xl font-bold text-gray-900">{{ $product->name }}</h1>
|
||||
<h1 class="text-xl font-bold text-base-content">{{ $product->name }}</h1>
|
||||
{{-- Status Badges --}}
|
||||
<span id="activeBadge" style="display: {{ $product->is_active ? 'inline-flex' : 'none' }};" class="items-center px-2.5 py-0.5 rounded text-xs font-medium bg-success text-white">
|
||||
Active
|
||||
@@ -35,39 +35,30 @@
|
||||
Featured
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 space-y-0.5">
|
||||
<div class="text-sm text-base-content/70 space-y-0.5">
|
||||
<div><span class="font-medium">SKU:</span> <span class="font-mono">{{ $product->sku ?? 'N/A' }}</span> <span class="mx-2">•</span> <span class="font-medium">Brand:</span> {{ $product->brand->name ?? 'N/A' }}</div>
|
||||
<div><span class="font-medium">Last updated:</span> {{ $product->updated_at->format('M j, Y g:i A') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Right: Action Buttons & Breadcrumb --}}
|
||||
{{-- Right: Action Buttons --}}
|
||||
<div class="flex flex-col gap-2 flex-shrink-0">
|
||||
{{-- View on Marketplace Button (White with border) --}}
|
||||
<a href="#" target="_blank" class="inline-flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{{-- View on Marketplace Button --}}
|
||||
<a href="#" target="_blank" class="btn btn-outline btn-sm">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
||||
</svg>
|
||||
View on Marketplace
|
||||
</a>
|
||||
|
||||
{{-- Manage BOM Button (Blue solid) --}}
|
||||
<a href="{{ route('seller.business.products.bom.index', [$business->slug, $product->id]) }}" class="inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md text-sm font-medium text-white bg-primary hover:bg-primary/90 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{{-- Manage BOM Button --}}
|
||||
<a href="{{ route('seller.business.products.bom.index', [$business->slug, $product->id]) }}" class="btn btn-primary btn-sm">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
|
||||
</svg>
|
||||
Manage BOM
|
||||
</a>
|
||||
|
||||
{{-- Breadcrumb Navigation --}}
|
||||
<nav class="flex text-xs text-gray-500 mt-1" aria-label="Breadcrumb">
|
||||
<a href="{{ route('seller.business.dashboard', $business->slug) }}" class="hover:text-gray-700">Dashboard</a>
|
||||
<span class="mx-2">></span>
|
||||
<a href="{{ route('seller.business.products.index', $business->slug) }}" class="hover:text-gray-700">Products</a>
|
||||
<span class="mx-2">></span>
|
||||
<span class="text-gray-900 font-medium">Edit</span>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,7 +95,7 @@
|
||||
{{-- LEFT SIDEBAR (1/4 width) --}}
|
||||
<div class="space-y-6">
|
||||
{{-- Product Images Card --}}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-sm">Product Images</h2>
|
||||
<div class="space-y-4">
|
||||
@@ -147,7 +138,7 @@
|
||||
</div>
|
||||
|
||||
{{-- Quick Stats Card --}}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-sm">Quick Stats</h2>
|
||||
<div class="space-y-2">
|
||||
@@ -173,7 +164,7 @@
|
||||
</div>
|
||||
|
||||
{{-- Audit Info Card --}}
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-xs">Audit Info</h2>
|
||||
<div class="space-y-1 text-xs text-base-content/70">
|
||||
@@ -186,7 +177,7 @@
|
||||
|
||||
{{-- MAIN CONTENT WITH TABS (3/4 width) --}}
|
||||
<div class="lg:col-span-3">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
{{-- Tabs Navigation --}}
|
||||
<div role="tablist" class="tabs tabs-bordered">
|
||||
@@ -405,7 +396,10 @@
|
||||
<template x-for="(image, index) in images" :key="image.id || index">
|
||||
<div class="relative group cursor-move border-2 border-base-300 rounded-lg overflow-hidden hover:border-primary transition-colors bg-base-200"
|
||||
:data-id="image.id">
|
||||
<img :src="'/storage/' + image.path" alt="Product image" class="w-full h-40 object-cover" @@error="$event.target.src='/images/placeholder.png'">
|
||||
<img :src="'/storage/' + image.path" alt="Product image" class="w-full h-40 object-cover" @error="$event.target.style.display='none'; $event.target.nextElementSibling.style.display='flex';">
|
||||
<div class="hidden items-center justify-center h-40 bg-base-200">
|
||||
<span class="icon-[lucide--image] size-16 text-base-content/40"></span>
|
||||
</div>
|
||||
<div class="absolute top-2 left-2">
|
||||
<span class="badge badge-sm" :class="index === 0 ? 'badge-primary' : 'badge-ghost badge-outline'" x-text="index === 0 ? 'Primary' : (index + 1)"></span>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,68 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Storage Test</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet" type="text/css" />
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="p-8">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">File Storage Test</h2>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<span class="text-sm">Current Disk: <strong>{{ config('filesystems.default') }}</strong></span>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('storage.test') }}" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Upload Test File</span>
|
||||
</label>
|
||||
<input type="file" name="test_file" class="file-input file-input-bordered" required>
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<button type="submit" class="btn btn-primary">Test Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="divider">Storage Info</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="font-semibold">Disk</td>
|
||||
<td>{{ config('filesystems.default') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-semibold">Driver</td>
|
||||
<td>{{ config('filesystems.disks.'.config('filesystems.default').'.driver') }}</td>
|
||||
</tr>
|
||||
@if(config('filesystems.default') === 's3')
|
||||
<tr>
|
||||
<td class="font-semibold">Endpoint</td>
|
||||
<td>{{ config('filesystems.disks.s3.endpoint') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-semibold">Bucket</td>
|
||||
<td>{{ config('filesystems.disks.s3.bucket') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-semibold">URL</td>
|
||||
<td>{{ config('filesystems.disks.s3.url') }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use App\Http\Controllers\StorageTestController;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -199,9 +198,3 @@ Route::prefix('api')->group(function () {
|
||||
->name('api.check-email')
|
||||
->middleware('throttle:10,1'); // Rate limit: 10 requests per minute
|
||||
});
|
||||
|
||||
// Storage Test Routes (Development/Testing Only)
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::get('/storage-test', [StorageTestController::class, 'form'])->name('storage.test.form');
|
||||
Route::post('/storage-test', [StorageTestController::class, 'test'])->name('storage.test');
|
||||
});
|
||||
|
||||
537
tests/Feature/ProductImageControllerTest.php
Normal file
537
tests/Feature/ProductImageControllerTest.php
Normal file
@@ -0,0 +1,537 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Brand;
|
||||
use App\Models\Business;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductImage;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProductImageControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Storage::fake('local');
|
||||
}
|
||||
|
||||
protected function withCsrfToken(): static
|
||||
{
|
||||
return $this->withSession(['_token' => 'test-token'])
|
||||
->withHeader('X-CSRF-TOKEN', 'test-token');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can upload valid product image
|
||||
*/
|
||||
public function test_seller_can_upload_valid_product_image(): void
|
||||
{
|
||||
// Create seller business with brand and product
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create valid test image (750x384 minimum)
|
||||
$image = UploadedFile::fake()->image('product.jpg', 750, 384);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$business->slug, $product->id]),
|
||||
['image' => $image]
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
// Verify image was created in database
|
||||
$this->assertDatabaseHas('product_images', [
|
||||
'product_id' => $product->id,
|
||||
'is_primary' => true, // First image should be primary
|
||||
]);
|
||||
|
||||
// Verify file was stored
|
||||
$productImage = ProductImage::where('product_id', $product->id)->first();
|
||||
Storage::disk('local')->assertExists($productImage->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test first uploaded image becomes primary
|
||||
*/
|
||||
public function test_first_image_becomes_primary(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
$image = UploadedFile::fake()->image('product.jpg', 750, 384);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$business->slug, $product->id]),
|
||||
['image' => $image]
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$productImage = ProductImage::where('product_id', $product->id)->first();
|
||||
$this->assertTrue($productImage->is_primary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test image upload validates minimum dimensions
|
||||
*/
|
||||
public function test_upload_validates_minimum_dimensions(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Image too small (below 750x384)
|
||||
$image = UploadedFile::fake()->image('product.jpg', 500, 300);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$business->slug, $product->id]),
|
||||
['image' => $image]
|
||||
);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonValidationErrors('image');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test upload validates file type
|
||||
*/
|
||||
public function test_upload_validates_file_type(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Invalid file type
|
||||
$file = UploadedFile::fake()->create('document.pdf', 100);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$business->slug, $product->id]),
|
||||
['image' => $file]
|
||||
);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonValidationErrors('image');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cannot upload more than 6 images per product
|
||||
*/
|
||||
public function test_cannot_upload_more_than_six_images(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create 6 existing images
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => "products/test-{$i}.jpg",
|
||||
'is_primary' => $i === 0,
|
||||
'sort_order' => $i,
|
||||
]);
|
||||
}
|
||||
|
||||
$image = UploadedFile::fake()->image('product.jpg', 750, 384);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$business->slug, $product->id]),
|
||||
['image' => $image]
|
||||
);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$response->assertJson([
|
||||
'success' => false,
|
||||
'message' => 'Maximum of 6 images allowed per product',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot upload image to another business's product (business_id isolation)
|
||||
*/
|
||||
public function test_seller_cannot_upload_image_to_other_business_product(): void
|
||||
{
|
||||
// Create two businesses
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$brandB = Brand::factory()->create(['business_id' => $businessB->id]);
|
||||
$productB = Product::factory()->create(['brand_id' => $brandB->id]);
|
||||
|
||||
$image = UploadedFile::fake()->image('product.jpg', 750, 384);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
|
||||
// Try to upload to businessB's product using businessA's slug
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.upload', [$businessA->slug, $productB->id]),
|
||||
['image' => $image]
|
||||
);
|
||||
|
||||
$response->assertNotFound(); // Product not found when scoped to businessA
|
||||
|
||||
// Verify no image was created
|
||||
$this->assertDatabaseMissing('product_images', [
|
||||
'product_id' => $productB->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can delete their product's image
|
||||
*/
|
||||
public function test_seller_can_delete_product_image(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create test file
|
||||
Storage::disk('local')->put('products/test.jpg', 'fake content');
|
||||
|
||||
$image = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->deleteJson(
|
||||
route('seller.business.products.images.delete', [$business->slug, $product->id, $image->id])
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
// Verify image was deleted from database
|
||||
$this->assertDatabaseMissing('product_images', ['id' => $image->id]);
|
||||
|
||||
// Verify file was deleted from storage
|
||||
Storage::disk('local')->assertMissing('products/test.jpg');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test deleting primary image sets next image as primary
|
||||
*/
|
||||
public function test_deleting_primary_image_sets_next_as_primary(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create two images
|
||||
Storage::disk('local')->put('products/test1.jpg', 'fake content 1');
|
||||
Storage::disk('local')->put('products/test2.jpg', 'fake content 2');
|
||||
|
||||
$image1 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test1.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$image2 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test2.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
|
||||
// Delete primary image
|
||||
$response = $this->deleteJson(
|
||||
route('seller.business.products.images.delete', [$business->slug, $product->id, $image1->id])
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
// Verify image2 is now primary
|
||||
$this->assertTrue($image2->fresh()->is_primary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot delete another business's product image
|
||||
*/
|
||||
public function test_seller_cannot_delete_other_business_product_image(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$brandB = Brand::factory()->create(['business_id' => $businessB->id]);
|
||||
$productB = Product::factory()->create(['brand_id' => $brandB->id]);
|
||||
|
||||
Storage::disk('local')->put('products/test.jpg', 'fake content');
|
||||
$imageB = ProductImage::create([
|
||||
'product_id' => $productB->id,
|
||||
'path' => 'products/test.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
|
||||
// Try to delete businessB's product image
|
||||
$response = $this->deleteJson(
|
||||
route('seller.business.products.images.delete', [$businessA->slug, $productB->id, $imageB->id])
|
||||
);
|
||||
|
||||
$response->assertNotFound();
|
||||
|
||||
// Verify image was NOT deleted
|
||||
$this->assertDatabaseHas('product_images', ['id' => $imageB->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can reorder product images
|
||||
*/
|
||||
public function test_seller_can_reorder_product_images(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create three images
|
||||
$image1 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test1.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$image2 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test2.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$image3 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test3.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 2,
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
|
||||
// Reorder: image3, image1, image2
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.reorder', [$business->slug, $product->id]),
|
||||
['order' => [$image3->id, $image1->id, $image2->id]]
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
// Verify new sort order
|
||||
$this->assertEquals(0, $image3->fresh()->sort_order);
|
||||
$this->assertEquals(1, $image1->fresh()->sort_order);
|
||||
$this->assertEquals(2, $image2->fresh()->sort_order);
|
||||
|
||||
// Verify first image (image3) is now primary
|
||||
$this->assertTrue($image3->fresh()->is_primary);
|
||||
$this->assertFalse($image1->fresh()->is_primary);
|
||||
$this->assertFalse($image2->fresh()->is_primary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot reorder another business's product images
|
||||
*/
|
||||
public function test_seller_cannot_reorder_other_business_product_images(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$brandB = Brand::factory()->create(['business_id' => $businessB->id]);
|
||||
$productB = Product::factory()->create(['brand_id' => $brandB->id]);
|
||||
|
||||
$image1 = ProductImage::create([
|
||||
'product_id' => $productB->id,
|
||||
'path' => 'products/test1.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$image2 = ProductImage::create([
|
||||
'product_id' => $productB->id,
|
||||
'path' => 'products/test2.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
|
||||
// Try to reorder businessB's product images
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.reorder', [$businessA->slug, $productB->id]),
|
||||
['order' => [$image2->id, $image1->id]]
|
||||
);
|
||||
|
||||
$response->assertNotFound();
|
||||
|
||||
// Verify order was NOT changed
|
||||
$this->assertEquals(0, $image1->fresh()->sort_order);
|
||||
$this->assertEquals(1, $image2->fresh()->sort_order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can set image as primary
|
||||
*/
|
||||
public function test_seller_can_set_image_as_primary(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create two images
|
||||
$image1 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test1.jpg',
|
||||
'is_primary' => true,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$image2 = ProductImage::create([
|
||||
'product_id' => $product->id,
|
||||
'path' => 'products/test2.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
|
||||
// Set image2 as primary
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.set-primary', [$business->slug, $product->id, $image2->id])
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
|
||||
// Verify image2 is now primary and image1 is not
|
||||
$this->assertTrue($image2->fresh()->is_primary);
|
||||
$this->assertFalse($image1->fresh()->is_primary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot set primary on another business's product image
|
||||
*/
|
||||
public function test_seller_cannot_set_primary_on_other_business_product_image(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$brandB = Brand::factory()->create(['business_id' => $businessB->id]);
|
||||
$productB = Product::factory()->create(['brand_id' => $brandB->id]);
|
||||
|
||||
$image = ProductImage::create([
|
||||
'product_id' => $productB->id,
|
||||
'path' => 'products/test.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
|
||||
// Try to set primary on businessB's product image
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.set-primary', [$businessA->slug, $productB->id, $image->id])
|
||||
);
|
||||
|
||||
$response->assertNotFound();
|
||||
|
||||
// Verify is_primary was NOT changed
|
||||
$this->assertFalse($image->fresh()->is_primary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cannot set primary on image that doesn't belong to product
|
||||
*/
|
||||
public function test_cannot_set_primary_on_image_from_different_product(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$brand = Brand::factory()->create(['business_id' => $business->id]);
|
||||
$product1 = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
$product2 = Product::factory()->create(['brand_id' => $brand->id]);
|
||||
|
||||
// Create image for product2
|
||||
$image = ProductImage::create([
|
||||
'product_id' => $product2->id,
|
||||
'path' => 'products/test.jpg',
|
||||
'is_primary' => false,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
|
||||
// Try to set it as primary for product1 (wrong product)
|
||||
$response = $this->postJson(
|
||||
route('seller.business.products.images.set-primary', [$business->slug, $product1->id, $image->id])
|
||||
);
|
||||
|
||||
$response->assertStatus(404);
|
||||
$response->assertJson([
|
||||
'success' => false,
|
||||
'message' => 'Image not found',
|
||||
]);
|
||||
}
|
||||
}
|
||||
324
tests/Feature/ProductLineControllerTest.php
Normal file
324
tests/Feature/ProductLineControllerTest.php
Normal file
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Business;
|
||||
use App\Models\ProductLine;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProductLineControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Test seller can create product line for their business
|
||||
*/
|
||||
public function test_seller_can_create_product_line(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->post(
|
||||
route('seller.business.product-lines.store', $business->slug),
|
||||
['name' => 'Premium Line']
|
||||
);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Product line created successfully.');
|
||||
|
||||
$this->assertDatabaseHas('product_lines', [
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test product line name is required
|
||||
*/
|
||||
public function test_product_line_name_is_required(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->post(
|
||||
route('seller.business.product-lines.store', $business->slug),
|
||||
['name' => '']
|
||||
);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
$this->assertDatabaseMissing('product_lines', [
|
||||
'business_id' => $business->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test product line name must be unique per business
|
||||
*/
|
||||
public function test_product_line_name_must_be_unique_per_business(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
// Create existing product line
|
||||
ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->post(
|
||||
route('seller.business.product-lines.store', $business->slug),
|
||||
['name' => 'Premium Line']
|
||||
);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test product line name can be duplicated across different businesses
|
||||
*/
|
||||
public function test_product_line_name_can_be_duplicated_across_businesses(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$sellerB = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerB->businesses()->attach($businessB->id);
|
||||
|
||||
// Create product line in business A
|
||||
ProductLine::create([
|
||||
'business_id' => $businessA->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
// Create product line with same name in business B (should work)
|
||||
$this->actingAs($sellerB);
|
||||
$response = $this->post(
|
||||
route('seller.business.product-lines.store', $businessB->slug),
|
||||
['name' => 'Premium Line']
|
||||
);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success');
|
||||
|
||||
// Verify both exist
|
||||
$this->assertDatabaseHas('product_lines', [
|
||||
'business_id' => $businessA->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('product_lines', [
|
||||
'business_id' => $businessB->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can update their product line
|
||||
*/
|
||||
public function test_seller_can_update_product_line(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$productLine = ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->put(
|
||||
route('seller.business.product-lines.update', [$business->slug, $productLine->id]),
|
||||
['name' => 'Ultra Premium Line']
|
||||
);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Product line updated successfully.');
|
||||
|
||||
$this->assertDatabaseHas('product_lines', [
|
||||
'id' => $productLine->id,
|
||||
'name' => 'Ultra Premium Line',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseMissing('product_lines', [
|
||||
'id' => $productLine->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test update validates name is required
|
||||
*/
|
||||
public function test_update_validates_name_is_required(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$productLine = ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->put(
|
||||
route('seller.business.product-lines.update', [$business->slug, $productLine->id]),
|
||||
['name' => '']
|
||||
);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
|
||||
// Verify name wasn't changed
|
||||
$this->assertEquals('Premium Line', $productLine->fresh()->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test update validates uniqueness per business
|
||||
*/
|
||||
public function test_update_validates_uniqueness_per_business(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$productLine1 = ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$productLine2 = ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Budget Line',
|
||||
]);
|
||||
|
||||
// Try to rename productLine2 to match productLine1
|
||||
$this->actingAs($seller);
|
||||
$response = $this->put(
|
||||
route('seller.business.product-lines.update', [$business->slug, $productLine2->id]),
|
||||
['name' => 'Premium Line']
|
||||
);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
|
||||
// Verify name wasn't changed
|
||||
$this->assertEquals('Budget Line', $productLine2->fresh()->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot update another business's product line
|
||||
*/
|
||||
public function test_seller_cannot_update_other_business_product_line(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$productLineB = ProductLine::create([
|
||||
'business_id' => $businessB->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
$response = $this->put(
|
||||
route('seller.business.product-lines.update', [$businessA->slug, $productLineB->id]),
|
||||
['name' => 'Hacked Name']
|
||||
);
|
||||
|
||||
$response->assertNotFound();
|
||||
|
||||
// Verify name wasn't changed
|
||||
$this->assertEquals('Premium Line', $productLineB->fresh()->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller can delete their product line
|
||||
*/
|
||||
public function test_seller_can_delete_product_line(): void
|
||||
{
|
||||
$business = Business::factory()->create(['business_type' => 'brand']);
|
||||
$seller = User::factory()->create(['user_type' => 'seller']);
|
||||
$seller->businesses()->attach($business->id);
|
||||
|
||||
$productLine = ProductLine::create([
|
||||
'business_id' => $business->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($seller);
|
||||
$response = $this->delete(
|
||||
route('seller.business.product-lines.destroy', [$business->slug, $productLine->id])
|
||||
);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Product line deleted successfully.');
|
||||
|
||||
$this->assertDatabaseMissing('product_lines', [
|
||||
'id' => $productLine->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller cannot delete another business's product line
|
||||
*/
|
||||
public function test_seller_cannot_delete_other_business_product_line(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$productLineB = ProductLine::create([
|
||||
'business_id' => $businessB->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
$response = $this->delete(
|
||||
route('seller.business.product-lines.destroy', [$businessA->slug, $productLineB->id])
|
||||
);
|
||||
|
||||
$response->assertNotFound();
|
||||
|
||||
// Verify product line wasn't deleted
|
||||
$this->assertDatabaseHas('product_lines', [
|
||||
'id' => $productLineB->id,
|
||||
'name' => 'Premium Line',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test seller from unauthorized business cannot access another business routes
|
||||
*/
|
||||
public function test_seller_cannot_access_unauthorized_business_routes(): void
|
||||
{
|
||||
$businessA = Business::factory()->create(['business_type' => 'brand']);
|
||||
$businessB = Business::factory()->create(['business_type' => 'brand']);
|
||||
|
||||
$sellerA = User::factory()->create(['user_type' => 'seller']);
|
||||
$sellerA->businesses()->attach($businessA->id);
|
||||
|
||||
$this->actingAs($sellerA);
|
||||
|
||||
// Try to access businessB routes
|
||||
$response = $this->post(
|
||||
route('seller.business.product-lines.store', $businessB->slug),
|
||||
['name' => 'Test Line']
|
||||
);
|
||||
|
||||
$response->assertForbidden();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user