| @@ -38,60 +38,91 @@ | ||
| 38 | 38 | * Register REST API routes |
| 39 | 39 | */ |
| 40 | 40 | public function register_routes(): void |
| 41 | 41 | { |
| 42 | + // Payment stats — view cap (read-only aggregates). | |
| 42 | 43 | register_rest_route($this->namespace, '/payments/stats', [ |
| 43 | 44 | [ |
| 44 | 45 | 'methods' => 'GET', |
| 45 | 46 | 'callback' => [$this, 'getPaymentStats'], |
| 46 | - 'permission_callback' => [$this, 'checkAdminPermission'], | |
| 47 | + 'permission_callback' => [$this, 'checkCanView'], | |
| 47 | 48 | ], |
| 48 | 49 | ]); |
| 49 | 50 | |
| 50 | - // List all payments | |
| 51 | + // List + create payments. Create needs the edit-bookings cap | |
| 52 | + // because adding a payment mutates the booking's payment state. | |
| 51 | 53 | register_rest_route($this->namespace, '/payments', [ |
| 52 | 54 | [ |
| 53 | 55 | 'methods' => 'GET', |
| 54 | 56 | 'callback' => [$this, 'getPayments'], |
| 55 | - 'permission_callback' => [$this, 'checkAdminPermission'], | |
| 57 | + 'permission_callback' => [$this, 'checkCanView'], | |
| 56 | 58 | ], |
| 57 | 59 | [ |
| 58 | 60 | 'methods' => 'POST', |
| 59 | 61 | 'callback' => [$this, 'createPayment'], |
| 60 | - 'permission_callback' => [$this, 'checkAdminPermission'], | |
| 62 | + 'permission_callback' => [$this, 'checkCanEdit'], | |
| 61 | 63 | ] |
| 62 | 64 | ]); |
| 63 | 65 | |
| 64 | - // Get single payment | |
| 66 | + // Single-payment read / update / delete. Update + delete are | |
| 67 | + // refund-equivalent operations from the customer's perspective | |
| 68 | + // (changing the amount or removing a recorded payment can | |
| 69 | + // affect what the customer owes), so we gate them on the | |
| 70 | + // dedicated refund cap. Accountant role holds refund without | |
| 71 | + // holding edit-bookings, so they can issue refunds without | |
| 72 | + // also being able to edit the underlying booking. | |
| 65 | 73 | register_rest_route($this->namespace, '/payments/(?P<id>\d+)', [ |
| 66 | 74 | [ |
| 67 | 75 | 'methods' => 'GET', |
| 68 | 76 | 'callback' => [$this, 'getPayment'], |
| 69 | - 'permission_callback' => [$this, 'checkAdminPermission'], | |
| 77 | + 'permission_callback' => [$this, 'checkCanView'], | |
| 70 | 78 | ], |
| 71 | 79 | [ |
| 72 | 80 | 'methods' => 'PUT', |
| 73 | 81 | 'callback' => [$this, 'updatePayment'], |
| 74 | - 'permission_callback' => [$this, 'checkAdminPermission'], | |
| 82 | + 'permission_callback' => [$this, 'checkCanRefund'], | |
| 75 | 83 | ], |
| 76 | 84 | [ |
| 77 | 85 | 'methods' => 'DELETE', |
| 78 | 86 | 'callback' => [$this, 'deletePayment'], |
| 79 | - 'permission_callback' => [$this, 'checkAdminPermission'], | |
| 87 | + 'permission_callback' => [$this, 'checkCanRefund'], | |
| 80 | 88 | ] |
| 81 | 89 | ]); |
| 82 | 90 | } |
| 83 | 91 | |
| 84 | 92 | /** |
| 85 | - * Check admin permission (align with bookings list — shop managers may only have yatra caps). | |
| 93 | + * Granular permission checks. WP administrators pass every cap | |
| 94 | + * via the Team module's admin-fallback filter, so an explicit | |
| 95 | + * `manage_options` check isn't needed at this layer. | |
| 86 | 96 | */ |
| 97 | + public function checkCanView(): bool | |
| 98 | + { | |
| 99 | + return current_user_can('yatra_view_bookings'); | |
| 100 | + } | |
| 101 | + | |
| 102 | + public function checkCanEdit(): bool | |
| 103 | + { | |
| 104 | + return current_user_can('yatra_edit_bookings'); | |
| 105 | + } | |
| 106 | + | |
| 107 | + public function checkCanRefund(): bool | |
| 108 | + { | |
| 109 | + // Refund cap is high-sensitivity. Held by Owner + Manager + | |
| 110 | + // Accountant by default. Sales Agent / Front Desk / Guide | |
| 111 | + // can record payments via the create endpoint above but | |
| 112 | + // cannot modify or delete existing ones. | |
| 113 | + return current_user_can('yatra_refund_bookings'); | |
| 114 | + } | |
| 115 | + | |
| 116 | + /** | |
| 117 | + * @deprecated Kept for any external code referencing the old | |
| 118 | + * method name. Routes to view — safer than the old "view OR | |
| 119 | + * manage_options" shorthand, and admin users still pass via | |
| 120 | + * the admin-fallback layer. | |
| 121 | + */ | |
| 87 | 122 | public function checkAdminPermission(): bool |
| 88 | 123 | { |
| 89 | - if (current_user_can('yatra_view_bookings')) { | |
| 90 | - return true; | |
| 91 | - } | |
| 92 | - | |
| 93 | - return current_user_can('manage_options'); | |
| 124 | + return $this->checkCanView(); | |
| 94 | 125 | } |
| 95 | 126 | |
| 96 | 127 | /** |
| 97 | 128 | * GET /payments/stats - Counts per status for admin toolbar |
| @@ -116,8 +147,12 @@ | ||
| 116 | 147 | 'gateway' => $request->get_param('gateway') ?: '', |
| 117 | 148 | 'search' => $request->get_param('search') ?: '', |
| 118 | 149 | 'date_from' => $request->get_param('date_from') ?: '', |
| 119 | 150 | 'date_to' => $request->get_param('date_to') ?: '', |
| 151 | + // Column sorting from the table headers. Both are validated against a | |
| 152 | + // whitelist in the repository — never interpolated raw into SQL. | |
| 153 | + 'orderby' => $request->get_param('orderby') ?: '', | |
| 154 | + 'order' => $request->get_param('order') ?: '', | |
| 120 | 155 | ]; |
| 121 | 156 | |
| 122 | 157 | $result = $this->paymentService->getPayments($filters); |
| 123 | 158 | |
| @@ -153,8 +188,16 @@ | ||
| 153 | 188 | $data = $request->get_json_params(); |
| 154 | 189 | |
| 155 | 190 | try { |
| 156 | 191 | $payment = $this->paymentService->createPayment($data); |
| 192 | + | |
| 193 | + // The service reports validation failures (unknown booking, invalid | |
| 194 | + // status) as ['success' => false]; answering 201 made the admin form | |
| 195 | + // redirect as if the payment had been saved. | |
| 196 | + if (is_array($payment) && isset($payment['success']) && !$payment['success']) { | |
| 197 | + return new WP_REST_Response($payment, 400); | |
| 198 | + } | |
| 199 | + | |
| 157 | 200 | return new WP_REST_Response($payment, 201); |
| 158 | 201 | } catch (\Exception $e) { |
| 159 | 202 | return new WP_Error('payment_creation_failed', $e->getMessage(), ['status' => 400]); |
| 160 | 203 | } |
| @@ -172,8 +215,12 @@ | ||
| 172 | 215 | $payment = $this->paymentService->updatePayment($id, $data); |
| 173 | 216 | |
| 174 | 217 | if (!$payment) { |
| 175 | 218 | return new WP_Error('payment_not_found', 'Payment not found', ['status' => 404]); |
| 219 | + } | |
| 220 | + | |
| 221 | + if (is_array($payment) && isset($payment['success']) && !$payment['success']) { | |
| 222 | + return new WP_REST_Response($payment, 400); | |
| 176 | 223 | } |
| 177 | 224 | |
| 178 | 225 | return new WP_REST_Response($payment, 200); |
| 179 | 226 | } catch (\Exception $e) { |