Files
a620906209 90d0a57e73
Run Tests / test (pull_request) Successful in 1m56s
feat(offers): 補齊 provider-verification 可見性缺口——子端點過濾與預約入口檢查
實作 openspec change provider-verification-gaps(8/8 tasks):
- schedules/reviews publicList 套用 visibleToPublic,隱藏課程回 404
- MemberBookingController::store Layer 1 擋未驗證教練課程的新預約(422)
- 政策:教練被取消驗證只擋新預約,既有 pending/confirmed/completed 照常
- 主規格 Notes 已知限制轉為正式 Requirements
- 測試 +5(可見性子端點 3、預約入口 2),既有 booking/review 測試 helper
  補 verified ProviderProfile
- 容器內全套件 151 passed / 388 assertions;另以 demo 資料端到端驗證
  (已驗證教練 200/200/200、未驗證 404/404/404)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 22:48:59 +08:00

132 lines
5.0 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\API;
use App\Enums\BookingStatus;
use App\Enums\ScheduleStatus;
use App\Http\Controllers\Controller;
use App\Models\CourseSchedule;
use App\Models\DivingOffer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ScheduleController extends Controller
{
public function index(Request $request)
{
$provider = $request->user();
$schedules = CourseSchedule::with('divingOffer')
->where('provider_id', $provider->id)
->orderBy('scheduled_date')
->orderBy('start_time')
->get()
->map(fn($s) => $this->formatSchedule($s));
return response()->json(['status' => true, 'data' => $schedules]);
}
public function store(Request $request)
{
$data = $request->validate([
'diving_offer_id' => 'required|integer|exists:diving_offers,id',
'scheduled_date' => 'required|date|after_or_equal:today',
'start_time' => 'required|date_format:H:i',
'max_participants' => 'required|integer|min:1',
]);
$offer = DivingOffer::findOrFail($data['diving_offer_id']);
if ($offer->provider_id !== $request->user()->id) {
return response()->json(['status' => false, 'message' => '無權操作此課程'], 403);
}
$schedule = CourseSchedule::create([
'diving_offer_id' => $data['diving_offer_id'],
'provider_id' => $request->user()->id,
'scheduled_date' => $data['scheduled_date'],
'start_time' => $data['start_time'],
'max_participants' => $data['max_participants'],
'current_participants'=> 0,
'status' => ScheduleStatus::Open,
]);
return response()->json(['status' => true, 'data' => $this->formatSchedule($schedule)], 201);
}
public function update(Request $request, int $id)
{
$schedule = CourseSchedule::findOrFail($id);
if ($schedule->provider_id !== $request->user()->id) {
return response()->json(['status' => false, 'message' => '無權操作此時段'], 403);
}
$data = $request->validate([
'start_time' => 'sometimes|date_format:H:i',
'max_participants' => 'sometimes|integer|min:1',
]);
if (isset($data['max_participants']) && $data['max_participants'] < $schedule->current_participants) {
return response()->json([
'status' => false,
'message' => '人數上限不可低於目前已確認人數(' . $schedule->current_participants . '',
], 422);
}
$schedule->update($data);
return response()->json(['status' => true, 'data' => $this->formatSchedule($schedule->fresh())]);
}
public function destroy(Request $request, int $id)
{
$schedule = CourseSchedule::findOrFail($id);
if ($schedule->provider_id !== $request->user()->id) {
return response()->json(['status' => false, 'message' => '無權操作此時段'], 403);
}
DB::transaction(function () use ($schedule) {
$schedule->update(['status' => ScheduleStatus::Cancelled]);
$schedule->bookings()
->whereIn('status', [BookingStatus::Pending->value, BookingStatus::Confirmed->value])
->update(['status' => BookingStatus::ProviderCancelled->value]);
});
return response()->json(['status' => true, 'message' => '時段已取消']);
}
public function publicList(int $offerId)
{
$offer = DivingOffer::visibleToPublic()->findOrFail($offerId);
$schedules = CourseSchedule::where('diving_offer_id', $offer->id)
->where('status', ScheduleStatus::Open->value)
->whereDate('scheduled_date', '>=', now()->toDateString())
->orderBy('scheduled_date')
->orderBy('start_time')
->get()
->map(fn($s) => [
'id' => $s->id,
'scheduled_date' => $s->scheduled_date->toDateString(),
'start_time' => $s->start_time,
'max_participants' => $s->max_participants,
'remaining_spots' => $s->remainingSpots(),
'status' => $s->status->value,
]);
return response()->json(['status' => true, 'data' => $schedules]);
}
private function formatSchedule(CourseSchedule $s): array
{
return [
'id' => $s->id,
'diving_offer_id' => $s->diving_offer_id,
'offer_title' => $s->divingOffer?->title,
'scheduled_date' => $s->scheduled_date?->toDateString(),
'start_time' => $s->start_time,
'max_participants' => $s->max_participants,
'current_participants' => $s->current_participants,
'remaining_spots' => $s->remainingSpots(),
'status' => $s->status->value,
];
}
}