, coupon:, pause_offer, downgrade_offer, retry_charge. * Steps are defined in SequenceLibrary (build phase); here we scaffold the storage + scheduling. */ final class EnrollmentService { public static function table(): string { global $wpdb; return $wpdb->prefix . 'abovault_enrollments'; } public static function schema(): string { global $wpdb; $table = self::table(); $collate = $wpdb->get_charset_collate(); return "CREATE TABLE {$table} ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, subscription_id BIGINT UNSIGNED NOT NULL, sequence VARCHAR(50) NOT NULL, trigger_event VARCHAR(50) NOT NULL DEFAULT '', step_index SMALLINT UNSIGNED NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'active', next_run_at DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_due (status, next_run_at), KEY idx_sub (subscription_id) ) {$collate};"; } /** * Enroll — but never double-enroll into the same active sequence. */ public static function enroll(int $subscriptionId, string $sequence, string $event = ''): void { global $wpdb; $table = self::table(); $exists = $wpdb->get_var($wpdb->prepare( "SELECT id FROM {$table} WHERE subscription_id = %d AND sequence = %s AND status = 'active'", $subscriptionId, $sequence )); if ($exists) { return; } $wpdb->insert($table, [ 'subscription_id' => $subscriptionId, 'sequence' => $sequence, 'trigger_event' => $event, 'step_index' => 0, 'status' => 'active', 'next_run_at' => current_time('mysql', true), // first step ASAP ]); do_action('abovault_enrolled', $subscriptionId, $sequence, $event); } }