Add e-transfer destination email (studio default + offering/booking overrides)
CI / No Debug Code (pull_request) Successful in 3s
CI / Coding Standards (pull_request) Successful in 46s
CI / Tests (PHP 8.1) (pull_request) Successful in 52s
CI / Tests (PHP 8.3) (pull_request) Successful in 52s
CI / Tests (PHP 8.2) (pull_request) Successful in 57s
CI / PHPStan (pull_request) Successful in 1m12s
CI / Build Plugin Zip (pull_request) Has been skipped

The e-transfer destination is resolved at booking time (offering override ->
studio default) and frozen onto the payment, so each record keeps where the
student was directed. It can then be corrected per booking.

- StudioSettings: us_etransfer_email option + a Default e-transfer email field
  on the Studio Settings page.
- Offering: etransfer_email column/field (instructor override) across VO, repo,
  REST endpoint, admin controller, and form.
- Payment: etransfer_email column on the payment (frozen record) +
  PaymentRepository::updateEtransferEmail; PaymentService freezes it from the
  offering override or studio default at creation; booking/enrolment pass the
  offering override.
- My Lessons: instructors edit the e-transfer email per pending lesson payment
  (ownership-checked).
- Payments queue: studio admin can correct the email at confirmation (for when
  a student sends it to the wrong place).
- Docs updated.

Tests: Payment/Offering rows + PaymentService freezing. composer test (148),
cs, and PHPStan level 6 all pass.

Refs #7

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 10:47:06 -03:00
parent 6c4097b385
commit 9873cb5e30
25 changed files with 225 additions and 49 deletions
+3
View File
@@ -39,6 +39,7 @@ class Offering {
public readonly ?string $termStart = null,
public readonly ?string $termEnd = null,
public readonly ?string $scheduleNote = null,
public readonly ?string $etransferEmail = null,
public readonly bool $isActive = true,
public readonly ?int $id = null,
) {}
@@ -58,6 +59,7 @@ class Offering {
termStart: $row->term_start,
termEnd: $row->term_end,
scheduleNote: $row->schedule_note,
etransferEmail: $row->etransfer_email,
isActive: (bool) $row->is_active,
id: (int) $row->id,
);
@@ -84,6 +86,7 @@ class Offering {
'term_start' => $this->termStart,
'term_end' => $this->termEnd,
'schedule_note' => $this->scheduleNote,
'etransfer_email' => $this->etransferEmail,
'is_active' => $this->isActive,
];
}
+1
View File
@@ -77,6 +77,7 @@ class OfferingController {
allowWeekly: isset( $_POST['allow_weekly'] ),
capacity: $capacity > 0 ? $capacity : null,
scheduleNote: $this->nullableText( sanitize_text_field( wp_unslash( $_POST['schedule_note'] ?? '' ) ) ),
etransferEmail: $this->nullableText( sanitize_email( wp_unslash( $_POST['etransfer_email'] ?? '' ) ) ),
)
);
// phpcs:enable WordPress.Security.NonceVerification.Missing
+8
View File
@@ -95,6 +95,7 @@ class OfferingEndpoint {
termStart: $this->nullableText( $request->get_param( 'term_start' ) ),
termEnd: $this->nullableText( $request->get_param( 'term_end' ) ),
scheduleNote: $this->nullableText( $request->get_param( 'schedule_note' ) ),
etransferEmail: $this->nullableEmail( $request->get_param( 'etransfer_email' ) ),
isActive: null === $request->get_param( 'is_active' ) ? true : (bool) $request->get_param( 'is_active' ),
);
@@ -139,6 +140,7 @@ class OfferingEndpoint {
termStart: $request->has_param( 'term_start' ) ? $this->nullableText( $request->get_param( 'term_start' ) ) : $existing->termStart,
termEnd: $request->has_param( 'term_end' ) ? $this->nullableText( $request->get_param( 'term_end' ) ) : $existing->termEnd,
scheduleNote: $request->has_param( 'schedule_note' ) ? $this->nullableText( $request->get_param( 'schedule_note' ) ) : $existing->scheduleNote,
etransferEmail: $request->has_param( 'etransfer_email' ) ? $this->nullableEmail( $request->get_param( 'etransfer_email' ) ) : $existing->etransferEmail,
isActive: $request->has_param( 'is_active' ) ? (bool) $request->get_param( 'is_active' ) : $existing->isActive,
id: $id,
);
@@ -186,6 +188,12 @@ class OfferingEndpoint {
return max( 0.0, (float) $value );
}
private function nullableEmail( mixed $value ): ?string {
$email = sanitize_email( (string) $value );
return '' !== $email ? $email : null;
}
private function nullableInt( mixed $value ): ?int {
return ( null === $value || '' === $value ) ? null : (int) $value;
}
+3 -2
View File
@@ -14,11 +14,11 @@ class OfferingRepository {
/**
* Column formats aligned to {@see columns()} (instructor_id, kind, title,
* description, duration_minutes, price, currency, billing_mode, allow_weekly,
* capacity, term_start, term_end, schedule_note, is_active).
* capacity, term_start, term_end, schedule_note, etransfer_email, is_active).
*
* @var list<string>
*/
private const COLUMN_FORMATS = [ '%d', '%s', '%s', '%s', '%d', '%f', '%s', '%s', '%d', '%d', '%s', '%s', '%s', '%d' ];
private const COLUMN_FORMATS = [ '%d', '%s', '%s', '%s', '%d', '%f', '%s', '%s', '%d', '%d', '%s', '%s', '%s', '%s', '%d' ];
public function insert( Offering $offering ): int {
$this->db->insert(
@@ -60,6 +60,7 @@ class OfferingRepository {
'term_start' => $offering->termStart,
'term_end' => $offering->termEnd,
'schedule_note' => $offering->scheduleNote,
'etransfer_email' => $offering->etransferEmail,
'is_active' => $offering->isActive ? 1 : 0,
];
}