CI / Tests (PHP 8.1) (pull_request) Successful in 47s
CI / Tests (PHP 8.2) (pull_request) Successful in 47s
CI / PHPStan (pull_request) Successful in 3m12s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m42s
CI / Build Plugin Zip (pull_request) Skipped
CI / No Debug Code (pull_request) Successful in 3s
CI / Coding Standards (pull_request) Successful in 2m52s
Cancelling a lesson that was already paid for now credits the student that money instead of leaving it as a manual refund, and the daily scheduled-billing scan applies any available credit against their due charges before emailing the notice. - New us_credits ledger + us_payments.credit_applied column (Payment::netDue). - PaymentService::creditForCancelledLesson issues a per-lesson share of the covering payment's total; wired into all three cancel paths (student self-cancel, instructor status update, admin student-detail cancel). - PaymentService::applyCredits draws credit down FIFO across a run's charges, marking a fully-covered charge paid-by-credit; the notice shows the credit applied and reduced total, and the admin queue shows net due. - Student detail page shows a student's credit balance and history. Ships as part of the unreleased 1.2.0 (same release as scheduled billing). Tests: composer test (585), composer lint, composer cs all pass. Co-Authored-By: Claude Opus 4.8 <[email protected]>
94 lines
3.4 KiB
PHP
94 lines
3.4 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Unsupervised\Schedular\Payment;
|
|
|
|
use Unsupervised\Schedular\Auth\RoleManager;
|
|
use Unsupervised\Schedular\Val;
|
|
|
|
class PaymentController {
|
|
|
|
public function __construct(
|
|
private PaymentRepository $payments,
|
|
private PaymentService $service,
|
|
) {}
|
|
|
|
public function renderPage(): void {
|
|
if ( ! current_user_can( RoleManager::CAP_MANAGE_BILLING ) ) {
|
|
wp_die( esc_html__( 'You do not have permission to manage payments.', 'unsupervised-schedular' ) );
|
|
}
|
|
|
|
if ( isset( $_POST['usc_action'] ) && check_admin_referer( 'usc_payment_action' ) ) {
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce checked above.
|
|
if ( 'mark_paid' === sanitize_key( Val::string( wp_unslash( $_POST['usc_action'] ) ) ) ) {
|
|
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
|
$paymentId = absint( Val::int( $_POST['payment_id'] ?? 0 ) );
|
|
$email = sanitize_email( Val::string( wp_unslash( $_POST['etransfer_email'] ?? '' ) ) );
|
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
|
if ( $paymentId > 0 ) {
|
|
// Record the destination it was actually sent to before confirming.
|
|
$this->payments->updateEtransferEmail( $paymentId, '' !== $email ? $email : null );
|
|
$this->service->markPaid( $paymentId );
|
|
}
|
|
}
|
|
}
|
|
|
|
$groups = $this->groupPending( $this->payments->findPending() );
|
|
|
|
include USC_PLUGIN_DIR . 'templates/admin/payments.php';
|
|
}
|
|
|
|
/**
|
|
* Group pending payments by their shared notice batch, so payments the daily
|
|
* scan emailed a student together (and which a single lump-sum e-transfer
|
|
* covers) are shown as one group with a combined total. Payments with no batch
|
|
* — legacy at-registration e-transfers — are each their own single-item group.
|
|
*
|
|
* @param list<Payment> $pending
|
|
* @return list<array{reference: string, is_group: bool, total: string, rows: list<array{id: int, student: string, amount: string, method: string, for: string, etransfer_email: string}>}>
|
|
*/
|
|
private function groupPending( array $pending ): array {
|
|
$groups = [];
|
|
|
|
foreach ( $pending as $payment ) {
|
|
$batch = (string) $payment->noticeBatch;
|
|
$key = '' !== $batch ? 'b:' . $batch : 's:' . (string) $payment->id;
|
|
|
|
if ( ! isset( $groups[ $key ] ) ) {
|
|
$groups[ $key ] = [
|
|
'reference' => $batch,
|
|
'currency' => $payment->currency,
|
|
'total_raw' => 0.0,
|
|
'rows' => [],
|
|
];
|
|
}
|
|
|
|
$student = get_userdata( $payment->studentId );
|
|
|
|
// Show what the student still owes — the amount less any account credit
|
|
// already applied to this payment.
|
|
$groups[ $key ]['total_raw'] += $payment->netDue();
|
|
$groups[ $key ]['rows'][] = [
|
|
'id' => (int) $payment->id,
|
|
'student' => $student ? $student->display_name : (string) $payment->studentId,
|
|
'amount' => number_format( $payment->netDue(), 2 ) . ' ' . $payment->currency,
|
|
'method' => $payment->method,
|
|
'for' => $payment->registrationType . ' #' . $payment->registrationId,
|
|
'etransfer_email' => (string) $payment->etransferEmail,
|
|
];
|
|
}
|
|
|
|
return array_values(
|
|
array_map(
|
|
static fn( array $group ): array => [
|
|
'reference' => $group['reference'],
|
|
'is_group' => count( $group['rows'] ) > 1,
|
|
'total' => number_format( $group['total_raw'], 2 ) . ' ' . $group['currency'],
|
|
'rows' => $group['rows'],
|
|
],
|
|
$groups
|
|
)
|
|
);
|
|
}
|
|
}
|