sanitizeMonth( isset( $_GET['month'] ) ? sanitize_text_field( wp_unslash( $_GET['month'] ) ) : '' ); $instructorId = isset( $_GET['instructor_id'] ) ? absint( $_GET['instructor_id'] ) : 0; // phpcs:enable WordPress.Security.NonceVerification.Recommended $instructorId = $this->scopeInstructor( $instructorId ); $report = $this->buildReport( $month, $instructorId ); $canExport = current_user_can( RoleManager::CAP_EXPORT_PAYMENTS ); $canFilter = current_user_can( RoleManager::CAP_VIEW_ALL_PAYMENTS ); $exportUrl = wp_nonce_url( admin_url( 'admin-post.php?action=' . self::EXPORT_ACTION . '&month=' . rawurlencode( $month ) . '&instructor_id=' . $instructorId ), self::EXPORT_ACTION ); $instructors = $canFilter ? get_users( [ 'role' => RoleManager::INSTRUCTOR, 'fields' => [ 'ID', 'display_name' ], ] ) : []; include USC_PLUGIN_DIR . 'templates/admin/payment-report.php'; } /** * Stream the report as a CSV download (admin_post handler). */ public function export(): void { if ( ! current_user_can( RoleManager::CAP_EXPORT_PAYMENTS ) ) { wp_die( esc_html__( 'You do not have permission to export payments.', 'unsupervised-schedular' ) ); } check_admin_referer( self::EXPORT_ACTION ); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- nonce checked above. $month = $this->sanitizeMonth( isset( $_GET['month'] ) ? sanitize_text_field( wp_unslash( $_GET['month'] ) ) : '' ); $instructorId = isset( $_GET['instructor_id'] ) ? absint( $_GET['instructor_id'] ) : 0; // phpcs:enable WordPress.Security.NonceVerification.Recommended $instructorId = $this->scopeInstructor( $instructorId ); $report = $this->buildReport( $month, $instructorId ); $filename = 'payments-' . $month . '.csv'; header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); echo $report->toCsv(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV body, not HTML. exit; } /** * Restrict the requested instructor to the current user when they may only * see their own payments. */ private function scopeInstructor( int $instructorId ): int { if ( ! current_user_can( RoleManager::CAP_VIEW_ALL_PAYMENTS ) ) { return get_current_user_id(); } return $instructorId; } /** * Build a report of paid payments for the given `Y-m` month, optionally for * one instructor. */ private function buildReport( string $month, int $instructorId ): PaymentReport { $start = $month . '-01 00:00:00'; $end = gmdate( 'Y-m-d H:i:s', strtotime( $month . '-01 00:00:00 +1 month' ) ); $rows = array_map( static function ( Payment $payment ): array { $student = get_userdata( $payment->studentId ); $instructor = get_userdata( $payment->instructorId ); return [ 'date' => substr( (string) $payment->paidAt, 0, 10 ), 'student' => $student ? $student->display_name : (string) $payment->studentId, 'instructor' => $instructor ? $instructor->display_name : (string) $payment->instructorId, 'method' => $payment->method, 'status' => $payment->status, 'amount' => (float) $payment->amount, 'tax_rate' => (float) $payment->taxRate, 'tax_amount' => (float) $payment->taxAmount, 'total' => $payment->total(), ]; }, $this->payments->findPaidBetween( $start, $end, $instructorId ) ); return new PaymentReport( $rows ); } /** * Validate a `Y-m` month string, defaulting to the current month. */ private function sanitizeMonth( string $month ): string { if ( 1 === preg_match( '/^\d{4}-\d{2}$/', $month ) ) { return $month; } return gmdate( 'Y-m' ); } }