from editing import *
replace('database/migrations/2026_10_20_190000_harden_publication_and_preserve_history.php',"$table->json('draft_recipient_ids')->nullable();", "$table->json('draft_recipient_ids')->nullable();\n            $table->timestamp('response_due_at')->nullable()->change();")
replace('app/Models/QuoteRequest.php',"'closed_at'];", "'closed_at', 'draft_recipient_ids'];")
replace('app/Models/QuoteRequest.php',"return ['filters' => 'array',", "return ['draft_recipient_ids' => 'array', 'filters' => 'array',")
write('app/Models/QuoteResponseVersion.php',r'''<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class QuoteResponseVersion extends Model
{
    protected $guarded = ['id'];
    protected $casts = ['amount' => 'decimal:2', 'submitted_at' => 'datetime'];
    public function response(): \Illuminate\Database\Eloquent\Relations\BelongsTo { return $this->belongsTo(QuoteResponse::class, 'quote_response_id'); }
}''')
replace('app/Models/QuoteResponse.php','    protected function casts()',r'''    public function versions(): \Illuminate\Database\Eloquent\Relations\HasMany
    {
        return $this->hasMany(QuoteResponseVersion::class)->latest('id');
    }

    protected function casts()''')
write('app/Http/Requests/QuoteRequestRequest.php',r'''<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class QuoteRequestRequest extends FormRequest
{
    public function authorize(): bool { return $this->user()?->isAdmin() || $this->user()?->isBuyer() || $this->user()?->isEntreprise(); }
    public function rules(): array
    {
        $sending = $this->input('action') === 'send';
        return [
            'titre' => ['required', 'string', 'max:160'],
            'description' => [$sending ? 'required' : 'nullable', 'string', 'max:5000'],
            'besoins' => ['nullable', 'string', 'max:5000'], 'filters' => ['nullable', 'array'],
            'filters.*' => ['array'], 'filters.*.*' => ['integer'],
            'date_souhaitee' => ['nullable', 'date', 'after_or_equal:'.now('America/Toronto')->toDateString()],
            'response_due_at' => [$sending ? 'required' : 'nullable', 'date', 'after:now'],
            'requester_entreprise_id' => ['nullable', 'integer', Rule::exists('entreprises', 'id')],
            'attachment' => ['nullable', 'file', 'max:10240', 'mimes:pdf,doc,docx,xls,xlsx'],
            'recipient_ids' => [$sending ? 'required' : 'nullable', 'array', ...($sending ? ['min:1'] : []), 'max:500'],
            'recipient_ids.*' => ['integer', 'distinct', Rule::exists('entreprises', 'id')],
            'action' => ['required', Rule::in(['draft', 'send'])],
        ];
    }
    protected function prepareForValidation(): void
    {
        if ($this->filled('response_due_at')) { $this->merge(['response_due_at' => self::utcDeadline((string) $this->input('response_due_at'))]); }
    }
    public static function utcDeadline(string $value): string
    {
        if (preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/', $value)) {
            try {
                $date = \Illuminate\Support\Carbon::createFromFormat('!Y-m-d\TH:i', $value, 'America/Toronto');
                if ($date->format('Y-m-d\TH:i') !== $value) { return 'date-invalide'; }
                return $date->utc()->toIso8601String();
            } catch (\Throwable) { return 'date-invalide'; }
        }
        return $value;
    }
}''')
write('app/Http/Controllers/Portal/QuoteRequestController.php',r'''<?php
namespace App\Http\Controllers\Portal;
use App\Http\Controllers\Controller;
use App\Http\Requests\QuoteRequestRequest;
use App\Http\Resources\PublicEntrepriseResource;
use App\Models\Entreprise;
use App\Models\QuoteRequest;
use App\Notifications\QuoteRequestReceivedNotification;
use App\Services\CatalogueSearch;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
class QuoteRequestController extends Controller
{
    public function __construct(private readonly CatalogueSearch $catalogueSearch) {}
    public function index(Request $request): \Inertia\Response
    {
        return Inertia::render('portal/QuoteRequests/Index', ['requests' => QuoteRequest::where('requester_id', $request->user()->id)->withCount('recipients')->latest()->paginate(20)->withQueryString()]);
    }
    private function canCreate(Request $request): void
    {
        abort_unless($request->user()->isAdmin() || $request->user()->isBuyer() || $request->user()->isEntreprise(), 403);
    }
    private function assertOwner(Request $request, QuoteRequest $quoteRequest): void
    {
        abort_unless($quoteRequest->requester_id === $request->user()->id || $request->user()->isAdmin(), 403);
    }
    public function create(Request $request): \Inertia\Response
    {
        $this->canCreate($request);
        return $this->form($request);
    }
    public function edit(Request $request, QuoteRequest $quoteRequest): \Inertia\Response
    {
        $this->assertOwner($request, $quoteRequest);
        abort_unless($quoteRequest->status === 'draft', 403);
        return $this->form($request, $quoteRequest);
    }
    private function form(Request $request, ?QuoteRequest $quoteRequest = null): \Inertia\Response
    {
        return Inertia::render('portal/QuoteRequests/Form', [
            'quoteRequest' => $quoteRequest,
            'filterOptions' => $this->catalogueSearch->filterOptions(),
            'entreprises' => $request->user()->entreprises()->orderBy('nom')->get(['id', 'nom']),
            'timezone' => 'America/Toronto',
        ]);
    }
    private function suppliers(Request $request, array $filters): \Illuminate\Database\Eloquent\Builder
    {
        return $this->catalogueSearch->query('', $filters)->whereNotNull('user_id')->where('user_id', '!=', $request->user()->id);
    }
    public function preview(Request $request): \Illuminate\Http\JsonResponse
    {
        $this->canCreate($request);
        $filters = $this->catalogueSearch->normalizeFilters((array) $request->input('filters', []));
        return response()->json(['recipients' => $this->suppliers($request, $filters)->get()->map(fn ($e) => (new PublicEntrepriseResource($e))->resolve($request))]);
    }
    public function store(QuoteRequestRequest $request): \Illuminate\Http\RedirectResponse
    {
        return $this->save($request);
    }
    public function update(QuoteRequestRequest $request, QuoteRequest $quoteRequest): \Illuminate\Http\RedirectResponse
    {
        $this->assertOwner($request, $quoteRequest);
        return $this->save($request, $quoteRequest);
    }
    private function save(QuoteRequestRequest $request, ?QuoteRequest $existing = null): \Illuminate\Http\RedirectResponse
    {
        $data = $request->validated();
        $entrepriseId = $data['requester_entreprise_id'] ?? null;
        if (! $request->user()->isAdmin()) {
            if ($request->user()->isBuyer()) { abort_if($entrepriseId !== null, 403); }
            else { abort_unless($entrepriseId && $request->user()->entreprises()->whereKey($entrepriseId)->exists(), 403); }
        }
        $filters = $this->catalogueSearch->normalizeFilters($data['filters'] ?? []);
        $ids = $data['recipient_ids'] ?? [];
        $suppliers = $data['action'] === 'send' ? $this->suppliers($request, $filters)->whereKey($ids)->with('user')->get() : collect();
        if ($data['action'] === 'send' && $suppliers->count() !== count($ids)) {
            throw ValidationException::withMessages(['recipient_ids' => 'Certains destinataires ne sont plus admissibles. Prévisualisez à nouveau les fournisseurs.']);
        }
        $path = $request->file('attachment')?->store('quote-requests', 'local');
        try {
            $quote = DB::transaction(function () use ($request, $existing, $data, $filters, $ids, $suppliers, $path): QuoteRequest {
                $quote = $existing ? QuoteRequest::whereKey($existing->id)->lockForUpdate()->firstOrFail() : new QuoteRequest;
                if ($existing && $quote->status !== 'draft') { throw ValidationException::withMessages(['action' => 'Cette demande a déjà été envoyée ou fermée.']); }
                $attributes = collect($data)->except(['action', 'recipient_ids', 'attachment'])->all();
                $quote->fill([...$attributes, 'description' => $data['description'] ?? '', 'filters' => $filters,
                    'requester_id' => $quote->exists ? $quote->requester_id : $request->user()->id,
                    'draft_recipient_ids' => $ids, 'status' => $data['action'] === 'send' ? 'sent' : 'draft',
                    'sent_at' => $data['action'] === 'send' ? now() : null,
                ]);
                if ($path) { $quote->attachment_path = $path; $quote->attachment_name = $request->file('attachment')->getClientOriginalName(); }
                $quote->save();
                foreach ($suppliers as $e) { $quote->recipients()->create(['entreprise_id' => $e->id, 'entreprise_nom' => $e->nom, 'email' => $e->user->email, 'status' => 'pending', 'sent_at' => now()]); }
                return $quote;
            });
        } catch (\Throwable $error) { if ($path) { Storage::disk('local')->delete($path); } throw $error; }
        if ($quote->status === 'sent') {
            foreach ($quote->recipients()->with(['entreprise.user', 'quoteRequest'])->get() as $recipient) { $recipient->entreprise->user?->notify(new QuoteRequestReceivedNotification($recipient)); }
        }
        return to_route('portal.requests.show', $quote)->with('success', $quote->status === 'sent' ? 'Demande envoyée.' : 'Brouillon enregistré. Vous pouvez le reprendre et l’envoyer plus tard.');
    }
    public function show(Request $request, QuoteRequest $quoteRequest): \Inertia\Response
    {
        $this->assertOwner($request, $quoteRequest);
        $quoteRequest->load(['requester.buyerProfile', 'requesterEntreprise', 'recipients.response.versions']);
        return Inertia::render('portal/QuoteRequests/Show', ['quoteRequest' => $quoteRequest]);
    }
    public function close(Request $request, QuoteRequest $quoteRequest): \Illuminate\Http\RedirectResponse
    {
        $this->assertOwner($request, $quoteRequest); abort_unless($quoteRequest->status === 'sent', 422);
        $quoteRequest->update(['status' => 'closed', 'closed_at' => now()]);
        return back()->with('success', 'Demande fermée. Les fournisseurs ne peuvent plus répondre.');
    }
    public function cancel(Request $request, QuoteRequest $quoteRequest): \Illuminate\Http\RedirectResponse
    {
        $this->assertOwner($request, $quoteRequest); abort_unless(in_array($quoteRequest->status, ['draft', 'sent']), 422);
        $quoteRequest->update(['status' => 'cancelled', 'closed_at' => now()]);
        return back()->with('success', 'Demande annulée. Les réponses déjà reçues sont conservées.');
    }
    public function reopen(Request $request, QuoteRequest $quoteRequest): \Illuminate\Http\RedirectResponse
    {
        $this->assertOwner($request, $quoteRequest); abort_unless(in_array($quoteRequest->status, ['closed', 'cancelled']), 422);
        $request->merge(['response_due_at' => QuoteRequestRequest::utcDeadline((string) $request->input('response_due_at'))]);
        $data = $request->validate(['response_due_at' => ['required', 'date', 'after:now']]);
        $quoteRequest->update([...$data, 'status' => $quoteRequest->sent_at ? 'sent' : 'draft', 'closed_at' => null]);
        return back()->with('success', 'Demande rouverte. Les réponses existantes sont conservées.');
    }
}''')
replace('routes/web.php',"Route::get('demandes/{quoteRequest}',", "Route::get('demandes/{quoteRequest}/modifier', [QuoteRequestController::class, 'edit'])->name('requests.edit');\n        Route::put('demandes/{quoteRequest}', [QuoteRequestController::class, 'update'])->name('requests.update');\n        Route::post('demandes/{quoteRequest}/rouvrir', [QuoteRequestController::class, 'reopen'])->name('requests.reopen');\n        Route::get('demandes/{quoteRequest}',")
replace('app/Http/Controllers/Portal/ReceivedQuoteRequestController.php',"'response']);", "'response.versions']);")
s=read('app/Http/Controllers/Portal/ReceivedQuoteRequestController.php')
a=s.index('        abort_unless($recipient->quoteRequest()->open()')
b=s.index("        $response->load('recipient.quoteRequest.requester');",a)
s=s[:a]+r'''        $attachment = $request->file('attachment');
        $path = $attachment?->store('quote-responses', 'local');
        try {
            $response = \Illuminate\Support\Facades\DB::transaction(function () use ($recipient, $request, $attachment, $path) {
                $recipient = QuoteRequestRecipient::whereKey($recipient->id)->lockForUpdate()->firstOrFail();
                if (! $recipient->quoteRequest()->open()->exists()) {
                    throw \Illuminate\Validation\ValidationException::withMessages(['message' => 'Cette demande est fermée ou son échéance est dépassée. Votre réponse n’a pas été envoyée.']);
                }
                $previous = $recipient->response;
                if ($previous) { $previous->versions()->create($previous->only(['amount', 'message', 'attachment_path', 'attachment_name', 'submitted_at'])); }
                $response = $recipient->response()->updateOrCreate([], [
                    'amount' => $request->validated('amount'), 'message' => $request->validated('message'),
                    'attachment_path' => $path ?? $previous?->attachment_path,
                    'attachment_name' => $attachment?->getClientOriginalName() ?? $previous?->attachment_name,
                    'submitted_at' => now(),
                ]);
                $recipient->update(['status' => 'responded']);
                return $response;
            });
        } catch (\Throwable $error) { if ($path) { \Illuminate\Support\Facades\Storage::disk('local')->delete($path); } throw $error; }
''' +s[b:]
write('app/Http/Controllers/Portal/ReceivedQuoteRequestController.php',s)
replace('app/Http/Requests/QuoteResponseRequest.php',"'numeric', 'min:0'", "'numeric', 'decimal:0,2', 'min:0'")
replace('app/Http/Controllers/Portal/QuoteAttachmentController.php','    public function responseAttachment(',r'''    public function versionAttachment(Request $request, QuoteResponse $response, \App\Models\QuoteResponseVersion $version): StreamedResponse
    {
        abort_unless($version->quote_response_id === $response->id, 404);
        $response->loadMissing('recipient.quoteRequest');
        $canRead = $response->recipient->quoteRequest->requester_id === $request->user()->id
            || $response->recipient->entreprise()->where('user_id', $request->user()->id)->exists() || $request->user()->isAdmin();
        abort_unless($canRead && $version->attachment_path, 403);
        return Storage::disk('local')->download($version->attachment_path, $version->attachment_name);
    }

    public function responseAttachment(''')
replace('routes/web.php',"Route::get('reponses/{response}/piece-jointe',", "Route::get('reponses/{response}/versions/{version}/piece-jointe', [QuoteAttachmentController::class, 'versionAttachment'])->name('responses.versions.attachment');\n        Route::get('reponses/{response}/piece-jointe',")
print('Phase 3 applied')
