diff --git a/app/Http/Controllers/ProjectionController.php b/app/Http/Controllers/ProjectionController.php index 050ab18..7a65daa 100644 --- a/app/Http/Controllers/ProjectionController.php +++ b/app/Http/Controllers/ProjectionController.php @@ -3,15 +3,20 @@ namespace App\Http\Controllers; use App\Http\Requests\CalculateProjectionRequest; +use App\Http\Requests\PreviewAllocationRequest; use App\Http\Resources\ProjectionResource; +use App\Models\Bucket; use App\Models\Scenario; +use App\Services\Projection\PipelineAllocationService; use App\Services\Projection\ProjectionGeneratorService; use Carbon\Carbon; +use Illuminate\Http\JsonResponse; class ProjectionController extends Controller { public function __construct( - private readonly ProjectionGeneratorService $projectionGeneratorService + private readonly ProjectionGeneratorService $projectionGeneratorService, + private readonly PipelineAllocationService $pipelineAllocationService, ) {} public function calculate(CalculateProjectionRequest $request, Scenario $scenario): ProjectionResource @@ -27,4 +32,45 @@ public function calculate(CalculateProjectionRequest $request, Scenario $scenari return new ProjectionResource($projections); } + + public function preview(PreviewAllocationRequest $request, Scenario $scenario): JsonResponse + { + $amountInCents = (int) round($request->input('amount') * 100); + + $draws = $this->pipelineAllocationService->allocateInflow($scenario, $amountInCents); + + /** @var array $bucketLookup */ + $bucketLookup = $scenario->buckets->keyBy('id')->all(); + + $allocations = $draws->map(function ($draw) use ($bucketLookup) { + $bucket = $bucketLookup[$draw->bucket_id]; + + return [ + 'bucket_id' => $bucket->uuid, + 'bucket_name' => $bucket->name, + 'bucket_type' => $bucket->type->value, + 'allocated_amount' => $draw->amount_currency, + 'remaining_capacity' => $this->remainingCapacity($bucket, $draw->amount_currency), + ]; + })->values(); + + $totalAllocatedCents = $draws->sum('amount'); + + return response()->json([ + 'allocations' => $allocations, + 'total_allocated' => round($totalAllocatedCents / 100, 2), + 'unallocated' => round(($amountInCents - $totalAllocatedCents) / 100, 2), + ]); + } + + private function remainingCapacity(Bucket $bucket, float $allocatedDollars): ?float + { + $effectiveCapacity = $bucket->getEffectiveCapacity(); + + if ($effectiveCapacity === PHP_FLOAT_MAX) { + return null; + } + + return round(max(0, $effectiveCapacity - $allocatedDollars), 2); + } } diff --git a/app/Http/Requests/PreviewAllocationRequest.php b/app/Http/Requests/PreviewAllocationRequest.php new file mode 100644 index 0000000..42cbb93 --- /dev/null +++ b/app/Http/Requests/PreviewAllocationRequest.php @@ -0,0 +1,20 @@ + ['required', 'numeric', 'min:0.01'], + ]; + } +} diff --git a/routes/web.php b/routes/web.php index f6d296d..32ba796 100644 --- a/routes/web.php +++ b/routes/web.php @@ -38,6 +38,7 @@ // Projection routes (no auth required for MVP) Route::post('/scenarios/{scenario}/projections/calculate', [ProjectionController::class, 'calculate'])->name('projections.calculate'); +Route::post('/scenarios/{scenario}/projections/preview', [ProjectionController::class, 'preview'])->name('projections.preview'); // Auth dashboard (hidden for single-scenario MVP, re-enable later) // Route::middleware(['auth', 'verified'])->group(function () {