CI / Coding Standards (pull_request) Successful in 29s
CI / Tests (PHP 8.2) (pull_request) Successful in 31s
CI / Tests (PHP 8.3) (pull_request) Successful in 31s
CI / Tests (PHP 8.5) (pull_request) Successful in 31s
CI / No Debug Code (pull_request) Successful in 9s
CI / Tests (PHP 8.1) (pull_request) Successful in 44s
CI / Static Analysis (pull_request) Successful in 51s
CI / Build Plugin Zip (pull_request) Skipped
The "Payment due" notice the daily billing scan sends was fixed wording
baked into PaymentDueMailer. Studio admins can now view, edit and preview
its subject and body under Studio Settings -> Payment Due Email.
- PaymentDueEmailTemplate stores subject/body/item-line as us_payment_due_email_*
options, each falling back to the built-in default when blank, and renders
via {token} substitution.
- PaymentDueMailer now builds a token map from the same consolidated
items/credit/e-transfer/reference data and renders the stored template; the
default template reproduces the previous wording exactly.
- PaymentEmailController serves the view/edit/save/reset admin page
(manage_billing, nonce-checked) with a server-rendered sample preview.
- PaymentEmailPreviewEndpoint (POST us-scheduler/v1/payment-email/preview,
manage_billing) + payment-email-admin.js drive a live preview of unsaved edits.
- Submenu wired under Studio Settings; endpoint wired in RestRegistrar.
- Bump to 1.6.0 and add CHANGELOG entry. New options default gracefully, so
existing sites need no migration and send the identical notice until edited.
Co-authored-by: anthropic/claude-opus-4-8
145 lines
5.2 KiB
PHP
145 lines
5.2 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Unsupervised\Schedular\Payment;
|
|
|
|
use Unsupervised\Schedular\Auth\RoleManager;
|
|
use Unsupervised\Schedular\Val;
|
|
|
|
/**
|
|
* Admin screen for viewing and editing the payment-due email template, with a
|
|
* live preview rendered from sample values. Save is a plain POST (nonce +
|
|
* capability checked); the preview updates client-side against a REST endpoint
|
|
* so an admin sees the effect of an edit before saving it.
|
|
*/
|
|
class PaymentEmailController {
|
|
|
|
public const NONCE_ACTION = 'usc_payment_email_action';
|
|
|
|
public function __construct( private PaymentDueEmailTemplate $template = new PaymentDueEmailTemplate() ) {}
|
|
|
|
public function renderPage(): void {
|
|
if ( ! current_user_can( RoleManager::CAP_MANAGE_BILLING ) ) {
|
|
wp_die( esc_html__( 'You do not have permission to manage billing settings.', 'unsupervised-schedular' ) );
|
|
}
|
|
|
|
$notice = '';
|
|
if ( isset( $_POST['usc_action'] ) && check_admin_referer( self::NONCE_ACTION ) ) {
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified immediately above.
|
|
$action = sanitize_key( Val::string( wp_unslash( $_POST['usc_action'] ) ) );
|
|
if ( 'reset' === $action ) {
|
|
$this->reset();
|
|
$notice = __( 'Template reset to the built-in default.', 'unsupervised-schedular' );
|
|
} else {
|
|
$this->save();
|
|
$notice = __( 'Payment due email template saved.', 'unsupervised-schedular' );
|
|
}
|
|
}
|
|
|
|
$subject = $this->template->subject();
|
|
$body = $this->template->body();
|
|
$itemLine = $this->template->itemLine();
|
|
$tokens = PaymentDueEmailTemplate::tokens();
|
|
$itemTokens = PaymentDueEmailTemplate::itemTokens();
|
|
$previewNonce = wp_create_nonce( 'wp_rest' );
|
|
$previewUrl = rest_url( 'us-scheduler/v1/payment-email/preview' );
|
|
|
|
// Server-render the initial preview from sample values so the panel is
|
|
// populated before any JavaScript runs (and if it never does).
|
|
$preview = self::renderSample( $this->template, $subject, $body, $itemLine );
|
|
|
|
include USC_PLUGIN_DIR . 'templates/admin/payment-email.php';
|
|
}
|
|
|
|
/**
|
|
* Render the given template text against a fixed set of sample values, using
|
|
* the real mailer so the preview matches a genuine scan exactly. The passed
|
|
* subject/body/item-line override the stored ones so an unsaved edit can be
|
|
* previewed.
|
|
*
|
|
* @return array{subject: string, body: string}
|
|
*/
|
|
public static function renderSample( PaymentDueEmailTemplate $stored, string $subject, string $body, string $itemLine ): array {
|
|
// A throwaway template returning the supplied (possibly unsaved) text.
|
|
$draft = new class( $subject, $body, $itemLine ) extends PaymentDueEmailTemplate {
|
|
public function __construct(
|
|
private string $draftSubject,
|
|
private string $draftBody,
|
|
private string $draftItemLine,
|
|
) {}
|
|
|
|
public function subject(): string {
|
|
return '' !== $this->draftSubject ? $this->draftSubject : self::defaultSubject();
|
|
}
|
|
|
|
public function body(): string {
|
|
return '' !== $this->draftBody ? $this->draftBody : self::defaultBody();
|
|
}
|
|
|
|
public function itemLine(): string {
|
|
return '' !== $this->draftItemLine ? $this->draftItemLine : self::defaultItemLine();
|
|
}
|
|
};
|
|
|
|
$mailer = new PaymentDueMailer( $draft );
|
|
$tokens = $mailer->buildTokens( self::sampleStudentName(), self::sampleItems(), self::sampleReference(), self::sampleCredit() );
|
|
|
|
return [
|
|
'subject' => $draft->renderSubject( $tokens ),
|
|
'body' => $draft->renderBody( $tokens ),
|
|
];
|
|
}
|
|
|
|
public static function sampleStudentName(): string {
|
|
return __( 'Alex Student', 'unsupervised-schedular' );
|
|
}
|
|
|
|
public static function sampleReference(): string {
|
|
return 'REF12345';
|
|
}
|
|
|
|
public static function sampleCredit(): float {
|
|
return 20.0;
|
|
}
|
|
|
|
/**
|
|
* The sample charges the preview is rendered against — two lessons on
|
|
* different dates so the {items} block and grand total are both exercised.
|
|
*
|
|
* @return list<array{label: string, amount: float, currency: string, due_date: ?string, etransfer_email: ?string}>
|
|
*/
|
|
public static function sampleItems(): array {
|
|
return [
|
|
[
|
|
'label' => __( 'Piano lesson', 'unsupervised-schedular' ),
|
|
'amount' => 35.0,
|
|
'currency' => 'CAD',
|
|
'due_date' => '2026-07-15',
|
|
'etransfer_email' => '[email protected]',
|
|
],
|
|
[
|
|
'label' => __( 'Guitar lesson', 'unsupervised-schedular' ),
|
|
'amount' => 40.0,
|
|
'currency' => 'CAD',
|
|
'due_date' => '2026-07-22',
|
|
'etransfer_email' => '[email protected]',
|
|
],
|
|
];
|
|
}
|
|
|
|
private function save(): void {
|
|
// Nonce is verified by the caller (renderPage) before this method runs.
|
|
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
|
$this->template->saveSubject( sanitize_text_field( Val::string( wp_unslash( $_POST['subject'] ?? '' ) ) ) );
|
|
$this->template->saveBody( sanitize_textarea_field( Val::string( wp_unslash( $_POST['body'] ?? '' ) ) ) );
|
|
$this->template->saveItemLine( sanitize_text_field( Val::string( wp_unslash( $_POST['item_line'] ?? '' ) ) ) );
|
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
|
}
|
|
|
|
private function reset(): void {
|
|
$this->template->saveSubject( '' );
|
|
$this->template->saveBody( '' );
|
|
$this->template->saveItemLine( '' );
|
|
}
|
|
}
|