security(auth): 新增 member/provider 角色 middleware #71
+120
-57
@@ -38,40 +38,48 @@ class MemberApiDoc
|
|||||||
* operationId="createBooking",
|
* operationId="createBooking",
|
||||||
* tags={"會員預約"},
|
* tags={"會員預約"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\RequestBody(
|
* @OA\RequestBody(
|
||||||
* required=true,
|
* required=true,
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
* required={"offer_id","schedule_id","participants"},
|
* required={"offer_id","schedule_id","participants"},
|
||||||
|
*
|
||||||
* @OA\Property(property="offer_id", type="integer", example=1, description="課程 ID"),
|
* @OA\Property(property="offer_id", type="integer", example=1, description="課程 ID"),
|
||||||
* @OA\Property(property="schedule_id", type="integer", example=2, description="時段 ID"),
|
* @OA\Property(property="schedule_id", type="integer", example=2, description="時段 ID"),
|
||||||
* @OA\Property(property="participants", type="integer", example=2, description="參加人數"),
|
* @OA\Property(property="participants", type="integer", example=2, description="參加人數"),
|
||||||
* @OA\Property(property="note", type="string", nullable=true, example="需要器材租借")
|
* @OA\Property(property="note", type="string", nullable=true, example="需要器材租借")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=201,
|
* response=201,
|
||||||
* description="預約建立成功",
|
* description="預約建立成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="預約成功"),
|
* @OA\Property(property="message", type="string", example="預約成功"),
|
||||||
* @OA\Property(property="data", ref="#/components/schemas/Booking")
|
* @OA\Property(property="data", ref="#/components/schemas/Booking")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=422,
|
* response=422,
|
||||||
* description="驗證失敗或時段已滿",
|
* description="驗證失敗或時段已滿",
|
||||||
|
*
|
||||||
* @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")
|
* @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=403,
|
* response=403,
|
||||||
* description="無權限(非 member 角色)",
|
* description="無權限(非 member 角色)",
|
||||||
|
*
|
||||||
* @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")
|
* @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")
|
||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function createBooking()
|
public function createBooking() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取得我的預約列表
|
* 取得我的預約列表
|
||||||
@@ -83,22 +91,26 @@ class MemberApiDoc
|
|||||||
* operationId="listMemberBookings",
|
* operationId="listMemberBookings",
|
||||||
* tags={"會員預約"},
|
* tags={"會員預約"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
|
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
|
||||||
* @OA\Parameter(name="per_page", in="query", description="每頁筆數", @OA\Schema(type="integer", default=15)),
|
* @OA\Parameter(name="per_page", in="query", description="每頁筆數", @OA\Schema(type="integer", default=15)),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="取得成功",
|
* description="取得成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Booking")),
|
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Booking")),
|
||||||
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
|
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
|
||||||
* )
|
* )
|
||||||
* )
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=403, description="非 member 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function listMemberBookings()
|
public function listMemberBookings() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取得單一預約
|
* 取得單一預約
|
||||||
@@ -109,22 +121,25 @@ class MemberApiDoc
|
|||||||
* operationId="getMemberBooking",
|
* operationId="getMemberBooking",
|
||||||
* tags={"會員預約"},
|
* tags={"會員預約"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="取得成功",
|
* description="取得成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="data", ref="#/components/schemas/Booking")
|
* @OA\Property(property="data", ref="#/components/schemas/Booking")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(response=404, description="預約不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
* @OA\Response(response=404, description="預約不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=403, description="無權限存取", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=403, description="非 member 角色或非本人預約", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function getMemberBooking()
|
public function getMemberBooking() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取消預約
|
* 取消預約
|
||||||
@@ -136,22 +151,25 @@ class MemberApiDoc
|
|||||||
* operationId="cancelBooking",
|
* operationId="cancelBooking",
|
||||||
* tags={"會員預約"},
|
* tags={"會員預約"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="取消成功",
|
* description="取消成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="預約已取消")
|
* @OA\Property(property="message", type="string", example="預約已取消")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="無權限或狀態不允許取消", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
*
|
||||||
|
* @OA\Response(response=403, description="非 member 角色、非本人預約或狀態不允許取消", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=404, description="預約不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=404, description="預約不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function cancelBooking()
|
public function cancelBooking() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Member Reviews
|
// Member Reviews
|
||||||
@@ -167,32 +185,37 @@ class MemberApiDoc
|
|||||||
* operationId="createReview",
|
* operationId="createReview",
|
||||||
* tags={"會員評價"},
|
* tags={"會員評價"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\RequestBody(
|
* @OA\RequestBody(
|
||||||
* required=true,
|
* required=true,
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
* required={"booking_id","rating"},
|
* required={"booking_id","rating"},
|
||||||
|
*
|
||||||
* @OA\Property(property="booking_id", type="integer", example=5),
|
* @OA\Property(property="booking_id", type="integer", example=5),
|
||||||
* @OA\Property(property="rating", type="integer", minimum=1, maximum=5, example=4),
|
* @OA\Property(property="rating", type="integer", minimum=1, maximum=5, example=4),
|
||||||
* @OA\Property(property="comment", type="string", nullable=true, example="課程非常棒!"),
|
* @OA\Property(property="comment", type="string", nullable=true, example="課程非常棒!"),
|
||||||
* @OA\Property(property="is_anonymous", type="boolean", example=false)
|
* @OA\Property(property="is_anonymous", type="boolean", example=false)
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=201,
|
* response=201,
|
||||||
* description="評價建立成功",
|
* description="評價建立成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="評價已提交"),
|
* @OA\Property(property="message", type="string", example="評價已提交"),
|
||||||
* @OA\Property(property="data", ref="#/components/schemas/Review")
|
* @OA\Property(property="data", ref="#/components/schemas/Review")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="預約未完成或非本人預約", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
*
|
||||||
|
* @OA\Response(response=403, description="非 member 角色、預約未完成或非本人預約", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=422, description="驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=422, description="驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function createReview()
|
public function createReview() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新評價
|
* 更新評價
|
||||||
@@ -203,30 +226,36 @@ class MemberApiDoc
|
|||||||
* operationId="updateReview",
|
* operationId="updateReview",
|
||||||
* tags={"會員評價"},
|
* tags={"會員評價"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="評價 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="評價 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\RequestBody(
|
* @OA\RequestBody(
|
||||||
* required=true,
|
* required=true,
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="rating", type="integer", minimum=1, maximum=5, example=5),
|
* @OA\Property(property="rating", type="integer", minimum=1, maximum=5, example=5),
|
||||||
* @OA\Property(property="comment", type="string", nullable=true, example="更新後的評語"),
|
* @OA\Property(property="comment", type="string", nullable=true, example="更新後的評語"),
|
||||||
* @OA\Property(property="is_anonymous", type="boolean", example=true)
|
* @OA\Property(property="is_anonymous", type="boolean", example=true)
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="更新成功",
|
* description="更新成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="data", ref="#/components/schemas/Review")
|
* @OA\Property(property="data", ref="#/components/schemas/Review")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="非本人評價", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
*
|
||||||
|
* @OA\Response(response=403, description="非 member 角色或非本人評價", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=404, description="評價不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=404, description="評價不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function updateReview()
|
public function updateReview() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刪除評價
|
* 刪除評價
|
||||||
@@ -237,22 +266,25 @@ class MemberApiDoc
|
|||||||
* operationId="deleteReview",
|
* operationId="deleteReview",
|
||||||
* tags={"會員評價"},
|
* tags={"會員評價"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="評價 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="評價 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="刪除成功",
|
* description="刪除成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="評價已刪除")
|
* @OA\Property(property="message", type="string", example="評價已刪除")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="非本人評價", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
*
|
||||||
|
* @OA\Response(response=403, description="非 member 角色或非本人評價", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=404, description="評價不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=404, description="評價不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function deleteReview()
|
public function deleteReview() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 標記評價為有幫助
|
* 標記評價為有幫助
|
||||||
@@ -264,22 +296,25 @@ class MemberApiDoc
|
|||||||
* operationId="markReviewHelpful",
|
* operationId="markReviewHelpful",
|
||||||
* tags={"會員評價"},
|
* tags={"會員評價"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="評價 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="評價 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="操作成功",
|
* description="操作成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="helpful_count", type="integer", example=4),
|
* @OA\Property(property="helpful_count", type="integer", example=4),
|
||||||
* @OA\Property(property="has_voted", type="boolean", example=true)
|
* @OA\Property(property="has_voted", type="boolean", example=true)
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(response=404, description="評價不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=404, description="評價不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function markReviewHelpful()
|
public function markReviewHelpful() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Booking Messages(即時訊息,Member + Provider 共用)
|
// Booking Messages(即時訊息,Member + Provider 共用)
|
||||||
@@ -295,10 +330,13 @@ class MemberApiDoc
|
|||||||
* operationId="getMessageUnreadCounts",
|
* operationId="getMessageUnreadCounts",
|
||||||
* tags={"即時訊息"},
|
* tags={"即時訊息"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="取得成功",
|
* description="取得成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(
|
* @OA\Property(
|
||||||
* property="data",
|
* property="data",
|
||||||
@@ -310,9 +348,7 @@ class MemberApiDoc
|
|||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function getMessageUnreadCounts()
|
public function getMessageUnreadCounts() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取得預約訊息列表
|
* 取得預約訊息列表
|
||||||
@@ -324,17 +360,23 @@ class MemberApiDoc
|
|||||||
* operationId="listBookingMessages",
|
* operationId="listBookingMessages",
|
||||||
* tags={"即時訊息"},
|
* tags={"即時訊息"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="booking", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="booking", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="取得成功",
|
* description="取得成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(
|
* @OA\Property(
|
||||||
* property="data",
|
* property="data",
|
||||||
* type="array",
|
* type="array",
|
||||||
|
*
|
||||||
* @OA\Items(
|
* @OA\Items(
|
||||||
* type="object",
|
* type="object",
|
||||||
|
*
|
||||||
* @OA\Property(property="id", type="integer", example=1),
|
* @OA\Property(property="id", type="integer", example=1),
|
||||||
* @OA\Property(property="booking_id", type="integer", example=12),
|
* @OA\Property(property="booking_id", type="integer", example=12),
|
||||||
* @OA\Property(property="sender_type", type="string", enum={"member","provider"}, example="member"),
|
* @OA\Property(property="sender_type", type="string", enum={"member","provider"}, example="member"),
|
||||||
@@ -346,13 +388,12 @@ class MemberApiDoc
|
|||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(response=403, description="非參與方或預約狀態不符", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
* @OA\Response(response=403, description="非參與方或預約狀態不符", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=404, description="預約不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=404, description="預約不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function listBookingMessages()
|
public function listBookingMessages() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 發送預約訊息
|
* 發送預約訊息
|
||||||
@@ -364,21 +405,29 @@ class MemberApiDoc
|
|||||||
* operationId="sendBookingMessage",
|
* operationId="sendBookingMessage",
|
||||||
* tags={"即時訊息"},
|
* tags={"即時訊息"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="booking", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="booking", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\RequestBody(
|
* @OA\RequestBody(
|
||||||
* required=true,
|
* required=true,
|
||||||
|
*
|
||||||
* @OA\MediaType(
|
* @OA\MediaType(
|
||||||
* mediaType="multipart/form-data",
|
* mediaType="multipart/form-data",
|
||||||
|
*
|
||||||
* @OA\Schema(
|
* @OA\Schema(
|
||||||
|
*
|
||||||
* @OA\Property(property="message", type="string", nullable=true, example="請問需要自備潛水裝備嗎?"),
|
* @OA\Property(property="message", type="string", nullable=true, example="請問需要自備潛水裝備嗎?"),
|
||||||
* @OA\Property(property="image", type="string", format="binary", nullable=true, description="圖片(jpg/png,message 與 image 至少擇一)")
|
* @OA\Property(property="image", type="string", format="binary", nullable=true, description="圖片(jpg/png,message 與 image 至少擇一)")
|
||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=201,
|
* response=201,
|
||||||
* description="訊息已送出",
|
* description="訊息已送出",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(
|
* @OA\Property(
|
||||||
* property="data",
|
* property="data",
|
||||||
@@ -393,13 +442,12 @@ class MemberApiDoc
|
|||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(response=403, description="非參與方或預約狀態不符", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
* @OA\Response(response=403, description="非參與方或預約狀態不符", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=422, description="message 與 image 皆為空", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=422, description="message 與 image 皆為空", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function sendBookingMessage()
|
public function sendBookingMessage() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 標記訊息為已讀
|
* 標記訊息為已讀
|
||||||
@@ -411,21 +459,24 @@ class MemberApiDoc
|
|||||||
* operationId="markMessagesRead",
|
* operationId="markMessagesRead",
|
||||||
* tags={"即時訊息"},
|
* tags={"即時訊息"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="booking", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="booking", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="標記成功",
|
* description="標記成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="已標記為已讀")
|
* @OA\Property(property="message", type="string", example="已標記為已讀")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(response=403, description="非參與方", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=403, description="非參與方", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function markMessagesRead()
|
public function markMessagesRead() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Notifications (Member + Provider 共用)
|
// Notifications (Member + Provider 共用)
|
||||||
@@ -441,18 +492,24 @@ class MemberApiDoc
|
|||||||
* operationId="listNotifications",
|
* operationId="listNotifications",
|
||||||
* tags={"通知"},
|
* tags={"通知"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
|
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
|
||||||
* @OA\Parameter(name="per_page", in="query", description="每頁筆數", @OA\Schema(type="integer", default=15)),
|
* @OA\Parameter(name="per_page", in="query", description="每頁筆數", @OA\Schema(type="integer", default=15)),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="取得成功",
|
* description="取得成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(
|
* @OA\Property(
|
||||||
* property="data",
|
* property="data",
|
||||||
* type="array",
|
* type="array",
|
||||||
|
*
|
||||||
* @OA\Items(
|
* @OA\Items(
|
||||||
* type="object",
|
* type="object",
|
||||||
|
*
|
||||||
* @OA\Property(property="id", type="integer", example=1),
|
* @OA\Property(property="id", type="integer", example=1),
|
||||||
* @OA\Property(property="type", type="string", example="booking_confirmed"),
|
* @OA\Property(property="type", type="string", example="booking_confirmed"),
|
||||||
* @OA\Property(property="title", type="string", example="預約已確認"),
|
* @OA\Property(property="title", type="string", example="預約已確認"),
|
||||||
@@ -466,9 +523,7 @@ class MemberApiDoc
|
|||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function listNotifications()
|
public function listNotifications() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取得未讀通知數量
|
* 取得未讀通知數量
|
||||||
@@ -479,19 +534,20 @@ class MemberApiDoc
|
|||||||
* operationId="getUnreadNotificationCount",
|
* operationId="getUnreadNotificationCount",
|
||||||
* tags={"通知"},
|
* tags={"通知"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="取得成功",
|
* description="取得成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="unread_count", type="integer", example=3)
|
* @OA\Property(property="unread_count", type="integer", example=3)
|
||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function getUnreadNotificationCount()
|
public function getUnreadNotificationCount() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 標記單一通知為已讀
|
* 標記單一通知為已讀
|
||||||
@@ -502,21 +558,24 @@ class MemberApiDoc
|
|||||||
* operationId="markNotificationRead",
|
* operationId="markNotificationRead",
|
||||||
* tags={"通知"},
|
* tags={"通知"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="通知 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="通知 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="標記成功",
|
* description="標記成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="通知已標記為已讀")
|
* @OA\Property(property="message", type="string", example="通知已標記為已讀")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(response=404, description="通知不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=404, description="通知不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function markNotificationRead()
|
public function markNotificationRead() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全部通知標記為已讀
|
* 全部通知標記為已讀
|
||||||
@@ -527,19 +586,20 @@ class MemberApiDoc
|
|||||||
* operationId="markAllNotificationsRead",
|
* operationId="markAllNotificationsRead",
|
||||||
* tags={"通知"},
|
* tags={"通知"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="標記成功",
|
* description="標記成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="所有通知已標記為已讀")
|
* @OA\Property(property="message", type="string", example="所有通知已標記為已讀")
|
||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function markAllNotificationsRead()
|
public function markAllNotificationsRead() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刪除通知
|
* 刪除通知
|
||||||
@@ -550,19 +610,22 @@ class MemberApiDoc
|
|||||||
* operationId="deleteNotification",
|
* operationId="deleteNotification",
|
||||||
* tags={"通知"},
|
* tags={"通知"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="通知 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="通知 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="刪除成功",
|
* description="刪除成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="通知已刪除")
|
* @OA\Property(property="message", type="string", example="通知已刪除")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(response=404, description="通知不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=404, description="通知不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function deleteNotification()
|
public function deleteNotification() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+179
-88
@@ -42,10 +42,13 @@ class ProviderApiDoc
|
|||||||
* operationId="getProviderVerification",
|
* operationId="getProviderVerification",
|
||||||
* tags={"教練驗證"},
|
* tags={"教練驗證"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="取得成功",
|
* description="取得成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(
|
* @OA\Property(
|
||||||
* property="data",
|
* property="data",
|
||||||
@@ -55,8 +58,10 @@ class ProviderApiDoc
|
|||||||
* @OA\Property(
|
* @OA\Property(
|
||||||
* property="certifications",
|
* property="certifications",
|
||||||
* type="array",
|
* type="array",
|
||||||
|
*
|
||||||
* @OA\Items(
|
* @OA\Items(
|
||||||
* type="object",
|
* type="object",
|
||||||
|
*
|
||||||
* @OA\Property(property="id", type="integer", example=1),
|
* @OA\Property(property="id", type="integer", example=1),
|
||||||
* @OA\Property(property="url", type="string", example="http://localhost:8080/storage/providers/5/certifications/uuid.jpg")
|
* @OA\Property(property="url", type="string", example="http://localhost:8080/storage/providers/5/certifications/uuid.jpg")
|
||||||
* )
|
* )
|
||||||
@@ -64,12 +69,11 @@ class ProviderApiDoc
|
|||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="無權限", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
*
|
||||||
|
* @OA\Response(response=403, description="非 provider 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function getProviderVerification()
|
public function getProviderVerification() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上傳證照圖片
|
* 上傳證照圖片
|
||||||
@@ -81,20 +85,27 @@ class ProviderApiDoc
|
|||||||
* operationId="uploadCertification",
|
* operationId="uploadCertification",
|
||||||
* tags={"教練驗證"},
|
* tags={"教練驗證"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\RequestBody(
|
* @OA\RequestBody(
|
||||||
* required=true,
|
* required=true,
|
||||||
|
*
|
||||||
* @OA\MediaType(
|
* @OA\MediaType(
|
||||||
* mediaType="multipart/form-data",
|
* mediaType="multipart/form-data",
|
||||||
|
*
|
||||||
* @OA\Schema(
|
* @OA\Schema(
|
||||||
* required={"certification"},
|
* required={"certification"},
|
||||||
|
*
|
||||||
* @OA\Property(property="certification", type="string", format="binary", description="證照圖片(jpg/png,最大 5MB)")
|
* @OA\Property(property="certification", type="string", format="binary", description="證照圖片(jpg/png,最大 5MB)")
|
||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="上傳成功",
|
* description="上傳成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(
|
* @OA\Property(
|
||||||
* property="data",
|
* property="data",
|
||||||
@@ -104,13 +115,12 @@ class ProviderApiDoc
|
|||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(response=422, description="圖片驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
* @OA\Response(response=422, description="圖片驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=403, description="無權限", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=403, description="非 provider 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function uploadCertification()
|
public function uploadCertification() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刪除證照圖片
|
* 刪除證照圖片
|
||||||
@@ -122,22 +132,25 @@ class ProviderApiDoc
|
|||||||
* operationId="deleteCertification",
|
* operationId="deleteCertification",
|
||||||
* tags={"教練驗證"},
|
* tags={"教練驗證"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="證照 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="證照 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="刪除成功",
|
* description="刪除成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="證照已刪除")
|
* @OA\Property(property="message", type="string", example="證照已刪除")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="無權限或當前狀態不可刪除", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
*
|
||||||
|
* @OA\Response(response=403, description="非 provider 角色、非本人證照或當前狀態不可刪除", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=404, description="證照不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=404, description="證照不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function deleteCertification()
|
public function deleteCertification() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 送出驗證申請
|
* 送出驗證申請
|
||||||
@@ -149,21 +162,23 @@ class ProviderApiDoc
|
|||||||
* operationId="submitVerification",
|
* operationId="submitVerification",
|
||||||
* tags={"教練驗證"},
|
* tags={"教練驗證"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="送審成功",
|
* description="送審成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="驗證申請已送出")
|
* @OA\Property(property="message", type="string", example="驗證申請已送出")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(response=422, description="尚未上傳證照或當前狀態不可送審", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
* @OA\Response(response=422, description="尚未上傳證照或當前狀態不可送審", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=403, description="無權限", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=403, description="非 provider 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function submitVerification()
|
public function submitVerification() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Provider Offers CRUD
|
// Provider Offers CRUD
|
||||||
@@ -179,22 +194,26 @@ class ProviderApiDoc
|
|||||||
* operationId="listProviderOffers",
|
* operationId="listProviderOffers",
|
||||||
* tags={"教練課程"},
|
* tags={"教練課程"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
|
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
|
||||||
* @OA\Parameter(name="per_page", in="query", description="每頁筆數", @OA\Schema(type="integer", default=15)),
|
* @OA\Parameter(name="per_page", in="query", description="每頁筆數", @OA\Schema(type="integer", default=15)),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="取得成功",
|
* description="取得成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/DivingOffer")),
|
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/DivingOffer")),
|
||||||
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
|
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
|
||||||
* )
|
* )
|
||||||
* )
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=403, description="非 provider 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function listProviderOffers()
|
public function listProviderOffers() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 建立課程
|
* 建立課程
|
||||||
@@ -206,10 +225,13 @@ class ProviderApiDoc
|
|||||||
* operationId="createProviderOffer",
|
* operationId="createProviderOffer",
|
||||||
* tags={"教練課程"},
|
* tags={"教練課程"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\RequestBody(
|
* @OA\RequestBody(
|
||||||
* required=true,
|
* required=true,
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
* required={"title","description","region","price","max_participants"},
|
* required={"title","description","region","price","max_participants"},
|
||||||
|
*
|
||||||
* @OA\Property(property="title", type="string", example="基礎開放水域課程"),
|
* @OA\Property(property="title", type="string", example="基礎開放水域課程"),
|
||||||
* @OA\Property(property="description", type="string", example="適合初學者的 OWD 課程"),
|
* @OA\Property(property="description", type="string", example="適合初學者的 OWD 課程"),
|
||||||
* @OA\Property(property="region", type="string", example="墾丁"),
|
* @OA\Property(property="region", type="string", example="墾丁"),
|
||||||
@@ -218,22 +240,24 @@ class ProviderApiDoc
|
|||||||
* @OA\Property(property="tags", type="array", @OA\Items(type="string"), example={"初學","OWD"})
|
* @OA\Property(property="tags", type="array", @OA\Items(type="string"), example={"初學","OWD"})
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=201,
|
* response=201,
|
||||||
* description="課程建立成功",
|
* description="課程建立成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="課程建立成功"),
|
* @OA\Property(property="message", type="string", example="課程建立成功"),
|
||||||
* @OA\Property(property="data", ref="#/components/schemas/DivingOffer")
|
* @OA\Property(property="data", ref="#/components/schemas/DivingOffer")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(response=422, description="驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
* @OA\Response(response=422, description="驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=403, description="無權限", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=403, description="非 provider 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function createProviderOffer()
|
public function createProviderOffer() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取得單一課程(Provider 視角)
|
* 取得單一課程(Provider 視角)
|
||||||
@@ -245,22 +269,25 @@ class ProviderApiDoc
|
|||||||
* operationId="getProviderOffer",
|
* operationId="getProviderOffer",
|
||||||
* tags={"教練課程"},
|
* tags={"教練課程"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="取得成功",
|
* description="取得成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="data", ref="#/components/schemas/DivingOffer")
|
* @OA\Property(property="data", ref="#/components/schemas/DivingOffer")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
*
|
||||||
|
* @OA\Response(response=403, description="非 provider 角色或非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=404, description="課程不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=404, description="課程不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function getProviderOffer()
|
public function getProviderOffer() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新課程
|
* 更新課程
|
||||||
@@ -271,10 +298,14 @@ class ProviderApiDoc
|
|||||||
* operationId="updateProviderOffer",
|
* operationId="updateProviderOffer",
|
||||||
* tags={"教練課程"},
|
* tags={"教練課程"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\RequestBody(
|
* @OA\RequestBody(
|
||||||
* required=true,
|
* required=true,
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="title", type="string", example="進階開放水域課程"),
|
* @OA\Property(property="title", type="string", example="進階開放水域課程"),
|
||||||
* @OA\Property(property="description", type="string", example="AOWD 課程"),
|
* @OA\Property(property="description", type="string", example="AOWD 課程"),
|
||||||
* @OA\Property(property="region", type="string", example="小琉球"),
|
* @OA\Property(property="region", type="string", example="小琉球"),
|
||||||
@@ -284,21 +315,23 @@ class ProviderApiDoc
|
|||||||
* @OA\Property(property="is_active", type="boolean", example=true)
|
* @OA\Property(property="is_active", type="boolean", example=true)
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="更新成功",
|
* description="更新成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="data", ref="#/components/schemas/DivingOffer")
|
* @OA\Property(property="data", ref="#/components/schemas/DivingOffer")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
*
|
||||||
|
* @OA\Response(response=403, description="非 provider 角色或非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=422, description="驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=422, description="驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function updateProviderOffer()
|
public function updateProviderOffer() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刪除課程
|
* 刪除課程
|
||||||
@@ -309,22 +342,25 @@ class ProviderApiDoc
|
|||||||
* operationId="deleteProviderOffer",
|
* operationId="deleteProviderOffer",
|
||||||
* tags={"教練課程"},
|
* tags={"教練課程"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="刪除成功",
|
* description="刪除成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="課程已刪除")
|
* @OA\Property(property="message", type="string", example="課程已刪除")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
*
|
||||||
|
* @OA\Response(response=403, description="非 provider 角色或非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=404, description="課程不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=404, description="課程不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function deleteProviderOffer()
|
public function deleteProviderOffer() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Provider Offer Images
|
// Provider Offer Images
|
||||||
@@ -340,32 +376,39 @@ class ProviderApiDoc
|
|||||||
* operationId="uploadOfferCover",
|
* operationId="uploadOfferCover",
|
||||||
* tags={"教練圖片"},
|
* tags={"教練圖片"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\RequestBody(
|
* @OA\RequestBody(
|
||||||
* required=true,
|
* required=true,
|
||||||
|
*
|
||||||
* @OA\MediaType(
|
* @OA\MediaType(
|
||||||
* mediaType="multipart/form-data",
|
* mediaType="multipart/form-data",
|
||||||
|
*
|
||||||
* @OA\Schema(
|
* @OA\Schema(
|
||||||
* required={"cover_image"},
|
* required={"cover_image"},
|
||||||
|
*
|
||||||
* @OA\Property(property="cover_image", type="string", format="binary", description="封面圖片(jpg/png,最大 5MB)")
|
* @OA\Property(property="cover_image", type="string", format="binary", description="封面圖片(jpg/png,最大 5MB)")
|
||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="上傳成功",
|
* description="上傳成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="cover_image_url", type="string", example="https://example.com/covers/1.jpg")
|
* @OA\Property(property="cover_image_url", type="string", example="https://example.com/covers/1.jpg")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(response=422, description="圖片驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
* @OA\Response(response=422, description="圖片驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=403, description="非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=403, description="非 provider 角色或非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function uploadOfferCover()
|
public function uploadOfferCover() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刪除封面圖片
|
* 刪除封面圖片
|
||||||
@@ -376,21 +419,24 @@ class ProviderApiDoc
|
|||||||
* operationId="deleteOfferCover",
|
* operationId="deleteOfferCover",
|
||||||
* tags={"教練圖片"},
|
* tags={"教練圖片"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="刪除成功",
|
* description="刪除成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="封面圖片已刪除")
|
* @OA\Property(property="message", type="string", example="封面圖片已刪除")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
*
|
||||||
|
* @OA\Response(response=403, description="非 provider 角色或非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function deleteOfferCover()
|
public function deleteOfferCover() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上傳相簿圖片
|
* 上傳相簿圖片
|
||||||
@@ -402,37 +448,45 @@ class ProviderApiDoc
|
|||||||
* operationId="uploadOfferImages",
|
* operationId="uploadOfferImages",
|
||||||
* tags={"教練圖片"},
|
* tags={"教練圖片"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="課程 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\RequestBody(
|
* @OA\RequestBody(
|
||||||
* required=true,
|
* required=true,
|
||||||
|
*
|
||||||
* @OA\MediaType(
|
* @OA\MediaType(
|
||||||
* mediaType="multipart/form-data",
|
* mediaType="multipart/form-data",
|
||||||
|
*
|
||||||
* @OA\Schema(
|
* @OA\Schema(
|
||||||
* required={"images[]"},
|
* required={"images[]"},
|
||||||
|
*
|
||||||
* @OA\Property(
|
* @OA\Property(
|
||||||
* property="images[]",
|
* property="images[]",
|
||||||
* type="array",
|
* type="array",
|
||||||
|
*
|
||||||
* @OA\Items(type="string", format="binary"),
|
* @OA\Items(type="string", format="binary"),
|
||||||
* description="相簿圖片(每張最大 5MB)"
|
* description="相簿圖片(每張最大 5MB)"
|
||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="上傳成功",
|
* description="上傳成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="images", type="array", @OA\Items(type="string"), example={"https://example.com/img/1.jpg"})
|
* @OA\Property(property="images", type="array", @OA\Items(type="string"), example={"https://example.com/img/1.jpg"})
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(response=422, description="圖片驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
* @OA\Response(response=422, description="圖片驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=403, description="非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=403, description="非 provider 角色或非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function uploadOfferImages()
|
public function uploadOfferImages() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刪除相簿圖片
|
* 刪除相簿圖片
|
||||||
@@ -443,22 +497,25 @@ class ProviderApiDoc
|
|||||||
* operationId="deleteOfferImage",
|
* operationId="deleteOfferImage",
|
||||||
* tags={"教練圖片"},
|
* tags={"教練圖片"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="圖片 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="圖片 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="刪除成功",
|
* description="刪除成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="圖片已刪除")
|
* @OA\Property(property="message", type="string", example="圖片已刪除")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="非本人圖片", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
*
|
||||||
|
* @OA\Response(response=403, description="非 provider 角色或非本人圖片", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=404, description="圖片不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=404, description="圖片不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function deleteOfferImage()
|
public function deleteOfferImage() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Provider Schedules
|
// Provider Schedules
|
||||||
@@ -474,20 +531,24 @@ class ProviderApiDoc
|
|||||||
* operationId="listProviderSchedules",
|
* operationId="listProviderSchedules",
|
||||||
* tags={"教練時段"},
|
* tags={"教練時段"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="offer_id", in="query", required=false, description="課程 ID 篩選", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="offer_id", in="query", required=false, description="課程 ID 篩選", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="取得成功",
|
* description="取得成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/CourseSchedule"))
|
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/CourseSchedule"))
|
||||||
* )
|
* )
|
||||||
* )
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=403, description="非 provider 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function listProviderSchedules()
|
public function listProviderSchedules() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 建立時段
|
* 建立時段
|
||||||
@@ -498,31 +559,36 @@ class ProviderApiDoc
|
|||||||
* operationId="createProviderSchedule",
|
* operationId="createProviderSchedule",
|
||||||
* tags={"教練時段"},
|
* tags={"教練時段"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\RequestBody(
|
* @OA\RequestBody(
|
||||||
* required=true,
|
* required=true,
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
* required={"offer_id","start_date","end_date","available_slots"},
|
* required={"offer_id","start_date","end_date","available_slots"},
|
||||||
|
*
|
||||||
* @OA\Property(property="offer_id", type="integer", example=1),
|
* @OA\Property(property="offer_id", type="integer", example=1),
|
||||||
* @OA\Property(property="start_date", type="string", format="date", example="2025-07-01"),
|
* @OA\Property(property="start_date", type="string", format="date", example="2025-07-01"),
|
||||||
* @OA\Property(property="end_date", type="string", format="date", example="2025-07-03"),
|
* @OA\Property(property="end_date", type="string", format="date", example="2025-07-03"),
|
||||||
* @OA\Property(property="available_slots", type="integer", example=4)
|
* @OA\Property(property="available_slots", type="integer", example=4)
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=201,
|
* response=201,
|
||||||
* description="時段建立成功",
|
* description="時段建立成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="data", ref="#/components/schemas/CourseSchedule")
|
* @OA\Property(property="data", ref="#/components/schemas/CourseSchedule")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(response=422, description="驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
* @OA\Response(response=422, description="驗證失敗", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=403, description="非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=403, description="非 provider 角色或非本人課程", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function createProviderSchedule()
|
public function createProviderSchedule() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新時段
|
* 更新時段
|
||||||
@@ -533,31 +599,37 @@ class ProviderApiDoc
|
|||||||
* operationId="updateProviderSchedule",
|
* operationId="updateProviderSchedule",
|
||||||
* tags={"教練時段"},
|
* tags={"教練時段"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="時段 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="時段 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\RequestBody(
|
* @OA\RequestBody(
|
||||||
* required=true,
|
* required=true,
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="start_date", type="string", format="date", example="2025-07-05"),
|
* @OA\Property(property="start_date", type="string", format="date", example="2025-07-05"),
|
||||||
* @OA\Property(property="end_date", type="string", format="date", example="2025-07-07"),
|
* @OA\Property(property="end_date", type="string", format="date", example="2025-07-07"),
|
||||||
* @OA\Property(property="available_slots", type="integer", example=6),
|
* @OA\Property(property="available_slots", type="integer", example=6),
|
||||||
* @OA\Property(property="is_active", type="boolean", example=true)
|
* @OA\Property(property="is_active", type="boolean", example=true)
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="更新成功",
|
* description="更新成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="data", ref="#/components/schemas/CourseSchedule")
|
* @OA\Property(property="data", ref="#/components/schemas/CourseSchedule")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="非本人時段", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
*
|
||||||
|
* @OA\Response(response=403, description="非 provider 角色或非本人時段", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=404, description="時段不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=404, description="時段不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function updateProviderSchedule()
|
public function updateProviderSchedule() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刪除時段
|
* 刪除時段
|
||||||
@@ -568,22 +640,25 @@ class ProviderApiDoc
|
|||||||
* operationId="deleteProviderSchedule",
|
* operationId="deleteProviderSchedule",
|
||||||
* tags={"教練時段"},
|
* tags={"教練時段"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="時段 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="時段 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="刪除成功",
|
* description="刪除成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="時段已刪除")
|
* @OA\Property(property="message", type="string", example="時段已刪除")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="非本人時段", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
*
|
||||||
|
* @OA\Response(response=403, description="非 provider 角色或非本人時段", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=404, description="時段不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=404, description="時段不存在", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function deleteProviderSchedule()
|
public function deleteProviderSchedule() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Provider Bookings Management
|
// Provider Bookings Management
|
||||||
@@ -599,22 +674,26 @@ class ProviderApiDoc
|
|||||||
* operationId="listProviderBookings",
|
* operationId="listProviderBookings",
|
||||||
* tags={"教練預約"},
|
* tags={"教練預約"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
|
* @OA\Parameter(name="page", in="query", description="頁碼", @OA\Schema(type="integer", default=1)),
|
||||||
* @OA\Parameter(name="per_page", in="query", description="每頁筆數", @OA\Schema(type="integer", default=15)),
|
* @OA\Parameter(name="per_page", in="query", description="每頁筆數", @OA\Schema(type="integer", default=15)),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="取得成功",
|
* description="取得成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Booking")),
|
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Booking")),
|
||||||
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
|
* @OA\Property(property="meta", ref="#/components/schemas/PaginationMeta")
|
||||||
* )
|
* )
|
||||||
* )
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=403, description="非 provider 角色", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function listProviderBookings()
|
public function listProviderBookings() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 確認預約
|
* 確認預約
|
||||||
@@ -625,23 +704,26 @@ class ProviderApiDoc
|
|||||||
* operationId="confirmBooking",
|
* operationId="confirmBooking",
|
||||||
* tags={"教練預約"},
|
* tags={"教練預約"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="確認成功",
|
* description="確認成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="預約已確認"),
|
* @OA\Property(property="message", type="string", example="預約已確認"),
|
||||||
* @OA\Property(property="data", ref="#/components/schemas/Booking")
|
* @OA\Property(property="data", ref="#/components/schemas/Booking")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="非本人課程的預約", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
*
|
||||||
|
* @OA\Response(response=403, description="非 provider 角色或非本人課程的預約", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=422, description="狀態不允許確認", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=422, description="狀態不允許確認", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function confirmBooking()
|
public function confirmBooking() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 拒絕預約
|
* 拒絕預約
|
||||||
@@ -652,23 +734,26 @@ class ProviderApiDoc
|
|||||||
* operationId="rejectBooking",
|
* operationId="rejectBooking",
|
||||||
* tags={"教練預約"},
|
* tags={"教練預約"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="拒絕成功",
|
* description="拒絕成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="預約已拒絕"),
|
* @OA\Property(property="message", type="string", example="預約已拒絕"),
|
||||||
* @OA\Property(property="data", ref="#/components/schemas/Booking")
|
* @OA\Property(property="data", ref="#/components/schemas/Booking")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="非本人課程的預約", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
*
|
||||||
|
* @OA\Response(response=403, description="非 provider 角色或非本人課程的預約", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=422, description="狀態不允許拒絕", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=422, description="狀態不允許拒絕", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function rejectBooking()
|
public function rejectBooking() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 標記預約完成
|
* 標記預約完成
|
||||||
@@ -679,23 +764,26 @@ class ProviderApiDoc
|
|||||||
* operationId="completeBookingByProvider",
|
* operationId="completeBookingByProvider",
|
||||||
* tags={"教練預約"},
|
* tags={"教練預約"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="標記成功",
|
* description="標記成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="預約已完成"),
|
* @OA\Property(property="message", type="string", example="預約已完成"),
|
||||||
* @OA\Property(property="data", ref="#/components/schemas/Booking")
|
* @OA\Property(property="data", ref="#/components/schemas/Booking")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="非本人課程的預約", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
*
|
||||||
|
* @OA\Response(response=403, description="非 provider 角色或非本人課程的預約", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=422, description="狀態不允許完成", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=422, description="狀態不允許完成", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function completeBookingByProvider()
|
public function completeBookingByProvider() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取消預約(Provider)
|
* 取消預約(Provider)
|
||||||
@@ -706,21 +794,24 @@ class ProviderApiDoc
|
|||||||
* operationId="cancelBookingByProvider",
|
* operationId="cancelBookingByProvider",
|
||||||
* tags={"教練預約"},
|
* tags={"教練預約"},
|
||||||
* security={{"bearerAuth": {}}},
|
* security={{"bearerAuth": {}}},
|
||||||
|
*
|
||||||
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
* @OA\Parameter(name="id", in="path", required=true, description="預約 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
* @OA\Response(
|
* @OA\Response(
|
||||||
* response=200,
|
* response=200,
|
||||||
* description="取消成功",
|
* description="取消成功",
|
||||||
|
*
|
||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
|
*
|
||||||
* @OA\Property(property="status", type="boolean", example=true),
|
* @OA\Property(property="status", type="boolean", example=true),
|
||||||
* @OA\Property(property="message", type="string", example="預約已取消"),
|
* @OA\Property(property="message", type="string", example="預約已取消"),
|
||||||
* @OA\Property(property="data", ref="#/components/schemas/Booking")
|
* @OA\Property(property="data", ref="#/components/schemas/Booking")
|
||||||
* )
|
* )
|
||||||
* ),
|
* ),
|
||||||
* @OA\Response(response=403, description="非本人課程的預約", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
*
|
||||||
|
* @OA\Response(response=403, description="非 provider 角色或非本人課程的預約", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse")),
|
||||||
* @OA\Response(response=422, description="狀態不允許取消", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
* @OA\Response(response=422, description="狀態不允許取消", @OA\JsonContent(ref="#/components/schemas/ApiErrorResponse"))
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
public function cancelBookingByProvider()
|
public function cancelBookingByProvider() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class EnsureMember
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
if (! $request->user() || ! $request->user()->isMember()) {
|
||||||
|
return response()->json(['status' => false, 'message' => '無權限存取'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class EnsureProvider
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
if (! $request->user() || ! $request->user()->isProvider()) {
|
||||||
|
return response()->json(['status' => false, 'message' => '無權限存取'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
$middleware->trustProxies(at: ['*']);
|
$middleware->trustProxies(at: ['*']);
|
||||||
$middleware->alias([
|
$middleware->alias([
|
||||||
'admin' => \App\Http\Middleware\EnsureAdmin::class,
|
'admin' => \App\Http\Middleware\EnsureAdmin::class,
|
||||||
|
'member' => \App\Http\Middleware\EnsureMember::class,
|
||||||
|
'provider' => \App\Http\Middleware\EnsureProvider::class,
|
||||||
]);
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions) {
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
|
|||||||
@@ -4,36 +4,36 @@
|
|||||||
|
|
||||||
## 1. Member 角色 middleware
|
## 1. Member 角色 middleware
|
||||||
|
|
||||||
- [ ] 1.1 [後端] 新增 `app/Http/Middleware/EnsureMember.php`(結構同 `EnsureAdmin`,改用 `isMember()`),並在 `bootstrap/app.php` 的 `$middleware->alias([...])` 加上 `'member' => \App\Http\Middleware\EnsureMember::class`;驗證:1.3 起的測試通過
|
- [x] 1.1 [後端] 新增 `app/Http/Middleware/EnsureMember.php`(結構同 `EnsureAdmin`,改用 `isMember()`),並在 `bootstrap/app.php` 的 `$middleware->alias([...])` 加上 `'member' => \App\Http\Middleware\EnsureMember::class`;驗證:1.3 起的測試通過
|
||||||
- [ ] 1.2 [後端] `routes/api.php` 的 member 群組改為 `Route::middleware(['auth:sanctum', 'member'])->prefix('member')`;驗證:`php artisan route:list --path=api/member` 顯示 `member` middleware,且 `register`、`login` 不受影響
|
- [x] 1.2 [後端] `routes/api.php` 的 member 群組改為 `Route::middleware(['auth:sanctum', 'member'])->prefix('member')`;驗證:`php artisan route:list --path=api/member` 顯示 `member` middleware,且 `register`、`login` 不受影響
|
||||||
- [ ] 1.3 [整合測試] `test_member_can_access_member_routes`:member 呼叫 `GET /api/member/profile`、`GET /api/member/bookings` 回 200(對應 Scenario「member 可存取 member 路由」)
|
- [x] 1.3 [整合測試] `test_member_can_access_member_routes`:member 呼叫 `GET /api/member/profile`、`GET /api/member/bookings` 回 200(對應 Scenario「member 可存取 member 路由」)
|
||||||
- [ ] 1.4 [整合測試] `test_provider_is_rejected_on_member_routes`:provider 呼叫 `GET /api/member/profile`、`GET /api/member/bookings`、`POST /api/member/bookings`、`POST /api/member/reviews`,皆回 403,並斷言 body 為 `{ status: false, message: "無權限存取" }`;`POST /api/member/reviews` 另斷言 `reviews` 筆數不變(對應 Scenario「provider 呼叫 member 路由被拒絕」)
|
- [x] 1.4 [整合測試] `test_provider_is_rejected_on_member_routes`:provider 呼叫 `GET /api/member/profile`、`GET /api/member/bookings`、`POST /api/member/bookings`、`POST /api/member/reviews`,皆回 403,並斷言 body 為 `{ status: false, message: "無權限存取" }`;`POST /api/member/reviews` 另斷言 `reviews` 筆數不變(對應 Scenario「provider 呼叫 member 路由被拒絕」)
|
||||||
- [ ] 1.5 [整合測試] `test_admin_is_rejected_on_member_routes`:admin 呼叫 `GET /api/member/profile`、`GET /api/member/bookings`、`POST /api/member/bookings`,皆回 403 與相同 body(對應 Scenario「admin 呼叫 member 路由被拒絕」)
|
- [x] 1.5 [整合測試] `test_admin_is_rejected_on_member_routes`:admin 呼叫 `GET /api/member/profile`、`GET /api/member/bookings`、`POST /api/member/bookings`,皆回 403 與相同 body(對應 Scenario「admin 呼叫 member 路由被拒絕」)
|
||||||
- [ ] 1.6 [整合測試] `test_non_member_booking_attempt_creates_no_booking`:建立可預約時段,provider 帶合法 `schedule_id` 呼叫 `POST /api/member/bookings`,回 403,`bookings` 筆數與 `current_participants` 不變(對應 Scenario「非 member 建立預約不會產生資料」)
|
- [x] 1.6 [整合測試] `test_non_member_booking_attempt_creates_no_booking`:建立可預約時段,provider 帶合法 `schedule_id` 呼叫 `POST /api/member/bookings`,回 403,`bookings` 筆數與 `current_participants` 不變(對應 Scenario「非 member 建立預約不會產生資料」)
|
||||||
- [ ] 1.7 [整合測試] `test_unauthenticated_request_is_rejected_on_member_routes`:不帶 token 呼叫 `GET /api/member/profile`、`GET /api/member/bookings` 回 401(對應 Scenario「未登入呼叫 member 路由」)
|
- [x] 1.7 [整合測試] `test_unauthenticated_request_is_rejected_on_member_routes`:不帶 token 呼叫 `GET /api/member/profile`、`GET /api/member/bookings` 回 401(對應 Scenario「未登入呼叫 member 路由」)
|
||||||
|
|
||||||
## 2. Provider 角色 middleware
|
## 2. Provider 角色 middleware
|
||||||
|
|
||||||
- [ ] 2.1 [後端] 新增 `app/Http/Middleware/EnsureProvider.php`(結構同 `EnsureAdmin`,改用 `isProvider()`,**不檢查** `verification_status`),並在 `bootstrap/app.php` 加上 `'provider' => \App\Http\Middleware\EnsureProvider::class`;驗證:2.3 起的測試通過
|
- [x] 2.1 [後端] 新增 `app/Http/Middleware/EnsureProvider.php`(結構同 `EnsureAdmin`,改用 `isProvider()`,**不檢查** `verification_status`),並在 `bootstrap/app.php` 加上 `'provider' => \App\Http\Middleware\EnsureProvider::class`;驗證:2.3 起的測試通過
|
||||||
- [ ] 2.2 [後端] `routes/api.php` 的 provider 群組改為 `Route::middleware(['auth:sanctum', 'provider'])->prefix('provider')`;驗證:`php artisan route:list --path=api/provider` 顯示 `provider` middleware,且 `register`、`login` 不受影響
|
- [x] 2.2 [後端] `routes/api.php` 的 provider 群組改為 `Route::middleware(['auth:sanctum', 'provider'])->prefix('provider')`;驗證:`php artisan route:list --path=api/provider` 顯示 `provider` middleware,且 `register`、`login` 不受影響
|
||||||
- [ ] 2.3 [整合測試] `test_provider_can_access_provider_routes`:provider(含 `providerProfile`)呼叫 `GET /api/provider/profile`、`/offers`、`/schedules`、`/bookings` 回 200(對應 Scenario「provider 可存取 provider 路由」)
|
- [x] 2.3 [整合測試] `test_provider_can_access_provider_routes`:provider(含 `providerProfile`)呼叫 `GET /api/provider/profile`、`/offers`、`/schedules`、`/bookings` 回 200(對應 Scenario「provider 可存取 provider 路由」)
|
||||||
- [ ] 2.4 [整合測試] `test_unapproved_provider_can_access_verification`:`verification_status` 為 `unsubmitted`、`pending`、`rejected` 的 provider 呼叫 `GET /api/provider/verification` 回 200(對應 Scenario「未通過審核的 provider 仍可進入 provider 路由」)
|
- [x] 2.4 [整合測試] `test_unapproved_provider_can_access_verification`:`verification_status` 為 `unsubmitted`、`pending`、`rejected` 的 provider 呼叫 `GET /api/provider/verification` 回 200(對應 Scenario「未通過審核的 provider 仍可進入 provider 路由」)
|
||||||
- [ ] 2.5 [整合測試] `test_member_is_rejected_on_provider_routes`:member 呼叫 `GET /api/provider/profile`、`GET /api/provider/offers`、`POST /api/provider/offers`、`POST /api/provider/schedules`、`GET /api/provider/bookings`、`GET /api/provider/verification`,皆回 403 與 `{ status: false, message: "無權限存取" }`;`POST /api/provider/schedules` 另斷言 `course_schedules` 筆數不變(對應 Scenario「member 呼叫 provider 路由被拒絕」)
|
- [x] 2.5 [整合測試] `test_member_is_rejected_on_provider_routes`:member 呼叫 `GET /api/provider/profile`、`GET /api/provider/offers`、`POST /api/provider/offers`、`POST /api/provider/schedules`、`GET /api/provider/bookings`、`GET /api/provider/verification`,皆回 403 與 `{ status: false, message: "無權限存取" }`;`POST /api/provider/schedules` 另斷言 `course_schedules` 筆數不變(對應 Scenario「member 呼叫 provider 路由被拒絕」)
|
||||||
- [ ] 2.6 [整合測試] `test_admin_is_rejected_on_provider_routes`:admin 呼叫 `GET /api/provider/profile`、`GET /api/provider/offers`、`POST /api/provider/offers`、`GET /api/provider/bookings`,皆回 403 與相同 body(對應 Scenario「admin 呼叫 provider 路由被拒絕」)
|
- [x] 2.6 [整合測試] `test_admin_is_rejected_on_provider_routes`:admin 呼叫 `GET /api/provider/profile`、`GET /api/provider/offers`、`POST /api/provider/offers`、`GET /api/provider/bookings`,皆回 403 與相同 body(對應 Scenario「admin 呼叫 provider 路由被拒絕」)
|
||||||
- [ ] 2.7 [整合測試] `test_non_provider_offer_attempt_creates_no_offer`:member 帶通過驗證的完整課程資料呼叫 `POST /api/provider/offers`,回 403,`diving_offers` 筆數不變(對應 Scenario「非 provider 建立課程不會產生資料」)
|
- [x] 2.7 [整合測試] `test_non_provider_offer_attempt_creates_no_offer`:member 帶通過驗證的完整課程資料呼叫 `POST /api/provider/offers`,回 403,`diving_offers` 筆數不變(對應 Scenario「非 provider 建立課程不會產生資料」)
|
||||||
- [ ] 2.8 [整合測試] `test_unauthenticated_request_is_rejected_on_provider_routes`:不帶 token 呼叫 `GET /api/provider/profile`、`GET /api/provider/offers` 回 401(對應 Scenario「未登入呼叫 provider 路由」)
|
- [x] 2.8 [整合測試] `test_unauthenticated_request_is_rejected_on_provider_routes`:不帶 token 呼叫 `GET /api/provider/profile`、`GET /api/provider/offers` 回 401(對應 Scenario「未登入呼叫 provider 路由」)
|
||||||
|
|
||||||
## 3. 共用路由與既有規格的回歸
|
## 3. 共用路由與既有規格的回歸
|
||||||
|
|
||||||
- [ ] 3.1 [整合測試] `test_member_and_provider_can_read_notifications`:member 與 provider 各自呼叫 `GET /api/notifications` 回 200(對應 Scenario「member 與 provider 皆可讀取通知」)
|
- [x] 3.1 [整合測試] `test_member_and_provider_can_read_notifications`:member 與 provider 各自呼叫 `GET /api/notifications` 回 200(對應 Scenario「member 與 provider 皆可讀取通知」)
|
||||||
- [ ] 3.2 [整合測試] `test_member_and_provider_can_read_unread_message_counts`:member 與 provider 各自呼叫 `GET /api/bookings/messages/unread-counts` 回 200(對應 Scenario「member 與 provider 皆可讀取未讀訊息數」)
|
- [x] 3.2 [整合測試] `test_member_and_provider_can_read_unread_message_counts`:member 與 provider 各自呼叫 `GET /api/bookings/messages/unread-counts` 回 200(對應 Scenario「member 與 provider 皆可讀取未讀訊息數」)
|
||||||
- [ ] 3.3 [整合測試] 執行 `php artisan test --filter=BookingListTest`,確認 `feature-test-coverage`「預約列表端點測試覆蓋」的四個 Scenario(member 隔離、provider 隔離、admin 全部、未認證 401)在新 middleware 下仍全綠(「非 member/非 provider 於 middleware 層 403」由 1.4、1.5、2.5、2.6 覆蓋);同時更新 `tests/Feature/BookingListTest.php` 第 19–24 行 docblock 中「無 role check」「不靠 middleware role 強制」的過時描述
|
- [x] 3.3 [整合測試] 執行 `php artisan test --filter=BookingListTest`,確認 `feature-test-coverage`「預約列表端點測試覆蓋」的四個 Scenario(member 隔離、provider 隔離、admin 全部、未認證 401)在新 middleware 下仍全綠(「非 member/非 provider 於 middleware 層 403」由 1.4、1.5、2.5、2.6 覆蓋);同時更新 `tests/Feature/BookingListTest.php` 第 19–24 行 docblock 中「無 role check」「不靠 middleware role 強制」的過時描述
|
||||||
|
|
||||||
## 4. 全套回歸、Swagger 與格式
|
## 4. 全套回歸、Swagger 與格式
|
||||||
|
|
||||||
- [ ] 4.1 [整合測試] 執行完整 `php artisan test`(需在容器內,因 `CourseImageTest` 需要 GD);若有既有測試因角色錯誤改回 403,判斷是測試用錯角色(修正測試並在 commit 說明)還是依賴跨角色行為(停下回報)
|
- [x] 4.1 [整合測試] 執行完整 `php artisan test`(需在容器內,因 `CourseImageTest` 需要 GD);若有既有測試因角色錯誤改回 403,判斷是測試用錯角色(修正測試並在 commit 說明)還是依賴跨角色行為(停下回報)
|
||||||
- [ ] 4.2 [後端] 核對 `app/Docs/MemberApiDoc.php`、`app/Docs/ProviderApiDoc.php` 每個端點的 403 回應描述涵蓋「角色不符」;缺的補上,並執行 `php artisan l5-swagger:generate` 確認無錯誤
|
- [x] 4.2 [後端] 核對 `app/Docs/MemberApiDoc.php`、`app/Docs/ProviderApiDoc.php` 每個端點的 403 回應描述涵蓋「角色不符」;缺的補上,並執行 `php artisan l5-swagger:generate` 確認無錯誤
|
||||||
- [ ] 4.3 [後端] 執行 `./vendor/bin/pint --test app/Http/Middleware/EnsureMember.php app/Http/Middleware/EnsureProvider.php bootstrap/app.php routes/api.php tests/Feature/RoleAuthorizationTest.php`(以及 4.1/4.2 若有修改的檔案),確認無格式問題
|
- [x] 4.3 [後端] 執行 `./vendor/bin/pint --test app/Http/Middleware/EnsureMember.php app/Http/Middleware/EnsureProvider.php bootstrap/app.php routes/api.php tests/Feature/RoleAuthorizationTest.php`(以及 4.1/4.2 若有修改的檔案),確認無格式問題
|
||||||
|
|
||||||
## 5. 手動驗證
|
## 5. 手動驗證
|
||||||
|
|
||||||
|
|||||||
+67
-67
@@ -1,22 +1,22 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Broadcast;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
use App\Http\Controllers\API\AuthController;
|
|
||||||
use App\Http\Controllers\API\DivingOfferController;
|
|
||||||
use App\Http\Controllers\API\ProviderOfferController;
|
|
||||||
use App\Http\Controllers\API\ScheduleController;
|
|
||||||
use App\Http\Controllers\API\ProviderBookingController;
|
|
||||||
use App\Http\Controllers\API\MemberBookingController;
|
|
||||||
use App\Http\Controllers\API\ReviewController;
|
|
||||||
use App\Http\Controllers\API\AdminReviewController;
|
|
||||||
use App\Http\Controllers\API\AdminBookingController;
|
use App\Http\Controllers\API\AdminBookingController;
|
||||||
use App\Http\Controllers\API\CourseImageController;
|
use App\Http\Controllers\API\AdminOfferController;
|
||||||
|
use App\Http\Controllers\API\AdminReviewController;
|
||||||
use App\Http\Controllers\API\AdminStatsController;
|
use App\Http\Controllers\API\AdminStatsController;
|
||||||
use App\Http\Controllers\API\AdminUserController;
|
use App\Http\Controllers\API\AdminUserController;
|
||||||
use App\Http\Controllers\API\AdminOfferController;
|
use App\Http\Controllers\API\AuthController;
|
||||||
use App\Http\Controllers\API\BookingMessageController;
|
use App\Http\Controllers\API\BookingMessageController;
|
||||||
|
use App\Http\Controllers\API\CourseImageController;
|
||||||
|
use App\Http\Controllers\API\DivingOfferController;
|
||||||
|
use App\Http\Controllers\API\MemberBookingController;
|
||||||
use App\Http\Controllers\API\NotificationController;
|
use App\Http\Controllers\API\NotificationController;
|
||||||
|
use App\Http\Controllers\API\ProviderBookingController;
|
||||||
|
use App\Http\Controllers\API\ProviderOfferController;
|
||||||
|
use App\Http\Controllers\API\ReviewController;
|
||||||
|
use App\Http\Controllers\API\ScheduleController;
|
||||||
|
use Illuminate\Support\Facades\Broadcast;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Broadcast::routes(['middleware' => ['auth:sanctum']]);
|
Broadcast::routes(['middleware' => ['auth:sanctum']]);
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ Broadcast::routes(['middleware' => ['auth:sanctum']]);
|
|||||||
Route::get('/diving-offers', [DivingOfferController::class, 'index']);
|
Route::get('/diving-offers', [DivingOfferController::class, 'index']);
|
||||||
Route::get('/diving-offers/{id}', [DivingOfferController::class, 'show']);
|
Route::get('/diving-offers/{id}', [DivingOfferController::class, 'show']);
|
||||||
Route::get('/diving-offers/{id}/schedules', [ScheduleController::class, 'publicList']);
|
Route::get('/diving-offers/{id}/schedules', [ScheduleController::class, 'publicList']);
|
||||||
Route::get('/diving-offers/{id}/reviews', [ReviewController::class, 'publicList']);
|
Route::get('/diving-offers/{id}/reviews', [ReviewController::class, 'publicList']);
|
||||||
|
|
||||||
// 會員註冊/登入
|
// 會員註冊/登入
|
||||||
Route::post('/member/register', [AuthController::class, 'registerMember']);
|
Route::post('/member/register', [AuthController::class, 'registerMember']);
|
||||||
@@ -37,7 +37,7 @@ Route::middleware([\Illuminate\Session\Middleware\StartSession::class])->group(f
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 會員專屬 API(需登入)
|
// 會員專屬 API(需登入)
|
||||||
Route::middleware(['auth:sanctum'])->prefix('member')->group(function () {
|
Route::middleware(['auth:sanctum', 'member'])->prefix('member')->group(function () {
|
||||||
// 會員登出
|
// 會員登出
|
||||||
Route::post('/logout', [AuthController::class, 'logoutMember']);
|
Route::post('/logout', [AuthController::class, 'logoutMember']);
|
||||||
// Token Refresh
|
// Token Refresh
|
||||||
@@ -49,14 +49,14 @@ Route::middleware(['auth:sanctum'])->prefix('member')->group(function () {
|
|||||||
// 修改密碼
|
// 修改密碼
|
||||||
Route::put('/change-password', [AuthController::class, 'changeMemberPassword']);
|
Route::put('/change-password', [AuthController::class, 'changeMemberPassword']);
|
||||||
// 預約
|
// 預約
|
||||||
Route::get('/bookings', [MemberBookingController::class, 'index']);
|
Route::get('/bookings', [MemberBookingController::class, 'index']);
|
||||||
Route::post('/bookings', [MemberBookingController::class, 'store']);
|
Route::post('/bookings', [MemberBookingController::class, 'store']);
|
||||||
Route::get('/bookings/{id}', [MemberBookingController::class, 'show']);
|
Route::get('/bookings/{id}', [MemberBookingController::class, 'show']);
|
||||||
Route::delete('/bookings/{id}', [MemberBookingController::class, 'destroy']);
|
Route::delete('/bookings/{id}', [MemberBookingController::class, 'destroy']);
|
||||||
// 評價
|
// 評價
|
||||||
Route::post('/reviews', [ReviewController::class, 'store']);
|
Route::post('/reviews', [ReviewController::class, 'store']);
|
||||||
Route::put('/reviews/{id}', [ReviewController::class, 'update']);
|
Route::put('/reviews/{id}', [ReviewController::class, 'update']);
|
||||||
Route::delete('/reviews/{id}',[ReviewController::class, 'destroy']);
|
Route::delete('/reviews/{id}', [ReviewController::class, 'destroy']);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 有幫助投票(需登入,但不限 member prefix)
|
// 有幫助投票(需登入,但不限 member prefix)
|
||||||
@@ -67,7 +67,7 @@ Route::post('/provider/register', [AuthController::class, 'registerProvider']);
|
|||||||
Route::middleware('throttle:10,1')->post('/provider/login', [AuthController::class, 'loginProvider']);
|
Route::middleware('throttle:10,1')->post('/provider/login', [AuthController::class, 'loginProvider']);
|
||||||
|
|
||||||
// 服務提供者專屬 API(需登入)
|
// 服務提供者專屬 API(需登入)
|
||||||
Route::middleware(['auth:sanctum'])->prefix('provider')->group(function () {
|
Route::middleware(['auth:sanctum', 'provider'])->prefix('provider')->group(function () {
|
||||||
// 服務提供者登出
|
// 服務提供者登出
|
||||||
Route::post('/logout', [AuthController::class, 'logoutProvider']);
|
Route::post('/logout', [AuthController::class, 'logoutProvider']);
|
||||||
// Token Refresh
|
// Token Refresh
|
||||||
@@ -79,32 +79,32 @@ Route::middleware(['auth:sanctum'])->prefix('provider')->group(function () {
|
|||||||
// 修改密碼
|
// 修改密碼
|
||||||
Route::put('/change-password', [AuthController::class, 'changeProviderPassword']);
|
Route::put('/change-password', [AuthController::class, 'changeProviderPassword']);
|
||||||
// 教練課程管理
|
// 教練課程管理
|
||||||
Route::get('/offers', [ProviderOfferController::class, 'index']);
|
Route::get('/offers', [ProviderOfferController::class, 'index']);
|
||||||
Route::post('/offers', [ProviderOfferController::class, 'store']);
|
Route::post('/offers', [ProviderOfferController::class, 'store']);
|
||||||
Route::get('/offers/{id}', [ProviderOfferController::class, 'show']);
|
Route::get('/offers/{id}', [ProviderOfferController::class, 'show']);
|
||||||
Route::put('/offers/{id}', [ProviderOfferController::class, 'update']);
|
Route::put('/offers/{id}', [ProviderOfferController::class, 'update']);
|
||||||
Route::delete('/offers/{id}', [ProviderOfferController::class, 'destroy']);
|
Route::delete('/offers/{id}', [ProviderOfferController::class, 'destroy']);
|
||||||
// 驗證申請(證照送審)
|
// 驗證申請(證照送審)
|
||||||
Route::get('/verification', [\App\Http\Controllers\API\ProviderVerificationController::class, 'show']);
|
Route::get('/verification', [\App\Http\Controllers\API\ProviderVerificationController::class, 'show']);
|
||||||
Route::post('/verification/certifications', [\App\Http\Controllers\API\ProviderVerificationController::class, 'uploadCertification']);
|
Route::post('/verification/certifications', [\App\Http\Controllers\API\ProviderVerificationController::class, 'uploadCertification']);
|
||||||
Route::delete('/verification/certifications/{id}', [\App\Http\Controllers\API\ProviderVerificationController::class, 'deleteCertification']);
|
Route::delete('/verification/certifications/{id}', [\App\Http\Controllers\API\ProviderVerificationController::class, 'deleteCertification']);
|
||||||
Route::post('/verification/submit', [\App\Http\Controllers\API\ProviderVerificationController::class, 'submit']);
|
Route::post('/verification/submit', [\App\Http\Controllers\API\ProviderVerificationController::class, 'submit']);
|
||||||
// 課程圖片
|
// 課程圖片
|
||||||
Route::post('/offers/{id}/cover', [CourseImageController::class, 'uploadCover']);
|
Route::post('/offers/{id}/cover', [CourseImageController::class, 'uploadCover']);
|
||||||
Route::delete('/offers/{id}/cover', [CourseImageController::class, 'deleteCover']);
|
Route::delete('/offers/{id}/cover', [CourseImageController::class, 'deleteCover']);
|
||||||
Route::post('/offers/{id}/images', [CourseImageController::class, 'uploadImage']);
|
Route::post('/offers/{id}/images', [CourseImageController::class, 'uploadImage']);
|
||||||
Route::delete('/images/{id}', [CourseImageController::class, 'deleteImage']);
|
Route::delete('/images/{id}', [CourseImageController::class, 'deleteImage']);
|
||||||
// 時段管理
|
// 時段管理
|
||||||
Route::get('/schedules', [ScheduleController::class, 'index']);
|
Route::get('/schedules', [ScheduleController::class, 'index']);
|
||||||
Route::post('/schedules', [ScheduleController::class, 'store']);
|
Route::post('/schedules', [ScheduleController::class, 'store']);
|
||||||
Route::put('/schedules/{id}', [ScheduleController::class, 'update']);
|
Route::put('/schedules/{id}', [ScheduleController::class, 'update']);
|
||||||
Route::delete('/schedules/{id}', [ScheduleController::class, 'destroy']);
|
Route::delete('/schedules/{id}', [ScheduleController::class, 'destroy']);
|
||||||
// 預約管理
|
// 預約管理
|
||||||
Route::get('/bookings', [ProviderBookingController::class, 'index']);
|
Route::get('/bookings', [ProviderBookingController::class, 'index']);
|
||||||
Route::put('/bookings/{id}/confirm', [ProviderBookingController::class, 'confirm']);
|
Route::put('/bookings/{id}/confirm', [ProviderBookingController::class, 'confirm']);
|
||||||
Route::put('/bookings/{id}/reject', [ProviderBookingController::class, 'reject']);
|
Route::put('/bookings/{id}/reject', [ProviderBookingController::class, 'reject']);
|
||||||
Route::put('/bookings/{id}/cancel', [ProviderBookingController::class, 'cancel']);
|
Route::put('/bookings/{id}/cancel', [ProviderBookingController::class, 'cancel']);
|
||||||
Route::put('/bookings/{id}/complete', [ProviderBookingController::class, 'complete']);
|
Route::put('/bookings/{id}/complete', [ProviderBookingController::class, 'complete']);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 管理員登入(帳號建立僅限主機端 php artisan app:create-admin,不提供公開註冊)
|
// 管理員登入(帳號建立僅限主機端 php artisan app:create-admin,不提供公開註冊)
|
||||||
@@ -129,42 +129,42 @@ Route::middleware(['auth:sanctum', 'admin'])->prefix('admin')->group(function ()
|
|||||||
// 統計數據
|
// 統計數據
|
||||||
Route::get('/stats', [AdminStatsController::class, 'index']);
|
Route::get('/stats', [AdminStatsController::class, 'index']);
|
||||||
// 用戶管理
|
// 用戶管理
|
||||||
Route::get('/members', [AdminUserController::class, 'members']);
|
Route::get('/members', [AdminUserController::class, 'members']);
|
||||||
Route::get('/members/{id}', [AdminUserController::class, 'member']);
|
Route::get('/members/{id}', [AdminUserController::class, 'member']);
|
||||||
Route::put('/members/{id}/toggle-active', [AdminUserController::class, 'toggleMemberActive']);
|
Route::put('/members/{id}/toggle-active', [AdminUserController::class, 'toggleMemberActive']);
|
||||||
Route::get('/providers', [AdminUserController::class, 'providers']);
|
Route::get('/providers', [AdminUserController::class, 'providers']);
|
||||||
Route::get('/providers/{id}', [AdminUserController::class, 'provider']);
|
Route::get('/providers/{id}', [AdminUserController::class, 'provider']);
|
||||||
Route::put('/providers/{id}/toggle-active', [AdminUserController::class, 'toggleProviderActive']);
|
Route::put('/providers/{id}/toggle-active', [AdminUserController::class, 'toggleProviderActive']);
|
||||||
// 教練審核(toggle-verified 已由審核狀態機取代)
|
// 教練審核(toggle-verified 已由審核狀態機取代)
|
||||||
Route::get('/verifications', [\App\Http\Controllers\API\AdminVerificationController::class, 'index']);
|
Route::get('/verifications', [\App\Http\Controllers\API\AdminVerificationController::class, 'index']);
|
||||||
Route::put('/verifications/{userId}/approve', [\App\Http\Controllers\API\AdminVerificationController::class, 'approve']);
|
Route::put('/verifications/{userId}/approve', [\App\Http\Controllers\API\AdminVerificationController::class, 'approve']);
|
||||||
Route::put('/verifications/{userId}/reject', [\App\Http\Controllers\API\AdminVerificationController::class, 'reject']);
|
Route::put('/verifications/{userId}/reject', [\App\Http\Controllers\API\AdminVerificationController::class, 'reject']);
|
||||||
// 課程管理
|
// 課程管理
|
||||||
Route::get('/offers', [AdminOfferController::class, 'index']);
|
Route::get('/offers', [AdminOfferController::class, 'index']);
|
||||||
Route::delete('/offers/{id}', [AdminOfferController::class, 'destroy']);
|
Route::delete('/offers/{id}', [AdminOfferController::class, 'destroy']);
|
||||||
// 預約管理
|
// 預約管理
|
||||||
Route::get('/bookings', [AdminBookingController::class, 'index']);
|
Route::get('/bookings', [AdminBookingController::class, 'index']);
|
||||||
Route::put('/bookings/{id}/complete', [AdminBookingController::class, 'complete']);
|
Route::put('/bookings/{id}/complete', [AdminBookingController::class, 'complete']);
|
||||||
// 評價管理
|
// 評價管理
|
||||||
Route::get('/reviews', [AdminReviewController::class, 'index']);
|
Route::get('/reviews', [AdminReviewController::class, 'index']);
|
||||||
Route::delete('/reviews/{id}', [AdminReviewController::class, 'destroy']);
|
Route::delete('/reviews/{id}', [AdminReviewController::class, 'destroy']);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 通知(Member + Provider 共用)
|
// 通知(Member + Provider 共用)
|
||||||
Route::middleware('auth:sanctum')->prefix('notifications')->group(function () {
|
Route::middleware('auth:sanctum')->prefix('notifications')->group(function () {
|
||||||
Route::get('/', [NotificationController::class, 'index']);
|
Route::get('/', [NotificationController::class, 'index']);
|
||||||
Route::get('/unread-count', [NotificationController::class, 'unreadCount']);
|
Route::get('/unread-count', [NotificationController::class, 'unreadCount']);
|
||||||
Route::patch('/read-all', [NotificationController::class, 'markAllRead']);
|
Route::patch('/read-all', [NotificationController::class, 'markAllRead']);
|
||||||
Route::patch('/{id}/read', [NotificationController::class, 'markRead']);
|
Route::patch('/{id}/read', [NotificationController::class, 'markRead']);
|
||||||
Route::delete('/{id}', [NotificationController::class, 'destroy']);
|
Route::delete('/{id}', [NotificationController::class, 'destroy']);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 即時訊息(Member + Provider 共用,依 booking 參與方驗證)
|
// 即時訊息(Member + Provider 共用,依 booking 參與方驗證)
|
||||||
Route::middleware('auth:sanctum')->group(function () {
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
// unread-counts 必須在 {booking} 之前,否則會被 route model binding 吃掉
|
// unread-counts 必須在 {booking} 之前,否則會被 route model binding 吃掉
|
||||||
Route::get('/bookings/messages/unread-counts', [BookingMessageController::class, 'unreadCounts']);
|
Route::get('/bookings/messages/unread-counts', [BookingMessageController::class, 'unreadCounts']);
|
||||||
Route::get('/bookings/{booking}/messages', [BookingMessageController::class, 'index']);
|
Route::get('/bookings/{booking}/messages', [BookingMessageController::class, 'index']);
|
||||||
Route::post('/bookings/{booking}/messages', [BookingMessageController::class, 'store']);
|
Route::post('/bookings/{booking}/messages', [BookingMessageController::class, 'store']);
|
||||||
Route::post('/bookings/{booking}/messages/read', [BookingMessageController::class, 'markRead']);
|
Route::post('/bookings/{booking}/messages/read', [BookingMessageController::class, 'markRead']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -172,4 +172,4 @@ Route::middleware('auth:sanctum')->group(function () {
|
|||||||
Route::middleware('auth:sanctum')->group(function () {
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
Route::post('/logout', [AuthController::class, 'logout']);
|
Route::post('/logout', [AuthController::class, 'logout']);
|
||||||
Route::get('/user', [AuthController::class, 'user']);
|
Route::get('/user', [AuthController::class, 'user']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ use Tests\TestCase;
|
|||||||
* 預約列表端點資料隔離(booking-lifecycle 規格)。
|
* 預約列表端點資料隔離(booking-lifecycle 規格)。
|
||||||
*
|
*
|
||||||
* 路由對應(routes/api.php 已確認):
|
* 路由對應(routes/api.php 已確認):
|
||||||
* GET /api/member/bookings → MemberBookingController::index(auth:sanctum,無 role check)
|
* GET /api/member/bookings → MemberBookingController::index(auth:sanctum + member middleware)
|
||||||
* GET /api/provider/bookings → ProviderBookingController::index(auth:sanctum,無 role check)
|
* GET /api/provider/bookings → ProviderBookingController::index(auth:sanctum + provider middleware)
|
||||||
* GET /api/admin/bookings → AdminBookingController::index(auth:sanctum + admin middleware)
|
* GET /api/admin/bookings → AdminBookingController::index(auth:sanctum + admin middleware)
|
||||||
*
|
*
|
||||||
* 資料隔離靠 controller query 條件,不靠 middleware role 強制,
|
* 角色 middleware 只擋跨角色呼叫(見 RoleAuthorizationTest);同角色之間的資料隔離
|
||||||
* 因此測試需分別以正確角色 actingAs 驗證隔離邊界。
|
* 靠 controller query 條件,因此測試需分別以正確角色 actingAs 驗證隔離邊界。
|
||||||
*/
|
*/
|
||||||
class BookingListTest extends TestCase
|
class BookingListTest extends TestCase
|
||||||
{
|
{
|
||||||
@@ -34,22 +34,22 @@ class BookingListTest extends TestCase
|
|||||||
|
|
||||||
$offer = DivingOffer::create([
|
$offer = DivingOffer::create([
|
||||||
'provider_id' => $provider->id,
|
'provider_id' => $provider->id,
|
||||||
'title' => 'Dive Course',
|
'title' => 'Dive Course',
|
||||||
'location' => 'Kenting',
|
'location' => 'Kenting',
|
||||||
'price' => 3000,
|
'price' => 3000,
|
||||||
'region' => '南部',
|
'region' => '南部',
|
||||||
'rating' => 0,
|
'rating' => 0,
|
||||||
'reviews' => 0,
|
'reviews' => 0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$schedule = CourseSchedule::create([
|
$schedule = CourseSchedule::create([
|
||||||
'diving_offer_id' => $offer->id,
|
'diving_offer_id' => $offer->id,
|
||||||
'provider_id' => $provider->id,
|
'provider_id' => $provider->id,
|
||||||
'scheduled_date' => now()->addDays(30)->toDateString(),
|
'scheduled_date' => now()->addDays(30)->toDateString(),
|
||||||
'start_time' => '09:00',
|
'start_time' => '09:00',
|
||||||
'max_participants' => 10,
|
'max_participants' => 10,
|
||||||
'current_participants' => 0,
|
'current_participants' => 0,
|
||||||
'status' => ScheduleStatus::Open,
|
'status' => ScheduleStatus::Open,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return [$provider, $schedule];
|
return [$provider, $schedule];
|
||||||
@@ -58,11 +58,11 @@ class BookingListTest extends TestCase
|
|||||||
private function makePendingBooking(User $member, CourseSchedule $schedule): Booking
|
private function makePendingBooking(User $member, CourseSchedule $schedule): Booking
|
||||||
{
|
{
|
||||||
return Booking::create([
|
return Booking::create([
|
||||||
'schedule_id' => $schedule->id,
|
'schedule_id' => $schedule->id,
|
||||||
'member_id' => $member->id,
|
'member_id' => $member->id,
|
||||||
'participants' => 1,
|
'participants' => 1,
|
||||||
'total_price' => 3000,
|
'total_price' => 3000,
|
||||||
'status' => BookingStatus::Pending,
|
'status' => BookingStatus::Pending,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,8 +71,8 @@ class BookingListTest extends TestCase
|
|||||||
public function test_member_sees_only_own_bookings(): void
|
public function test_member_sees_only_own_bookings(): void
|
||||||
{
|
{
|
||||||
[$provider, $schedule] = $this->makeProviderWithSchedule();
|
[$provider, $schedule] = $this->makeProviderWithSchedule();
|
||||||
$memberA = User::factory()->create(['role' => 'member']);
|
$memberA = User::factory()->create(['role' => 'member']);
|
||||||
$memberB = User::factory()->create(['role' => 'member']);
|
$memberB = User::factory()->create(['role' => 'member']);
|
||||||
$bookingA = $this->makePendingBooking($memberA, $schedule);
|
$bookingA = $this->makePendingBooking($memberA, $schedule);
|
||||||
$bookingB = $this->makePendingBooking($memberB, $schedule);
|
$bookingB = $this->makePendingBooking($memberB, $schedule);
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ class BookingListTest extends TestCase
|
|||||||
{
|
{
|
||||||
[$providerA, $scheduleA] = $this->makeProviderWithSchedule();
|
[$providerA, $scheduleA] = $this->makeProviderWithSchedule();
|
||||||
[$providerB, $scheduleB] = $this->makeProviderWithSchedule();
|
[$providerB, $scheduleB] = $this->makeProviderWithSchedule();
|
||||||
$member = User::factory()->create(['role' => 'member']);
|
$member = User::factory()->create(['role' => 'member']);
|
||||||
$bookingA = $this->makePendingBooking($member, $scheduleA);
|
$bookingA = $this->makePendingBooking($member, $scheduleA);
|
||||||
$bookingB = $this->makePendingBooking($member, $scheduleB);
|
$bookingB = $this->makePendingBooking($member, $scheduleB);
|
||||||
|
|
||||||
@@ -106,11 +106,11 @@ class BookingListTest extends TestCase
|
|||||||
|
|
||||||
public function test_admin_sees_all_bookings(): void
|
public function test_admin_sees_all_bookings(): void
|
||||||
{
|
{
|
||||||
$admin = User::factory()->create(['role' => 'admin']);
|
$admin = User::factory()->create(['role' => 'admin']);
|
||||||
[$providerA, $scheduleA] = $this->makeProviderWithSchedule();
|
[$providerA, $scheduleA] = $this->makeProviderWithSchedule();
|
||||||
[$providerB, $scheduleB] = $this->makeProviderWithSchedule();
|
[$providerB, $scheduleB] = $this->makeProviderWithSchedule();
|
||||||
$memberA = User::factory()->create(['role' => 'member']);
|
$memberA = User::factory()->create(['role' => 'member']);
|
||||||
$memberB = User::factory()->create(['role' => 'member']);
|
$memberB = User::factory()->create(['role' => 'member']);
|
||||||
$bookingA = $this->makePendingBooking($memberA, $scheduleA);
|
$bookingA = $this->makePendingBooking($memberA, $scheduleA);
|
||||||
$bookingB = $this->makePendingBooking($memberB, $scheduleB);
|
$bookingB = $this->makePendingBooking($memberB, $scheduleB);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Enums\ScheduleStatus;
|
||||||
|
use App\Models\CourseSchedule;
|
||||||
|
use App\Models\DivingOffer;
|
||||||
|
use App\Models\ProviderProfile;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Member / Provider 路由群組角色邊界(role-authorization 規格)。
|
||||||
|
*
|
||||||
|
* 在 EnsureMember / EnsureProvider 之前,任何已登入帳號都能跨角色呼叫:
|
||||||
|
* member 可建立課程(provider_id 被設成自己)、provider 可建立預約。
|
||||||
|
* 這裡驗證角色不符一律在 middleware 層回 403 且不產生資料;
|
||||||
|
* POST 請求都帶通過驗證的完整資料,確保 403 不是被 422 或資源檢查遮蓋。
|
||||||
|
*/
|
||||||
|
class RoleAuthorizationTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private const FORBIDDEN_BODY = ['status' => false, 'message' => '無權限存取'];
|
||||||
|
|
||||||
|
private function createProvider(string $verificationStatus = 'approved'): User
|
||||||
|
{
|
||||||
|
$provider = User::factory()->create(['role' => 'provider']);
|
||||||
|
ProviderProfile::create(['user_id' => $provider->id, 'verification_status' => $verificationStatus]);
|
||||||
|
|
||||||
|
return $provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createOfferWithOpenSchedule(User $provider): CourseSchedule
|
||||||
|
{
|
||||||
|
$offer = DivingOffer::create([
|
||||||
|
'provider_id' => $provider->id,
|
||||||
|
'title' => 'Dive Course',
|
||||||
|
'location' => 'Kenting',
|
||||||
|
'price' => 3000,
|
||||||
|
'region' => '南部',
|
||||||
|
'rating' => 0,
|
||||||
|
'reviews' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return CourseSchedule::create([
|
||||||
|
'diving_offer_id' => $offer->id,
|
||||||
|
'provider_id' => $provider->id,
|
||||||
|
'scheduled_date' => now()->addDays(30)->toDateString(),
|
||||||
|
'start_time' => '09:00',
|
||||||
|
'max_participants' => 10,
|
||||||
|
'current_participants' => 0,
|
||||||
|
'status' => ScheduleStatus::Open,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validOfferPayload(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => 'Cross Role Course',
|
||||||
|
'location' => 'Green Island',
|
||||||
|
'price' => 2000,
|
||||||
|
'region' => '東部',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertForbidden(User $user, string $method, string $uri, array $payload = []): void
|
||||||
|
{
|
||||||
|
$this->actingAs($user)
|
||||||
|
->json($method, $uri, $payload)
|
||||||
|
->assertStatus(403)
|
||||||
|
->assertExactJson(self::FORBIDDEN_BODY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Member 路由 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function test_member_can_access_member_routes(): void
|
||||||
|
{
|
||||||
|
$member = User::factory()->create(['role' => 'member']);
|
||||||
|
|
||||||
|
$this->actingAs($member)->getJson('/api/member/profile')->assertOk()->assertJsonPath('status', true);
|
||||||
|
$this->actingAs($member)->getJson('/api/member/bookings')->assertOk()->assertJsonPath('status', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_provider_is_rejected_on_member_routes(): void
|
||||||
|
{
|
||||||
|
$provider = $this->createProvider();
|
||||||
|
$schedule = $this->createOfferWithOpenSchedule($provider);
|
||||||
|
|
||||||
|
$this->assertForbidden($provider, 'GET', '/api/member/profile');
|
||||||
|
$this->assertForbidden($provider, 'GET', '/api/member/bookings');
|
||||||
|
$this->assertForbidden($provider, 'POST', '/api/member/bookings', [
|
||||||
|
'schedule_id' => $schedule->id,
|
||||||
|
'participants' => 1,
|
||||||
|
]);
|
||||||
|
$this->assertForbidden($provider, 'POST', '/api/member/reviews', [
|
||||||
|
'diving_offer_id' => $schedule->diving_offer_id,
|
||||||
|
'rating' => 5,
|
||||||
|
'comment' => 'self review',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// provider 為自己的課程寫評價會灌水評分
|
||||||
|
$this->assertDatabaseCount('reviews', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_admin_is_rejected_on_member_routes(): void
|
||||||
|
{
|
||||||
|
$admin = User::factory()->create(['role' => 'admin']);
|
||||||
|
$schedule = $this->createOfferWithOpenSchedule($this->createProvider());
|
||||||
|
|
||||||
|
$this->assertForbidden($admin, 'GET', '/api/member/profile');
|
||||||
|
$this->assertForbidden($admin, 'GET', '/api/member/bookings');
|
||||||
|
$this->assertForbidden($admin, 'POST', '/api/member/bookings', [
|
||||||
|
'schedule_id' => $schedule->id,
|
||||||
|
'participants' => 1,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_non_member_booking_attempt_creates_no_booking(): void
|
||||||
|
{
|
||||||
|
$provider = $this->createProvider();
|
||||||
|
$schedule = $this->createOfferWithOpenSchedule($provider);
|
||||||
|
|
||||||
|
$this->assertForbidden($provider, 'POST', '/api/member/bookings', [
|
||||||
|
'schedule_id' => $schedule->id,
|
||||||
|
'participants' => 2,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 越權預約若寫入會佔用真實學員的名額
|
||||||
|
$this->assertDatabaseCount('bookings', 0);
|
||||||
|
$this->assertSame(0, $schedule->fresh()->current_participants);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_unauthenticated_request_is_rejected_on_member_routes(): void
|
||||||
|
{
|
||||||
|
$this->getJson('/api/member/profile')->assertStatus(401);
|
||||||
|
$this->getJson('/api/member/bookings')->assertStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Provider 路由 ────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function test_provider_can_access_provider_routes(): void
|
||||||
|
{
|
||||||
|
$provider = $this->createProvider();
|
||||||
|
|
||||||
|
foreach (['/api/provider/profile', '/api/provider/offers', '/api/provider/schedules', '/api/provider/bookings'] as $uri) {
|
||||||
|
$this->actingAs($provider)->getJson($uri)->assertOk()->assertJsonPath('status', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_unapproved_provider_can_access_verification(): void
|
||||||
|
{
|
||||||
|
// 未通過審核的教練必須能進入 provider 路由才能送審或補件
|
||||||
|
foreach (['unsubmitted', 'pending', 'rejected'] as $status) {
|
||||||
|
$provider = $this->createProvider($status);
|
||||||
|
|
||||||
|
$this->actingAs($provider)
|
||||||
|
->getJson('/api/provider/verification')
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.verification_status', $status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_member_is_rejected_on_provider_routes(): void
|
||||||
|
{
|
||||||
|
$member = User::factory()->create(['role' => 'member']);
|
||||||
|
$schedule = $this->createOfferWithOpenSchedule($this->createProvider());
|
||||||
|
|
||||||
|
$this->assertForbidden($member, 'GET', '/api/provider/profile');
|
||||||
|
$this->assertForbidden($member, 'GET', '/api/provider/offers');
|
||||||
|
$this->assertForbidden($member, 'POST', '/api/provider/offers', $this->validOfferPayload());
|
||||||
|
$this->assertForbidden($member, 'POST', '/api/provider/schedules', [
|
||||||
|
'diving_offer_id' => $schedule->diving_offer_id,
|
||||||
|
'scheduled_date' => now()->addDays(10)->toDateString(),
|
||||||
|
'start_time' => '10:00',
|
||||||
|
'max_participants' => 5,
|
||||||
|
]);
|
||||||
|
$this->assertForbidden($member, 'GET', '/api/provider/bookings');
|
||||||
|
$this->assertForbidden($member, 'GET', '/api/provider/verification');
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('course_schedules', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_admin_is_rejected_on_provider_routes(): void
|
||||||
|
{
|
||||||
|
$admin = User::factory()->create(['role' => 'admin']);
|
||||||
|
|
||||||
|
$this->assertForbidden($admin, 'GET', '/api/provider/profile');
|
||||||
|
$this->assertForbidden($admin, 'GET', '/api/provider/offers');
|
||||||
|
$this->assertForbidden($admin, 'POST', '/api/provider/offers', $this->validOfferPayload());
|
||||||
|
$this->assertForbidden($admin, 'GET', '/api/provider/bookings');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_non_provider_offer_attempt_creates_no_offer(): void
|
||||||
|
{
|
||||||
|
$member = User::factory()->create(['role' => 'member']);
|
||||||
|
|
||||||
|
$this->assertForbidden($member, 'POST', '/api/provider/offers', $this->validOfferPayload());
|
||||||
|
|
||||||
|
// 越權建立的課程 provider_id 會是 member,出現在公開列表
|
||||||
|
$this->assertDatabaseCount('diving_offers', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_unauthenticated_request_is_rejected_on_provider_routes(): void
|
||||||
|
{
|
||||||
|
$this->getJson('/api/provider/profile')->assertStatus(401);
|
||||||
|
$this->getJson('/api/provider/offers')->assertStatus(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 跨角色共用路由 ───────────────────────────────────────────
|
||||||
|
|
||||||
|
public function test_member_and_provider_can_read_notifications(): void
|
||||||
|
{
|
||||||
|
$member = User::factory()->create(['role' => 'member']);
|
||||||
|
$provider = $this->createProvider();
|
||||||
|
|
||||||
|
$this->actingAs($member)->getJson('/api/notifications')->assertOk();
|
||||||
|
$this->actingAs($provider)->getJson('/api/notifications')->assertOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_member_and_provider_can_read_unread_message_counts(): void
|
||||||
|
{
|
||||||
|
$member = User::factory()->create(['role' => 'member']);
|
||||||
|
$provider = $this->createProvider();
|
||||||
|
|
||||||
|
$this->actingAs($member)->getJson('/api/bookings/messages/unread-counts')->assertOk();
|
||||||
|
$this->actingAs($provider)->getJson('/api/bookings/messages/unread-counts')->assertOk();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user