A parent registers once and manages lessons for one or more children, who need no login of their own. A child is a real wp_users row with the student role but no usable login — so student_id keeps meaning "a WordPress user" on every table, and booking, credits, policies and enrolments work unchanged. A us_guardians link table maps guardian to child. The signup form gains a parent/guardian tick that reveals a block per child, with the account-signup questions asked per child rather than per guardian — they describe the student, not the account holder. Signup policies are recorded once per child with the guardian as the acceptor, which is the record that actually means something. A family that half-creates is rolled back entirely rather than leaving a guardian who cannot re-register. The booking and enrolment forms gain a "Who is this for?" picker listing children first, so the default selection is never the parent — booking for the wrong child is correctable, quietly billing a parent for their kid's lesson is not. POST /bookings and POST /enrollments take an optional student_id honoured only for that child's guardian; anything else is a 403. That check is the authorisation boundary of the feature. Payments and credits gain a payer: the charge names the child it was for and the guardian who owes it, so per-child reporting is unchanged while notices, receipts and the payment step reach the parent. Credit is held by the payer, so one child's cancellation can settle a sibling's charge, and the daily billing scan sends a guardian one notice covering every child. Closes #132 Co-Authored-By: Claude Opus 5 <[email protected]>
271 lines
13 KiB
PHP
271 lines
13 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Unsupervised\Schedular;
|
|
|
|
class Schema {
|
|
|
|
/**
|
|
* Returns CREATE TABLE statements for dbDelta.
|
|
*
|
|
* @return list<string>
|
|
*/
|
|
public static function tables( string $prefix, string $charset ): array {
|
|
return [
|
|
"CREATE TABLE {$prefix}us_availability (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
instructor_id BIGINT UNSIGNED NOT NULL,
|
|
offering_id BIGINT UNSIGNED DEFAULT NULL,
|
|
start_dt DATETIME NOT NULL,
|
|
end_dt DATETIME NOT NULL,
|
|
duration_minutes SMALLINT UNSIGNED NOT NULL DEFAULT 60,
|
|
is_booked TINYINT(1) NOT NULL DEFAULT 0,
|
|
recurrence_group BIGINT UNSIGNED DEFAULT NULL,
|
|
created_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id),
|
|
KEY instructor_id (instructor_id),
|
|
KEY offering_id (offering_id),
|
|
KEY start_dt (start_dt),
|
|
KEY recurrence_group (recurrence_group)
|
|
) {$charset};",
|
|
|
|
"CREATE TABLE {$prefix}us_lessons (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
slot_id BIGINT UNSIGNED NOT NULL,
|
|
offering_id BIGINT UNSIGNED DEFAULT NULL,
|
|
student_id BIGINT UNSIGNED NOT NULL,
|
|
instructor_id BIGINT UNSIGNED NOT NULL,
|
|
recurrence VARCHAR(10) NOT NULL DEFAULT 'single',
|
|
series_id BIGINT UNSIGNED DEFAULT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
payment_id BIGINT UNSIGNED DEFAULT NULL,
|
|
notes TEXT,
|
|
created_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id),
|
|
KEY slot_id (slot_id),
|
|
KEY offering_id (offering_id),
|
|
KEY student_id (student_id),
|
|
KEY instructor_id (instructor_id),
|
|
KEY series_id (series_id)
|
|
) {$charset};",
|
|
|
|
"CREATE TABLE {$prefix}us_offerings (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
instructor_id BIGINT UNSIGNED NOT NULL,
|
|
kind VARCHAR(20) NOT NULL,
|
|
title VARCHAR(191) NOT NULL,
|
|
description TEXT,
|
|
duration_minutes SMALLINT UNSIGNED DEFAULT NULL,
|
|
price DECIMAL(10,2) NOT NULL DEFAULT 0,
|
|
currency VARCHAR(3) NOT NULL DEFAULT 'CAD',
|
|
billing_mode VARCHAR(20) NOT NULL DEFAULT 'one_time',
|
|
allow_weekly TINYINT(1) NOT NULL DEFAULT 0,
|
|
capacity SMALLINT UNSIGNED DEFAULT NULL,
|
|
term_start DATE DEFAULT NULL,
|
|
term_end DATE DEFAULT NULL,
|
|
class_time TIME DEFAULT NULL,
|
|
enrollment_deadline DATE DEFAULT NULL,
|
|
withdrawal_deadline DATE DEFAULT NULL,
|
|
schedule_note VARCHAR(191) DEFAULT NULL,
|
|
etransfer_email VARCHAR(191) DEFAULT NULL,
|
|
cancellation_cutoff_hours SMALLINT UNSIGNED DEFAULT NULL,
|
|
access_mode VARCHAR(20) NOT NULL DEFAULT 'public',
|
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
|
created_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id),
|
|
KEY instructor_id (instructor_id),
|
|
KEY kind (kind),
|
|
KEY is_active (is_active)
|
|
) {$charset};",
|
|
|
|
"CREATE TABLE {$prefix}us_questions (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
offering_id BIGINT UNSIGNED DEFAULT NULL,
|
|
scope VARCHAR(20) NOT NULL DEFAULT 'offering',
|
|
label VARCHAR(255) NOT NULL,
|
|
field_type VARCHAR(20) NOT NULL DEFAULT 'text',
|
|
options TEXT,
|
|
is_required TINYINT(1) NOT NULL DEFAULT 0,
|
|
sort_order INT NOT NULL DEFAULT 0,
|
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
|
created_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id),
|
|
KEY offering_id (offering_id),
|
|
KEY scope (scope),
|
|
KEY is_active (is_active)
|
|
) {$charset};",
|
|
|
|
"CREATE TABLE {$prefix}us_question_answers (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
question_id BIGINT UNSIGNED NOT NULL,
|
|
registration_type VARCHAR(20) NOT NULL,
|
|
registration_id BIGINT UNSIGNED NOT NULL,
|
|
student_id BIGINT UNSIGNED NOT NULL,
|
|
answer_value TEXT,
|
|
created_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id),
|
|
KEY question_id (question_id),
|
|
KEY registration (registration_type, registration_id),
|
|
KEY student_id (student_id)
|
|
) {$charset};",
|
|
|
|
"CREATE TABLE {$prefix}us_policies (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
title VARCHAR(191) NOT NULL,
|
|
slug VARCHAR(191) NOT NULL,
|
|
current_version_id BIGINT UNSIGNED DEFAULT NULL,
|
|
acceptance_scope VARCHAR(20) NOT NULL DEFAULT 'booking',
|
|
created_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id),
|
|
UNIQUE KEY slug (slug),
|
|
KEY acceptance_scope (acceptance_scope)
|
|
) {$charset};",
|
|
|
|
"CREATE TABLE {$prefix}us_policy_versions (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
policy_id BIGINT UNSIGNED NOT NULL,
|
|
version_number INT NOT NULL DEFAULT 1,
|
|
body LONGTEXT,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'draft',
|
|
published_at DATETIME DEFAULT NULL,
|
|
created_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id),
|
|
KEY policy_id (policy_id),
|
|
KEY status (status)
|
|
) {$charset};",
|
|
|
|
"CREATE TABLE {$prefix}us_policy_acceptances (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
policy_version_id BIGINT UNSIGNED NOT NULL,
|
|
student_id BIGINT UNSIGNED NOT NULL,
|
|
accepted_by BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
|
registration_type VARCHAR(20) NOT NULL,
|
|
registration_id BIGINT UNSIGNED NOT NULL,
|
|
accepted_at DATETIME NOT NULL,
|
|
ip_address VARCHAR(45) DEFAULT NULL,
|
|
PRIMARY KEY (id),
|
|
KEY policy_version_id (policy_version_id),
|
|
KEY student_id (student_id),
|
|
KEY accepted_by (accepted_by),
|
|
KEY registration (registration_type, registration_id)
|
|
) {$charset};",
|
|
|
|
"CREATE TABLE {$prefix}us_payments (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
student_id BIGINT UNSIGNED NOT NULL,
|
|
payer_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
|
instructor_id BIGINT UNSIGNED NOT NULL,
|
|
registration_type VARCHAR(20) NOT NULL,
|
|
registration_id BIGINT UNSIGNED NOT NULL,
|
|
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
|
|
currency VARCHAR(3) NOT NULL DEFAULT 'CAD',
|
|
method VARCHAR(20) NOT NULL DEFAULT 'etransfer',
|
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
tax_rate DECIMAL(5,2) NOT NULL DEFAULT 0,
|
|
tax_amount DECIMAL(10,2) NOT NULL DEFAULT 0,
|
|
credit_applied DECIMAL(10,2) NOT NULL DEFAULT 0,
|
|
due_date DATE DEFAULT NULL,
|
|
period_key VARCHAR(20) DEFAULT NULL,
|
|
notice_batch VARCHAR(32) DEFAULT NULL,
|
|
etransfer_email VARCHAR(191) DEFAULT NULL,
|
|
stripe_payment_intent_id VARCHAR(255) DEFAULT NULL,
|
|
receipt_number VARCHAR(50) DEFAULT NULL,
|
|
receipt_sent_at DATETIME DEFAULT NULL,
|
|
created_at DATETIME NOT NULL,
|
|
paid_at DATETIME DEFAULT NULL,
|
|
PRIMARY KEY (id),
|
|
KEY student_id (student_id),
|
|
KEY payer_id (payer_id),
|
|
KEY instructor_id (instructor_id),
|
|
KEY registration (registration_type, registration_id),
|
|
KEY status (status)
|
|
) {$charset};",
|
|
|
|
"CREATE TABLE {$prefix}us_credits (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
student_id BIGINT UNSIGNED NOT NULL,
|
|
payer_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
|
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
|
|
remaining DECIMAL(10,2) NOT NULL DEFAULT 0,
|
|
currency VARCHAR(3) NOT NULL DEFAULT 'CAD',
|
|
source_payment_id BIGINT UNSIGNED DEFAULT NULL,
|
|
source_lesson_id BIGINT UNSIGNED DEFAULT NULL,
|
|
reason VARCHAR(191) DEFAULT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'available',
|
|
created_at DATETIME NOT NULL,
|
|
updated_at DATETIME DEFAULT NULL,
|
|
PRIMARY KEY (id),
|
|
KEY student_id (student_id),
|
|
KEY payer_id (payer_id),
|
|
KEY status (status),
|
|
KEY source_lesson_id (source_lesson_id)
|
|
) {$charset};",
|
|
|
|
"CREATE TABLE {$prefix}us_group_enrollments (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
offering_id BIGINT UNSIGNED NOT NULL,
|
|
student_id BIGINT UNSIGNED NOT NULL,
|
|
instructor_id BIGINT UNSIGNED NOT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
payment_id BIGINT UNSIGNED DEFAULT NULL,
|
|
enrolled_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id),
|
|
KEY offering_id (offering_id),
|
|
KEY student_id (student_id),
|
|
KEY instructor_id (instructor_id),
|
|
KEY status (status)
|
|
) {$charset};",
|
|
|
|
"CREATE TABLE {$prefix}us_invites (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
email VARCHAR(191) NOT NULL,
|
|
token VARCHAR(64) NOT NULL,
|
|
role VARCHAR(32) NOT NULL DEFAULT 'us_student',
|
|
kind VARCHAR(10) NOT NULL DEFAULT 'personal',
|
|
offering_id BIGINT UNSIGNED DEFAULT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
invited_by BIGINT UNSIGNED DEFAULT NULL,
|
|
accepted_user_id BIGINT UNSIGNED DEFAULT NULL,
|
|
created_at DATETIME NOT NULL,
|
|
accepted_at DATETIME DEFAULT NULL,
|
|
expires_at DATETIME DEFAULT NULL,
|
|
PRIMARY KEY (id),
|
|
UNIQUE KEY token (token),
|
|
KEY email (email),
|
|
KEY status (status)
|
|
) {$charset};",
|
|
|
|
// Links a parent/guardian account to a child who books through it. The
|
|
// child is a real (login-less) wp_users row, so student_id keeps meaning
|
|
// "a WordPress user" on every other table.
|
|
"CREATE TABLE {$prefix}us_guardians (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
guardian_id BIGINT UNSIGNED NOT NULL,
|
|
student_id BIGINT UNSIGNED NOT NULL,
|
|
relationship VARCHAR(50) NOT NULL DEFAULT '',
|
|
created_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id),
|
|
UNIQUE KEY guardian_student (guardian_id, student_id),
|
|
KEY guardian_id (guardian_id),
|
|
KEY student_id (student_id)
|
|
) {$charset};",
|
|
|
|
"CREATE TABLE {$prefix}us_group_access (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
offering_id BIGINT UNSIGNED NOT NULL,
|
|
student_id BIGINT UNSIGNED DEFAULT NULL,
|
|
email VARCHAR(191) NOT NULL DEFAULT '',
|
|
invite_id BIGINT UNSIGNED DEFAULT NULL,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'invited',
|
|
invited_by BIGINT UNSIGNED DEFAULT NULL,
|
|
created_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id),
|
|
KEY offering_id (offering_id),
|
|
KEY student_id (student_id),
|
|
KEY email (email),
|
|
KEY status (status)
|
|
) {$charset};",
|
|
];
|
|
}
|
|
}
|