From c4aa5cdb5e90ad4b420e22772797d16980232a2b Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Wed, 15 Apr 2026 18:38:55 +0000
Subject: [PATCH] =Updating custom tables to utilize CustomTable.php

---
 inc/managers/CustomTable.php                                                         |  226 +
 inc/managers/SEO/render/SchemaOutput.php                                             |  169 
 inc/managers/SEO/render/Traits/_Properties/dayOfWeekTrait.php                        |   13 
 jvb.php                                                                              |    2 
 inc/managers/SEO/render/Thing/Intangible/StructuredValue/PriceSpecification.php      |    3 
 inc/managers/SEO/render/Thing/Action/InteractAction.php                              |   24 
 inc/rest/routes/TermRoutes.php                                                       |   17 
 inc/managers/SEO/render/Thing/CreativeWork/Comment/_setup.php                        |    4 
 inc/registrar/config/seo/Resolver.php                                                |   29 
 inc/managers/Notifications/_setup.php                                                |    0 
 inc/registrar/_setup.php                                                             |    3 
 inc/managers/_setup.php                                                              |    5 
 inc/managers/FavouritesManager.php                                                   |  576 +++
 inc/managers/SEO/render/Traits/_Properties/aggregateRatingTrait.php                  |    7 
 JVBase.php                                                                           |    7 
 inc/managers/SEO/render/DataType/Time.php                                            |   13 
 inc/managers/SEO/render/Traits/_Helpers/enumerationHelper.php                        |   18 
 inc/managers/SEO/render/Traits/_Properties/closesTrait.php                           |    5 
 inc/managers/SEO/render/Traits/_Properties/upvoteCountTrait.php                      |   21 
 inc/managers/EmailManager.php                                                        |   35 
 inc/managers/SEO/render/DataType/Date.php                                            |    6 
 inc/ui/CRUDSkeleton.php                                                              |    2 
 inc/admin/SEOAdmin.php                                                               |    3 
 inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/CollectionPage.php |   15 
 inc/managers/SEO/render/Traits/_Properties/additionalPropertyTrait.php               |   13 
 inc/managers/SEO/render/Traits/_Properties/heightTrait.php                           |    8 
 inc/managers/SEO/render/Traits/_Properties/widthTrait.php                            |    8 
 inc/managers/SEO/render/Thing/CreativeWork/Comment/Question.php                      |   16 
 inc/managers/SEO/render/Thing/Intangible/ItemList/_setup.php                         |    2 
 inc/managers/SEO/render/Traits/_Properties/geoTrait.php                              |   10 
 inc/managers/SEO/render/Traits/_Properties/targetTrait.php                           |   23 
 activate.php                                                                         |    9 
 inc/blocks/_setup.php                                                                |    5 
 inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/ImageGallery.php   |   13 
 inc/managers/SEO/render/Thing/CreativeWork/Comment/Comment.php                       |   17 
 inc/registrar/Posts.php                                                              |  117 
 inc/managers/SEO/render/Thing/Intangible/StructuredValue/_setup.php                  |    1 
 inc/managers/SEO/render/Traits/_Properties/downvoteCountTrait.php                    |   21 
 inc/managers/SEO/render/Traits/_Properties/logoTrait.php                             |    3 
 inc/managers/SEO/render/Thing/CreativeWork/_setup.php                                |    1 
 inc/registrar/helpers/MakeVerification.php                                           |   19 
 inc/managers/SEO/render/Thing/Action/Action.php                                      |   30 
 inc/managers/SEO/render/Thing/Intangible/_setup.php                                  |    1 
 inc/managers/SEO/render/Traits/_Properties/parentItemTrait.php                       |   32 
 inc/managers/SEO/render/Traits/_Properties/imageTrait.php                            |   11 
 inc/managers/SEO/render/Thing/CreativeWork/MediaObject/MediaObject.php               |    7 
 inc/managers/SEO/render/Traits/_Properties/acceptedAnswerTrait.php                   |   24 
 inc/managers/ErrorHandler.php                                                        |   38 
 inc/registrar/fields/Field.php                                                       |   16 
 inc/managers/ApprovalManager.php                                                     |  432 ++
 inc/managers/SEO/render/Thing/Intangible/ItemList/OfferCatalog.php                   |    2 
 inc/registrar/helpers/HideSingle.php                                                 |  103 
 inc/managers/SEO/render/Thing/Action/CommunicateAction.php                           |   14 
 inc/registrar/config/SEO.php                                                         |    2 
 inc/managers/SEO/render/Traits/_Properties/suggestedAnswerTrait.php                  |   24 
 inc/registrar/config/Breadcrumbs.php                                                 |    2 
 inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/MediaGallery.php   |   15 
 inc/managers/VerifyEntryManager.php                                                  |  163 
 inc/managers/InvitationsManager.php                                                  |   59 
 inc/rest/routes/VoteRoutes.php                                                       |  216 -
 inc/helpers/terms.php                                                                |   16 
 inc/managers/ForumManager.php                                                        |   99 
 inc/managers/SEO/render/Traits/_Helpers/arrayHelper.php                              |   18 
 inc/managers/Notifications/Preferences.php                                           |  124 
 inc/managers/Notifications/Notifications.php                                         |  470 ++
 inc/managers/SEO/render/Thing/Intangible/StructuredValue/InteractionCounter.php      |    7 
 inc/managers/SEO/render/Thing/CreativeWork/Comment/Answer.php                        |   14 
 inc/registrar/helpers/MakeTimelineType.php                                           |   47 
 inc/managers/RoleManager.php                                                         |   41 
 inc/managers/SEO/render/Thing/_setup.php                                             |    3 
 inc/registrar/config/seo/Schema.php                                                  |  266 +
 inc/managers/SEO/render/Traits/_Properties/mainEntityTrait.php                       |   13 
 inc/blocks/TimelineBlock.php                                                         |    2 
 inc/managers/SEO/render/Thing/Intangible/StructuredValue/UnitPriceSpecification.php  |   14 
 inc/managers/TaxonomyRelationships.php                                               |  360 -
 inc/managers/NotificationManager.php                                                 | 1799 ----------
 inc/managers/Notifications/EmailDigests.php                                          |  391 ++
 inc/managers/SEO/render/Traits/_Properties/_setup.php                                |    9 
 inc/registrar/fields/UploadField.php                                                 |   12 
 inc/helpers/all.php                                                                  |    9 
 inc/managers/SEO/render/Traits/_Properties/founderTrait.php                          |    1 
 inc/registry/CheckCustomTables.php                                                   | 1250 +++---
 inc/managers/SEO/BreadcrumbManager.php                                               |   52 
 inc/managers/SEO/render/Thing/Action/_setup.php                                      |    9 
 inc/managers/KarmaManager.php                                                        |  342 ++
 inc/registrar/Registrar.php                                                          |  157 
 inc/managers/SEO/render/Thing/CreativeWork/WebPage/_setup.php                        |    4 
 inc/managers/UserTermsManager.php                                                    |  643 +--
 inc/managers/SEO/render/Thing/Intangible/Enumeration/DayOfWeek.php                   |   19 
 inc/managers/SEO/render/Traits/_Properties/interactionTypeTrait.php                  |    2 
 inc/rest/routes/FavouritesRoutes.php                                                 |    8 
 inc/managers/SEO/render/Traits/_Properties/opensTrait.php                            |    5 
 inc/managers/ReferralManager.php                                                     |  325 +
 inc/managers/SEO/render/Thing/Action/ScheduleAction.php                              |   10 
 inc/managers/SEO/render/Traits/_Properties/potentialActionTrait.php                  |   14 
 inc/managers/SEO/render/Traits/_Properties/scheduledTimeTrait.php                    |   26 
 inc/managers/ResponseManager.php                                                     |   64 
 inc/managers/SEO/render/Traits/_Properties/answerCountTrait.php                      |   21 
 base/membership.php                                                                  |    4 
 inc/managers/queue/Queue.php                                                         |    4 
 inc/helpers/members.php                                                              |   30 
 inc/registrar/Fields.php                                                             |    3 
 base/seo.php                                                                         |   27 
 inc/managers/SEO/render/Traits/_Properties/addressTrait.php                          |    8 
 inc/managers/Notifications/Content.php                                               |  108 
 inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php        |   28 
 inc/registrar/helpers/MakeTrackChanges.php                                           |  121 
 inc/managers/SEO/render/Traits/_Properties/answerExplanationTrait.php                |   24 
 inc/managers/SEO/render/Thing/Action/PlanAction.php                                  |   13 
 inc/managers/SEO/render/Traits/ThingSchema.php                                       |   45 
 inc/managers/SEO/render/Thing/Action/OrganizeAction.php                              |   10 
 inc/integrations/GoogleMaps.php                                                      |    3 
 /dev/null                                                                            |   21 
 inc/managers/SEO/render/Traits/_Properties/sharedContentTrait.php                    |   27 
 inc/managers/SEO/render/Traits/_Helpers/_setup.php                                   |    1 
 inc/managers/SEO/render/Traits/_Properties/hasOfferCatalogTrait.php                  |   30 
 116 files changed, 6,190 insertions(+), 3,663 deletions(-)

diff --git a/JVBase.php b/JVBase.php
index 255ff5f..a4efe54 100644
--- a/JVBase.php
+++ b/JVBase.php
@@ -20,6 +20,7 @@
 use JVBase\admin\SEOAdmin;
 use JVBase\managers\AdminPages;
 use JVBase\managers\NotificationManager;
+use JVBase\managers\TaxonomyRelationships;
 use JVBase\managers\UserTermsManager;
 use JVBase\registrar\Registrar;
 use JVBase\rest\routes\FeedRoutes;
@@ -106,6 +107,7 @@
 //			'uploads'	=> new UploadManager(),
 			'userTerms' => new UserTermsManager(),
 			'email' 	=> new EmailManager(),
+			'terms'		=> new TaxonomyRelationships(),
 		];
 
 		$this->routes = [
@@ -387,4 +389,9 @@
 	{
 		return $this->managers['schemaHelper'];
 	}
+
+	public function termRelationships():TaxonomyRelationships
+	{
+		return $this->managers['terms'];
+	}
 }
diff --git a/activate.php b/activate.php
index a87076f..12625f6 100644
--- a/activate.php
+++ b/activate.php
@@ -4,6 +4,8 @@
 use JVBase\managers\Cache;
 use JVBase\managers\CustomTable;
 use JVBase\managers\DirectoryManager;
+use JVBase\managers\ErrorHandler;
+use JVBase\managers\InvitationsManager;
 use JVBase\managers\queue\Queue;
 use JVBase\managers\ReferralManager;
 use JVBase\managers\RoleManager;
@@ -17,7 +19,6 @@
 
 function jvbActivatePlugin():void
 {
-
 //	$validator = new JVBase\utility\Validator();
 //	$validation = $validator->validateAll();
 //	error_log('Validation result: '.print_r($validation, true));
@@ -160,16 +161,16 @@
             'time'              => '12:03am tomorrow',
         ],
         //NotificationManager.php
-        'jvb_notification_digest_daily' =>
+        BASE.'notification_digest_daily' =>
             [
                 'time'  => '8:08am tomorrow',
             ],
-        'jvb_notification_digest_weekly' =>
+        BASE.'notification_digest_weekly' =>
             [
                 'time'       => 'monday 6:07am',
                 'recurrence' => 'weekly',
             ],
-        'jvb_notification_digest_monthly' =>
+        BASE.'notification_digest_monthly' =>
             [
                 'time'       => '2025-05-05 9:00am',
                 'recurrence' => 'monthly',
diff --git a/base/membership.php b/base/membership.php
index 11752bc..2ed3fcb 100644
--- a/base/membership.php
+++ b/base/membership.php
@@ -27,5 +27,7 @@
  * ]
  */
 
-$membership = apply_filters('jvb_membership', []);
+$membership = apply_filters('jvb_membership', [
+	'can_invite' => [],
+]);
 define('JVB_MEMBERSHIP', $membership);
diff --git a/base/seo.php b/base/seo.php
index 41b285d..492e1a3 100644
--- a/base/seo.php
+++ b/base/seo.php
@@ -112,19 +112,37 @@
 					'type'	=> 'JVBase\managers\SEO\render\Thing\CreativeWork\WebSite',
 					'name' => get_bloginfo('name'),
 					'url' => get_home_url(),
-					'id' => get_home_url() . '#website',
+					'id' => get_home_url() . '/#website',
 					'description' => get_bloginfo('description'),
 					'inLanguage' => 'en-CA'
 				],
 				default => []
 			},
 			'archive' => [
-				'type'	=> 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage',
+				'type'	=> 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage',
 				'name'	=> '{{name}}'
 			],
 			default => [],
 		};
-		return apply_filters(BASE.ucfirst($type).ucfirst($format).'Default', $defaults);
+		error_log('Getting defaults for: '.BASE.ucfirst($type).ucfirst($format).'Default');
+		$result = apply_filters(BASE.ucfirst($type).ucfirst($format).'Default', $defaults);
+		if ($format === 'reference' && empty($result)) {
+			$full = self::getDefault($type, 'schema');
+			if (!empty($full)) {
+				$result = [
+					'type'	=> $full['type'],
+					'name'	=> $full['name'],
+					'description'=> $full['description']
+				];
+				$check = ['id', 'url'];
+				foreach ($check as $ch) {
+					if (array_key_exists($ch, $full)) {
+						$result[$ch] = $full[$ch];
+					}
+				}
+			}
+		}
+		return $result;
 	}
 
 	public static function updateHistory(string $type, string $format, array $newest):bool
@@ -257,8 +275,9 @@
 		unset($config['type']);
 		$class = new $className();
 
+
 		foreach ($config as $property => $value) {
-			if (is_array($value)) {
+			if (is_array($value) && array_key_exists('type', $value)) {
 				$value = self::classFromConfig($value, $meta);
 			}
 			$method = 'set'.ucfirst($property);
diff --git a/inc/admin/SEOAdmin.php b/inc/admin/SEOAdmin.php
index ec664d3..45e215f 100644
--- a/inc/admin/SEOAdmin.php
+++ b/inc/admin/SEOAdmin.php
@@ -2,7 +2,6 @@
 namespace JVBase\admin;
 
 use JVBase\meta\Form;
-use JVBase\meta\Sanitizer;
 use JVBase\registrar\Registrar;
 use JVBase\ui\Tabs;
 
@@ -29,7 +28,7 @@
 		'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\Webpage'		=> ' - - WebPage',
 		'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\AboutPage'	=> ' - - - About Page',
 		'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CheckoutPage'	=> ' - - - Checkout Page',
-		'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage'	=> ' - - - Collection Page',
+		'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage' => ' - - - Collection Page',
 		'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\ContactPage'		=> ' - - - Contact Page',
 		'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\FAQPage'			=> ' - - - FAQ Page',
 		'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\ItemPage'		=> ' - - - Item Page',
diff --git a/inc/blocks/TimelineBlock.php b/inc/blocks/TimelineBlock.php
index e2831d4..849ce91 100644
--- a/inc/blocks/TimelineBlock.php
+++ b/inc/blocks/TimelineBlock.php
@@ -48,7 +48,7 @@
     public function render(array $attributes, string $content, WP_Block $block)
     {
 		global $post;
-		$registrar = Registrar::getInstance($post->post_type));
+		$registrar = Registrar::getInstance($post->post_type);
 		if (!$post || !$registrar || !$registrar->hasFeature('is_timeline') ) {
 			return '';
 		}
diff --git a/inc/blocks/_setup.php b/inc/blocks/_setup.php
index a2ad8e4..25fc594 100644
--- a/inc/blocks/_setup.php
+++ b/inc/blocks/_setup.php
@@ -21,15 +21,16 @@
 	new JVBase\blocks\FAQBlock();
 }
 
-
-if (!empty(Registrar::getFeatured('is_gallery'))) {
+if (!empty(Registrar::getFeatured('is_glossary'))) {
 	require(JVB_DIR . '/inc/blocks/GlossaryBlock.php');
 	new JVBase\blocks\GlossaryBlock();
 }
+
 if (!empty(Registrar::getFeatured('is_timeline'))) {
 	require(JVB_DIR . '/inc/blocks/TimelineBlock.php');
 	new JVBase\blocks\TimelineBlock();
 }
+
 require(JVB_DIR . '/inc/blocks/SummaryBlock.php');
 new JVBase\blocks\SummaryBlock();
 
diff --git a/inc/helpers/all.php b/inc/helpers/all.php
index 5b7f02b..bd2d0a7 100644
--- a/inc/helpers/all.php
+++ b/inc/helpers/all.php
@@ -57,3 +57,12 @@
         update_option(BASE.'do_these_once', $options);
     }
 }
+
+
+function jvbResponse(bool $success, ?string $msg = null):array
+{
+	return [
+		'success'	=> $success,
+		'message'	=> is_null($msg) ? ($success ? 'Completed successfully' : 'Something went wrong') : $msg
+	];
+}
diff --git a/inc/helpers/members.php b/inc/helpers/members.php
index d62b383..d2fdb9f 100644
--- a/inc/helpers/members.php
+++ b/inc/helpers/members.php
@@ -2,6 +2,7 @@
 
 use JVBase\managers\Cache;
 use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -217,3 +218,32 @@
         $user->roles)
     ))[0];
 }
+
+function  jvbUserProfileLink(int $userID):string|false
+{
+	$cache = Cache::for('userLink')->connect('user');
+	return $cache->remember(
+		$userID,
+		function() use ($userID) {
+			$user = get_userdata($userID);
+			if (!$user) {
+				return false;
+			}
+			$role = jvbUserRole($userID);
+			$registrar = Registrar::getInstance($role);
+			if (!$registrar || !$registrar->profile_link) {
+				return false;
+			}
+			$link = get_user_meta($userID, BASE.'profile_link', true);
+			//Try to create it
+			if (empty($link)) {
+				$link = JVB()->roles()->addUserLink($user, $role);
+				if (!$link) {
+					return false;
+				}
+			}
+			$status = get_post_status($link);
+			return ($status === 'publish') ? get_the_permalink($link) : false;
+		}
+	);
+}
diff --git a/inc/helpers/terms.php b/inc/helpers/terms.php
index e30cc9b..919d3af 100644
--- a/inc/helpers/terms.php
+++ b/inc/helpers/terms.php
@@ -6,6 +6,19 @@
 	exit;
 }
 
+
+function jvbGetTermOwners(int $termID, bool $includeManagers = true):array
+{
+	$owners = get_term_meta($termID, BASE.'owners', true);
+	$owners = empty($owners) ? [] : $owners;
+	if ($includeManagers) {
+		$managers = get_term_meta($termID, BASE.'managers', true);
+		$managers = empty($managers) ? [] : $managers;
+		$owners = array_merge($owners, $managers);
+	}
+	return $owners;
+
+}
 /**
  * @param string $term
  * @param int|false $ID
@@ -128,7 +141,8 @@
     if ($users === '') {
         $term = get_term($termID);
         $taxonomy = $term->taxonomy;
-        if (taxIsJVBContentTax($taxonomy)) {
+		$registrar = Registrar::getInstance($taxonomy);
+        if ($registrar->hasFeature('is_content')) {
             $posts = new WP_Query([
                 'post_type'    => jvbCheckBase($user),
                 'posts_per_page'    => -1,
diff --git a/inc/integrations/GoogleMaps.php b/inc/integrations/GoogleMaps.php
index 2990c7e..68d58fa 100644
--- a/inc/integrations/GoogleMaps.php
+++ b/inc/integrations/GoogleMaps.php
@@ -5,6 +5,7 @@
  */
 namespace JVBase\integrations;
 
+use JVBase\registrar\Registrar;
 use WP_Error;
 
 if (!defined('ABSPATH')) {
@@ -541,7 +542,7 @@
 			return true;
 		}
 
-		if (isJVBContentTax() && get_term_meta(get_queried_object_id(), BASE . 'has_map', true) === true) {
+		if (!empty(Registrar::getFeatured('is_content', 'term')) && get_term_meta(get_queried_object_id(), BASE . 'has_map', true) === true) {
 			return true;
 		}
 
diff --git a/inc/managers/ApprovalManager.php b/inc/managers/ApprovalManager.php
new file mode 100644
index 0000000..f381d58
--- /dev/null
+++ b/inc/managers/ApprovalManager.php
@@ -0,0 +1,432 @@
+<?php
+namespace JVBase\managers;
+
+
+use DateTime;
+use JVBase\registrar\Registrar;
+use JVBase\managers\CustomTable;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class ApprovalManager {
+	protected array $tables =[];
+	protected int $expiresAt = 21; //days for request
+	protected int $requiredVotes = 3; //Number of votes before a term is approved
+	public function __construct()
+	{
+		$this->defineTables();
+		if (empty($this->tables)) {
+			return;
+		}
+	}
+
+	protected function defineTables():void
+	{
+		$types = Registrar::getFeatured('approve_new');
+		foreach ($types as $type) {
+			$requests = CustomTable::for("approval_{$type}_requests");
+
+			$requests->setColumns([
+				'id'				=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+				'user_id'			=> "{$requests->getUserIDType()} NOT NULL",
+				'name'				=> 'varchar(255) NOT NULL',
+				'parent_id'			=> 'bigint(20) unsigned DEFAULT 0',
+				'status'			=> "ENUM('pending', 'approved', 'rejected', 'appealed', 'expired') DEFAULT 'pending'",
+				'required_approvals'=> 'int unsigned DEFAULT 3',
+				'approvals'	=> 'int unsigned DEFAULT 0',
+				'rejections'=> 'int unsigned DEFAULT 0',
+				'expires_at'		=> 'datetime DEFAULT NULL',
+				'created_at'		=> 'datetime DEFAULT CURRENT_TIMESTAMP',
+				'updated_at'		=> 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
+				'approved_by'		=> 'json DEFAULT NULL',
+				'rejected_by'		=> 'json DEFAULT NULL',
+				'created_item_id'	=> 'bigint(20) unsigned DEFAULT NULL',
+			]);
+			$requests->setKeys([
+				['key' => 'PRIMARY', 'value' => 'id'],
+				['key' => 'UNIQUE', 'value' => '`unique_request` (`user_id`, `name`)'],
+				'`name` (`name`)',
+				'`name_parent` (`name`, `parent_id`)',
+				'`status` (`status`)',
+				'`expiring_requests` (`status`, `expires_at`)'
+			]);
+			$base = BASE;
+			$requests->setConstraints([
+				"CONSTRAINTS `{$base}{$type}_approval_requester` FOREIGN KEY (`user_id`)
+				REFERENCES `{$requests->getUserTable()}` (`ID`) ON DELETE CASCADE",
+				"CONSTRAINTS `{$base}{$type}_approval_parent_term` FOREIGN KEY (`parent_id`)
+				REFERENCES `{$requests->getTermTable()}` (`term_id`) ON DELETE CASCADE"
+			]);
+			$requests->defineTable();
+
+			$votes = CustomTable::for("approval_{$type}_votes");
+
+			$votes->setColumns([
+				'id'			=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+				'request_id'	=> 'bigint(20) unsigned NOT NULL',
+				'user_id'		=> "{$votes->getUserIDType()} NOT NULL",
+				'vote'			=> "ENUM('approve', 'reject', 'dismiss') NOT NULL",
+				'notes'			=> 'text DEFAULT NULL',
+				'created_at'	=> 'datetime DEFAULT CURRENT_TIMESTAMP',
+				'updated_at'	=> 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
+			]);
+
+			$votes->setKeys([
+				['key' => 'PRIMARY', 'value' => 'id'],
+				['key' => 'UNIQUE', 'value' => '`unique_vote` (`request_id`, `user_id`)'],
+				'`user_votes` (`user_id`)'
+			]);
+
+			$votes->setConstraints([
+				"CONSTRAINT `{$base}{$type}_user_approval_request` FOREIGN KEY (`request_id`)
+				REFERENCES `{$requests->getFullTableName()} (`id`) ON DELETE CASCADE",
+				"CONSTRAINT `{$base}{$type}_user_approval_voter` FOREIGN KEY (`user_id`)
+				REFERENCES `{$votes->getUserTable()}` (`ID`) ON DELETE CASCADE"
+			]);
+
+			$votes->defineTable();
+
+			$this->tables[$type] = [
+				'requests'	=> $requests,
+				'votes'		=> $votes
+			];
+		}
+	}
+
+	public function canApprove(int $userID, string $type):bool
+	{
+		$type = jvbNoBase($type);
+		if (!array_key_exists($type, $this->tables)) {
+			return false;
+		}
+
+		return user_can($userID, "approve_{$type}");
+	}
+	protected function requests(string $type):?CustomTable
+	{
+		$type = jvbNoBase($type);
+		if (!array_key_exists($type, $this->tables)){
+			return null;
+		}
+		return $this->tables[$type]['requests']??null;
+	}
+
+	protected function votes(string $type):?CustomTable
+	{
+		$type = jvbNoBase($type);
+		if (!array_key_exists($type, $this->tables)){
+			return null;
+		}
+		return $this->tables[$type]['votes']??null;
+	}
+
+	public function response(bool $success, string $msg = ''):array
+	{
+		$return = ['success' => $success];
+		if ($msg !== '') {
+			$return['message'] = $msg;
+		}
+		return $return;
+	}
+
+	public function createApproval(int $userID, string $type, string $name, int $parentID = 0):array
+	{
+		if (!$this->canApprove($userID, $type)) {
+			return $this->response(false, 'Cannot create request');
+		}
+		$type = jvbNoBase($type);
+		if (!array_key_exists($type, $this->tables)) {
+			$this->response(false, 'Invalid type');
+		}
+		$table = $this->requests($type);
+		if (!$table) {
+			return $this->response(false, 'Invalid type');
+		}
+
+		//Check for existing request
+		$existing = $table->get([
+			'name'	=> sanitize_text_field($name)
+		]);
+		if (!$existing) {
+			$this->approve($userID, $existing->id);
+			return $this->response(true, 'Existing request - added your vote!');
+		}
+
+		if ($parentID > 0) {
+			$parent = get_term($parentID, jvbCheckBase($type));
+			if (!$parent || is_wp_error($parent)) {
+				$parent = 0;
+			}
+		}
+
+		$date = new DateTime('+' . $this->expiresAt . ' days');
+		$expires = $date->format('Y-m-d H:i:s');
+
+
+		$result = $table->create([
+			'user_id'	=> $userID,
+			'name'		=> sanitize_text_field($name),
+			'parent_id'	=> $parentID,
+			'expires_at'=> $expires
+		]);
+
+		return $this->response((bool)$result, $result?'Request created successfully' : 'Request could not be created');
+	}
+
+	public function markApproval(int $request_id, int $userID, string $type, string $vote, ?string $note = null):array
+	{
+		if (!$this->canApprove($userID, $type)) {
+			return $this->response(false, 'Sorry, you cannot do this.');
+		}
+		$requests = $this->requests($type);
+		if (!$requests) {
+			return $this->response(false, 'Invalid type');
+		}
+		$request = $requests->get(['request_id' => $request_id]);
+		if (!$request) {
+			return $this->response(false, 'Invalid request id');
+		}
+
+		$votes = $this->votes($type);
+		if (!$votes) {
+			return $this->response(false, 'Invalid type');
+		}
+
+		if (!in_array($vote, ['approve', 'reject', 'dismiss'])) {
+			return $this->response(false, 'Invalid vote');
+		}
+
+		$response = $votes->findOrCreate([
+			'request_id'	=> $request_id,
+			'user_id'		=> $userID
+		], [
+			'vote'	=> 'approve',
+			'notes'	=> $note ? sanitize_text_field($note) : $note
+		]);
+
+		if (!$response) {
+			return $this->response(false, 'Could not store vote for some reason.');
+		}
+
+		switch ($vote) {
+			case 'reject':
+				$request['rejections']++;
+				break;
+			case 'approve':
+				$request['approvals']++;
+				break;
+			default:
+				return $this->response(true, 'Successfully dismissed approval request.');
+		}
+
+		if ($request['rejections'] === $request['required_approvals']) {
+			$this->finalizeDenial($request_id, $type);
+		} else if ($request['approvals'] === $request['required_approvals']) {
+			$this->finalizeApproval($request_id, $type);
+		}
+		$updated = $requests->update([
+			'rejections'=> $request['rejections'],
+			'approvals'	=> $request['approvals']
+		],
+		[
+			'request_id' => $request_id
+		]);
+
+		if ($updated) {
+			return $this->response(true, 'Successfully voted');
+		}
+		return $this->response(false, 'Could not finalize vote for some reason');
+	}
+
+	public function approveRequest(int $request_id, int $userID, string $type, ?string $note = null):array
+	{
+		return $this->markApproval($request_id, $userID, $type, 'approve', $note);
+	}
+
+	public function denyRequest(int $request_id, int $userID, string $type, string $note = ''):array
+	{
+		return $this->markApproval($request_id, $userID, $type, 'reject', $note);
+	}
+
+	public function dismissRequest(int $request_id, int $userID, string $type, string $note = ''):array
+	{
+		return $this->markApproval($request_id, $userID, $type, 'dismiss', $note);
+	}
+
+	public function isApproved(int $request_id, string $type):bool
+	{
+		$table = $this->requests($type);
+		if (!$table) {
+			return false;
+		}
+		$request = $table->get(['request_id' => $request_id]);
+		if (!$request) {
+			return false;
+		}
+		$votes = $this->votes($type);
+		if (!$votes) {
+			return false;
+		}
+		$total = $votes->getMany([
+			'request_id'	=> $request_id,
+			'status'		=> ['IN' => ['approve', 'reject']]
+		]);
+		$approvals = count(array_filter($total, function($item) { return $item->status === 'approve'; }));
+		$denials = count(array_filter($total, function($item) { return $item->status === 'reject'; }));
+		if ($denials === $this->requiredVotes) {
+			return false;
+		}
+		return $approvals === $this->requiredVotes;
+	}
+
+	public function finalizeApproval(int $request_id, string $type):void
+	{
+		$requests = $this->requests($type);
+		$request = $requests->get(['request_id' => $request_id]);
+		if (!$request){
+			error_log('Could not finalize denial for request: '.$request_id);
+			return;
+		}
+
+		$updatedID = null;
+		$registrar = Registrar::getInstance($type);
+		if ($registrar->getType() === 'term') {
+			$term = wp_insert_term($request['name'], $registrar->getBased(), ['parent' => $request['parent_id']]);
+			if (!is_wp_error($term)) {
+				$updatedID = $term['term_id'];
+			}
+		} elseif ($registrar->getType() === 'user') {
+
+		}
+
+		$updates = [
+			'status'	=> 'approved'
+		];
+		if ($updatedID) {
+			$updates['created_item_id'] = $updatedID;
+		}
+		$updated = $requests->update(
+			$updates,
+			[
+				'request_id' => $request_id
+			]);
+
+
+		JVB()->notification()->addNotification($request['user_id'], $type.'approved', null, 'Your suggestion "'.$request['name'].'" was denied.');
+	}
+	public function finalizeDenial(int $request_id, string $type):void
+	{
+		$requests = $this->requests($type);
+		$request = $requests->get(['request_id' => $request_id]);
+		if (!$request){
+			error_log('Could not finalize denial for request: '.$request_id);
+			return;
+		}
+		$updated = $requests->update([
+			'status'	=> 'rejected'
+		],
+			[
+				'request_id' => $request_id
+			]);
+
+		JVB()->notification()->addNotification($request['user_id'], $type.'denied', null, 'Your suggestion "'.$request['name'].'" was denied.');
+	}
+
+	public function getApprovalRequests(int $userID, ?string $type = null):array
+	{
+		$return = [];
+		if ($type) {
+			if (!array_key_exists($type, $this->tables)) {
+				return $return;
+			}
+			$requests = $this->requests($type);
+			$active = $requests->pluck('request_id', [
+				'status' => 'pending'
+			]);
+			$votes = $this->votes($type);
+			$voted = $votes->pluck('request_id',[
+				'user_id'	=> $userID
+			]);
+
+			$all = array_diff($active, $voted);
+			$allRequests = $requests->getMany([
+				'where'	=> [
+					'request_id' => ['IN' => $all]
+				]
+			]);
+
+			$return = array_map(function ($request) use ($type) {
+				return $this->format_for_notification($request, $type);
+			}, $allRequests);
+		} else {
+			foreach ($this->tables as $type => $tables) {
+				$requests = $tables['requests'];;
+				$active = $requests->pluck('request_id', [
+					'status'	=> 'pending',
+				]);
+				$votes = $tables['votes'];
+				$voted = $votes->pluck('request_id', [
+					'user_id'	=> $userID
+				]);
+
+				$all = array_diff($active, $voted);
+				$allRequests = $requests->getMany([
+					'where'	=> [
+						'request_id' => ['IN' => $all]
+					]
+				]);
+				$return = array_merge($return, array_map(function ($request) use ($type) {
+					return $this->format_for_notification($request, $type);
+				}, $allRequests));
+			}
+		}
+
+		return $return;
+	}
+
+	protected function format_for_notification(array $request, string $type): array
+	{
+		$registrar = Registrar::getInstance($type);
+		$message = '';
+		$user = get_userdata($request['user_id']);
+		if ($user) {
+			$message = $user->first_name;
+		} else {
+			$message = 'Someone';
+		}
+		$message .= ' thinks we need a new '.$registrar->getSingular().'.
+		They think: "'.$request['name'].'"
+		';
+		if ($request['parent_id'] > 0) {
+			$parent = get_term($request['parent_id'], $registrar->getBased());
+			if ($parent && !is_wp_error($parent)) {
+				$message .= 'And nest it under: '.$parent->name.'
+				';
+			}
+		}
+		$message .= '
+		What say you?';
+		return [
+			'id'		=> $request['id'],
+			'type'		=> $type.'_new',
+			'message'	=> $message,
+			'created_at'=> $request['created_at'],
+			'status'	=> 'pending',
+			'requires_action' => true,
+			'action_taken'	=> false,
+			'icon'		=> $registrar->getIcon(),
+			'priority'	=> 'normal',
+			'target'	=> [
+				'id'	=> $request['id'],
+				'type'	=> 'approval'
+			],
+			'actions'	=> [
+				'approve'	=> 'Approve',
+				'deny'		=> 'Deny',
+				'dismiss'	=> 'Dismiss'
+			]
+		];
+	}
+}
diff --git a/inc/managers/CustomTable.php b/inc/managers/CustomTable.php
index 4299b71..e344576 100644
--- a/inc/managers/CustomTable.php
+++ b/inc/managers/CustomTable.php
@@ -34,8 +34,12 @@
 
 	protected static string $userTable;
 	protected static string $userIDType;
+	protected static string $termTable;
 	protected static string $termIDType;
 	protected static string $postIDType;
+	protected static string $postTable;
+
+	protected Cache $cache;
 
 	/**
 	 * Fluent factory method
@@ -54,6 +58,19 @@
 
 		return self::$instances[$tableName];
 	}
+	public static function destroyInstance(string $tableName):void
+	{
+		if (isset(self::$instances[$tableName])) {
+			unset(self::$instances[$tableName]);
+		}
+	}
+	public static function getInstance(string $tableName):self|false
+	{
+		if (!isset(self::$instances[$tableName])) {
+			return false;
+		}
+		return self::$instances[$tableName];
+	}
 
 	public function ensureDefined() {
 		$this->ensureUserTable();
@@ -94,12 +111,25 @@
 			}
 		}
 
+		protected function ensureTermTable():void
+		{
+			if (!isset(static::$termTable)) {
+				static::$termTable = $this->wpdb->terms;
+			}
+		}
+
 		protected function ensurePostIDType():void
 		{
 			if (!isset(static::$postIDType)) {
 				static::$postIDType = $this->getColumnType($this->wpdb->posts, 'ID');
 			}
 		}
+		protected function ensurePostTable():void
+		{
+			if (!isset(static::$postTable)) {
+				static::$postTable = $this->wpdb->posts;
+			}
+		}
 
 	/**
 	 *
@@ -165,11 +195,12 @@
 		}
 
 		// Constraints
-		foreach ($this->constraints as $constraint => $references) {
-			$parts[] = "CONSTRAINT {$constraint} REFERENCES {$references}";
+		foreach ($this->constraints as $constraint) {
+			$parts[] = $constraint;
 		}
 
 		$this->definition = "(\n    " . implode(",\n    ", $parts) . "\n)";
+		$this->createTable();
 		return $this;
 	}
 
@@ -190,6 +221,11 @@
 			error_log('[CustomTable]No definition set for '.$this->tableName);
 			return;
 		}
+		if ($this->wpdb->get_var("SHOW TABLES LIKE '{$this->fullTableName}'")) {
+			return;
+		}
+		require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
+		error_log('Creating Database Table: '. $this->tableName);
 		$charset = self::$charsetCollate;
 		$schema = "CREATE TABLE IF NOT EXISTS {$this->fullTableName} {$this->definition} {$charset}";
 		$this->wpdb->flush();
@@ -232,6 +268,8 @@
 		$this->fullTableName = $wpdb->prefix . apply_filters('jvb_base', BASE) . $tableName;
 		$this->useTransactions = $useTransactions;
 
+		$this->cache = Cache::for($tableName);
+
 		$usersStatus = $this->wpdb->get_row("SHOW TABLE STATUS LIKE '{$this->wpdb->users}'");
 		$parentCollation = $usersStatus->Collation ?? 'utf8mb4_general_ci';
 		self::$charsetCollate = "DEFAULT CHARACTER SET utf8mb4 COLLATE {$parentCollation}";
@@ -409,6 +447,8 @@
 			return false;
 		}
 
+		$this->cache->flush();
+
 		return $this->wpdb->insert_id;
 	}
 
@@ -439,26 +479,19 @@
 	 * );
 	 * // Returns: ['id' => 456, 'created' => false, 'record' => object]
 	 */
-	public function findOrCreate(array $searchData, array $createData = []): array
+	public function findOrCreate(array $searchData, array $createData = []):int|false
 	{
 		$record = $this->get($searchData);
 
 		if ($record) {
-			return [
-				'id' => $record->id ?? 0,
-				'created' => false,
-				'record' => $record
-			];
+			if (!empty($createData)) {
+				return $this->update($createData, $searchData);
+			}
+			return $record->id;
 		}
 
 		$data = array_merge($searchData, $createData);
-		$id = $this->insert($data);
-
-		return [
-			'id' => $id,
-			'created' => true,
-			'record' => $this->get(['id' => $id])
-		];
+		return $this->insert($data);
 	}
 
 	/**
@@ -536,10 +569,17 @@
 	 */
 	public function get(array $where, string $output = OBJECT): object|array|null
 	{
-		$query = "SELECT * FROM {$this->fullTableName} WHERE " . $this->buildWhereClause($where);
-		$values = array_values($where);
+		return $this->cache->remember(
+			$this->cache->generateKey(array_merge(['output' => $output], $where)),
+			function() use ($where, $output) {
+				$query = "SELECT * FROM {$this->fullTableName}";
+				$where = $this->buildWhereClause($where);
+				$query .= " WHERE {$where['sql']}";
+				$values = $where['values'];
 
-		return $this->wpdb->get_row($this->wpdb->prepare($query, $values), $output);
+				return $this->wpdb->get_row($this->wpdb->prepare($query, $values), $output);
+			}
+		);
 	}
 
 	/**
@@ -559,34 +599,76 @@
 	 */
 	public function getMany(array $args = [], string $output = OBJECT): array
 	{
-		$query = "SELECT * FROM {$this->fullTableName}";
-		$values = [];
+		return $this->cache->remember(
+			$this->cache->generateKey(array_merge($args, ['output' => $output])),
+			function () use ($args, $output) {
+				$query = "SELECT * FROM {$this->fullTableName}";
+				$values = [];
 
-		// WHERE clause
-		if (!empty($args['where'])) {
-			$query .= " WHERE " . $this->buildWhereClause($args['where']);
-			$values = array_merge($values, array_values($args['where']));
-		}
+				// WHERE clause
+				if (!empty($args['where'])) {
+					$clause = $this->buildWhereClause($args['where']);
 
-		// ORDER BY
-		if (!empty($args['orderby'])) {
-			$orderby = sanitize_sql_orderby($args['orderby']);
-			$order = (!empty($args['order']) && strtoupper($args['order']) === 'ASC') ? 'ASC' : 'DESC';
-			$query .= " ORDER BY {$orderby} {$order}";
-		}
+					$query .= " WHERE {$clause['sql']}";
+					$values = array_merge($values, $clause['values']);
+				}
 
-		// LIMIT
-		if (!empty($args['limit'])) {
-			$limit = absint($args['limit']);
-			$offset = !empty($args['offset']) ? absint($args['offset']) : 0;
-			$query .= " LIMIT {$offset}, {$limit}";
-		}
+				// ORDER BY
+				if (!empty($args['orderby'])) {
+					$orderby = sanitize_sql_orderby($args['orderby']);
+					$order = (!empty($args['order']) && strtoupper($args['order']) === 'ASC') ? 'ASC' : 'DESC';
+					$query .= " ORDER BY {$orderby} {$order}";
+				}
 
-		if (empty($values)) {
-			return $this->wpdb->get_results($query, $output);
-		}
+				// LIMIT
+				if (!empty($args['limit'])) {
+					$limit = absint($args['limit']);
+					$offset = !empty($args['offset']) ? absint($args['offset']) : 0;
+					$query .= " LIMIT {$offset}, {$limit}";
+				}
 
-		return $this->wpdb->get_results($this->wpdb->prepare($query, $values), $output);
+				if (empty($values)) {
+					return $this->wpdb->get_results($query, $output);
+				}
+
+				return $this->wpdb->get_results($this->wpdb->prepare($query, $values), $output);
+			}
+		);
+
+	}
+
+	/**
+	 * Get a specific column value from all matches
+	 * @param string $column
+	 * @param array $where
+	 * @return array
+	 */
+	public function pluck(string $column, array $where = [], ?string $orderby = null, ?string $order = null, ?int $limit = null): array
+	{
+		$key = array_merge($where, ['column' => $column, 'orderby' => $orderby, 'order' => $order, 'limit' => $limit]);
+
+		return $this->cache->remember(
+			$this->cache->generateKey($key),
+			function() use ($column, $where, $orderby, $order, $limit) {
+				if (!empty($this->columns) && !array_key_exists($column, $this->columns)) {
+					$this->logError('pluck', ['reason' => "Column '{$column}' not in definition"]);
+					return [];
+				}
+				$args = ['where' => $where];
+				if ($orderby) {
+					$args['orderby'] = $orderby;
+				}
+				if ($order) {
+					$args['order'] = $order;
+				}
+				if ($limit) {
+					$args['limit'] = $limit;
+				}
+
+				return array_column($this->getMany($args), $column);
+			}
+		);
+
 	}
 
 	/**
@@ -597,19 +679,26 @@
 	 */
 	public function count(array $where = []): int
 	{
-		$query = "SELECT COUNT(*) FROM {$this->fullTableName}";
-		$values = [];
+		return $this->cache->remember(
+			$this->cache->generateKey(array_merge(['source' => 'count'], $where)),
+			function() use ($where) {
+				$query = "SELECT COUNT(*) FROM {$this->fullTableName}";
+				$values = [];
 
-		if (!empty($where)) {
-			$query .= " WHERE " . $this->buildWhereClause($where);
-			$values = array_values($where);
-		}
+				if (!empty($where)) {
+					$clause = $this->buildWhereClause($where);
+					$query .= " WHERE {$clause['sql']}";
+					$values = $clause['values'];
+				}
 
-		if (empty($values)) {
-			return (int) $this->wpdb->get_var($query);
-		}
+				if (empty($values)) {
+					return (int) $this->wpdb->get_var($query);
+				}
 
-		return (int) $this->wpdb->get_var($this->wpdb->prepare($query, $values));
+				return (int) $this->wpdb->get_var($this->wpdb->prepare($query, $values));
+			}
+		);
+
 	}
 
 	/**
@@ -660,6 +749,7 @@
 		if ($result === false) {
 			$this->logError('update', ['data' => $data, 'where' => $where]);
 		}
+		$this->cache->flush();
 
 		return $result;
 	}
@@ -689,6 +779,7 @@
 		if ($result === false) {
 			$this->logError('delete', ['where' => $where]);
 		}
+		$this->cache->flush();
 
 		return $result;
 	}
@@ -811,7 +902,7 @@
 		} catch (Exception $e) {
 			$this->rollback();
 			$this->logError('transaction', ['error' => $e->getMessage()]);
-			throw $e;
+			return false;
 		}
 	}
 
@@ -866,11 +957,21 @@
 		$this->ensureTermIDType();
 		return static::$termIDType;
 	}
+	public function getTermTable():string
+	{
+		$this->ensureTermTable();
+		return static::$termTable;
+	}
 	public function getPostIDType():string
 	{
 		$this->ensurePostIDType();
 		return static::$postIDType;
 	}
+	public function getPostTable():string
+	{
+		$this->ensurePostTable();
+		return static::$postTable;
+	}
 
 	// =========================================================================
 	// PRIVATE HELPERS
@@ -909,18 +1010,35 @@
 	/**
 	 * Build WHERE clause from associative array
 	 */
-	private function buildWhereClause(array $where): string
+	private function buildWhereClause(array $where):array
 	{
 		$conditions = [];
+		$values = [];
 		foreach ($where as $column => $value) {
 			$column_safe = esc_sql($column);
+
 			if ($value === null) {
 				$conditions[] = "`{$column_safe}` IS NULL";
+			} elseif (is_array($value) && count($value) === 2 && is_string($value[0])) {
+				[$operator, $operand] = $value;
+				$operator = strtoupper(trim($operator));
+
+				if (in_array($operator, ['IN', 'NOT IN']) && is_array($operand)) {
+					$placeholders = implode(',', array_map([$this, 'getPlaceholder'], $operand));
+					$conditions[] = "`{$column_safe}` {$operator} ({$placeholders})";
+					$values       = array_merge($values, $operand);
+				} else {
+					// <, >, <=, >=, !=, LIKE, etc.
+					$conditions[] = "`{$column_safe}` {$operator} " . $this->getPlaceholder($operand);
+					$values[]     = $operand;
+				}
 			} else {
 				$conditions[] = "`{$column_safe}` = " . $this->getPlaceholder($value);
+				$values[]     = $value;
 			}
 		}
-		return implode(' AND ', $conditions);
+
+		return ['sql' => implode(' AND ', $conditions), 'values' => $values];
 	}
 
 	/**
diff --git a/inc/managers/EmailManager.php b/inc/managers/EmailManager.php
index f1c7e33..68bbdef 100644
--- a/inc/managers/EmailManager.php
+++ b/inc/managers/EmailManager.php
@@ -756,11 +756,35 @@
 		);
 	}
 
-	public function grid(array $items, int $columns = 2):string
+	public function grid(array $items, int $columns = 2, string $title = '', string|array $description = '', string $after = ''):string
 	{
 		$width = floor(100 / $columns) - 2; // 2% gap
 
-		$html = '<div style="display:table;width:100%;margin:20px 0;">';
+		$html = '';
+		if (!empty($title) || !empty($description)) {
+			$html .= '<div>';
+			if (!empty($title)) {
+				$html .= sprintf(
+					'<h2>%s</h2>',
+					$title
+				);
+			}
+			if (!empty($description)) {
+				if (is_string($description)) {
+					if (str_starts_with($description, '<p>')) {
+						$html .= $description;
+					}else {
+						$html .= sprintf(
+							'<p>%s</p>',
+							$description
+						);
+					}
+				} else {
+					$html .= implode('',array_map(function ($p) { return sprintf('<p>%s</p>', $p); }, $description));
+				}
+			}
+		}
+		$html .= '<div style="display:table;width:100%;margin:20px 0;">';
 		foreach ($items as $index => $item) {
 			if ($index > 0 && $index % $columns === 0) {
 				$html .= '</div><div style="display:table;width:100%;margin:20px 0;">';
@@ -771,11 +795,16 @@
 				$item
 			);
 		}
-		$html .= '</div>';
+		$html .= '</div>'.$after;
 
+
+		if (!empty($title) || !empty($description)) {
+			$html .= '</div>';
+		}
 		return $html;
 	}
 
+
 	public function image(string $src, string $alt = '', int $maxWidth = 600):string
 	{
 		return sprintf(
diff --git a/inc/managers/ErrorHandler.php b/inc/managers/ErrorHandler.php
index 9e6cc9a..da34d19 100644
--- a/inc/managers/ErrorHandler.php
+++ b/inc/managers/ErrorHandler.php
@@ -24,11 +24,14 @@
         'critical' => 4
     ];
 
+	protected CustomTable $table;
+
     public function __construct()
     {
-        global $wpdb;
-        $this->wpdb = $wpdb;
-		$this->tableName = $wpdb->prefix . BASE . 'error_log';
+		$this->defineTables();
+//        global $wpdb;
+//        $this->wpdb = $wpdb;
+//		$this->tableName = $wpdb->prefix . BASE . 'error_log';
 
         add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
 
@@ -39,6 +42,35 @@
 //        add_filter(BASE.'admin_action_filter', [$this, 'adminActionFilter'], 10, 3);
     }
 
+	public function defineTables():void
+	{
+		$table = CustomTable::for('error_log');
+		$table->setColumns([
+			'id'			=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+			'error_type'	=> 'varchar(50) NOT NULL',
+			'component'		=> 'varchar(100) NOT NULL',
+			'method'		=> 'varchar(100) DEFAULT NULL',
+			'page_url'		=> 'varchar(255) DEFAULT NULL',
+			'message'		=> 'text NOT NULL',
+			'context'		=> 'JSON',
+			'severity'		=> 'ENUM(\'high\',\'normal\',\'low\') DEFAULT \'normal\'',
+			'user_id'		=> $table->getUserIDType().' DEFAULT NULL',
+			'user_was_logged_in'	=> 'tinyint(1) NOT NULL',
+			'source'		=> 'ENUM(\'frontend\', \'backend\') NOT NULL',
+			'created_at'	=> 'timestamp DEFAULT CURRENT_TIMESTAMP',
+		]);
+
+		$table->setKeys([
+			['key' => 'PRIMARY', 'value' => 'id'],
+			'`created_at` (`created_at`)',
+			'`component_severity_date` (`component`, `severity`, `created_at`)',
+			'`error_type_date` (`error_type`, `created_at`)',
+			'`severity_date` (`severity`, `created_at`)'
+		]);
+
+		$table->defineTable();
+		$this->table = $table;
+	}
     public function registerAdminAction():void
     {
         $admin = JVB()->admin();
diff --git a/inc/managers/FavouritesManager.php b/inc/managers/FavouritesManager.php
new file mode 100644
index 0000000..a2cfb88
--- /dev/null
+++ b/inc/managers/FavouritesManager.php
@@ -0,0 +1,576 @@
+<?php
+namespace JVBase\managers;
+
+use JVBase\registrar\Registrar;
+
+if (!defined('ABSPATH')) {
+	exit; // Exit if accessed directly
+}
+class FavouritesManager
+{
+	private Cache $cache;
+
+	protected CustomTable $favourites;
+	protected CustomTable $lists;
+	protected CustomTable $listItems;
+	protected CustomTable $listShares;
+
+	public function __construct()
+	{
+		$this->defineTables();
+	}
+
+	protected function defineTables():void
+	{
+		$this->defineFavouriteTable();
+		$this->defineListTable();
+		$this->defineListItemsTable();
+		$this->defineListSharesTable();
+	}
+		private function defineFavouriteTable():void
+		{
+			$table = CustomTable::for('favourites');
+
+			$table->setColumns([
+				'id'		=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+				'user_id'	=> "{$table->getUserIDType()} NOT NULL",
+				'type'		=> 'varchar(50) NOT NULL', 					//As in, post type/user/taxonomy
+				'target_id'	=> 'bigint(20) unsigned NOT NULL',
+				'notes'		=> 'text DEFAULT NULL',
+				'created_at'	=> 'datetime DEFAULT CURRENT_TIMESTAMP',
+				'updated_at'	=> 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
+			]);
+
+			$table->setKeys([
+				['key' => 'PRIMARY', 'value' => '(`id`)'],
+				['key' => 'UNIQUE', 'value' => '`unique_favourite` (`user_id`, `type`, `target_id`)'],
+				'`user_type` (`user_id`, `type`)',
+				'`target_type` (`target_id`, `type`)'
+			]);
+			$base = BASE;
+			$table->setConstraints([
+				"CONSTRAINT `{$base}favourites_user` FOREIGN KEY (`user_id`)
+				REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
+			]);
+
+			$table->defineTable();
+			$this->favourites = $table;
+		}
+		private function defineListTable():void
+		{
+			$table = CustomTable::for('favourites_lists');
+
+			$table->setColumns([
+				'id'		=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+				'user_id'	=> "{$table->getUserIDType()} NOT NULL",
+				'name'		=> 'varchar(255) NOT NULL',
+				'description'=> 'text',
+				'notes'		=> 'JSON DEFAULT (\'{}\')',
+				'created_at'=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP',
+				'updated_at'=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
+			]);
+
+			$table->setKeys([
+				['key' => 'PRIMARY', 'value' => '(`id`)'],
+				'`user_lists` (`user_id`)'
+			]);
+			$base = BASE;
+			$table->setConstraints([
+				"CONSTRAINT `{$base}favourites_list_user` FOREIGN KEY (`user_id`)
+				REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
+			]);
+
+			$table->defineTable();
+			$this->lists = $table;
+		}
+		private function defineListItemsTable():void
+		{
+			$table = CustomTable::for('favourites_list_items');
+
+			$table->setColumns([
+				'id'		=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+				'user_id'	=> "{$table->getUserIDType()} NOT NULL",
+				'list_id'	=> 'bigint(20) unsigned NOT NULL',
+				'item_type'	=> 'varchar(50) NOT NULL',
+				'target_id'	=> 'bigint(20) unsigned NOT NULL',
+				'favourite_id'=> 'bigint(20) unsigned NOT NULL',
+				'notes'		=> 'JSON DEFAULT (\'{}\')',
+				'added_at'	=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP',
+			]);
+
+			$table->setKeys([
+				['key' => 'PRIMARY', 'value' => '(`id`)'],
+				['key' => 'UNIQUE', 'value' => '`unique_list_item` (`list_id`, `item_type`, `target_id`)'],
+				'`list_items` (`list_id`)',
+				'`favourite_id` (`favourite_id`)'
+			]);
+			$base = BASE;
+			$table->setConstraints([
+				"CONSTRAINT `{$base}favourites_list_items_user` FOREIGN KEY (`user_id`)
+				REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE",
+				"CONSTRAINT `{$base}favourite_list_items` FOREIGN KEY (`list_id`)
+				REFERENCES `{$this->lists->getFullTableName()}` (`id`) ON DELETE CASCADE",
+				"CONSTRAINT `{$base}list_favourite` FOREIGN KEY (`favourite_id`)
+				REFERENCES `{$this->favourites->getFullTableName()}` (`id`) ON DELETE CASCADE"
+			]);
+
+			$table->defineTable();
+			$this->listItems = $table;
+		}
+		private function defineListSharesTable():void
+		{
+			$table = CustomTable::for('favourites_list_shares');
+
+			$table->setColumns([
+				'id'			=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+				'list_id'		=> 'bigint(20) unsigned NOT NULL',
+				'user_id'		=> "{$table->getUserIDType()} NOT NULL",
+				'email'			=> 'varchar(255) NOT NULL',
+				'permission'	=> "ENUM('view', 'edit') NOT NULL DEFAULT 'view'",
+				'status'		=> "ENUM('pending', 'accepted', 'rejected', 'revoked') NOT NULL DEFAULT 'pending'",
+				'created_at'	=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP',
+				'updated_at'	=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
+			]);
+
+			$table->setKeys([
+				['key' => 'PRIMARY', 'value' => '(`id`)'],
+				['key'	=> 'UNIQUE', 'value' => '`unique_share_user` (`list_id`, `user_id`)'],
+				['key'=> 'UNIQUE', 'value' => '`unique_share_email` (`list_id`, `email`'],
+				'`list_shares` (`list_id`)',
+				'`list_user` (`list_id`, `user_id`)',
+				'`status_index` (`status`)'
+			]);
+			$base = BASE;
+			$table->setConstraints([
+				"CONSTRAINT `{$base}favourites_share_list` FOREIGN KEY (`list_id`)
+				REFERENCES `{$this->lists->getFullTableName()}` (`id`) ON DELETE CASCADE",
+				"CONSTRAINT `{$base}favourites_share_user` FOREIGN KEY (`user_id`)
+				REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
+			]);
+
+			$table->defineTable();
+			$this->listShares = $table;
+		}
+	public function toggleFavourite(bool $favourite, int $user_id, int $target_id, string $type):bool
+	{
+		if (!get_userdata($user_id)) {
+			error_log('[FAVOURITES]::toggleFavourite Invalid user '.$user_id);
+			return false;
+		}
+		$registrar = Registrar::getInstance($type);
+		if (!$registrar) {
+			error_log('[FAVOURITES]::toggleFavourite Invalid type: '.print_r($type, true));
+			return false;
+		}
+		$type = jvbCheckBase($type);
+		$registerType = $registrar->getType();
+		$test = false;
+		switch ($registerType) {
+			case 'post':
+				$test = get_post($target_id);
+				break;
+			case 'term':
+				$test = get_term($target_id, $type);
+				break;
+			case 'user':
+				$test = get_userdata($target_id);
+				break;
+		}
+		if (!$test || is_wp_error($test)) {
+			error_log('[FAVOURITES]::toggleFavourite Could not find target: '.$target_id);
+			return false;
+		}
+
+		$owner = null;
+		if ($registrar->getType() === 'post') {
+			$post =  get_post($target_id);
+			$owner = $post->post_author;
+		} elseif ($registrar->hasFeature('is_ownable')) {
+			$term = get_term($target_id, jvbCheckBase($type));
+			$owner = jvbGetTermOwners($target_id);
+		}
+
+		if ($favourite) {
+			$result = $this->favourites->findOrCreate(
+				[
+					'user_id'	=> $user_id,
+					'target_id'	=> $target_id,
+					'type'		=> $type
+				],
+			);
+
+			if ($owner) {
+				JVB()->notification()->addNotification(
+					$owner,
+					'new_favourite',
+					$user_id,
+					'',
+					$target_id,
+					$type
+				);
+			}
+
+
+		} else {
+			$result = $this->favourites->delete([
+				'user_id'	=> $user_id,
+				'target_id'	=> $target_id,
+				'type'		=> $type
+			]);
+
+			if (in_array($registerType, ['term', 'user'])) {
+				JVB()->notification()->deleteUserPreference($user_id, $target_id, $type);
+			}
+			if ($owner) {
+				JVB()->notification()->removeNotification(
+					$owner,
+					[
+						'from_user' => $user_id,
+						'target_id' => $target_id,
+						'target_type' => $type
+					]
+				);
+			}
+		}
+		return (bool)$result;
+	}
+
+		public function favourite(int $user_id, int $target_id, string $type):bool
+		{
+			return $this->toggleFavourite(true, $user_id, $target_id, $type);
+		}
+		public function unfavourite(int $user_id, int $target_id, string $type):bool
+		{
+			return $this->toggleFavourite(false, $user_id, $target_id, $type);
+		}
+
+		public function addNoteFromTarget(int $user_id, int $target_id, string $type, string $note):bool
+		{
+			$favourite = $this->favourites->get([
+				'user_id'	=> $user_id,
+				'target_id'	=> $target_id,
+				'type'		=> $type
+			]);
+			if (!$favourite) {
+				return false;
+			}
+			return $this->addNote($user_id, $favourite->id, $note);
+		}
+		public function addNote(int $user_id, int $favourite_id, string $note):bool
+		{
+			$favourite = $this->favourites->get(['id' => $favourite_id]);
+			if (!$favourite) {
+				return false;
+			}
+			if ($favourite->user_id !== $user_id) {
+				return false;
+			}
+			return (bool) $this->favourites->update([
+				'note'	=> sanitize_textarea_field($note)
+			], [
+				'favourite_id' => $favourite_id
+			]);
+		}
+		public function deleteNote(int $user_id, int $favourite_id):bool
+		{
+			if (!$this->userCanEditFavourite($user_id, ['id' => $favourite_id])) {
+				return false;
+			}
+			return (bool) $this->favourites->update([
+				'note'	=> '',
+			], [
+				'favourite_id' => $favourite_id
+			]);
+		}
+
+
+		public function addListItemNote(int $user_id, int $listItemId, string|array $note):bool
+		{
+			if (!$this->userCanEditListItem($user_id, $listItemId)) {
+				return false;
+			}
+			$listItem = $this->listItems->get(['id' => $listItemId]);
+			$notes = json_decode($listItem->notes, true);
+
+			$notes = $this->addNoteToArray($notes, $user_id, $note);
+
+			return $this->listItems->update(['id' => $listItemId], ['notes' => json_encode($notes)]);
+		}
+		public function removeListItemNote(int $user_id, int $listItemId, string $noteID):bool
+		{
+			if (!$this->userCanEditListItem($user_id, $listItemId)) {
+				return false;
+			}
+			$listItem = $this->listItems->get(['id' => $listItemId]);
+			$notes = json_decode($listItem->notes, true);
+			$index = array_search($noteID, array_column($notes, 'id'));
+			if ($index === false) {
+				return false;
+			}
+			$note = $notes[$index];
+			if ($user_id !== $note['user_id']) {
+				return false;
+			}
+
+			$notes = $this->removeNoteFromArray($notes, $user_id, $noteID);
+
+			return $this->listItems->update(['id' => $listItemId], ['notes' => json_encode($notes)]);
+		}
+
+		public function addListNote(int $user_id, int $listId, string $note):bool
+		{
+			if (!$this->userCanEditList($user_id, $listId)) {
+				return false;
+			}
+			$list = $this->listItems->get(['id' => $listId]);
+			$notes = json_decode($list->notes, true);
+
+			$notes = $this->addNoteToArray($notes, $user_id, $note);
+
+			return $this->listItems->update(['id' => $listId], ['notes' => json_encode($notes)]);
+		}
+
+		public function removeListNote(int $user_id, int $listId, string $noteID):bool
+		{
+			if (!$this->userCanEditList($user_id, $listId)) {
+				return false;
+			}
+			$list = $this->lists->get(['id' => $listId]);
+			$notes = json_decode($list->notes, true);
+			$index = array_search($noteID, array_column($notes, 'id'));
+			if ($index === false) {
+				return false;
+			}
+			$note = $notes[$index];
+			if ($user_id !== $note['user_id']) {
+				return false;
+			}
+
+			$notes = $this->removeNoteFromArray($notes, $user_id, $noteID);
+
+			return $this->lists->update(['id' => $listId], ['notes' => json_encode($notes)]);
+		}
+
+		public function shareList(int $user_id, int $listId, bool $viewOnly = true, ?int $shareWithUser = null, ?array $newUser = null):bool
+		{
+			$list = $this->lists->get(['id' => $listId]);
+			if (!$list) {
+				return false;
+			}
+			if ($list->user_id !== $user_id) {
+				return false;
+			}
+
+			if (!$shareWithUser && (empty($newUser) || !array_key_exists('email', $newUser) || !array_key_exists('name', $newUser))) {
+				return false;
+			}
+
+			$args = [];
+			if ($shareWithUser) {
+				$args['user_id'] = $shareWithUser;
+				JVB()->notification()->addNotification($shareWithUser, 'list_shared', $user_id, '', $listId);
+			} elseif ($newUser) {
+				$email = sanitize_email($newUser['email']);
+				$name = sanitize_text_field($newUser['name']);
+				$args['email'] = $email;
+				//TODO: Magic Link setup
+			}
+			if (empty($args)) {
+				return false;
+			}
+
+			return (bool) $this->listShares->findOrCreate(array_merge($args, ['list_id' => $listId, 'permission' => $viewOnly ? 'view' : 'edit']));
+		}
+		public function unshareList(int $user_id, int $listId, int|string $userIDorEmail):bool
+		{
+			$list = $this->lists->get(['id' => $listId]);
+			if (!$list) {
+				return false;
+			}
+			if ($list->user_id !== $user_id) {
+				return false;
+			}
+
+			$where = [
+				'list_id'	=> $listId
+			];
+			if (is_int($userIDorEmail)) {
+				$where['user_id'] = $userIDorEmail;
+
+				$notification = JVB()->notification()->table();
+				$n = $notification->get([
+					'for_user' => $userIDorEmail,
+					'from_user' => $user_id,
+					'type' => 'list_shared',
+					'target_id' => $listId,
+				]);
+				if ($n) {
+					$notification->delete([
+						'for_user' => $userIDorEmail,
+						'from_user' => $user_id,
+						'type' => 'list_shared',
+						'target_id' => $listId,
+					]);
+				}
+			} else if (is_email($userIDorEmail)) {
+				$where['email'] = sanitize_email($userIDorEmail);
+			} else {
+				return false;
+			}
+
+			return $this->listShares->update($where, ['status' => 'revoked']);
+		}
+
+		public function acceptListShare(int $listId, int|string $userIDorEmail):bool
+		{
+			return $this->respondToShare(true, $listId, $userIDorEmail);
+		}
+
+		public function rejectListShare(int $listId, int|string $userIDorEmail):bool
+		{
+			return $this->respondToShare(false, $listId, $userIDorEmail);
+		}
+		public function respondToShare(bool $accept, int $listId, int|string $userIDorEmail):bool
+		{
+			$args =  [
+				'listId' => $listId
+			];
+			if (is_int($userIDorEmail)) {
+				$args['user_id'] = $userIDorEmail;
+
+				$list = $this->lists->get(['id' => $listId]);
+				if ($list) {
+					$user = get_userdata($userIDorEmail);
+
+					$message = $accept ? 'accepted' : 'rejected';
+					$message = $user->first_name . ' ' . $message . ' your list share invitation.';
+					JVB()->notification()->addNotification($list->user_id, 'list_share_status', $userIDorEmail, $message);
+				}
+			}else if (is_email($userIDorEmail)) {
+				$args['email'] = sanitize_email($userIDorEmail);
+			} else {
+				return  false;
+			}
+			$share = $this->listShares->get($args);
+			if (!$share) {
+				return false;
+			}
+
+
+			return $this->listShares->update([
+				'status'	=> $accept ? 'accepted' : 'rejected',
+			], $args);
+		}
+	/***************************************************************
+	 * UTILITY METHODS
+	 **************************************************************/
+	/**
+	 * @param int $user_id
+	 * @param array $args
+	 * @return bool
+	 */
+	public function userCanEditFavourite(int $user_id, array $args):bool
+	{
+		if (array_key_exists('id', $args)) {
+			$where = [
+				'id'	=> $args['id']
+			];
+		} else {
+			if (!array_key_exists('target_id', $args)
+			|| !array_key_exists('type', $args)) {
+				error_log('[Favourites]::userCanEditFavourite missing required target_id or type');
+				return false;
+			}
+			$where = [
+				'target_id'	=> absint($args['target_id']),
+				'type'		=> $args['type']
+			];
+		}
+
+		$get = $this->favourites->get($where);
+		return !is_null($get) && $get->user_id === $user_id;
+	}
+
+	public function userCanViewList(int $user_id, int $list_id):bool
+	{
+		$list = $this->lists->get(['id' => $list_id]);
+		if (!$list) {
+			return false;
+		}
+		if ($list->user_id === $user_id) {
+			return true;
+		}
+		$share = $this->listShares->get(['user_id' => $user_id, 'list_id' => $list_id]);
+		return !is_null($share);
+	}
+	public function userCanEditList(int $user_id, int $list_id):bool
+	{
+		$list = $this->lists->get(['id' => $list_id]);
+		if (!$list) {
+			return false;
+		}
+		if ($list->user_id === $user_id) {
+			return true;
+		}
+		$share = $this->listShares->get(['user_id' => $user_id, 'list_id' => $list_id]);
+		if (!$share) return false;
+		return $share->permission === 'edit';
+	}
+
+	public function userCanEditListItem(int $user_id, int $list_item_id):bool
+	{
+		$item = $this->listItems->get(['id' => $list_item_id]);
+		if (!$item) {
+			return false;
+		}
+		if ($item->user_id === $user_id) {
+			return true;
+		}
+		return $this->userCanEditList($user_id, $item->list_id);
+	}
+
+	protected function addNoteToArray(array $notes, int $user_id, string|array $note):array
+	{
+		$updated = date('Y-m-d H:i:s');
+		if (is_string($note)) {
+			//It is a new note
+			$notes[] = [
+				'id'	=> uniqid(),
+				'note' => sanitize_textarea_field($note),
+				'user_id' => $user_id,
+				'updated' => $updated
+			];
+		} else {
+			$index = array_search($note['id'], array_column($notes, 'id'));
+			if ($index === false) {
+				//Shouldn't happen, but just in case: add it to the array
+				$notes[] = [
+					'id'	=> sanitize_text_field($note['id']),
+					'note'	=> sanitize_textarea_field($note['note']),
+					'user_id' => $user_id,
+					'updated' => $updated
+				];
+			} else {
+				$notes[$index]['note'] = sanitize_textarea_field($note['note']);
+				$notes[$index]['updated'] = $updated;
+			}
+		}
+
+		return $notes;
+	}
+
+	protected function removeNoteFromArray(array $notes, int $user_id, string $noteID):array
+	{
+		$index = array_search($noteID, array_column($notes, 'id'));
+		if ($index === false) {
+			return $notes;
+		}
+		$note = $notes[$index];
+		if ($user_id !== $note['user_id']) {
+			return $notes;
+		}
+
+		unset($notes[$index]);
+		return array_values($notes); //reindexed array
+	}
+}
diff --git a/inc/managers/ForumManager.php b/inc/managers/ForumManager.php
new file mode 100644
index 0000000..a0dcbb3
--- /dev/null
+++ b/inc/managers/ForumManager.php
@@ -0,0 +1,99 @@
+<?php
+namespace JVBase\managers;
+
+use JVBase\registrar\Registrar;
+
+if (!defined('ABSPATH')) {
+	exit; // Exit if accessed directly
+}
+/**
+ * Forum Manager
+ * TODO
+ * Handles a forum functionality
+ */
+class ForumManager
+{
+	protected CustomTable $relationships;
+	protected CustomTable $forum;
+	protected KarmaManager $karma;
+
+	public function __construct()
+	{
+		$this->defineTables();
+		$this->registerHooks();
+	}
+	public function registerHooks():void
+	{
+		add_action('jvbDefineRegistrar', [$this, 'registerForum']);
+		add_action('jvbDefineRegistrarFields', [$this, 'registerForumFields']);
+		add_action('plugins_loaded', [$this, 'registerForum'], 1);
+		add_action('plugins_loaded', [$this, 'registerForum'], 2);
+	}
+		public function registerForum():void
+		{
+			Registrar::forPost('jvbforum', 'Post', 'Posts')
+				->setIcon('note')
+				->make([
+					'public'	=> false,
+					'taxonomies' => [
+						'topic'
+					]
+				]);
+
+			Registrar::forTerm('jvbtopic', 'Topic', 'Topics')
+				->setIcon('folder')
+				->make([
+					'hierarchical' => true,
+					'for'	=> ['jvbforum']
+				]);
+		}
+		public function registerForumFields():void
+		{
+			$forum = Registrar::getInstance('jvbforum');
+			$fields = $forum->fields();
+
+			$topic = Registrar::getInstance('jvbtopic');
+			$topicFields = $topic->fields();
+		}
+
+
+	protected function defineTables():void
+	{
+		$this->definedRelationshipsTable();
+		$this->karma = KarmaManager::for('forum', 'post');
+	}
+		protected function definedRelationshipsTable():void
+		{
+			$table = CustomTable::for('forum_relationship');
+
+			$table->setColumns([
+				'id'	=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+				'term_id'	=> "{$table->getTermIDType()} NOT NULL",
+				'user_id'	=> "{$table->getUserIDType()} NOT NULL",
+				'profile_id'=> "{$table->getPostIDType()} NOT NULL",
+				'forum_count'	=> 'int(10) unsigned NOT NULL',
+				'last_post_date'=> 'datetime DEFAULT NULL',
+				'updated_at'	=> 'timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
+			]);
+
+			$table->setKeys([
+				['key' => 'PRIMARY', 'value' => '(`id`)'],
+				['key' => 'UNIQUE', 'value' => '`term_user` (`term_id`, `user_id`)'],
+				'`term_id` (`term_id`)',
+				'`user_id` (`user_id`)',
+				'`profile_id` (`profile_id`)',
+			]);
+			$base = BASE;
+			$table->setConstraints([
+				"CONSTRAINT `{$base}fr_term_forum_term` FOREIGN KEY (`term_id`)
+				REFERENCES `{$table->getTermTable()}` (`term_id`) ON DELETE CASCADE",
+				"CONSTRAINT `{$base}fr_term_forum_user` FOREIGN KEY (`user_id`)
+				REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE",
+				"CONSTRAINT `{$base}fr_term_news_profile` FOREIGN KEY (`profile_id`)
+				REFERENCES `{$table->getPostTable()}` (`ID`) ON DELETE CASCADE",
+			]);
+
+			$table->defineTable();
+			$this->relationships = $table;
+		}
+}
diff --git a/inc/managers/InvitationsManager.php b/inc/managers/InvitationsManager.php
index 088556f..fab93bd 100644
--- a/inc/managers/InvitationsManager.php
+++ b/inc/managers/InvitationsManager.php
@@ -20,9 +20,12 @@
 	protected Cache $cache;
 	public function __construct()
 	{
+		$this->defineTable();
+		if (!isset($this->table)) {
+			return;
+		}
 		$this->setInviteConfig();
 		$this->cache = Cache::for('invitations');
-		$this->table = CustomTable::for('invitations');
 		add_action('init', [$this, 'registerInvitationExecutors'], 5);
 
 		add_action('user_register', [$this, 'checkInvitation']);
@@ -31,6 +34,60 @@
 		add_filter(BASE . 'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
 	}
 
+	public function defineTable():void
+	{
+		$terms = Registrar::getFeatured('invitable', 'term');
+		$roles = Features::forMembership()->get('can_invite');
+		if (empty($terms) && empty($roles)) {
+			return;
+		}
+
+		$table = CustomTable::for('invitations');
+		$columns = [
+			'id'				=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+			'name'				=> 'varchar(255) NOT NULL',
+			'email'				=> 'varchar(255) NOT NULL',
+			'invitation_token'	=> 'varchar(255) NOT NULL',
+			'invited_role'		=> 'varchar(50) NOT NULL COMMENT \'Role being invited to\'',
+			'status'			=> 'ENUM(\'pending\',\'accepted\',\'rejected\',\'expired\',\'revoked\') DEFAULT \'pending\'',
+			'inviters'			=> 'JSON NOT NULL COMMENT \'Array of {user_id, invited_at}\'',
+			'new_user_id'		=> $table->getUserIDType().' DEFAULT NULL',
+			'expires_at'		=> 'datetime NOT NULL',
+			'accepted_at'		=> 'datetime DEFAULT NULL',
+			'created_at'		=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP',
+			'updated_at'		=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
+		];
+
+		foreach ($terms as $tax) {
+			$columns["to_{$tax}"] = $table->getTermIDType().' DEFAULT NULL';
+		}
+		$table->setColumns($columns);
+
+
+		$table->setKeys([
+			['key' => 'PRIMARY', 'value' => '(`id`)'],
+			['key' => 'UNIQUE KEY', 'value' => '`unique_email_role` (`email`, `invited_role`)'],
+			'`token_lookup` (`invitation_token`)',
+			'`status_expiry` (`status`, `expires_at`)',
+			'`role_status` (`invited_role`, `status`)',
+			'`email_status` (`email`, `status`)'
+		]);
+		$constraints = [];
+		$base = BASE;
+		global $wpdb;
+		foreach ($terms as $tax) {
+			$constraints[] = "CONSTRAINT `{$base}invitations_{$tax}_fk` FOREIGN KEY (`to_{$tax}`) REFERENCES `{$wpdb->terms}` (`term_id`)
+            ON DELETE SET NULL";
+		}
+		$constraints[] = "CONSTRAINT `{$base}invitations_user_fk`  FOREIGN KEY (`new_user_id`) REFERENCES `{$table->getUserTable()}` (`ID`)
+        ON DELETE SET NULL";
+
+		$table->setConstraints($constraints);
+
+		$table->defineTable();
+		$this->table = $table;
+	}
+
 	protected function setInviteConfig():void
 	{
 		$this->inviteConfig = get_option(BASE.'invitation_config', [
diff --git a/inc/managers/KarmaManager.php b/inc/managers/KarmaManager.php
new file mode 100644
index 0000000..6bba6da
--- /dev/null
+++ b/inc/managers/KarmaManager.php
@@ -0,0 +1,342 @@
+<?php
+namespace JVBase\managers;
+
+use Exception;
+use WP_Error;
+use WP_Post;
+use WP_Term;
+use WP_User;
+
+if (!defined('ABSPATH')) {
+	exit; // Exit if accessed directly
+}
+/**
+ * Response Manager
+ *
+ * Handles a responses
+ *
+ */
+class KarmaManager
+{
+	protected static array $instances;
+	protected string $key;
+	protected string $references;
+	protected CustomTable $table;
+	protected static Cache $cache;
+
+	private function __construct(string $key, ?string $references = null)
+	{
+		$this->key = $key;
+		if (is_null($references)) {
+			$references = match($key){
+				'post'	=> 'post',
+				'term'	=> 'term',
+				'user'	=> 'user',
+				default => false
+			};
+			if (!$references) {
+				error_log('[KarmaManager]::__construct No references set, and no default found. Could not create instance');
+				unset(self::$instances[$key]);
+				return;
+			}
+		}
+
+		$this->references = $references;
+		$test = $this->defineTable();
+		if (!$test) {
+			unset(self::$instances[$key]);
+		}
+
+//		$this->cache = Cache::for($this->references.'_karma');
+//		switch ($this->references) {
+//			case 'post':
+//				$this->cache->connect('post');
+//				break;
+//			case 'term':
+//				$this->cache->connect('term');
+//				break;
+//			case 'user':
+//				$this->cache->connect('user');
+//				break;
+//		}
+		if (!isset(self::$cache)) {
+			self::$cache = Cache::for('user_karma')->connect('user');
+		}
+
+		add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
+	}
+
+	public static function getInstance(string $key):KarmaManager|false
+	{
+		if (!isset(self::$instances[$key])) {
+			return false;
+		}
+		return self::$instances[$key];
+	}
+
+	public static function for(string $key, ?string $references = null): KarmaManager
+	{
+		$key = sanitize_key($key);
+		if (!isset(self::$instances[$key])) {
+			self::$instances[$key] = new KarmaManager($key, $references);
+		}
+
+		return self::$instances[$key];
+	}
+	protected function getReferenceTable(CustomTable $table):array
+	{
+		switch ($this->references) {
+			case 'post':
+			case 'posts':
+				return [
+					$table->getPostIDType(),
+					$table->getPostTable(),
+					'ID'
+				];
+			case 'term':
+			case 'terms':
+				return [
+					$table->getTermIDType(),
+					$table->getTermTable(),
+					'term_id'
+				];
+			case 'user':
+			case 'users':
+				return [
+					$table->getUserIDType(),
+					$table->getUserTable(),
+					'ID'
+				];
+		}
+		//If we get here, it is a custom table.
+		$custom = CustomTable::getInstance($this->references);
+		if (!$custom) {
+			return [false,false,false];
+		}
+		return [
+			'bigint(20)',
+			$custom->getFullTableName(),
+			'id'
+		];
+	}
+	protected function defineTable():bool
+	{
+		$table = CustomTable::for('karma_'.$this->key);
+		[$type, $table, $column] = $this->getReferenceTable($table);
+		if (!$type) {
+			error_log('[KarmaManager]::defineTable Attempted to build reference for invalid table: '.$this->references);
+			CustomTable::destroyInstance('karma_'.$this->key);
+			return false;
+		}
+
+		$table->setColumns([
+			'id'		=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+			'item_id'	=> "{$type} NOT NULL",
+			'user_id'	=> "{$table->getUserIDType()} NOT NULL",
+			'content'	=> 'varchar(255) NOT NULL',
+			'vote'		=> "ENUM('up,'down') NOT NULL",
+			'created_at'=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP',
+			'updated_at'=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
+		]);
+		$table->setKeys([
+			['key' => 'PRIMARY', 'value' => 'id'],
+			['key' => 'UNIQUE', 'value' => '`user_item` (`user_id`, `item_id`, `content`)'],
+			'`item_vote` (`item_id`,`content`,`vote`)',
+			'`item_id` (`item_id`)',
+			'`user_id` (`user_id`)'
+		]);
+		$base = BASE;
+		$table->setConstraints([
+			"CONSTRAINT `{$base}kt_{$type}_item_id` FOREIGN KEY (`item_id`)
+			REFERENCES `{$table}` (`{$column}`) ON DELETE CASCADE",
+			"CONSTRAINT `{$base}kt_{$type}_user_id` FOREIGN KEY (`user_id`)
+			REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
+		]);
+		$table->defineTable();
+		$this->table = $table;
+		return true;
+	}
+
+	protected function response(?bool $success = null, string $message =''):array
+	{
+		$response = [
+			'success'	=> is_null($success) ? 'partial' : $success,
+		];
+		if (!empty($message)) {
+			$response['message'] = $message;
+		}
+		return $response;
+	}
+
+	protected function getItem(int $itemID, string $content):object|false
+	{
+		$based = jvbCheckBase($content);
+		return match($this->references) {
+			'post'	=> get_post($itemID),
+			'term'	=> get_term($itemID, $based),
+			'user'	=> get_userdata($itemID),
+			'response' => JVB()->responses()->response->get(['id' => $itemID])??false,
+			default => false	//TODO if we do have custom tables, we'll have to set that up here
+		};
+	}
+
+	public function vote(int $userID, int $itemID, string $content, ?bool $vote = null):array
+	{
+
+		$content = jvbNoBase($content);
+		$item = $this->getItem($itemID, $content);
+
+		if (!$item || is_wp_error($item)) {
+			return $this->response(false, 'Could not find target');
+		}
+
+		$message = '';
+		//removing record
+		if (is_null($vote)){
+			$success = $this->table->delete([
+				'user_id'	=> $userID,
+				'item_id'	=> $itemID,
+				'content'	=> $content
+			]);
+			if ($success) {
+				$message = 'Vote removed successfully';
+			}
+		} else {
+
+			$success = $this->table->findOrCreate(
+				[
+					'user_id'	=> $userID,
+					'item_id'	=> $itemID,
+					'content'	=> $content
+				],
+				[
+					'vote'	=> $vote ? 'up' : 'down'
+				]
+			);
+			if (!$success) {
+				$message = 'Could not cast your vote';
+			} else {
+				$message = 'Vote cast successfully';
+			}
+		}
+		self::$cache->forget($userID);
+
+		JVB()->queue()->queueOperation(
+			'karmic',
+			0,
+			[
+				'items'	=> [
+					'itemID'	=> $itemID,
+					'content'	=> $content,
+				],
+				'type'		=> $this->references,
+			],
+			[
+				'chunk_key' => 'items',
+			]
+		);
+		return $this->response((bool) $success, $message);
+	}
+
+	public function getCount(int $itemID, string $content):array
+	{
+
+		$upvotes = count($this->table->pluck(
+			'id',
+		[
+			'item_id'	=> $itemID,
+			'content'	=> $content,
+			'vote'		=> 'up'
+		]));
+		$downvotes = count($this->table->pluck(
+			'id',
+			[
+				'item_id'	=> $itemID,
+				'content'	=> $content,
+				'vote'		=> 'down'
+			]
+		));
+		return [
+			'upvotes'	=> $upvotes,
+			'downvotes'	=> $downvotes,
+			'karmic_score'	=> $upvotes - $downvotes
+		];
+	}
+
+	public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array
+	{
+		if ($operation->type !== 'karmic') {
+			return $result;
+		}
+		if ($data['type'] !== $this->references) {
+			return $result;
+		}
+		$results = [];
+		foreach ($data['items'] as $item) {
+			[$upvotes, $downvotes, $karma] = $this->getCount($item['itemID'], $item['content']);
+			try {
+				switch ($this->references) {
+					case 'post':
+						update_post_meta($item['itemID'], BASE.'upvotes', $upvotes);
+						update_post_meta($item['itemID'], BASE.'downvotes', $downvotes);
+						update_post_meta($item['itemID'], BASE.'karma', $karma);
+						break;
+					case 'term':
+						update_term_meta($item['itemID'], BASE.'upvotes', $upvotes);
+						update_term_meta($item['itemID'], BASE.'downvotes', $downvotes);
+						update_term_meta($item['itemID'], BASE.'karma', $karma);
+						break;
+					case 'user':
+						update_user_meta($item['itemID'], BASE.'upvotes', $upvotes);
+						update_user_meta($item['itemID'], BASE.'downvotes', $downvotes);
+						update_user_meta($item['itemID'], BASE.'karma', $karma);
+						break;
+					case 'response':
+						JVB()->responses()->response->update(
+							[
+								'upvotes'	=> $upvotes,
+								'downvotes'	=> $downvotes,
+								'karma'		=> $karma
+							],
+							[
+								'id'	=> $item['itemID']
+							]
+						);
+						break;
+				}
+				$results[$item['itemID']] = true;
+			} catch (Exception $e) {
+				$results[$item['itemID']] = false;
+			}
+
+		}
+		return [
+			'success'	=> true,
+			'result'	=> $results
+		];
+	}
+
+	public static function getUserVotes(int $userID):array
+	{
+		return self::$cache->remember(
+			$userID,
+			function() use($userID) {
+				$votes = [];
+				foreach (self::$instances as $instance) {
+					$results = $instance->table->getMany([
+						'where'	=> [
+							'user_id'	=> $userID
+						]
+					]);
+					foreach ($results as $result) {
+						if (!array_key_exists($result['content'], $votes)) {
+							$votes[$result['content']] = [];
+						}
+						$votes[$result['content']][$result['item_id']] = $result['vote'];
+					}
+				}
+				return $votes;
+			}
+		);
+	}
+}
diff --git a/inc/managers/NotificationManager.php b/inc/managers/NotificationManager.php
index 2aaa4cf..eeec9a6 100644
--- a/inc/managers/NotificationManager.php
+++ b/inc/managers/NotificationManager.php
@@ -1,17 +1,14 @@
 <?php
 namespace JVBase\managers;
 
-use JVBase\JVB;
-use JVBase\registrar\Registrar;
-use WP_Error;
-use Exception;
-use WP_Post;
-use WP_User;
+use JVBase\managers\Notifications\Content;
+use JVBase\managers\Notifications\EmailDigests;
+use JVBase\managers\Notifications\Notifications;
+use JVBase\managers\Notifications\Preferences;
 
 if (!defined('ABSPATH')) {
     exit; // Exit if accessed directly
 }
-//TODO: Ensure this works with the constants setup
 /**
  * NotificationManager - Centralized notification system for edmonton.ink
  *
@@ -21,1747 +18,77 @@
  */
 class NotificationManager
 {
-    protected Cache $userCache; //the individual notifications
-    protected Cache $contentCache;	//the 'shared' notifications on new content that has been created
-	protected Cache $artistsCache;
-	protected Cache $favouritesCache;
-	protected Cache $followerCache;
-    protected string $campaign;
-	protected string $table = BASE.'notifications';
-	protected string $contentTable = BASE.'notifications_content';
-	protected string $seenTable = BASE.'notifications_user_seen';
-	protected string $preferencesTable = BASE.'notification_preferences';
-    protected array $notification_types = [
-        // System notifications
-        'new_favourite'     => [
-            'icon'            => 'heart',
-            'priority'        => 'low',
-            'requires_action' => false,
-            'audience'        => 'content_owner',
-            'email_digest'    => true
-        ],
-        'artist_approved'   => [
-            'icon'            => 'check',
-            'priority'        => 'high',
-            'requires_action' => false,
-            'audience'        => 'single',
-            'email_digest'    => true
-        ],
-        'artist_joined'     => [
-            'icon'              => 'artist',
-            'priority'          => 'low',
-            'requires_action'   => false,
-            'email_digest'      => false,
-        ],
-        'artist_rejected'   => [
-            'icon'            => 'x',
-            'priority'        => 'high',
-            'requires_action' => false,
-            'audience'        => 'single',
-            'email_digest'    => true
-        ],
-        'artist_invitation' => [
-            'icon'            => 'invite',
-            'priority'        => 'high',
-            'requires_action' => true,
-            'audience'        => 'single',
-            'email_digest'    => true
-        ],
-        'artist_request'    => [
-            'icon'            => 'artist',
-            'priority'        => 'high',
-            'requires_action' => true,
-            'audience'        => 'single',
-            'email_digest'    => true,
-        ],
-        'shop_accepted'     => [
-            'icon'            => 'shop',
-            'priority'        => 'high',
-            'requires_action' => false,
-            'audience'        => 'single',
-            'email_digest'    => true,
-        ],
-        'shop_rejected'     => [
-            'icon'            => 'shop',
-            'priority'        => 'high',
-            'requires_action' => false,
-            'audience'        => 'single',
-            'email_digest'    => true,
-        ],
-        'new_term'          => [
-            'icon'            => 'style',
-            'priority'        => 'medium',
-            'requires_action' => true,
-            'audience'        => 'artists',
-            'email_digest'    => false
-        ],
-        'term_approved'     => [
-            'icon'            => 'check',
-            'priority'        => 'high',
-            'requires_action' => false,
-            'audience'        => 'single',
-            'email_digest'    => true
-        ],
-        'term_rejected'     => [
-            'icon'            => 'x',
-            'priority'        => 'medium',
-            'requires_action' => false,
-            'audience'        => 'single',
-            'email_digest'    => true
-        ],
-        'list_shared'       => [
-            'icon'            => 'share',
-            'priority'        => 'medium',
-            'requires_action' => false,
-            'audience'        => 'single',
-            'email_digest'    => true
-        ],
-        'list_share_accepted' => [
-            'icon'            => 'check',
-            'priority'        => 'low',
-            'requires_action' => false,
-            'audience'        => 'single',
-            'email_digest'    => true
-        ],
-        'list_share_revoked' => [
-            'icon'            => 'close',
-            'priority'        => 'medium',
-            'requires_action' => false,
-            'audience'        => 'single',
-            'email_digest'    => true
-        ],
-        'system_message'    => [
-            'icon'            => 'info',
-            'priority'        => 'high',
-            'requires_action' => false,
-            'audience'        => 'all',
-            'email_digest'    => false
-        ]
-    ];
+   protected Notifications $notifications;
+   protected Content $content;
+   protected EmailDigests $digest;
+   protected Preferences $preferences;
 
-    /**
-     * Constructor
-     */
-    public function __construct()
-    {
-        $this->userCache = Cache::for('userNotifications', WEEK_IN_SECONDS);
-        $this->contentCache = Cache::for('contentNotifications', WEEK_IN_SECONDS)->connect('post', true)->connect('taxonomy', true);
-        $this->artistsCache = Cache::for('artist', WEEK_IN_SECONDS)->connect('post');
-        $this->favouritesCache = Cache::for('favouritedUsers', WEEK_IN_SECONDS)->connect('favourites');
-        $this->followerCache = Cache::for('totalFollowers', WEEK_IN_SECONDS)->connect('favourites');
+   public function __construct()
+   {
+	   $this->notifications = new Notifications();
+	   $this->content = new Content();
+	   $this->digest = new EmailDigests();
+	   $this->preferences = new Preferences();
+   }
 
-        // Add filter for bulk operation handling
-        add_filter(BASE . 'handle_bulk_operation', [ $this, 'processOperation' ], 10, 3);
-
-        add_action(BASE . 'cleanup_notifications', [$this, 'cleanupOldNotifications']);
-
-        // Track content creation for notifications
-        add_action('save_post', [ $this, 'trackContentCreation' ], 10, 3);
-        add_action('saved_term', [ $this, 'track_shop_creation' ], 10, 3);
-
-        // Generate content summaries when users log in
-        add_action('wp_login', [ $this, 'generateUserContentNotifications' ], 10, 2);
-
-        // Register digest cron jobs
-        $this->registerCron();
-    }
-
-    /**
-     * Registers the digest cron jobs
-     * @return void
-     */
-    protected function registerCron():void
-    {
-        add_action(BASE . 'notification_digest_daily', [ $this, 'runDailyDigests' ]);
-        add_action(BASE . 'notification_digest_weekly', [ $this, 'runWeeklyDigests' ]);
-        add_action(BASE . 'notification_digest_monthly', [ $this, 'runMonthlyDigests' ]);
-    }
-
-    /************************************************************
-     * Basic Notification Methods
-     ************************************************************/
-    /**
-     * @return array
-     */
-    public function getNotificationTypes():array
-    {
-        return $this->notification_types;
-    }
-    /**
-     * Add a new notification
-     *
-     * @param int|array $user_ids Recipient user ID(s)
-     * @param string $type Notification type
-     * @param int|null $action_user_id User who performed the action (optional)
-     * @param string $message Notification message (optional)
-     * @param int|null $target_id Related target ID (optional)
-     * @param string|null $target_type Related target type (optional)
-     * @param array|null $context Additional contextual data (optional)
-     *
-     * @return bool|WP_Error Success or error
-     */
-    public function addNotification(
-        mixed $user_ids,
-        string $type,
-        int|null $action_user_id = null,
-        string $message = '',
-        int|null $target_id = null,
-        string|null $target_type = null,
-        array|null $context = null
-    ):bool|WP_Error {
-        // Validate notification type
-        if (!isset($this->notification_types[$type])) {
-            $this->logError("Invalid notification type: $type", [
-                'user_ids' => is_array($user_ids) ? implode(',', $user_ids) : $user_ids,
-                'message' => $message
-            ], 'warning');
-            return new WP_Error('invalid_type', 'Invalid notification type');
-        }
-
-        // Handle single user or array of users
-        $user_ids = is_array($user_ids) ? $user_ids : [$user_ids];
-
-        if (empty($user_ids)) {
-            return false;
-        }
-        // Queue the bulk notification operation
-        $queue = JVB()->queue();
-        $queue->queueOperation(
-            'addNotification',
-            $action_user_id,
-            [
-                'user_ids' => $user_ids,
-                'type' => $type,
-                'action_user_id' => $action_user_id,
-                'message' => $message,
-                'target_id' => $target_id,
-                'target_type' => $target_type,
-                'context' => $context
-            ],
-            [
-                'count' => count($user_ids),
-                'priority' => 'normal',
-				'chunk_key'	=> 'user_ids',
-				'chunk_size'=> 50,
-                'operation_id' => 'notifications' . uniqid()
-            ]
-        );
-
-        return true;
-    }
-
-    /**
-     * Process notification operation
-     *
-     * @param int|array $user_ids Recipient user ID(s)
-     * @param string $type Notification type
-     * @param int|null $action_user_id User who performed the action (optional)
-     * @param string $message Notification message (optional)
-     * @param int|null $target_id Related target ID (optional)
-     * @param string|null $target_type Related target type (optional)
-     * @param array|null $context Additional contextual data (optional)
-     *
-     * @return bool|WP_Error Success or error
-     */
-    public function processNotification(
-        int|array $user_ids,
-        string $type,
-        int|null $action_user_id = null,
-        string $message = '',
-        int|null $target_id = null,
-        string|null $target_type = null,
-        array|null $context = null
-    ):bool|WP_Error {
-        $config = $this->notification_types[$type];
-        $notifications = [];
-        $errors = [];
-
-        $user_ids = is_array($user_ids) ? $user_ids : [$user_ids];
-
-        global $wpdb;
-        foreach ($user_ids as $user_id) {
-            // Skip invalid users
-            if (!$user_id || $user_id <= 0) {
-                $errors[] = "Invalid user ID: $user_id";
-                continue;
-            }
-
-            // Skip sending notifications to self if action_user_id == user_id
-            if ($action_user_id && $action_user_id == $user_id && !($config['notify_self'] ?? false)) {
-                continue;
-            }
-
-            try {
-                // Prepare context data
-                $context_json = null;
-                if (!empty($context)) {
-                    $context_json = json_encode($context);
-                }
-
-                // Insert new notification
-                $result = $wpdb->insert(
-                    $wpdb->prefix . $this->table,
-                    [
-                        'owner_id' => $user_id,
-                        'action_user_id' => $action_user_id,
-                        'type' => $type,
-                        'status' => 'unread',
-                        'message' => $message,
-                        'priority' => $config['priority'] ?? 'normal',
-                        'target_id' => $target_id,
-                        'target_type' => $target_type,
-                        'context' => $context_json,
-                        'requires_action' => !empty($config['requires_action']) ? 1 : 0,
-                        'created_at' => current_time('mysql'),
-                        'updated_at' => current_time('mysql')
-                    ]
-                );
-
-                if ($result) {
-                    $notification_id = $wpdb->insert_id;
-                    $notifications[] = $notification_id;
-
-                    // Clear cache for this user
-                    $this->clearNotificationCache($user_id);
-                } else {
-                    $errors[] = "Database error for user $user_id: " . $wpdb->last_error;
-                    $this->logError("Failed to insert notification", [
-                        'user_id' => $user_id,
-                        'type' => $type,
-                        'db_error' => $wpdb->last_error
-                    ]);
-                }
-            } catch (Exception $e) {
-                $errors[] = "Exception for user $user_id: " . $e->getMessage();
-                $this->logError("Error adding notification", [
-                    'user_id' => $user_id,
-                    'type' => $type,
-                    'message' => $message,
-                    'error' => $e->getMessage()
-                ]);
-            }
-        }
-
-        // Return results
-        if (!empty($notifications) && !empty($errors)) {
-            $this->logError("Some notifications failed", [
-                'successful' => count($notifications),
-                'errors' => $errors
-            ], 'warning');
-        }
-
-        if (empty($notifications) && !empty($errors)) {
-            return new WP_Error('notification_failed', 'All notifications failed', [
-                'errors' => $errors
-            ]);
-        }
-
-        return !empty($notifications);
-    }
-
-    /**
-     * Wrapper for notifying verified artists
-     *
-     * @param string $type Notification type
-     * @param int|null $action_user_id User who performed the action (optional)
-     * @param string $message Notification message (optional)
-     * @param int|null $target_id Related target ID (optional)
-     * @param string|null $target_type Related target type (optional)
-     * @param array|null $context Additional contextual data (optional)
-     *
-     * @return bool|WP_Error Success or error
-     */
-    public function notifyVerifiedArtists(string $type, int|null $action_user_id = null, string $message = '', int|null $target_id = null, string|null $target_type = null, array|null $context = null):bool|WP_Error
-    {
-        $artists = $this->getVerified('artist');
-        return $this->addNotification($artists, $type, $action_user_id, $message, $target_id, $target_type, $context);
-    }
-    /**
-     * Wrapper for notifying verified partners
-     *
-     * @param string $type Notification type
-     * @param int|null $action_user_id User who performed the action (optional)
-     * @param string $message Notification message (optional)
-     * @param int|null $target_id Related target ID (optional)
-     * @param string|null $target_type Related target type (optional)
-     * @param array|null $context Additional contextual data (optional)
-     *
-     * @return bool|WP_Error Success or error
-     */
-    public function notifyVerifiedPartners(string $type, int|null $action_user_id = null, string $message = '', int|null $target_id = null, string|null $target_type = null, array|null $context = null):bool|WP_Error
-    {
-        $artists = $this->getVerified('partner');
-        return $this->addNotification($artists, $type, $action_user_id, $message, $target_id, $target_type, $context);
-    }
-    /**
-     * Wrapper for notifying verified partners
-     *
-     * @param string $type Notification type
-     * @param int|null $action_user_id User who performed the action (optional)
-     * @param string $message Notification message (optional)
-     * @param int|null $target_id Related target ID (optional)
-     * @param string|null $target_type Related target type (optional)
-     * @param array|null $context Additional contextual data (optional)
-     *
-     * @return bool|WP_Error Success or error
-     */
-    public function notifyEnthusiasts(string $type, int|null $action_user_id = null, string $message = '', int|null $target_id = null, string|null $target_type = null, array|null $context = null):bool|WP_Error
-    {
-        $artists = $this->getUserIDs('enthusiast');
-        return $this->addNotification($artists, $type, $action_user_id, $message, $target_id, $target_type, $context);
-    }
-    /**
-     * Wrapper for notifying verified partners
-     *
-     * @param string $type Notification type
-     * @param int|null $action_user_id User who performed the action (optional)
-     * @param string $message Notification message (optional)
-     * @param int|null $target_id Related target ID (optional)
-     * @param string|null $target_type Related target type (optional)
-     * @param array|null $context Additional contextual data (optional)
-     *
-     * @return bool|WP_Error Success or error
-     */
-    public function notifyEveryone(string $type, int|null $action_user_id = null, string $message = '', int|null $target_id = null, string|null $target_type = null, array|null $context = null):bool|WP_Error
-    {
-        $artists = $this->getUserIDs(Registrar::getRegistered('user'));
-        return $this->addNotification($artists, $type, $action_user_id, $message, $target_id, $target_type, $context);
-    }
-
-
-
-    /************************************************************
-     * Content Notification Methods
-     ************************************************************/
-
-    /**
-     * Track content creation or updates for notification purposes
-     *
-     * @param int $post_id Post ID
-     * @param WP_Post $post Post object
-     * @param bool $update Whether this is an update
-     * @return void
-     */
-    public function trackContentCreation(int $post_id, WP_POST $post, bool $update):void
-    {
-		// SAFETY: Skip attachments and other non-content post types
-		if (in_array($post->post_type, jvbIgnoredPostTypes())) {
-			return;
-		}
-        // Skip if not a published post
-        if ($post->post_status !== 'publish') {
-            return;
-        }
-
-        // Check if this is a relevant content type
-        $content_types = array_map(function($type) {return jvbCheckBase($type->getSlug()); }, Registrar::getFeatured('show_feed', 'post'));
-        if (!in_array($post->post_type, $content_types)) {
-            return;
-        }
-
-        // Check if the artist has any followers before tracking
-        $follower_count = $this->getFollowerCount($post->post_author);
-        if ($follower_count === 0) {
-            return; // No need to track if nobody follows this artist
-        }
-
-        // Get the clean content type for storing
-        $content_type = str_replace(BASE, '', $post->post_type);
-
-        // Update the artist's content notification records
-        $this->updateContentNotificationRecord($post->post_author, $content_type, $post_id, $update);
-    }
-
-    /**
-     * Update content notification record for an artist
-     *
-     * @param int $artist_id Artist user ID
-     * @param string $content_type Content type (tattoo, artwork, etc)
-     * @param int $content_id Content post ID
-     * @param bool $is_update Whether this is an update to existing content
-     *
-     * @return bool Success or failure
-     */
-    protected function updateContentNotificationRecord(int $artist_id, string $content_type, int $content_id, bool $is_update = false):bool
-    {
-        global $wpdb;
-        $table = $wpdb->prefix . $this->contentTable;
-
-        // Start transaction
-        $wpdb->query('START TRANSACTION');
-        $success = false;
-
-        try {
-            // Get today's date
-            $today = date('Y-m-d');
-            $frequency_updates = [];
-
-            // Update records for each frequency
-            foreach (['daily', 'weekly', 'monthly'] as $frequency) {
-                // Find or create a record for this artist, date and frequency
-                $record = $wpdb->get_row($wpdb->prepare(
-                    "SELECT * FROM {$table}
-                 WHERE user_id = %d AND date = %s AND frequency = %s",
-                    $artist_id,
-                    $today,
-                    $frequency
-                ));
-
-                if ($record) {
-                    $frequency_updates[$frequency] = $this->updateExistingContentRecord(
-                        $record,
-                        $content_type,
-                        $content_id,
-                        $is_update
-                    );
-                } else {
-                    $frequency_updates[$frequency] = $this->createNewContentRecord(
-                        $artist_id,
-                        $frequency,
-                        $content_type,
-                        $content_id,
-                        $is_update
-                    );
-                }
-            }
-
-            // Check if all updates were successful
-            $success = !in_array(false, $frequency_updates, true);
-
-            if ($success) {
-                $wpdb->query('COMMIT');
-                return true;
-            } else {
-                // If any update failed, roll back
-                $wpdb->query('ROLLBACK');
-                $this->logError("Failed to update content notification records", [
-                    'artist_id' => $artist_id,
-                    'content_type' => $content_type,
-                    'content_id' => $content_id,
-                    'updates' => $frequency_updates
-                ]);
-                return false;
-            }
-        } catch (Exception $e) {
-            $wpdb->query('ROLLBACK');
-            $this->logError("Exception in content notification update", [
-                'artist_id' => $artist_id,
-                'content_type' => $content_type,
-                'content_id' => $content_id,
-                'error' => $e->getMessage()
-            ]);
-            return false;
-        }
-    }
-
-    /**
-     * Update an existing content notification record
-     *
-     * @param object $record Existing database record
-     * @param string $content_type Content type
-     * @param int $content_id Content ID
-     * @param bool $is_update Whether this is an update
-     *
-     * @return bool Success or failure
-     */
-    protected function updateExistingContentRecord(object $record, string $content_type, int $content_id, bool $is_update):bool
-    {
-        global $wpdb;
-        $table = $wpdb->prefix . $this->contentTable;
-
-        // Decode existing JSON data
-        $new_items     = json_decode($record->new_items, true) ?: [];
-        $updated_items = json_decode($record->updated_items, true) ?: [];
-
-        // Initialize arrays if not present
-        if (!isset($new_items[BASE.$content_type])) {
-            $new_items[BASE.$content_type] = [];
-        }
-        if (!isset($updated_items[BASE.$content_type])) {
-            $updated_items[BASE.$content_type] = [];
-        }
-
-        // Add ID to appropriate array
-        if ($is_update) {
-            // Add to updated array if not already there and not in new items
-            if (!in_array($content_id, $updated_items[BASE.$content_type]) &&
-                 !in_array($content_id, $new_items[BASE.$content_type])) {
-                $updated_items[BASE.$content_type][] = $content_id;
-            }
-        } else {
-            // Add to new array if not already there
-            if (!in_array($content_id, $new_items[BASE.$content_type])) {
-                $new_items[BASE.$content_type][] = $content_id;
-            }
-        }
-
-        // Prepare update data
-        $update_data = [
-            'new_items'     => json_encode($new_items),
-            'updated_items' => json_encode($updated_items),
-            'updated_at'    => current_time('mysql')
-        ];
-
-        // Update appropriate count column
-        $count_column = "{$content_type}_count";
-        if (property_exists($record, $count_column)) {
-            $update_data[ $count_column ] = $record->$count_column + ( $is_update ? 0 : 1 );
-        }
-
-        // Update total count
-        $update_data['total_items'] = $record->total_items + ( $is_update ? 0 : 1 );
-
-        // Update the record
-        return $wpdb->update(
-            $table,
-            $update_data,
-            [ 'id' => $record->id ]
-        ) !== false;
-    }
-
-    /**
-     * Create a new content notification record
-     *
-     * @param int $artist_id Artist user ID
-     * @param string $frequency Frequency (daily, weekly, monthly)
-     * @param string $content_type Content type
-     * @param int $content_id Content ID
-     * @param bool $is_update Whether this is an update
-     *
-     * @return bool Success or failure
-     */
-    protected function createNewContentRecord(int $artist_id, string $frequency, string $content_type, int $content_id, bool $is_update):bool
-    {
-        global $wpdb;
-        $table = $wpdb->prefix . $this->contentTable;
-
-        // Initialize arrays for new and updated items
-        $new_items     = [];
-        $updated_items = [];
-
-        if ($is_update) {
-            $updated_items[BASE.$content_type] = [ $content_id ];
-        } else {
-            $new_items[BASE.$content_type] = [ $content_id ];
-        }
-
-        // Prepare insert data
-        $insert_data = [
-            'user_id'       => $artist_id,
-            'date'          => date('Y-m-d'),
-            'frequency'     => $frequency,
-            'total_items'   => 1,
-            'new_items'     => json_encode($new_items),
-            'updated_items' => json_encode($updated_items),
-            'created_at'    => current_time('mysql'),
-            'updated_at'    => current_time('mysql')
-        ];
-
-        // Set count column
-        $insert_data["{$content_type}_count"] = $is_update ? 0 : 1;
-
-        // Insert the record
-        return $wpdb->insert($table, $insert_data) !== false;
-    }
-
-    /**
-     * Generate content notifications for a user upon login
-     *
-     * @param string $username Username
-     * @param WP_User $user User object
-     * @return void
-     */
-    public function generateUserContentNotifications(string $username, WP_User $user):void
-    {
-        $this->createContentSeenRecords($user->ID);
-    }
-
-    /**
-     * Create content seen records for a user's followed artists
-     *
-     * @param int $user_id User ID
-     *
-     * @return int Number of notifications created
-     */
-    protected function createContentSeenRecords(int $user_id):int
-    {
-        global $wpdb;
-
-        try {
-            // Get followed artists
-            $followed_artists = $this->getFollowedArtists($user_id);
-            if (empty($followed_artists)) {
-                return 0;
-            }
-
-            // Get the last time notifications were checked
-            $last_check = get_user_meta($user_id, BASE . 'last_content_check', true);
-            $since_date = $last_check ? date('Y-m-d', strtotime($last_check)) : date('Y-m-d', strtotime('-7 days'));
-
-            // Create placeholders for SQL query
-            $placeholders = implode(',', array_fill(0, count($followed_artists), '%d'));
-
-            // Get content notifications since last check
-            $content_records = $wpdb->get_results(
-                $wpdb->prepare(
-                    "SELECT * FROM {$wpdb->prefix}{$this->contentTable}
-                     WHERE user_id IN ($placeholders)
-                     AND date >= %s
-                     AND total_items > 0",
-                    array_merge($followed_artists, [ $since_date ])
-                )
-            );
-
-            if (empty($content_records)) {
-                return 0;
-            }
-
-            // Add user seen records for each content notification
-            $count           = 0;
-            $user_seen_table = $wpdb->prefix . $this->seenTable;
-
-            foreach ($content_records as $record) {
-                // Check if record already exists
-                $exists = $wpdb->get_var(
-                    $wpdb->prepare(
-                        "SELECT id FROM {$user_seen_table}
-                         WHERE user_id = %d AND content_notification_id = %d",
-                        $user_id,
-                        $record->id
-                    )
-                );
-
-                if (!$exists) {
-                    // Create new seen record
-                    $wpdb->insert(
-                        $user_seen_table,
-                        [
-                            'user_id'                 => $user_id,
-                            'content_notification_id' => $record->id,
-                            'status'                  => 'unread',
-                            'created_at'              => current_time('mysql')
-                        ]
-                    );
-                    $count ++;
-                }
-            }
-
-            // Update last check time
-            update_user_meta($user_id, BASE . 'last_content_check', current_time('mysql'));
-
-            // Clear cache
-            $this->clearNotificationCache($user_id);
-
-            return $count;
-        } catch (Exception $e) {
-            $this->logError("Error creating content seen records: " . $e->getMessage(), [
-                'user_id' => $user_id
-            ]);
-
-            return 0;
-        }
-    }
-
-
-    /************************************************************
-     * Notification Digest Methods
-     ************************************************************/
-
-    /**
-     * Process daily notification digests
-     */
-    public function runDailyDigests():void
-    {
-        $this->campaign = 'daily_digest_' . date('Y-m-d');
-        $this->processDigest('daily');
-    }
-
-    /**
-     * Process weekly notification digests
-     */
-    public function runWeeklyDigests():void
-    {
-        $this->campaign = 'weekly_digest_' . date('Y-m-d');
-        $this->processDigest('weekly');
-    }
-
-    /**
-     * Process monthly notification digests
-     */
-    public function runMonthlyDigests():void
-    {
-        $this->campaign = 'monthly_digest_' . date('Y-m-d');
-        $this->processDigest('monthly');
-    }
-
-    /**
-     * Process digests for a specific frequency
-     *
-     * @param string $frequency Digest frequency (daily, weekly, monthly)
-     *
-     * @return bool Success or failure
-     */
-    protected function processDigest(string $frequency):bool
-    {
-        // Get users with this digest frequency preference
-        $users = $this->getUsersForDigest($frequency);
-
-        if (empty($users)) {
-            return true; // No users to process
-        }
-
-        // Queue digest generation
-        $queue = JVB()->queue();
-        $queue->queueOperation(
-            'email_notification_digest',
-            0, // System user
-            [
-                'frequency' => $frequency,
-                'users'     => $users
-            ],
-            [
-                'count' => count($users),
-				'chunk_key'	=> 'users',
-				'chunk_size' => 20,
-                'priority'        => 'normal',
-                'operation_id'    => 'notification_digest_' . date('Y_m_d')
-            ]
-        );
-
-        return true;
-    }
-
-    /**
-     * Get users who have subscribed to a specific digest frequency
-     *
-     * @param string $frequency Digest frequency
-     *
-     * @return array Array of user IDs
-     */
-    protected function getUsersForDigest(string $frequency):array
-    {
-        global $wpdb;
-        $table = $wpdb->prefix . $this->preferencesTable;
-
-        // Get users with this frequency setting
-        $users = $wpdb->get_col(
-            $wpdb->prepare(
-                "SELECT DISTINCT user_id FROM $table
-                 WHERE frequency = %s",
-                $frequency
-            )
-        );
-
-        return $users ?: [];
-    }
-
-    /**
-     * Generate and send a digest email for a user
-     *
-     * @param int $user_id User ID
-     * @param string $frequency Digest frequency
-     *
-     * @return bool Success status
-     */
-    public function generateUserDigest(int $user_id, string $frequency):bool
-    {
-        try {
-            $user = get_userdata($user_id);
-            if (!$user || !is_email($user->user_email)) {
-                return false;
-            }
-
-            // Get date range based on frequency
-            $today      = date('Y-m-d');
-            $since_date = $this->getSinceDate($frequency, $today);
-
-            // Get regular notifications
-            $notifications = $this->getDigestNotifications($user_id);
-
-            // Get content updates from followed artists
-            $content_updates = $this->getContentUpdatesForDigest($user_id, $since_date);
-
-            if (empty($notifications) && empty($content_updates)) {
-                return true; // Nothing to send
-            }
-
-            // Generate and send email
-            $sent = $this->sendDigestEmail($user, $frequency, $notifications, $content_updates);
-
-            if ($sent) {
-                // Update preferences last_sent timestamp
-                $this->updateDigestTimestamps($user_id, $frequency);
-
-                return true;
-            }
-
-            return false;
-        } catch (Exception $e) {
-            $this->logError("Error generating digest for user $user_id: " . $e->getMessage());
-
-            return false;
-        }
-    }
-
-    /**
-     * Get notifications for a digest
-     *
-     * @param int $user_id User ID
-     *
-     * @return array Notification objects
-     */
-    protected function getDigestNotifications(int $user_id):array
-    {
-        global $wpdb;
-
-        // Get unread, non-emailed notifications
-        return $wpdb->get_results(
-            $wpdb->prepare(
-                "SELECT * FROM {$wpdb->prefix}{$this->table}
-                 WHERE user_id = %d
-                 AND status = 'unread'
-                 AND emailed_at IS NULL
-                 ORDER BY priority DESC, created_at DESC",
-                $user_id
-            )
-        );
-    }
-
-    /**
-     * Get content updates for digest
-     *
-     * @param int $user_id User ID
-     * @param string $since_date Date string (YYYY-MM-DD)
-     *
-     * @return array Content update records
-     */
-    protected function getContentUpdatesForDigest(int $user_id, string $since_date):array
-    {
-        // Get followed artists
-        $followed_artists = $this->getFollowedArtists($user_id);
-        if (empty($followed_artists)) {
-            return [];
-        }
-
-        global $wpdb;
-
-        // Create placeholders for SQL IN clause
-        $placeholders = implode(',', array_fill(0, count($followed_artists), '%d'));
-
-        // Get content records since the date
-        return $wpdb->get_results(
-            $wpdb->prepare(
-                "SELECT * FROM {$wpdb->prefix}{$this->contentTable}
-                 WHERE user_id IN ($placeholders)
-                 AND date >= %s
-                 AND total_items > 0
-                 ORDER BY date DESC",
-                array_merge($followed_artists, [ $since_date ])
-            )
-        );
-    }
-
-    /**
-     * Get since date for a frequency
-     *
-     * @param string $frequency Digest frequency
-     * @param string $today Today's date
-     *
-     * @return string Date string (YYYY-MM-DD)
-     */
-    protected function getSinceDate(string $frequency, string $today):string
-    {
-        switch ($frequency) {
-            case 'daily':
-                return date('Y-m-d', strtotime('-1 day', strtotime($today)));
-            case 'weekly':
-                return date('Y-m-d', strtotime('-1 week', strtotime($today)));
-            case 'monthly':
-                return date('Y-m-d', strtotime('-1 month', strtotime($today)));
-            default:
-                return '';
-        }
-    }
-
-    /**
-     * Send a digest email
-     *
-     * @param WP_User $user User object
-     * @param string $frequency Digest frequency
-     * @param array $notifications Regular notifications
-     * @param array $content_updates Content update records
-     *
-     * @return bool Whether the email was sent
-     */
-    protected function sendDigestEmail(WP_User $user, string $frequency, array $notifications, array $content_updates):bool
-    {
-        // Generate subject based on frequency
-        $subjects = [
-            'daily'   => [
-                "[edmonton.ink] Your " . date('l') . " Ink Update ♡",
-                "[edmonton.ink] Fresh Ink Alert - Your " . date('l') . " Digest",
-                "[edmonton.ink] What You Missed in Edmonton's Tattoo Scene Today"
-            ],
-            'weekly'  => [
-                "[edmonton.ink] Your Weekly Roundup from edmonton.ink ♡",
-                "[edmonton.ink] This Week in Edmonton's Tattoo Scene",
-                "[edmonton.ink] Weekly Ink Update - Fresh From the Scene"
-            ],
-            'monthly' => [
-                "[edmonton.ink] Monthly Ink Roundup ♡",
-                "[edmonton.ink] Your Monthly Scene Report: " . date('F') . " Edition",
-                "[edmonton.ink] " . date('F') . " in Edmonton Tattoos"
-            ]
-        ];
-
-        // Randomly select a subject for variety
-        $subject_options = $subjects[ $frequency ] ?? [ "Your edmonton.ink Update" ];
-        $subject         = $subject_options[ array_rand($subject_options) ];
-
-        // Generate email content
-        $content = $this->generateDigestContent($user, $frequency, $notifications, $content_updates);
-
-        // Add tracking pixel
-        $tracking_code = sprintf(
-            '<img src="%s/track-email.php?uid=%s&digest=%s" width="1" height="1" alt="" />',
-            site_url(),
-            base64_encode($user->ID),
-            $frequency
-        );
-        $content       .= $tracking_code;
-
-        // Set header based on frequency
-        $header = match ($frequency) {
-            'daily' => "TODAY'S INK DROP",
-            'weekly' => "THIS WEEK'S SCENE REPORT",
-            'monthly' => "MONTHLY INK ROUNDUP",
-            default => "YOUR INK UPDATES",
-        };
-
-        // Send the email
-        return JVB()->email()->sendEmail($user->user_email, $subject, $content, $header);
-    }
-
-    /**
-     * Generate HTML content for digest email
-     *
-     * @param WP_User $user User object
-     * @param string $frequency Digest frequency
-     * @param array $notifications Regular notifications
-     * @param array $content_updates Content update records
-     *
-     * @return string HTML email content
-     */
-	protected function generateDigestContent(WP_User $user, string $frequency, array $notifications, array $content_updates):string
+   /************************************************
+	* PREFERENCES
+   ************************************************/
+	public function getUsersByFrequency(string $frequency):array
 	{
-		$content = sprintf('<p>Hey %s,</p>', $user->first_name ?: $user->display_name);
-
-		// Intro text based on frequency
-		switch ($frequency) {
-			case 'daily':
-				$content .= '<p>Here\'s what happened in Edmonton\'s tattoo scene today:</p>';
-				break;
-			case 'weekly':
-				$content .= '<p>Here\'s what you missed in Edmonton\'s tattoo scene this week:</p>';
-				break;
-			case 'monthly':
-				$content .= sprintf('<p>Here\'s your monthly roundup of what happened in %s in Edmonton\'s tattoo scene:</p>', date('F'));
-				break;
-		}
-
-		// Process artist content updates - the most visually interesting part
-		$content .= $this->generateContentUpdatesSection($content_updates);
-
-		// Process regular notifications
-		if (!empty($notifications)) {
-			$content .= $this->generateNotificationsSection($notifications);
-		}
-
-		// Add footer content
-		$content .= JVB()->email()->divider();
-		$settings_url = add_query_arg([
-			'utm_source'   => 'email',
-			'utm_medium'   => 'digest',
-			'utm_campaign' => $this->campaign
-		], site_url('/dash/settings/'));
-
-		$content .= sprintf(
-			'<p>You\'re receiving this %s digest because you follow artists on edmonton.ink. You can %s at any time.</p>',
-			$frequency,
-			'<a href="' . esc_url($settings_url) . '">adjust your notification settings</a>'
-		);
-
-		return $content;
+		return $this->preferences->getUsersByFrequency($frequency);
 	}
 
-    /**
-     * Generate HTML section for content updates
-     *
-     * @param array $content_updates Content update records
-     *
-     * @return string HTML content
-     * TODO: This needs some work
-     */
-    protected function generateContentUpdatesSection(array $content_updates):string
-    {
-        if (empty($content_updates)) {
-            return '';
-        }
-
-        $content = '';
-        $cache   = Cache::for('digest_content', HOUR_IN_SECONDS * 6); // Cache for 6 hours
-
-        // Group updates by artist
-        $updates_by_artist = [];
-        foreach ($content_updates as $update) {
-            if (!isset($updates_by_artist[$update->user_id])) {
-                $updates_by_artist[ $update->user_id ] = [];
-            }
-            $updates_by_artist[ $update->user_id ][] = $update;
-        }
-
-        // Process each artist's updates
-        foreach ($updates_by_artist as $artist_id => $updates) {
-            $artist_data = $this->getArtistData($artist_id);
-            if (!$artist_data) {
-                continue;
-            }
-
-            // Combine all content updates from this artist
-            $combined_new_items = [];
-
-            foreach ($updates as $update) {
-                $new_items = json_decode($update->new_items, true) ?: [];
-
-                foreach ($new_items as $type => $ids) {
-                    if (!isset($combined_new_items[ $type ])) {
-                        $combined_new_items[ $type ] = [];
-                    }
-                    $combined_new_items[ $type ] = array_merge($combined_new_items[ $type ], $ids);
-                    // Remove duplicates
-                    $combined_new_items[ $type ] = array_unique($combined_new_items[ $type ]);
-                }
-            }
-
-            // Skip if no content to show
-            if (empty($combined_new_items)) {
-                continue;
-            }
-
-            // Add artist header
-            $content .= sprintf(
-                '<h3><a href="%s" class="text-link">%s</a></h3>',
-                esc_url(add_query_arg([
-                    'utm_source'   => 'email',
-                    'utm_medium'   => 'digest',
-                    'utm_campaign' => $this->campaign
-                ], $artist_data['url'])),
-                esc_html($artist_data['display_name'])
-            );
-
-            // Process each content type
-            foreach ($combined_new_items as $type => $ids) {
-                if (empty($ids)) {
-                    continue;
-                }
-
-                $clean_type = str_replace(BASE, '', $type);
-                $type_label = $this->getContentTypeLabel($clean_type);
-
-                // Add subheading for content type
-                $content .= sprintf('<h4>%s</h4>', esc_html($type_label));
-
-                // Display up to 3 items in a row
-                $display_ids = array_slice($ids, 0, 3);
-                $content     .= '<table><tr>';
-
-                foreach ($display_ids as $item_id) {
-                    // Get cached item data or fetch it
-                    $cache_key = "digest_{$type}_{$item_id}";
-                    $item_data = $cache->get($cache_key);
-
-                    if ($item_data === false) {
-                        $item_data = $this->getContentDetails($type, $item_id, $artist_id);
-                        if ($item_data) {
-                            $cache->set($cache_key, $item_data);
-                        }
-                    }
-
-                    if (!$item_data) {
-                        continue;
-                    }
-
-                    // Add item to email
-                    $content .= '<td style="padding: 10px; width: 33.3%; vertical-align: top; text-align: center;">';
-                    $content .= '<div style="margin-bottom: 10px;">';
-                    $content .= sprintf(
-                        '<a href="%s"><img src="%s" alt="%s" style="width: 100%%; max-width: 150px; height: auto; border-radius: 4px;"></a>',
-                        esc_url(add_query_arg([
-                            'utm_source'   => 'email',
-                            'utm_medium'   => 'digest',
-                            'utm_campaign' => $this->campaign
-                        ], $item_data['url'])),
-                        esc_url($item_data['image']),
-                        esc_attr($item_data['title'])
-                    );
-                    $content .= '</div>';
-                    $content .= sprintf('<p style="margin: 5px 0; font-weight: bold;">%s</p>', esc_html($item_data['title']));
-                    $content .= '</td>';
-                }
-
-                // Fill empty cells if needed
-                for ($i = count($display_ids); $i < 3; $i++) {
-                    $content .= '<td style="width: 33.3%;"></td>';
-                }
-
-                $content .= '</tr></table>';
-
-                // Add "See all" link if there are more items
-                if (count($ids) > 3) {
-                    $content .= sprintf(
-                        '<p style="text-align: right;"><a href="%s" class="text-link">See all %d %s →</a></p>',
-                        esc_url(add_query_arg([
-                            'utm_source'   => 'email',
-                            'utm_medium'   => 'digest',
-                            'utm_campaign' => $this->campaign
-                        ], site_url('/dash/feed/?type=' . $clean_type . '&artist=' . $artist_id))),
-                        count($ids),
-                        $this->pluralize($clean_type)
-                    );
-                }
-            }
-
-            $content .= '<div class="divider"></div>';
-        }
-
-        return $content;
-    }
-
-    /**
-     * Generate HTML section for regular notifications
-     *
-     * @param array $notifications Notification objects
-     *
-     * @return string HTML content
-     */
-	protected function generateNotificationsSection(array $notifications):string
+	public function getUserSubscriptions(int $userID, string $frequency):array
 	{
-		if (empty($notifications)) {
-			return '';
-		}
-
-		$items = [];
-
-		// Group notifications by type
-		$by_type = [];
-		foreach ($notifications as $notification) {
-			if (!isset($by_type[$notification->type])) {
-				$by_type[$notification->type] = [];
-			}
-			$by_type[$notification->type][] = $notification;
-		}
-
-		// Process each type
-		foreach ($by_type as $type => $type_notifications) {
-			foreach ($type_notifications as $notification) {
-				$message = $notification->message;
-				if (empty($message)) {
-					$message = $this->generateNotificationMessage($notification);
-				}
-
-				if (!empty($message)) {
-					$items[] = ['label' => '', 'value' => $message];
-				}
-			}
-		}
-
-		return JVB()->email()->table($items, 'Other Updates');
+		return $this->preferences->getUserSubscriptions($userID, $frequency);
 	}
 
-    /**
-     * Generate a message for a notification when none is provided
-     *
-     * @param object $notification Notification object
-     *
-     * @return string Formatted message
-     */
-    protected function generateNotificationMessage(object $notification):string
-    {
-        switch ($notification->type) {
-            case 'new_favourite':
-                return 'Someone favourited your content';
-
-            case 'artist_approved':
-                return 'Your artist profile has been approved';
-
-            case 'artist_invitation':
-                return 'You have a new invitation to join a shop';
-
-            case 'new_term':
-                return 'New term suggestion requires your approval';
-
-            case 'term_approved':
-                return 'Your term suggestion was approved';
-
-            case 'term_rejected':
-                return 'Your term suggestion was not approved';
-
-            case 'list_shared':
-                return 'Someone shared a list with you';
-
-            default:
-                return 'You have a new notification';
-        }
-    }
-
-    /**
-     * Get content details for digest
-     *
-     * @param string $type Content type (with jvb_ prefix)
-     * @param int $content_id Content ID
-     * @param int $artist_id Artist ID
-     *
-     * @return array|false Content details or false if not found
-     */
-    protected function getContentDetails(string $type, int $content_id, int $artist_id):array|false
-    {
-        // Build post type from content type
-        $post_type = $type; // Already has jvb_ prefix
-
-        // Get the post
-        $post = get_post($content_id);
-        if (!$post || $post->post_type !== $post_type || $post->post_status !== 'publish') {
-            return false;
-        }
-
-        // Get artist data
-        $artist_data = $this->getArtistData($artist_id);
-        if (!$artist_data) {
-            return false;
-        }
-
-        // Get featured image if available
-        $image_url = get_the_post_thumbnail_url($content_id, 'medium');
-        if (!$image_url) {
-            // Try meta fields for image
-            $meta_image_id = get_post_meta($content_id, BASE . 'image', true);
-            if ($meta_image_id !== '') {
-                $image_url = wp_get_attachment_image_url($meta_image_id, 'medium');
-            }
-        }
-
-        if (!$image_url) {
-            return false; // Skip items without images for digest
-        }
-
-        return [
-            'id'          => $content_id,
-            'title'       => $post->post_title,
-            'url'         => get_permalink($content_id),
-            'image'       => $image_url,
-            'date'        => $post->post_date,
-            'artist_id'   => $artist_id,
-            'artist_name' => $artist_data['display_name'],
-            'artist_url'  => $artist_data['url'],
-            'type'        => str_replace(BASE, '', $type)
-        ];
-    }
-
-    /**
-     * Get human-readable label for content type
-     *
-     * @param string $type Content type (without jvb_ prefix)
-     *
-     * @return string Label
-     */
-    protected function getContentTypeLabel(string $type):string
-    {
-        $labels = [
-            'tattoo'   => 'New Tattoos',
-            'artwork'  => 'New Artwork',
-            'piercing' => 'New Piercings',
-            'event'    => 'Upcoming Events',
-            'news'     => 'News & Updates',
-            'offer'    => 'Special Offers'
-        ];
-
-        return $labels[$type] ?? ucfirst($type . 's');
-    }
-
-
-    /**
-     * Update digest timestamps for notification preferences
-     *
-     * @param int $user_id User ID
-     * @param string $frequency Digest frequency
-     *
-     * @return bool Success or failure
-     */
-    protected function updateDigestTimestamps(int $user_id, string $frequency):bool
-    {
-        global $wpdb;
-        $table = $wpdb->prefix . $this->preferencesTable;
-
-        $result = $wpdb->update(
-            $table,
-            [
-                'last_sent' => current_time('mysql')
-            ],
-            [
-                'user_id'   => $user_id,
-                'frequency' => $frequency
-            ]
-        );
-
-        return $result !== false;
-    }
-
-    /************************************************************
-     * Bulk Operation Handlers
-     ************************************************************/
-
-    /**
-     * Handle bulk operations for notifications
-     *
-     * @param WP_Error|array $result Default result
-     * @param object $operation Operation object
-     * @param array $data Current item
-     *
-     * @return WP_Error|array Operation result
-     */
-    public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array
-    {
-        switch ($operation->type) {
-            case 'email_notification_digest':
-                return $this->handleDigestBatch($operation, $data);
-
-            case 'addNotification':
-                return $this->handleAddNotificationOperation($operation, $data);
-            default:
-                return $result;
-        }
-    }
-
-    /**
-     * Handle digest batch operation
-     *
-     * @param object $operation Operation object
-     * @param array $data
-     *
-     * @return WP_Error|array|bool Operation result
-     */
-    protected function handleDigestBatch(object $operation, array $data):WP_Error|array|bool
-    {
-        try {
-
-            // Process one user at a time
-            $user_id = $data['users'][ $operation->progress_count ] ?? null;
-
-            if (!$user_id) {
-                return [
-                    'success' => false,
-                    'message' => 'Invalid user ID'
-                ];
-            }
-
-            $result = $this->generateUserDigest($user_id, $data['frequency']);
-
-            return [
-                'success' => $result,
-                'result' => $result ? 'Digest sent successfully' : 'Failed to send digest',
-            ];
-        } catch (Exception $e) {
-            $this->logError("Error processing digest batch: " . $e->getMessage(), [
-                'operation_id' => $operation->id
-            ]);
-
-            return false;
-        }
-    }
-
-    /**
-     * @param object $operation
-     * @param array $data
-     *
-     * @return WP_Error|array|bool
-     */
-    protected function handleAddNotificationOperation(object $operation, array $data):WP_Error|array|bool
-    {
-        try {
-            $user_ids = $data['user_ids'] ?? [];
-            $type = $data['type'] ?? '';
-            $message = $data['message'] ?? '';
-            $target_id = $data['target_id'] ?? null;
-            $target_type = $data['target_type'] ?? null;
-
-            if (empty($user_ids) || empty($type)) {
-                return new WP_Error('invalid_data', 'Missing required notification data');
-            }
-
-            foreach ($user_ids as $key => $user_id) {
-                if (!$this->checkUser($user_id)) {
-                    unset($user_ids[$key]);
-                }
-            }
-
-            // Add notification for this user
-            $result = $this->processNotification($user_ids, $type, $message, $target_id, $target_type);
-
-            return [
-                'success' => !is_wp_error($result),
-                'result' => is_wp_error($result) ? $result->get_error_message() : $result,
-            ];
-        } catch (Exception $e) {
-            return [
-				'success'	=> false,
-				'result'	=>  $e->getMessage()
-			];
-        }
-    }
-
-
-    /**
-     * @param int $user_id
-     *
-     * @return array|false|mixed
-     */
-    protected function getArtistData(int $user_id):array|false
-    {
-        // Try to get from cache
-		$artist_id = get_user_meta($user_id, BASE . 'link', true);
-		if (!$artist_id || $artist_id === '') {
-			return false;
-		}
-        $cached = $this->artistsCache->get($artist_id);
-
-        if ($cached !== false) {
-            return $cached;
-        }
-
-        // Get basic artist data
-        $artist_post = get_post($artist_id);
-        if (!$artist_post) {
-            return false;
-        }
-
-        $data = [
-            'id' => $artist_id,
-            'user_id' => $user_id,
-            'display_name' => get_the_title($artist_id),
-            'first_name' => get_post_meta($artist_id, BASE . 'first_name', true),
-            'url' => get_permalink($artist_id),
-            'image' => get_post_thumbnail_id($artist_id)
-        ];
-
-        // Cache the result
-        $this->artistsCache->set($artist_id, $data);
-
-        return $data;
-    }
-
-    /**
-     * @param int $user_id
-     *
-     * @return array
-     */
-    protected function getFollowedArtists(int $user_id):array
-    {
-		return $this->favouritesCache->remember(
-			$user_id,
-			function() use ($user_id) {
-				global $wpdb;
-				$favourites_table = $wpdb->prefix . BASE . 'favourites';
-
-				// Get artists this user has favourited
-				return $wpdb->get_col($wpdb->prepare(
-				"SELECT f.target_id
-					 FROM {$favourites_table} f
-					 JOIN {$wpdb->posts} p ON f.target_id = p.ID
-					 WHERE f.user_id = %d
-					 AND f.type = 'artist'
-					 AND p.post_status = 'publish'",
-					$user_id
-				));
-			}
-		);
-
-    }
-
-    /**
-     * @param int $artist_id
-     *
-     * @return int
-     */
-    protected function getFollowerCount(int $artist_id):int
-    {
-		return $this->followerCache->remember(
-			$artist_id,
-			function() use ($artist_id) {
-				global $wpdb;
-				$favourites_table = $wpdb->prefix . BASE . 'favourites';
-
-				return $wpdb->get_var($wpdb->prepare(
-					"SELECT COUNT(DISTINCT user_id)
-					 FROM {$favourites_table}
-					 WHERE target_id = %d AND type = 'artist'",
-					$artist_id
-				));
-			}
-		);
-
-    }
-
-    /**
-     * @param string $word
-     *
-     * @return string
-     */
-    protected function pluralize(string $content):string
-    {
-		$registrar = Registrar::getInstance($content);
-		return ($registrar) ? $registrar->getPlural()
-			: str_replace('_', ' ', $content.'s');
-    }
-
-    /**
-     * @param int $user_id
-     *
-     * @return void
-     */
-    protected function clearNotificationCache(int $user_id):void
-    {
-		$this->userCache->forget($user_id);
-		$this->contentCache->forget($user_id);
-    }
-
-    /**
-     * Log errors with proper context using the error handler
-     *
-     * @param string $message Error message
-     * @param array $context Additional context data
-     * @param string $severity Error severity (debug, info, warning, error, critical)
-     * @return void
-     */
-    protected function logError(string $message, array $context = [], string $severity = 'error'):void
-    {
-        try {
-            // Use the ErrorHandler through the JVB singleton
-            JVB()->error()->log(
-                'notifications', // component
-                $message,
-                $context,
-                $severity
-            );
-        } catch (Exception $e) {
-            // Fallback if error handler fails
-            error_log("NotificationManager Error: $message - " . json_encode($context));
-        }
-    }
-
-    /**
-     * @return void
-     */
-    public function cleanupOldNotifications():void
-    {
-        global $wpdb;
-        $notifications_table = $wpdb->prefix . $this->table;
-        $seen_table = $wpdb->prefix . $this->seenTable;
-
-        // Keep track of current time
-        $current_time = current_time('mysql');
-
-        // Delete regular notifications older than 3 months
-        $wpdb->query($wpdb->prepare(
-            "DELETE FROM {$notifications_table}
-         WHERE created_at < DATE_SUB(%s, INTERVAL 3 MONTH)
-         AND status IN ('read', 'actioned', 'dismissed')",
-            $current_time
-        ));
-
-        // Delete dismissed content notifications older than 1 month
-        $wpdb->query($wpdb->prepare(
-            "DELETE FROM {$seen_table}
-         WHERE status = 'dismissed'
-         AND created_at < DATE_SUB(%s, INTERVAL 1 MONTH)",
-            $current_time
-        ));
-
-        // Delete read content notifications older than 3 months
-        $wpdb->query($wpdb->prepare(
-            "DELETE FROM {$seen_table}
-         WHERE status = 'read'
-         AND created_at < DATE_SUB(%s, INTERVAL 3 MONTH)",
-            $current_time
-        ));
-    }
-
-    /**
-     * @return array
-     */
-    protected function getVerified(string|array $userRoles):array
-    {
-		$userRoles = $this->checkRoles($userRoles);
-
-		if (empty($userRoles)) {
-			return [];
-		}
-		$cache = Cache::for('verifiedUsers', DAY_IN_SECONDS)->connect('user',true);
-		return $cache->remember(
-			'verified',
-			function() use ($userRoles) {
-				return get_users([
-					'role'          => $userRoles,
-					'capability'    => 'skip_moderation',
-					'fields'        => 'ID'
-				]);
-			}
-		);
-    }
-
-	protected function getUserIDs(array|string $roles):array
+	public function addUserPreference(int $userID, int $item_id, string $item_type, string $frequency):bool
 	{
-		$roles = $this->checkRoles($roles);
-		if (empty($roles)) {
-			return [];
-		}
-		$cache = Cache::for('everyone', DAY_IN_SECONDS)->connect('user', true);
-		return $cache->remember(
-			$cache->generateKey($roles),
-			function() use ($roles) {
-				return get_users([
-					'role'	=> $roles,
-					'fields'	=> 'ID'
-				]);
-			}
-		);
+		return $this->preferences->addUserPreference($userID, $item_id, $item_type, $frequency);
 	}
 
-	protected function checkRoles(string|array $roles):array
+	public function deleteUserPreference(int $userID, int $item_id, string $item_type):bool
 	{
-		if (!is_array($roles)) {
-			$roles = explode(',',$roles);
-		}
-
-		return array_map(function ($r) {
-			return jvbCheckBase(trim($r));
-		}, array_filter($roles, function ($r) {
-			return Registrar::getInstance(trim($r))!== false;
-		}));
+		return $this->preferences->deleteUserPreference($userID, $item_id, $item_type);
 	}
+   /************************************************
+	* NOTIFICATIONS
+   ************************************************/
+	public function notify(int|array $user_ids, string $notification_type, int $fromUser = 0, array $args = []):bool
+	{
+		return $this->notifications->notify($user_ids, $notification_type, $fromUser, $args);
+	}
+	public function unnotify(int|array $user_ids, string $notification_type, int $fromUser = 0, array $args = []):bool
+	{
+		return $this->notifications->unnotify($user_ids, $notification_type, $fromUser, $args);
+	}
+   public function getUserNotifications(int $user_id, array $args = []):array
+   {
+	   return $this->notifications->getUserNotifications($user_id, $args);
+   }
 
-    /**
-     * @param int $userID
-     *
-     * @return bool|mixed
-     */
-    protected function checkUser(int $userID):bool
-    {
-		$cache = Cache::for('checked_users', DAY_IN_SECONDS)->connect('user', true);
-		return $cache->remember(
-			$userID,
-			function() use ($userID) {
-				return (bool)get_userdata($userID)?:null;
-			}
-		);
-    }
+   public function markRead(int $userID, int|array $notification_id):bool
+   {
+	   return $this->notifications->markRead($userID, $notification_id);
+   }
+   public function markDismissed(int $userID, int|array $notification_id):bool
+   {
+	   return $this->notifications->markDismissed($userID, $notification_id);
+   }
+   public function markActioned(int $userID, int|array $notification_id, array $result = []):bool
+   {
+	   return $this->notifications->markActioned($userID, $notification_id, $result);
+   }
+
+	/************************************************
+	 * CONTENT NOTIFICATIONS
+	 * These are pooled notifications of new content for:
+	 * 		- a particular artist
+	 * 		- a particular term
+	 ************************************************/
+   /************************************************
+	* EMAIL DIGESTS
+	************************************************/
 }
diff --git a/inc/managers/Notifications/Content.php b/inc/managers/Notifications/Content.php
new file mode 100644
index 0000000..776d585
--- /dev/null
+++ b/inc/managers/Notifications/Content.php
@@ -0,0 +1,108 @@
+<?php
+namespace JVBase\managers\Notifications;
+
+use JVBase\managers\Cache;
+use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
+use WP_Query;
+
+if (!defined('ABSPATH')) {
+	exit; // Exit if accessed directly
+}
+/**
+ * ContentNotifications
+ *  Gets notifications of
+ **/
+class Content
+{
+	protected static Cache $cache;
+	public function __construct()
+	{
+		self::$cache = Cache::for('content_notifications')->connect('post');
+	}
+	public static function getSinceDate(string $date):string
+	{
+		return match ($date) {
+			'week' => '-1 week',
+			'month' => '-1 month',
+			default => '-1 day',
+		};
+	}
+	public static function forUser(int $userID, ?string $sinceDate = null, bool $modified = false):array
+	{
+		$date = self::getSinceDate($sinceDate);
+
+		$posts = new WP_Query([
+			'posts_per_page' 	=> -1,
+			'post_status'		=> 'publish',
+			'post_author'		=> $userID,
+			'date_query'		=> [
+				[
+					'after'		=> $date,
+					'inclusive' => true,
+					'column'	=> $modified ? 'post_modified' : 'post_date'
+				]
+			],
+			'fields' => 'ids',
+		]);
+		wp_reset_postdata();
+		return $posts->posts;
+	}
+
+	public static function forTerm(int $termID, string $taxonomy, ?string $sinceDate = null, bool $modified = false):array
+	{
+		if (!in_array($taxonomy, ['category', 'tag'])){
+			$taxonomy = jvbCheckBase($taxonomy);
+		}
+		$date = self::getSinceDate($sinceDate);
+		$posts = new WP_Query([
+			'posts_per_page'	=> -1,
+			'post_status'		=> 'publish',
+			'date_query'		=> [
+				[
+					'after'		=> $date,
+					'inclusive' => true,
+					'column'	=> $modified ? 'post_modified' : 'post_date'
+				]
+			],
+			'tax_query'			=> [
+				[
+					'taxonomy'	=> $taxonomy,
+					'terms'		=> $termID,
+				]
+			],
+			'fields'	=> 'ids',
+		]);
+		wp_reset_postdata();
+		return $posts->posts;
+	}
+
+	public static function formatForNotification(array $IDs, string $type):array
+	{
+		$posts = [];
+		foreach ($IDs as $ID) {
+			$posts[] = self::$cache->remember(
+				$ID,
+				function() use ($ID) {
+					$meta = Meta::forPost($ID);
+					$img = $meta->get('post_thumbnail');
+					$registrar = Registrar::getInstance(get_post_type($ID));
+					return array_merge([
+						'url'		=> get_the_permalink($ID),
+						'content' 	=> $registrar->getType(),
+						'icon'		=> $registrar->getIcon(),
+						'image'		=> (!empty($img) ? jvbImageData($img) : [])
+					],
+						$meta->getAll([
+							'post_date',
+							'post_modified',
+							'post_title',
+							'post_excerpt',
+						])
+					);
+				}
+			);
+		}
+		return $posts;
+	}
+}
diff --git a/inc/managers/Notifications/EmailDigests.php b/inc/managers/Notifications/EmailDigests.php
new file mode 100644
index 0000000..978ff7f
--- /dev/null
+++ b/inc/managers/Notifications/EmailDigests.php
@@ -0,0 +1,391 @@
+<?php
+namespace JVBase\managers\Notifications;
+
+use JVBase\managers\Cache;
+use JVBase\managers\CustomTable;
+use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
+use WP_Error;
+use WP_Query;
+
+if (!defined('ABSPATH')) {
+	exit; // Exit if accessed directly
+}
+/**
+ * EmailDigests
+ *  Prepares the email digest for daily/weekly/monthly email summaries
+ **/
+class EmailDigests
+{
+	protected string $campaign;
+	protected Cache $terms;
+	protected Cache $users;
+	protected CustomTable $userIndex;
+	protected CustomTable $termIndex;
+	public function __construct()
+	{
+		$this->registerCron();
+		$this->registerTable();
+
+		add_filter(BASE . 'handle_bulk_operation', [ $this, 'processOperation' ], 10, 3);
+	}
+
+	protected function registerCron():void
+	{
+		add_action(BASE . 'notification_digest_daily', [ $this, 'runDailyDigests' ]);
+		add_action(BASE . 'notification_digest_weekly', [ $this, 'runWeeklyDigests' ]);
+		add_action(BASE . 'notification_digest_monthly', [ $this, 'runMonthlyDigests' ]);
+	}
+
+	protected function registerTable():void
+	{
+		$this->registerUserIndex();
+		$this->registerTermIndex();
+	}
+		protected function registerUserIndex():void
+		{
+			$table = CustomTable::for('user_notification_email_digest');
+//		$types = implode(',',array_map(function($item) { return "'{$item}'"; }, Registrar::getFeatured('favouritable')));
+			$table->setColumns([
+				'id'			=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+				'user_id'		=> "{$table->getUserIDType()} NOT NULL",
+				'frequency'		=> "ENUM('daily', 'weekly', 'monthly') NOT NULL",
+				'content_ids'	=> 'JSON DEFAULT NULL',
+				'output'		=> 'TEXT DEFAULT NULL',
+				'total_sent'	=> 'int unsigned DEFAULT 0',
+				'created_at'	=> 'datetime DEFAULT CURRENT_TIMESTAMP',
+			]);
+
+			$table->setKeys([
+				['key' => 'PRIMARY', 'value' => '(`id`)'],
+				['key' => 'UNIQUE', 'value' => '`user_frequency` (`user_id`, `frequency`)'],
+				'user_id (`user_id`)'
+			]);
+			$base = BASE;
+			$table->setConstraints([
+				"CONSTRAINT `{$base}digest_user` FOREIGN KEY (`user_id`)
+			REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
+			]);
+
+			$table->defineTable();
+			$this->userIndex = $table;
+		}
+		protected function registerTermIndex():void
+		{
+			$table = CustomTable::for('user_notification_email_digest');
+			$types = implode(',',array_map(function($item) { return "'{$item}'"; }, Registrar::getFeatured('favouritable', 'term')));
+			$table->setColumns([
+				'id'			=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+				'term_id'		=> "{$table->getTermIDType()} NOT NULL",
+				'type'			=> "ENUM({$types}) NOT NULL",
+				'frequency'		=> "ENUM('daily', 'weekly', 'monthly') NOT NULL",
+				'content_ids'	=> 'JSON DEFAULT NULL',
+				'output'		=> 'TEXT DEFAULT NULL',
+				'total_sent'	=> 'int unsigned DEFAULT 0',
+				'created_at'	=> 'datetime DEFAULT CURRENT_TIMESTAMP',
+			]);
+
+			$table->setKeys([
+				['key' => 'PRIMARY', 'value' => '(`id`)'],
+				['key' => 'UNIQUE', 'value' => '`term_frequency` (`term_id`, `type`, `frequency`)'],
+				'term_id (`term_id`)'
+			]);
+			$base = BASE;
+			$table->setConstraints([
+				"CONSTRAINT `{$base}digest_term` FOREIGN KEY (`term_id`)
+				REFERENCES `{$table->getTermTable()}` (`term_id`) ON DELETE CASCADE"
+			]);
+
+			$table->defineTable();
+			$this->termIndex = $table;
+		}
+
+	public function runDailyDigests():void
+	{
+		$this->campaign = 'daily_digest_' .date('Y-m-d');
+		$this->processDigest('daily');
+	}
+	public function runWeeklyDigests():void
+	{
+		$this->campaign = 'weekly_digest_'.date('Y-m-d');
+		$this->processDigest('weekly');
+	}
+
+	public function runMonthlyDigests():void
+	{
+		$this->campaign = 'monthly_digest_'.date('Y-m-d');
+		$this->processDigest('monthly');
+	}
+
+	protected function processDigest(string $frequency):void
+	{
+		$users = $this->getUsers($frequency);
+		if (empty($users)){
+			return;
+		}
+
+		JVB()->queue()->add(
+			'email_notification_digest',
+			0,
+			[
+				'frequency' => $frequency,
+				'users'		=> $users,
+			],
+			[
+				'chunk_key'	=> 'users',
+				'chunk_size'=> 20,
+				'operation_id'=> 'notification_digest_'.date('Y_m_d_His')
+			]
+		);
+	}
+
+	public function getUsers(string $frequency):array
+	{
+		if (!in_array($frequency, ['never', 'daily', 'weekly', 'monthly'])) {
+			return [];
+		}
+		return JVB()->notification()->getUsersByFrequency($frequency);
+	}
+
+	public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array
+	{
+		if ($operation->type !== 'email_notification_digest') {
+			return $result;
+		}
+
+		$results = [];
+		foreach ($data['user'] as $userID) {
+			$result = $this->generateUserDigest($userID, $data['frequency']);
+			if ($result) {
+				$results[$userID] = $result;
+			}
+			usleep(50);
+		}
+
+		return [
+			'success'	=> true,
+			'result'	=> $results
+		];
+	}
+	protected function getSinceDate(string $frequency):string
+	{
+		return match ($frequency) {
+			'weekly' => '-1 week',
+			'monthly' => '-1 month',
+			default => '-1 day',
+		};
+	}
+
+	protected function generateUserDigest(int $userID, string $frequency):string|false
+	{
+		$subscription = JVB()->notification()->getUserSubscriptions($userID, $frequency);
+
+		//TODO
+		$notifications = JVB()->notification()->getUserNotifications($userID);
+
+		if (empty($subscription) && empty($notifications)) {
+			return false;
+		}
+		$content = '';
+		foreach ($subscription as $item) {
+			$temp = match ($item['item_type']) {
+				array_merge(['user'], Registrar::getFeatured('favouritable', 'user')) => $this->getUserUpdates($item['item_id'], $frequency),
+				Registrar::getFeatured('favouritable', 'term') => $this->getTermUpdates($item['item_id'], $item['item_type'], $frequency),
+				default => false,
+			};
+			if ($temp) {
+				$content .= $temp;
+			}
+		}
+		if (empty($content)) {
+			return false;
+		}
+		return $content;
+	}
+
+	protected function getUserUpdates(int $userID, string $frequency):string|false
+	{
+		$frequency = strtolower($frequency);
+		$frequency = in_array($frequency, ['daily', 'weekly', 'monthly']) ? $frequency : 'daily';
+		$user = get_userdata($userID);
+		if (!$user || is_wp_error($user)) {
+			return false;
+		}
+		$since = $this->getSinceDate($frequency);
+
+		$entry = $this->userIndex->get([
+			'user_id'	=> $userID,
+			'frequency'	=> $frequency
+		]);
+		if ($entry) {
+			return $entry->output;
+		}
+		$new = [
+			'user_id'	=> $userID,
+			'frequency'	=> $frequency,
+		];
+		//Didn't do it yet, create it here
+		$IDs = Content::forUser($userID, $since);
+		$new['content_ids'] = json_encode($IDs);
+		$new['output'] = $this->contentForUser($userID, $IDs);
+
+		$this->userIndex->insert($new);
+		return $new['output'];
+	}
+
+	protected function contentForUser(int $userID, array $IDs):string
+	{
+		if (empty($IDs)) {
+			return '';
+		}
+		$user = get_userdata($userID);
+		if (!$user || is_wp_error($user)) {
+			return '';
+		}
+		$role = jvbUserRole($userID);
+		$registrar = Registrar::getInstance($role);
+		if (!$registrar) {
+			return '';
+		}
+		if ($registrar->profile_link) {
+			$meta = Meta::forPost(jvbUserProfileLink($userID));
+			$title = $meta->get('post_title');
+			$goTo = get_the_permalink(jvbUserProfileLink($userID));
+		}else {
+			$meta = Meta::forUser($userID);
+			$title = $meta->get('display_name');
+			$goTo = get_author_posts_url($userID);
+		}
+
+		$output = [];
+
+		$count = count($IDs);
+		$max = (min($count, 5));
+		$canIncrease = $count > $max;
+		for ($i = 0; $i <=$max; $i++) {
+			$ID = $IDs[$i];
+			$m = Meta::forPost($ID);
+			$img = $m->get('post_thumbnail');
+			if (empty($img)) {
+				if ($canIncrease){
+					$max++;
+				}
+				continue;
+			}
+			$image =  wp_get_attachment_image_src($img, 'medium');
+			if (!$image) {
+				if ($canIncrease){
+					$max++;
+				}
+				continue;
+			}
+			$url = get_the_permalink($ID);
+			$image = sprintf(
+				'<a href="%s">%s</a>',
+				$url,
+				JVB()->email()->image($image[0], get_post_meta($img, '_wp_attachment_image_alt', true))
+			);
+			$t = sprintf(
+				'<a href="%s">%s</a>',
+				$url,
+				$m->get('post_title')
+			);
+			$output[] = JVB()->email()->card($image, $t);
+		}
+
+		$final = JVB()->email()->button($goTo, 'Want to See More?');
+		return JVB()->email()->grid($output, 3, 'New from '.$title,'',$final);
+	}
+
+	protected function getTermUpdates(int $termID, string $type, string $frequency):string|false
+	{
+		$frequency = strtolower($frequency);
+		$frequency = in_array($frequency, ['daily', 'weekly', 'monthly']) ? $frequency : 'daily';
+		$taxonomy = jvbCheckBase($type);
+		$term = get_term($termID, $taxonomy);
+		if (!$term || is_wp_error($term)) {
+			return false;
+		}
+		$since = $this->getSinceDate($frequency);
+
+		$entry = $this->termIndex->get([
+			'term_id'	=> $termID,
+			'type'		=> $type,
+			'frequency'	=> $frequency
+		]);
+		if ($entry) {
+			return $entry->output;
+		}
+		$new = [
+			'term_id'	=> $termID,
+			'type'		=> $type,
+			'frequency'	=> $frequency
+		];
+		//Didn't do it yet, create it here
+		$IDs = Content::forTerm($termID, $taxonomy, $since);
+		$new['content_ids'] = json_encode($IDs);
+		$new['output'] = $this->contentForTerm($termID, $type, $IDs);
+
+		$this->termIndex->insert($new);
+		return $new['output'];
+	}
+
+	protected function contentForTerm(int $termID, string $tax, array $IDs):string
+	{
+		if (empty($IDs)) {
+			return '';
+		}
+		$taxonomy = jvbCheckBase($tax);
+		$term = get_term($termID, $taxonomy);
+		if (!$term || is_wp_error($term)) {
+			return '';
+		}
+		$registrar = Registrar::getInstance($tax);
+		if (!$registrar) {
+			return '';
+		}
+		$meta = Meta::forTerm($termID);
+		$title = $meta->get('name');
+
+		$goTo = get_term_link($termID,$taxonomy);
+
+		$output = [];
+
+		$count = count($IDs);
+		$max = (min($count, 5));
+		$canIncrease = $count > $max;
+		for ($i = 0; $i <=$max; $i++) {
+			$ID = $IDs[$i];
+			$m = Meta::forPost($ID);
+			$img = $m->get('post_thumbnail');
+			if (empty($img)) {
+				if ($canIncrease){
+					$max++;
+				}
+				continue;
+			}
+			$image =  wp_get_attachment_image_src($img, 'medium');
+			if (!$image) {
+				if ($canIncrease){
+					$max++;
+				}
+				continue;
+			}
+			$url = get_the_permalink($ID);
+			$image = sprintf(
+				'<a href="%s">%s</a>',
+				$url,
+				JVB()->email()->image($image[0], get_post_meta($img, '_wp_attachment_image_alt', true))
+			);
+			$t = sprintf(
+				'<a href="%s">%s</a>',
+				$url,
+				$m->get('post_title')
+			);
+			$output[] = JVB()->email()->card($image, $t);
+		}
+
+		$final = JVB()->email()->button($goTo, 'See More');
+		return JVB()->email()->grid($output, 3, 'New in '.$title, '', $final);
+	}
+}
diff --git a/inc/managers/Notifications/Notifications.php b/inc/managers/Notifications/Notifications.php
new file mode 100644
index 0000000..af3fd15
--- /dev/null
+++ b/inc/managers/Notifications/Notifications.php
@@ -0,0 +1,470 @@
+<?php
+namespace JVBase\managers\Notifications;
+
+use Exception;
+use JVBase\managers\CustomTable;
+use JVBase\registrar\Registrar;
+use JVBase\utility\Features;
+
+if (!defined('ABSPATH')) {
+	exit; // Exit if accessed directly
+}
+/**
+ * Preferences
+ *  Manages preferences for a user's favourites
+ **/
+class Notifications
+{
+	protected array $types;
+	protected CustomTable $notifications;
+
+	public function __construct()
+	{
+		$this->defineTypes();
+		$this->defineTable();
+	}
+
+	private function defineTypes():void
+	{
+		$types = ['system_message' => [
+			'icon'	=> 'info',
+			'digest'=> true
+		]];
+		if (Features::forSite()->has('favourites')) {
+			$types = array_merge($types, [
+				'new_favourite' => [
+					'icon'	=> 'heart',
+					'digest'=> true
+				],
+				'list_shared'  => [
+					'icon'	=> 'list-heart',
+					'digest'=> false
+				],
+				'list_share_status'  => [
+					'icon'	=> 'list-heart',
+					'digest'=> false
+				]
+			]);
+		}
+		$contentTax = Registrar::getFeatured('is_content', 'term');
+		$verifyEntry = Registrar::getFeatured('verify_entry', 'term');
+		if (!empty(array_intersect($contentTax, $verifyEntry))) {
+			$types = array_merge($types, [
+				'entry_requested'  => [
+					'icon'	=> 'user-circle-plus',
+					'digest'=> true,
+					'action'=> ['approve', 'reject', 'dismiss']
+				],
+				'entry_approved' => [
+					'icon'	=> 'user-circle-check',
+					'digest'=> false
+				],
+				'entry_denied' => [
+					'icon'	=> 'prohibit',
+					'digest'=> false
+				],
+				'entry_revoked' => [
+					'icon'	=> 'warning-circle',
+					'digest'=> false
+				]
+			]);
+		}
+		$invitable = Registrar::getFeatured('invitable');
+		if (!empty($invitable)) {
+			$types = array_merge($types, [
+				'invitation_requested' => [
+					'icon'	=> 'plus-circle',
+					'digest'=> true,
+					'note'	=> true,
+					'action'=> ['approve','reject','dismiss']
+				],
+				'invitation_granted' => [
+					'icon'	=> 'check-circle',
+					'digest'=> false
+				],
+				'invitation_revoked' => [
+					'icon'	=> 'x-circle',
+					'digest'=> false
+				],
+				'invitation_accepted' => [
+					'icon'	=> 'check-circle',
+					'digest'=> false
+				],
+				'invitation_refused'  => [
+					'icon'	=> 'x-circle',
+					'digest'=> false
+				]
+			]);
+		}
+
+		$approvals = Registrar::getFeatured('approve_new');
+		if (!empty($approvals)) {
+			$tmp = ['user', 'term', 'post'];
+			$app = [];
+			foreach ($tmp as $t) {
+				$approvals = Registrar::getFeatured('approve_new', $t);
+				if (!empty($approvals)) {
+					$app = array_merge($app, [
+						$t.'_new' => [
+							'icon'	=> 'plus-circle',
+							'digest'=> true
+						],			//For newly created items
+						$t.'_approved' => [
+							'icon'	=> 'check-circle',
+							'digest'=> false
+						],		//For notices of acceptance
+						$t.'_denied' => [
+							'icon'	=> 'x-circle',
+							'digest'=> false
+						],		//For notices of denial
+						$t.'_status' => [
+							'icon'	=> 'info',
+							'digest'=> true
+						],		//For final status
+					]);
+				}
+			}
+			$types = array_merge($types, $app);
+		}
+
+		$this->types = $types;
+	}
+
+	private function defineTable():void
+	{
+		$table = CustomTable::for('notifications_main');
+
+		$typeEnum = implode(',',array_map(function($type) { return '`'.$type.'`';}, array_keys($this->types)));
+
+		$table->setColumns([
+			'id'			=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+			'for_user'		=> $table->getUserIDType().' NOT NULL',
+			'from_user'		=> $table->getUserIDType().' NOT NULL',
+			'target_id'		=> 'bigint(20) DEFAULT NULL',
+			'target_type'	=> 'varchar(30) DEFAULT NULL',
+			'type'			=> "ENUM({$typeEnum}) NOT NULL DEFAULT 'system_message'",
+			'status'		=> "ENUM('unread', 'read', 'actioned', 'dismissed') NOT NULL DEFAULT 'unread'",
+			'priority'		=> "ENUM('low','normal','high') NOT NULL DEFAULT 'normal'",
+			'message'		=> 'varchar(255) DEFAULT NULL',
+			'action_taken'	=> 'tinyint(1) NOT NULL DEFAULT 0',
+			'result'		=> 'JSON DEFAULT NULL',
+			'created_at'	=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP',
+			'read_at'		=> 'datetime DEFAULT NULL',
+			'actioned_at'	=> 'datetime DEFAULT NULL',
+			'updated_at'	=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
+		]);
+
+		$table->setKeys([
+			['key' => 'PRIMARY', 'value' => '(`id`)'],
+			'`user_status` (`for_user`, `status`)',
+			'`target_lookup` (`target_id`, `target_type`)',
+			'`unread_notifications` (`for_user`, `status`, `created_at`)',
+			'`requires_action` (`for_user`, `requires_action`, `action_taken`)',
+			'`from_user_lookup` (`for_user`, `from_user`, `target_id`, `target_type`, `type`)'
+		]);
+
+		$base = BASE;
+		$table->setConstraints([
+			"CONSTRAINT `{$base}notify_owner` FOREIGN KEY (`for_user`)
+				REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE",
+			"CONSTRAINT `{$base}from_user` FOREIGN KEY (`from_user`)
+				REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
+		]);
+
+		$table->defineTable();
+		$this->notifications = $table;
+	}
+
+	protected function checkArgs(array $args):array
+	{
+		if (empty($args)) {
+			return $args;
+		}
+		$allowedKeys = ['target_id', 'target_type','priority','message'];
+		$allowed = array_filter($args, function($key) use ($allowedKeys) {
+			return !in_array($key, $allowedKeys);
+		}, ARRAY_FILTER_USE_KEY);
+		$notAllowed = array_diff($allowedKeys, $allowed);
+		if (!empty($notAllowed)) {
+			error_log('[Notifications]::checkArgs Attempted keys not allowed:'.print_r($notAllowed, true));
+		}
+		$out = [];
+		foreach ($allowed as $key => $value) {
+			switch ($key) {
+				case 'target_id':
+					$value = absint($value);
+					if ($value > 0) {
+						$out[$key] = $value;
+					}
+					break;
+				case 'target_type':
+					$registrar = Registrar::getInstance($value);
+					if ($registrar) {
+						$out[$key] = $value;
+					}
+					break;
+				case 'priority':
+					$value = strtolower($value);
+					$value = in_array($value, ['low','normal','high']) ? $value : false;
+					if ($value){
+						$out[$key] = $value;
+					}
+					break;
+				case 'message':
+					$value = sanitize_text_field($value);
+					$out[$key] = $value;
+					break;
+			}
+		}
+		return $out;
+	}
+	public function notify(int|array $user_ids, string $n_type, int $fromUser = 0, array $args = []):bool
+	{
+		$args = $this->checkArgs($args);
+		$n_type = strtolower($n_type);
+		if (!in_array($n_type, array_keys($this->types))) {
+			error_log('[Notifications]::notify Invalid notification type: '.$n_type);
+			return false;
+		}
+
+		if (is_array($user_ids)) {
+			return $this->notifications->transaction(
+				function() use ($user_ids, $n_type, $fromUser, $args) {
+					$results = [];
+					foreach ($user_ids as $user_id) {
+						$results[$user_id] = $this->notifications->findOrCreate([
+							'for_user'	=> $user_id,
+							'from_user' => $fromUser,
+							'target_id' => $args['target_id']??null,
+							'target_type'=> $args['target_type']??null,
+							'type'		=> $n_type
+						], [
+							'message' => $args['message']??null,
+						]);
+					}
+					return true;
+				}
+			);
+		}
+		return $this->notifications->findOrCreate([
+			'for_user'	=> $user_ids,
+			'from_user' => $fromUser,
+			'target_id' => $args['target_id']??null,
+			'target_type'=> $args['target_type']??null,
+			'type'		=> $n_type
+		], [
+			'message' => $args['message']??null,
+		]);
+	}
+
+	public function unnotify(int|array $user_ids, string $n_type, int $fromUser = 0, array $args = []):bool
+	{
+		$args = $this->checkArgs($args);
+		$n_type = strtolower($n_type);
+		if (!in_array($n_type, array_keys($this->types))) {
+			error_log('[Notifications]::notify Invalid notification type: '.$n_type);
+			return false;
+		}
+
+		if (is_array($user_ids)) {
+			return $this->notifications->transaction(
+				function() use ($user_ids, $n_type, $fromUser, $args) {
+					foreach ($user_ids as $user_id) {
+						$item = $this->notifications->get([
+							'for_user'	=> $user_id,
+							'from_user' => $fromUser,
+							'target_id' => $args['target_id']??null,
+							'target_type'=> $args['target_type']??null,
+							'type'		=> $n_type
+						]);
+						if ($item) {
+							$this->notifications->delete([
+								'for_user'	=> $user_id,
+								'from_user' => $fromUser,
+								'target_id' => $args['target_id']??null,
+								'target_type'=> $args['target_type']??null,
+								'type'		=> $n_type
+							]);
+						}
+					}
+					return true;
+				}
+			);
+		}
+		$item = $this->notifications->get([
+			'for_user'	=> $user_ids,
+			'from_user' => $fromUser,
+			'target_id' => $args['target_id']??null,
+			'target_type'=> $args['target_type']??null,
+			'type'		=> $n_type
+		]);
+		if ($item) {
+			return $this->notifications->delete([
+				'for_user'	=> $user_ids,
+				'from_user' => $fromUser,
+				'target_id' => $args['target_id']??null,
+				'target_type'=> $args['target_type']??null,
+				'type'		=> $n_type
+			]);
+		}
+		return true;
+	}
+
+	public function getUserNotifications(int $user_id, array $where = []):array
+	{
+		$gotWhere = $where;
+		$allowedKeys = ['from_user','target_id', 'target_type','type','status','priority','action_taken','created_at','read_at','actioned_at','updated_at'];
+		$where = array_filter($gotWhere, function($key) use ($allowedKeys) {
+			return in_array($key, $allowedKeys);
+		}, ARRAY_FILTER_USE_KEY);
+		$notAllowed = array_diff($gotWhere, $allowedKeys);
+		if (!empty($notAllowed)){
+			error_log('[Notifications]::getUserNotifications Invalid where arguments, removed from request: '.print_r($notAllowed, true));
+		}
+
+		return $this->notifications->getMany(array_merge([
+			'for_user'	=> $user_id,
+			'status'	=> 'unread',	//default to unread, but the merge will override if different
+		], $where));
+	}
+
+	public function markRead(int $userID, int|array $notification_id):bool
+	{
+		if (is_int($notification_id)) {
+			$notification = $this->notifications->get(['id' => $notification_id]);
+			if (!$notification) {
+				return false;
+			}
+			if ($notification['for_user'] !== $userID) {
+				return false;
+			}
+
+			return (bool) $this->notifications->update([
+				'status'	=> 'read'
+			], [
+				'id'	=> $notification_id
+			]);
+		}
+		try {
+			$this->notifications->transaction(
+				function () use ($notification_id, $userID) {
+					foreach ($notification_id as $id) {
+						$notification = $this->notifications->get(['id' => $notification_id]);
+						if (!$notification) {
+							continue;
+						}
+						if ($notification['for_user'] !== $userID) {
+							continue;
+						}
+
+						$this->notifications->update([
+							'status'	=> 'read'
+						], [
+							'id'	=> $id
+						]);
+					}
+				}
+			);
+			return true;
+		} catch (Exception $e) {
+			return false;
+		}
+	}
+
+	public function markDismissed(int $userID, int|array $notification_id):bool
+	{
+		if (is_int($notification_id)) {
+			$notification = $this->notifications->get(['id' => $notification_id]);
+			if (!$notification) {
+				return false;
+			}
+			if ($notification['for_user'] !== $userID) {
+				return false;
+			}
+
+			return (bool) $this->notifications->update([
+				'status'	=> 'dismissed'
+			], [
+				'id'	=> $notification_id
+			]);
+		}
+
+		try {
+			$this->notifications->transaction(
+				function () use ($notification_id, $userID) {
+					foreach ($notification_id as $id) {
+						$notification = $this->notifications->get(['id' => $notification_id]);
+						if (!$notification) {
+							continue;
+						}
+						if ($notification['for_user'] !== $userID) {
+							continue;
+						}
+
+						$this->notifications->update([
+							'status'	=> 'dismissed'
+						], [
+							'id'	=> $id
+						]);
+					}
+				}
+			);
+			return true;
+		} catch (Exception $e) {
+			return false;
+		}
+	}
+
+	public function markActioned(int $userID, int|array $notification_id, array $result = []):bool
+	{
+		$update = [
+			'status'	=> 'actioned',
+			'action_taken'=> 1,
+		];
+		if (!empty($result)) {
+			$update['result'] = json_encode($result);
+		}
+		if (is_int($notification_id)) {
+			$notification = $this->notifications->get(['id' => $notification_id]);
+			if (!$notification) {
+				return false;
+			}
+			if ($notification['for_user'] !== $userID) {
+				return false;
+			}
+			return (bool) $this->notifications->update(
+				$update,
+				[
+					'id'	=> $notification_id
+				]
+			);
+		}
+
+		try {
+			$this->notifications->transaction(
+				function () use ($notification_id, $userID, $update) {
+					foreach ($notification_id as $id) {
+						$notification = $this->notifications->get(['id' => $notification_id]);
+						if (!$notification) {
+							continue;
+						}
+						if ($notification['for_user'] !== $userID) {
+							continue;
+						}
+						$this->notifications->update(
+							$update,
+							[
+								'id'	=> $notification_id
+							]
+						);
+					}
+				}
+			);
+			return true;
+		} catch (Exception $e) {
+			return false;
+		}
+	}
+
+
+}
diff --git a/inc/managers/Notifications/Preferences.php b/inc/managers/Notifications/Preferences.php
new file mode 100644
index 0000000..42c1f54
--- /dev/null
+++ b/inc/managers/Notifications/Preferences.php
@@ -0,0 +1,124 @@
+<?php
+namespace JVBase\managers\Notifications;
+
+use JVBase\managers\CustomTable;
+use JVBase\registrar\Registrar;
+
+if (!defined('ABSPATH')) {
+	exit; // Exit if accessed directly
+}
+/**
+ * Preferences
+ *  Manages preferences for a user's favourites
+ **/
+class Preferences
+{
+	protected CustomTable $preferences;
+	public function __construct()
+	{
+		$this->defineTable();
+	}
+
+	private function defineTable():void
+	{
+		$table = CustomTable::for('notifications_preferences');
+		$table->setColumns([
+			'id'				=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+			'user_id'			=> "{$table->getUserIDType()} NOT NULL",
+			'item_id'			=> 'bigint(20) NOT NULL',
+			'item_type'			=> 'varchar(50) NOT NULL',
+			'frequency'			=> "ENUM('never', 'daily', 'weekly', 'monthly') DEFAULT 'never'",
+			'last_sent'			=> 'datetime DEFAULT NULL',
+			'created_at'		=> 'datetime DEFAULT CURRENT_TIMESTAMP',
+			'updated_at'		=> 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
+		]);
+
+		$table->setKeys([
+			['key' => 'PRIMARY', 'value' => '(`id`)'],
+			['key' => 'UNIQUE', 'value' => '`user_type` (`user_id`, `item_id`)'],
+			'`user_frequency` (`user_id`, `frequency`)',
+			'`frequency_lookup` (`frequency`, `last_sent`)'
+		]);
+
+		$base = BASE;
+		$table->setConstraints([
+			"CONSTRAINT `{$base}notification_pref_user` FOREIGN KEY (`user_id`)
+				REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
+		]);
+
+		$table->defineTable();
+		$this->preferences = $table;
+	}
+
+	public function getUsersByFrequency(string $frequency):array
+	{
+		if (!in_array($frequency, ['never', 'daily', 'weekly', 'monthly'])) {
+			return [];
+		}
+		return array_unique($this->preferences->pluck('user_id', ['frequency' => $frequency]));
+	}
+
+	public function getUserSubscriptions(int $userID, string $frequency):array
+	{
+		if (!in_array($frequency, ['never', 'daily', 'weekly', 'monthly'])) {
+			return [];
+		}
+		return $this->preferences->getMany([
+			'user_id'	=> $userID,
+			'frequency'	=> $frequency
+		]);
+	}
+
+	public function addUserPreference(int $userID, int $item_id, string $item_type, string $frequency):bool
+	{
+		if (!in_array($frequency, ['never', 'daily', 'weekly', 'monthly'])) {
+			return false;
+		}
+		$user = get_userdata($userID);
+		if (!$user || is_wp_error($user)) {
+			return false;
+		}
+		$registrar = Registrar::getInstance($item_type);
+		if (!$registrar) {
+			return false;
+		}
+		$type = $registrar->getType();
+		switch ($type) {
+			case 'term':
+				$term = get_term($item_id, $registrar->getBased());
+				if (!$term || is_wp_error($term)) {
+					return false;
+				}
+				break;
+			case 'user':
+				$user = get_userdata($item_id);
+				if (!$user || is_wp_error($user)) {
+					return false;
+				}
+				break;
+		}
+		return $this->preferences->findOrCreate(
+			[
+				'user_id'	=> $userID,
+				'item_id'	=> $item_id,
+				'item_type'	=> $item_type
+			],
+			[
+				'frequency'	=> $frequency
+			]
+		);
+	}
+
+	public function deleteUserPreference(int $userID, int $item_id, string $item_type):bool
+	{
+		$hasRecord = $this->preferences->get(['user_id' => $userID, 'item_id' => $item_id, 'item_type' => $item_type]);
+		if (!$hasRecord) {
+			return false;
+		}
+		return $this->preferences->delete([
+			'user_id'	=> $userID,
+			'item_id'	=> $item_id,
+			'item_type'	=> $item_type
+		]);
+	}
+}
diff --git a/inc/managers/Notifications/_setup.php b/inc/managers/Notifications/_setup.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/inc/managers/Notifications/_setup.php
diff --git a/inc/managers/ReferralManager.php b/inc/managers/ReferralManager.php
index 48245fa..af9f0b2 100644
--- a/inc/managers/ReferralManager.php
+++ b/inc/managers/ReferralManager.php
@@ -31,6 +31,12 @@
 	protected ?int $referralPage = null;
 	protected string $rewards_table;
 
+	protected CustomTable $referrals;
+	protected CustomTable $codes;
+	protected CustomTable $janeClients;
+	protected CustomTable $rewards;
+	protected CustomTable $treatments;
+
 	// Default reward settings
 	protected array $default_settings = [
 		'referrer_reward_applies_to' => 'per_user',  // 'per_user' or 'flat_total'
@@ -48,6 +54,7 @@
 
 	public function __construct()
 	{
+		$this->defineTables();
 		global $wpdb;
 		$this->wpdb = $wpdb;
 		$this->cache = Cache::for('referrals', WEEK_IN_SECONDS);
@@ -63,7 +70,7 @@
 
 		add_action('jvbUserRegistered', [$this, 'processRegistrationToken'], 10, 3);
 		add_action('jvb_add_token_inputs', [$this, 'addLoginInputs'], 10, 1);
-		add_action('jvbUserRegistered', [$this, 'processReferral'], 10, 1);
+		add_action('user_register', [$this, 'processReferral'], 10, 1);
 
 		// Add meta boxes for admin to manage referrals
 		add_action('show_user_profile', [$this, 'displayUserReferralInfo']);
@@ -97,6 +104,182 @@
 		add_filter('jvb_admin_page_submission', [$this, 'handleAdminSubmission'], 10, 3);
 	}
 
+	protected function defineTables():void
+	{
+		$this->defineReferralsTable();
+		$this->defineCodeTable();
+		$this->defineJaneClientsTable();
+		$this->defineRewardsTable();
+		$this->defineTreatmentsTable();
+	}
+		protected function defineReferralsTable():void
+		{
+			$table = CustomTable::for('referrals');
+
+			$table->setColumns([
+				'id'		=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+				'from_user'	=> "{$table->getUserIDType()} NOT NULL",
+				'to_user'	=> "{$table->getUserIDType()} NOT NULL",
+				'to_name'	=> 'varchar(255) NOT NULL',
+				'to_email'	=> 'varchar(255) NOT NULL',
+				'to_phone'	=> 'varchar(50) NOT NULL',
+				'referral_code'=> 'varchar(50) NOT NULL',
+				'status'	=> "ENUM('pending', 'consulted', 'treated', 'cancelled') DEFAULT 'pending'",
+				'created_at'=> 'datetime DEFAULT CURRENT_TIMESTAMP',
+				'consulted_at'=> 'datetime DEFAULT NULL',
+				'treated_at'=> 'datetime DEFAULT NULL',
+				'treatment_count' => 'int DEFAULT 0',
+				'notes'		=> 'text DEFAULT NULL',
+			]);
+
+			$table->setKeys([
+				['key' => 'PRIMARY', 'value' => 'id'],
+				['key' => 'UNIQUE', 'value' => 'to_user (`to_user`)'],
+				'from_user (`from_user`)',
+				'status (`status`)',
+				'code (`referral_code`)',
+				'date (`created_at`)',
+				'consult (`consulted_at`)'
+			]);
+
+			$base = BASE;
+			$table->setConstraints([
+				"CONSTRAINT `{$base}referral_from_user_fk` FOREIGN KEY (`from_user`)
+				REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE",
+				"CONSTRAINT `{$base}referral_to_user_fk` FOREIGN KEY (`to_user`)
+				REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
+			]);
+			$table->defineTable();
+			$this->referrals = $table;
+		}
+
+		protected function defineCodeTable():void
+		{
+			$table = CustomTable::for('referrals_codes');
+
+			$table->setColumns([
+				'id'		=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+				'user_id'	=> "{$table->getUserIDType()} NOT NULL",
+				'code'		=> 'varchar(50) NOT NULL',
+				'created_at'=> 'datetime DEFAULT CURRENT_TIMESTAMP',
+			]);
+
+			$table->setKeys([
+				['key' => 'PRIMARY', 'value' => 'id'],
+				['key' => 'UNIQUE', 'value' => 'code (`code`)'],
+				'user (`user_id`)',
+			]);
+
+			$base = BASE;
+			$table->setConstraints([
+				"CONSTRAINT `{$base}referral_code_user_fk` FOREIGN KEY (`user_id`)
+					REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE",
+			]);
+			$table->defineTable();
+			$this->codes = $table;
+		}
+		protected function defineJaneClientsTable():void
+		{
+			$table = CustomTable::for('referrals_jane_clients');
+
+			$table->setColumns([
+				'id'			=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+				'patient_guid'	=> 'varchar(50) NOT NULL',
+				'user_id'		=> "{$table->getUserIDType()} NOT NULL",
+				'first_name'	=> 'varchar(100) NOT NULL',
+				'last_name'		=> 'varchar(100) NOT NULL',
+				'email'			=> 'varchar(255) NOT NULL',
+				'imported_at'	=> 'datetime DEFAULT CURRENT_TIMESTAMP',
+				'updated_at'	=> 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
+			]);
+
+			$table->setKeys([
+				['key' => 'PRIMARY', 'value' => '(`id`)'],
+				['key' => 'UNIQUE', 'value' => 'patient_guid (`patient_guid`)'],
+				'user (`user_id`)',
+				'email (`email`)',
+			]);
+
+			$base = BASE;
+			$table->setConstraints([
+				"CONSTRAINT `{$base}jane_clients_user` FOREIGN KEY (`user_id`)
+				REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE",
+			]);
+			$table->defineTable();
+			$this->janeClients = $table;
+		}
+		protected function defineRewardsTable():void
+		{
+			$table = CustomTable::for('referrals_rewards');
+
+			$table->setColumns([
+				'id'				=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+				'referral_id'		=> 'bigint(20) unsigned NOT NULL',
+				'user_id'			=> "{$table->getUserIDType()} NOT NULL",
+				'reward_type'		=> "ENUM('referrer', 'referee') NOT NULL",
+				'amount'			=> 'decimal(10,2) NOT NULL',
+				'reward_calculation'=> "ENUM('percentage', 'fixed')",
+				'status'			=> "ENUM('available', 'redeemed', 'expired', 'cancelled') DEFAULT 'available'",
+				'created_at'		=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP',
+				'updated_at'		=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
+				'redeemed_at'		=> 'datetime DEFAULT NULL',
+				'expires_at'		=> 'datetime DEFAULT NULL',
+				'notes'				=> 'text DEFAULT NULL',
+			]);
+
+			$table->setKeys([
+				['key' => 'PRIMARY', 'value' => '(`id`)'],
+				'referral (`referral_id`)',
+				'user (`user_id`)',
+				'status (`status`)',
+				'type (`reward_type`)'
+			]);
+
+			$base = BASE;
+			$table->setConstraints([
+				"CONSTRAINT `{$base}reward_referral` FOREIGN KEY (`referral_id`)
+				REFERENCES {$this->referrals->getFullTableName()} (`id`) ON DELETE CASCADE",
+				"CONSTRAINT `{$base}reward_user` FOREIGN KEY (`user_id`)
+				REFERENCES {$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
+			]);
+			$table->defineTable();
+			$this->rewards = $table;
+		}
+		protected function defineTreatmentsTable():void
+		{
+			$table = CustomTable::for('referral_treatments');
+
+			$table->setColumns([
+				'id'			=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+				'referral_id'	=> 'bigint(20) unsigned NOT NULL',
+				'user_id'		=> "{$table->getUserIDType()} NOT NULL",
+				'treatment_type'=> 'varchar(100) NOT NULL',		//Tier 1-6, Brows, etc
+				'treatment_date'=> 'datetime NOT NULL',
+				'invoice_number'=> 'varchar(50) DEFAULT NULL',
+				'amount'		=> 'decimal(10,2) DEFAULT NULL',
+				'status'		=> "ENUM('completed', 'no_show', 'cancelled') DEFAULT 'completed'",
+				'imported_at'	=> 'datetime DEFAULT CURRENT_TIMESTAMP',
+			]);
+
+			$table->setKeys([
+				['key' => 'PRIMARY', 'value' => '(`id`)'],
+				'referral (`referral_id`)',
+				'user (`user_id`)',
+				'date (`treatment_date`)',
+				'type (`treatment_type`)',
+			]);
+
+			$base = BASE;
+			$table->setConstraints([
+				"CONSTRAINT `{$base}treatment_referral` FOREIGN KEY (`referral_id`)
+				REFERENCES `{$this->referrals->getFullTableName()}` (`id`) ON DELETE CASCADE",
+				"CONSTRAINT `{$base}treatment_user` FOREIGN KEY (`user_id`)
+				REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
+			]);
+			$table->defineTable();
+			$this->treatments = $table;
+		}
+
 	public function getSettings():array
 	{
 		return $this->settings;
@@ -186,6 +369,26 @@
 		}
 	}
 
+	public function createCode(int $user_id, string $code):string|false
+	{
+		$code = sanitize_title($code);
+		$existing = $this->codes->get(['code' => $code]);
+		if ($existing) {
+			if ($existing['user_id'] !== $user_id) {
+				return false;
+			}
+			return $code;
+		}
+		$success = $this->codes->insert([
+			'user_id' 	=> $user_id,
+			'code'		=> $code
+		]);
+		if ($success) {
+			return $code;
+		}
+		return false;
+	}
+
 	/**
 	 * Generate or get existing referral code for a user
 	 *
@@ -193,31 +396,34 @@
 	 * @param string|null $custom_code Optional custom code
 	 * @return string|WP_Error
 	 */
-	public function getUserReferralCode(int $user_id, ?string $custom_code = null)
+	public function getUserReferralCode(int $user_id, ?string $custom_code = null):array|wp_error
 	{
 		$user = get_userdata($user_id);
 		if (!$user) {
 			return new WP_Error('invalid_user', 'User not found');
 		}
 
-		// Check if user already has a code
-		$existing_code = get_user_meta($user_id, BASE . 'referral_code', true);
-
-		if ($existing_code && !$custom_code) {
-			return $existing_code;
+		$existing = $this->codes->pluck('code', ['user_id' => $user_id],'created_at', 'DESC');
+		if ($existing && !$custom_code) {
+			return $existing;
+		}
+		if ($custom_code && !in_array($custom_code, $existing)) {
+			$test = $this->createCode($user_id, $custom_code);
+			if ($test) {
+				return $this->codes->pluck('code', ['user_id' => $user_id], 'created_at', 'DESC');
+			}
+		} else {
+			return $existing;
 		}
 
 		// Generate new code if custom provided or none exists
-		$code = $custom_code ?: $this->generateReferralCode($user);
+		$code = $this->generateReferralCode($user);
 
-		// Validate uniqueness
-		if ($this->isCodeTaken($code, $user_id)) {
-			return new WP_Error('code_taken', 'This referral code is already in use');
+		$success = $this->createCode($user_id, $code);
+		if ($success) {
+			return $this->codes->pluck('code', ['user_id' => $user_id], 'created_at', 'DESC');
 		}
 
-		// Save the code
-		update_user_meta($user_id, BASE . 'referral_code', $code);
-
 		return $code;
 	}
 
@@ -251,24 +457,11 @@
 	 * Check if a referral code is already taken
 	 *
 	 * @param string $code
-	 * @param int|null $exclude_user_id
 	 * @return bool
 	 */
-	protected function isCodeTaken(string $code, ?int $exclude_user_id = null): bool
+	protected function isCodeTaken(string $code): bool
 	{
-		$args = [
-			'meta_key' => BASE . 'referral_code',
-			'meta_value' => $code,
-			'fields' => 'ID',
-			'number' => 1
-		];
-
-		if ($exclude_user_id) {
-			$args['exclude'] = [$exclude_user_id];
-		}
-
-		$users = get_users($args);
-		return !empty($users);
+		return (bool) $this->codes->get(['code' => $code]);
 	}
 
 	public function processRegistrationToken(int $user_id, string $email, array $data): void
@@ -295,60 +488,58 @@
 	 * Track a new referral when user registers
 	 *
 	 * @param int $user_id
+	 * @param array $userData
 	 * @return bool;
 	 */
-	public function processReferral(int $user_id): bool
+	public function processReferral(int $user_id, array $userData): bool
 	{
-		// Try to get code from user meta first (set during registration)
-		$referral_code = get_user_meta($user_id, BASE . 'pending_referral_code', true);
+		$referral = $this->referrals->get(['to_user' => $user_id]);
 
-		if (empty($referral_code)) {
+		if (empty($referral)) {
+			$referral = $this->referrals->get(['to_email' => $userData['email']]);
+		}
+		if (empty($referral)) {
 			// Check session/cookie if not in meta
 			if (session_status() === PHP_SESSION_NONE) {
 				session_start();
 			}
 			$referral_code = $_SESSION[BASE . 'referral_code'] ?? $_COOKIE[BASE . 'referral_code'] ?? '';
+			if (!empty ($referral_code)) {
+				$referral = [
+					'to_user'	=> $user_id,
+					'referral_code'	=> $referral_code,
+					'to_email'		=> $userData['user_email']
+				];
+			}
 		}
 
-		if (empty($referral_code)) {
+		if (empty($referral)) {
 			return false; // No referral code - regular registration
 		}
 
 		// Find the referrer
-		$referrer = $this->getUserByReferralCode($referral_code);
-		if (!$referrer) {
-			delete_user_meta($user_id, BASE . 'pending_referral_code');
+		$referrer = $this->codes->pluck('user_id', ['code' => $referral['referral_code']]);
+		if (empty($referrer)) {
+			//This should not happen, but whatever
+			return false;
+		}
+		$referrer = $referrer[0];
+		$record = $this->referrals->findOrCreate([
+			'to_user'	=> $user_id,
+			'referral_code'	=> $referral['referral_code'],
+		], [
+			'from_user'		=> $referrer,
+			'to_email'		=> $referral['to_email'],
+			'to_name'		=> $userData['first_name'],
+//			'to_phone'		=>
+			'status'		=> 'pending'
+		]);
+
+		if (!$record) {
+			error_log('[ReferralManager]::processReferral Could not update record for user: '.print_r($referral, true));
 			return false;
 		}
 
-		$user = get_userdata($user_id);
-
-		// Check if referral already exists for this user
-		$existing = $this->wpdb->get_row($this->wpdb->prepare(
-			"SELECT * FROM {$this->referrals_table}
-		WHERE referrer_id = %d AND (referee_email = %s OR referee_id = %d)",
-			$referrer->ID,
-			$user->user_email,
-			$user_id
-		));
-
-		if (!$existing) {
-			// Create new referral record - referred_at captures registration time
-			$this->wpdb->insert(
-				$this->referrals_table,
-				[
-					'referrer_id' => $referrer->ID,
-					'referee_id' => $user_id,
-					'referee_name' => $user->display_name,
-					'referee_email' => $user->user_email,
-					'referee_phone' => get_user_meta($user_id, BASE . 'phone', true) ?: '',
-					'referral_code' => $referral_code,
-					'status' => 'pending', // pending first treatment
-					'referred_at' => current_time('mysql') // When they registered
-				],
-				['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s']
-			);
-		}
 
 		// Clean up temp data
 		delete_user_meta($user_id, BASE . 'pending_referral_code');
@@ -363,10 +554,10 @@
 		$this->cache->flush();
 
 		// Fire action for tracking
-		do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral_code);
+		do_action('jvb_referral_processed', $user_id, $referrer->ID, $referral['referral_code']);
 
 		// Send notification to referrer
-		$this->sendReferrerNotification($referrer->ID, $user->display_name);
+		$this->sendReferrerNotification($referrer->ID, $userData['display_name']);
 		return true;
 	}
 
diff --git a/inc/managers/ResponseManager.php b/inc/managers/ResponseManager.php
new file mode 100644
index 0000000..fe12ede
--- /dev/null
+++ b/inc/managers/ResponseManager.php
@@ -0,0 +1,64 @@
+<?php
+namespace JVBase\managers;
+
+if (!defined('ABSPATH')) {
+	exit; // Exit if accessed directly
+}
+/**
+ * Response Manager
+ *
+ * Handles a responses
+ * TODO
+ */
+class ResponseManager
+{
+	protected CustomTable $response;
+	protected KarmaManager $karma;
+
+	public function __construct()
+	{
+		$this->defineTables();
+	}
+	protected function defineTables():void
+	{
+		$table = CustomTable::for('responses');
+
+		$table->setColumns([
+			'id'		=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+			'item_id'	=> "{$table->getPostIDType()} NOT NULL",
+			'type'		=> 'varchar(50) NOT NULL',
+			'user_id'	=> "{$table->getUserIdType()} NOT NULL",
+			'parent_id'	=> 'bigint(20) unsigned DEFAULT 0',
+			'response'	=> 'text NOT NULL',
+			'upvotes'	=> 'int NOT NULL DEFAULT 0',
+			'downvotes'	=> 'int NOT NULL DEFAULT 0',
+			'karma'		=> 'int NOT NULL DEFAULT 0',
+			'status'	=> "ENUM('publish', 'hidden','flagged','deleted') DEFAULT 'publish'",
+			'is_user_deleted' => 'tinyint(1) DEFAULT 0',
+			'created_at'=> 'datetime DEFAULT CURRENT_TIMESTAMP',
+			'updated_at'=> 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
+		]);
+
+		$table->setKeys([
+			['key' => 'PRIMARY', 'value' => '`id`'],
+			'`item_lookup` (`item_id`)',
+			'`user_lookup` (`user_id`)',
+			'`parent_lookup` (`parent_id`)',
+			'`karma` (`karma`)',
+			'`upvote` (`upvotes`)',
+			'`downvote` (`downvotes`)',
+		]);
+
+		$base = BASE;
+		$table->setConstraints([
+			"CONSTRAINT `{$base}responses_item` FOREIGN KEY (`item_id`)
+			REFERENCES `{$table->getPostTable()}` (`ID`) ON DELETE CASCADE",
+		]);
+		$table->defineTable();
+
+		$this->response = $table;
+
+		$this->karma = KarmaManager::for('responses', 'responses');
+	}
+
+}
diff --git a/inc/managers/RoleManager.php b/inc/managers/RoleManager.php
index 0f235d6..4de3271 100644
--- a/inc/managers/RoleManager.php
+++ b/inc/managers/RoleManager.php
@@ -16,9 +16,7 @@
 
     public function __construct()
     {
-       $this->roles = array_keys(array_map(function ($instance) {
-		   return $instance->slug;
-	   }, Registrar::getRegistered('user')));
+       $this->roles = Registrar::getRegistered('user');
 
 	   $this->content = array_map(function($content) {
 		   $registrar = Registrar::getInstance($content);
@@ -475,6 +473,15 @@
 		$user->add_cap(BASE . 'can_own_' . $termID);
 		$user->add_cap(BASE . 'can_manage_' . $termID);
 
+		$owners = get_term_meta($termID, BASE.'owners', true);
+		if (empty($owners)) {
+			$owners = [];
+		}
+		$owners[] = $userID;
+		$owners = array_unique($owners);
+		update_term_meta($termID, BASE.'owners', $owners);
+
+
 		do_action(BASE . 'granted_ownership', $userID, $termID, $taxonomy);
 
 		return true;
@@ -500,6 +507,16 @@
 			return false;
 		}
 
+		$owners = get_term_meta($termID, BASE.'owners', true);
+		if (empty($owners)) {
+			$owners = [];
+		}
+		if (in_array($userID, $owners)) {
+			unset($owners[array_search($userID, $owners)]);
+		}
+		$owners = array_unique($owners);
+		update_term_meta($termID, BASE.'owners', $owners);
+
 		$user->remove_cap(BASE . 'can_own_' . $termID);
 
 		do_action(BASE . 'revoked_ownership', $userID, $termID, $taxonomy);
@@ -537,6 +554,14 @@
 
 		$user->add_cap(BASE . 'can_manage_' . $termID);
 
+		$managers = get_term_meta($termID, BASE.'managers', true);
+		if (empty($managers)) {
+			$managers = [];
+		}
+		$managers[] = $userID;
+		$managers = array_unique($managers);
+		update_term_meta($termID, BASE.'managers', $managers);
+
 		do_action(BASE . 'granted_management', $userID, $termID, $taxonomy);
 
 		return true;
@@ -564,6 +589,16 @@
 
 		$user->remove_cap(BASE . 'can_manage_' . $termID);
 
+		$managers = get_term_meta($termID, BASE.'managers', true);
+		if (empty($managers)) {
+			$managers = [];
+		}
+		if (in_array($userID, $managers)) {
+			unset($managers[array_search($userID, $managers)]);
+		}
+		$managers = array_unique($managers);
+		update_term_meta($termID, BASE.'managers', $managers);
+
 		do_action(BASE . 'revoked_management', $userID, $termID, $taxonomy);
 
 		return true;
diff --git a/inc/managers/SEO/BreadcrumbManager.php b/inc/managers/SEO/BreadcrumbManager.php
index 8d3288e..72929a2 100644
--- a/inc/managers/SEO/BreadcrumbManager.php
+++ b/inc/managers/SEO/BreadcrumbManager.php
@@ -121,10 +121,10 @@
 					'name' => $contentRegistrar->getConfig('breadcrumbs')['title']??$contentRegistrar->getPlural(),
 					'url'  => get_post_type_archive_link(jvbCheckBase($content)),
 				];
-				$crumbs[] = [
-					'name' => 'By ' . $registrar->getSingular(),
-					'url'  => false,
-				];
+//				$crumbs[] = [
+//					'name' => 'By ' . $registrar->getSingular(),
+//					'url'  => false,
+//				];
 			}
 		}
 
@@ -150,9 +150,11 @@
 		// Add directory if exists
 		$content = jvbNoBase($post->post_type);
 		$registrar = Registrar::getInstance($content);
+		$crumbConfig = false;
 		if ($registrar){
 			$crumbConfig = $registrar->getConfig('breadcrumbs');
 		}
+
 		if($registrar && $registrar->hasFeature('show_directory')) {
 			$directory = JVB()->directories()?->directories($content)??[];
 			if (!empty($directory)) {
@@ -209,7 +211,7 @@
 		} elseif (is_post_type_archive() && $registrar && $registrar->hasFeature('show_directory')) {
 
 			$crumbs[] = [
-				'name' => $registrar->getConfig('breadcrumb')['title'] ?? $registrar->getPlural(),
+				'name' => $registrar->getConfig('breadcrumbs')['title'] ?? $registrar->getPlural(),
 				'url'  => get_post_type_archive_link($type)
 			];
 		}
@@ -366,29 +368,33 @@
 		}
 	}
 
-	public function addTaxToCrumbs(array $crumbs, string $taxonomy):array
+	public function addTaxToCrumbs(array $crumbs, string|array $taxonomy):array
 	{
 		$ID = get_the_ID();
-		$taxonomy = jvbCheckBase($taxonomy);
-		$terms = get_the_terms($ID, $taxonomy);
-		if ($terms && !is_wp_error($terms)) {
-			$term = $terms[0];
-			$ancestors = get_ancestors($term->term_id, $taxonomy, 'taxonomy');
-			$ancestors = array_reverse($ancestors);
-			foreach ($ancestors as $ancestor) {
-				$aTerm = get_term($ancestor, $taxonomy);
-				if ($aTerm && !is_wp_error($aTerm)) {
-					$crumbs[] = [
-						'name' => $aTerm->name,
-						'url'   => get_term_link($ancestor, $taxonomy)
-					];
+		$taxonomies = is_string($taxonomy) ? [$taxonomy] : $taxonomy;
+		foreach ($taxonomies as $tax) {
+			$taxonomy = jvbCheckBase($tax);
+			$terms = get_the_terms($ID, $taxonomy);
+			if ($terms && !is_wp_error($terms)) {
+				$term = $terms[0];
+				$ancestors = get_ancestors($term->term_id, $taxonomy, 'taxonomy');
+				$ancestors = array_reverse($ancestors);
+				foreach ($ancestors as $ancestor) {
+					$aTerm = get_term($ancestor, $taxonomy);
+					if ($aTerm && !is_wp_error($aTerm)) {
+						$crumbs[] = [
+							'name' => $aTerm->name,
+							'url'   => get_term_link($ancestor, $taxonomy)
+						];
+					}
 				}
+				$crumbs[] = [
+					'name' => html_entity_decode($term->name),
+					'url'   => get_term_link($term, $taxonomy)
+				];
 			}
-			$crumbs[] = [
-				'name' => html_entity_decode($term->name),
-				'url'   => get_term_link($term, $taxonomy)
-			];
 		}
+
 		return $crumbs;
 	}
 }
diff --git a/inc/managers/SEO/render/DataType/Date.php b/inc/managers/SEO/render/DataType/Date.php
index 7521f08..b15cfbc 100644
--- a/inc/managers/SEO/render/DataType/Date.php
+++ b/inc/managers/SEO/render/DataType/Date.php
@@ -26,13 +26,9 @@
 		return $this->date;
 	}
 
-	/**
-	 * @throws \DateMalformedStringException
-	 */
 	public function setDate(string $date):void
 	{
-		$time = new \DateTime(strtotime($date));
-		$time = $time->format('c');
+		$time = date('c', strtotime($date));
 		if ($time){
 			$this->date = $time;
 		}
diff --git a/inc/managers/SEO/render/DataType/Time.php b/inc/managers/SEO/render/DataType/Time.php
index 0069ea4..1efc50e 100644
--- a/inc/managers/SEO/render/DataType/Time.php
+++ b/inc/managers/SEO/render/DataType/Time.php
@@ -1,7 +1,7 @@
 <?php
 namespace JVBase\managers\SEO\render\DataType;
-use DateMalformedStringException;
-use DateTime;
+use JVBase\meta\Sanitizer;
+
 if (!defined('ABSPATH')) {
 	exit;
 }
@@ -20,14 +20,7 @@
 		return $this->time;
 	}
 
-	/**
-	 * @throws DateMalformedStringException
-	 */
 	public function setTime(string $time):void {
-		$time = new DateTime(strtotime($time));
-		$time = $time->format('H:i:s');
-		if ($time){
-			$this->time = $time;
-		}
+		$this->time = Sanitizer::sanitize($time, ['type'=> 'time']);
 	}
 }
diff --git a/inc/managers/SEO/render/SchemaOutput.php b/inc/managers/SEO/render/SchemaOutput.php
index 909e6bb..4619d94 100644
--- a/inc/managers/SEO/render/SchemaOutput.php
+++ b/inc/managers/SEO/render/SchemaOutput.php
@@ -1,13 +1,11 @@
 <?php
 namespace JVBase\managers\SEO\render;
 
+use JVBase\inc\managers\SEO\render\Thing\Intangible\ItemList\OfferCatalog;
 use JVBase\managers\Cache;
 use JVBase\managers\SEO\BreadcrumbManager;
-use JVBase\managers\SEO\render\Thing\CreativeWork\WebSite;
-use JVBase\managers\SEO\render\Thing\Intangible\OfferCatalog;
 use JVBase\managers\SEO\render\Thing\Intangible\Service;
 use JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\LocalBusiness;
-use JVBase\meta\Meta;
 use JVBase\registrar\config\seo\Resolver;
 use JVBase\registrar\Registrar;
 
@@ -25,8 +23,9 @@
 	{
 		$schema = [];
 
-		$schema[] = $this->buildWebsiteSchema(false);
+
 		if (is_front_page()) {
+			$schema[] = $this->buildWebsiteSchema(true);
 			$test = $this->buildOrganizationSchema();
 			if (!empty($test)) {
 				$schema[] = $test;
@@ -35,17 +34,17 @@
 		if (is_singular($this->types)) {
 			$type = get_post_type();
 			$registrar = Registrar::getInstance($type);
-			if ($registrar && !empty($registrar->getConfig('seo')['schema']??[])) {
+			if ($registrar) {
 				$seo = $registrar->getSEO();
 				$schema[] = $seo->schema()->outputSingularSchema();
 			}
 		}elseif (is_post_type_archive($this->types)) {
-
+			error_log('It is a post type archive');
 			$type = get_queried_object();
 			$type = $type->name;
 			$registrar = Registrar::getInstance($type);
 
-			if ($registrar && !empty($registrar->getConfig('seo')['schema']??[])) {
+			if ($registrar) {
 				$seo = $registrar->getSEO();
 				$schema[] =$seo->schema()->outputArchiveSchema();
 			}
@@ -53,7 +52,7 @@
 			$type = get_queried_object();
 			$type = jvbNoBase($type->taxonomy);
 			$registrar = Registrar::getInstance($type);
-			if ($registrar && !empty($registrar->getConfig('seo')['schema']??[])) {
+			if ($registrar ) {
 				$seo = $registrar->getSEO();
 				error_log('SEO: '.print_r($seo->schema(), true));
 				$schema[] = $seo->schema()->outputArchiveSchema();
@@ -66,10 +65,13 @@
 		if (is_page($isContent)){
 			$type = get_post_meta(get_the_id(), BASE.'for_type', true);
 			error_log('Type: '.print_r($type, true));
-			$registrar = Registrar::getInstance($type);
-			if ($registrar) {
-				$schema[] = $registrar->getSEO()->schema()->outputContentTaxArchiveSchema();
+			if (!empty($type)) {
+				$registrar = Registrar::getInstance($type);
+				if ($registrar) {
+					$schema[] = $registrar->getSEO()->schema()->outputContentTaxArchiveSchema();
+				}
 			}
+
 		}
 
 
@@ -80,19 +82,22 @@
 		}
 
 		if (!empty($schema)) {
-			$website = JVB()->schemaHelper()::schema('website');
-			if (!empty($website)) {
-				if (JVB_TESTING) {
-					Cache::for('websiteSchema')->flush();
-				}
-				$website = Cache::for('websiteSchema')->remember(
-					'schema',
-					function () {
-						return $this->websiteSchema();
+			if (!is_front_page()) {
+				$website = JVB()->schemaHelper()::schema('website');
+				if (!empty($website)) {
+					if (JVB_TESTING) {
+						Cache::for('websiteSchema')->flush();
 					}
-				);
-				array_unshift($schema, $website);
+					$website = Cache::for('websiteSchema')->remember(
+						'schema',
+						function () {
+							return $this->websiteSchema();
+						}
+					);
+					array_unshift($schema, $website);
+				}
 			}
+
 			$schema = [
 				'@context'		=> 'https://schema.org',
 				'@graph'		=> $schema
@@ -113,8 +118,8 @@
 		if (!$website->getName() || empty($website->getName())){
 			$website->setName(get_bloginfo('name'));
 		}
-		if(!$website->getCreator() || empty($website->getCreator())) {
-			$website->setCreator($this->getCreator());
+		if(!$website->getCreator() || empty($website->getCreator(true))) {
+			$website->setCreator($this->getCreator(true));
 		}
 		return $website->outputSchema();
 	}
@@ -125,9 +130,17 @@
 		if (empty($schema)) {
 			return;
 		}
+//		$encoded =  wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
+//		$encoded =  wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT, 1024);
+		$encoded = wp_json_encode($schema, JSON_UNESCAPED_SLASHES, 512);
+		if ($encoded === false) {
+			error_log('wp_json_encode failed: ' . json_last_error_msg());
+			return;
+		}
 		echo "\n<!-- SEO Schema by JakeVan -->\n";
 		echo '<script type="application/ld+json">' . "\n";
-		echo wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
+
+		echo $encoded;
 		echo "\n" . '</script>' . "\n";
 		echo "\n" . '<!-- / SEO Schema by JakeVan -->'."\n";
 	}
@@ -185,7 +198,6 @@
 		return Cache::for('websiteSchema')->remember(
 			'full',
 			function () use ($storedWebsite) {
-				error_log('StoredWebsite: '.print_r($storedWebsite, true));
 				$website = JVB()->schemaHelper()::classFromConfig($storedWebsite);
 				if (!$website->getName() || empty($website->getName())) {
 					$website->setName(get_bloginfo('name'));
@@ -217,71 +229,78 @@
 	}
 
 
-	public function getCreator(bool $reference = false):LocalBusiness
+	public function getCreator(bool $reference = false, bool $jakeVan = false):LocalBusiness
 	{
 		if (JVB_TESTING){
 			Cache::for('JakeVanCreator')->flush();
 		}
 
-		if ($reference) {
+		if ($reference && $jakeVan) {
 			return Cache::for('JakeVanCreator')->remember(
 				'reference',
 				function () {
 					$creator = new LocalBusiness();
+					$creator->setId('jakevan');
 					$creator->setName('JakeVan');
-					$creator->setAlternateName('Jake Vanderwerf');
 					$creator->setUrl('https://jakevan.ca/');
 					$creator->setDescription('Let\'s bring your idea to life.');
 					return $creator;
 				}
 			);
 		}
+		if ($jakeVan) {
+			return Cache::for('JakeVanCreator')->remember(
+				'full',
+				function () {
+					$creator = new LocalBusiness();
+					$creator->setName('JakeVan');
+					$creator->setId('https://jakevan.ca/#localbusiness');
+					$creator->setAlternateName(['Jake Vanderwerf', 'Rooted Romantic', 'Jacob Vanderwerf']);
+					$creator->setUrl('https://jakevan.ca');
+					$creator->setSameAs([
+						'https://bsky.app/profile/jakevan.ca',
+					]);
+					$creator->setAreaServed(['Edmonton, Alberta', 'Alberta', 'Canada']);
+					$offers = new OfferCatalog();
+					$offers->setName('Services');
+					$offers->setUrl('https://jakevan.ca/services');
 
-		return Cache::for('JakeVanCreator')->remember(
-			'full',
-			function () {
-				$creator = new LocalBusiness();
-				$creator->setName('JakeVan');
-				$creator->setId('https://jakevan.ca/#localbusiness');
-				$creator->setAlternateName(['Jake Vanderwerf', 'Rooted Romantic', 'Jacob Vanderwerf']);
-				$creator->setUrl('https://jakevan.ca');
-				$creator->setSameAs([
-					'https://bsky.app/profile/jakevan.ca',
-				]);
-				$creator->setAreaServed(['Edmonton, Alberta', 'Alberta', 'Canada']);
-				$offers = new OfferCatalog();
-				$offers->setName('Services');
-				$offers->setUrl('https://jakevan.ca/services');
+					$graphicDesign = new Service();
+					$graphicDesign->setName('Graphic Design');
+					$graphicDesign->setUrl('https://jakevan.ca/services/design/');
+					$graphicDesign->setDescription('From print to digital design.');
+					$websiteDesign = new Service();
+					$websiteDesign->setName('Development');
+					$websiteDesign->setUrl('https://jakevan.ca/services/development/');
+					$websiteDesign->setDescription('From basic websites to custom functionality.');
+					$strategy = new Service();
+					$strategy->setName('Strategy');
+					$strategy->setUrl('https://jakevan.ca/services/strategy/');
+					$strategy->setDescription('From developing your business plan to SEO.');
+					$art = new Service();
+					$art->setName('Art');
+					$art->setUrl('https://jakevan.ca/services/art/');
+					$art->setDescription('From unique, custom, handmade pieces to small-scale wholesale.');
 
-				$graphicDesign = new Service();
-				$graphicDesign->setName('Graphic Design');
-				$graphicDesign->setUrl('https://jakevan.ca/services/design/');
-				$graphicDesign->setDescription('From print to digital design.');
-				$websiteDesign = new Service();
-				$websiteDesign->setName('Development');
-				$websiteDesign->setUrl('https://jakevan.ca/services/development/');
-				$websiteDesign->setDescription('From basic websites to custom functionality.');
-				$strategy = new Service();
-				$strategy->setName('Strategy');
-				$strategy->setUrl('https://jakevan.ca/services/strategy/');
-				$strategy->setDescription('From developing your business plan to SEO.');
-				$art = new Service();
-				$art->setName('Art');
-				$art->setUrl('https://jakevan.ca/services/art/');
-				$art->setDescription('From unique, custom, handmade pieces to small-scale wholesale.');
-
-				$offers->setItemListElement([
-					$graphicDesign,
-					$websiteDesign,
-					$strategy,
-					$art
-				]);
+					$offers->setItemListElement([
+						$graphicDesign,
+						$websiteDesign,
+						$strategy,
+						$art
+					]);
 
 
-				$creator->setHasOfferCatalog($offers);
-				return $creator;
-			}
-		);
+					$creator->setHasOfferCatalog($offers);
+					return $creator;
+				}
+			);
+		}
+		if ($reference) {
+			return $this->getOptionSchemaReference('organization');
+		}
+		$config = JVB()->schemaHelper()::schema('organization');
+
+		return JVB()->schemaHelper()::classFromConfig($config);
 	}
 
 	public function getOptionSchemaReference(string $option):mixed
@@ -294,8 +313,8 @@
 			error_log('Attempted to get schema reference for: '.$option.', but it does not exist');
 			return null;
 		}
-		$action = BASE.ucfirst($option).'Schema';
-		$stored = JVB()->schemaHelper()::reference($action);
+
+		$stored = JVB()->schemaHelper()::reference($option);
 
 		if (empty($stored)){
 			error_log('Attempted to get schema reference for: '.$option.', but defaults not set.');
@@ -308,7 +327,7 @@
 		}
 		$class = new $type();
 		unset($stored['type']);
-		$minimal = apply_filters($action.'Reference', ['name', 'url', 'sameAs', 'logo']);
+		$minimal = apply_filters(BASE.'OrganizationReference', ['name', 'url', 'sameAs', 'logo']);
 
 		foreach ($minimal as $property) {
 			$method = 'set'.ucfirst($property);
@@ -316,6 +335,7 @@
 			if (!$value) {continue;}
 
 			if (str_contains($value, '{{')) {
+				$meta = null;
 				$value = Resolver::resolve($property, $value);
 			}
 			$class->$method($value);
@@ -326,6 +346,7 @@
 	public function buildOrganizationSchema():array
 	{
 		$config = JVB()->schemaHelper()::schema('organization');
+
 		$class = JVB()->schemaHelper()::classFromConfig($config);
 		return ($class)? $class->outputSchema() : [];
 	}
diff --git a/inc/managers/SEO/render/Thing/Action.php b/inc/managers/SEO/render/Thing/Action.php
deleted file mode 100644
index a82035b..0000000
--- a/inc/managers/SEO/render/Thing/Action.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-namespace JVBase\managers\SEO\render\Thing;
-
-use JVBase\managers\SEO\render\DataType\DateTime;
-use JVBase\managers\SEO\render\DataType\Time;
-use JVBase\managers\SEO\render\Thing\CreativeWork\HowTo;
-use JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress;
-use JVBase\managers\SEO\render\Thing\Intangible\EntryPoint;
-use JVBase\managers\SEO\render\Thing\Intangible\Enumeration\ActionStatusType;
-use JVBase\managers\SEO\render\Thing\Intangible\VirtualLocation;
-use JVBase\managers\SEO\render\Thing\Organization\Organization;
-use JVBase\managers\SEO\render\Thing\Person\Person;
-use JVBase\managers\SEO\render\Thing\Place\Place;
-use JVBase\managers\SEO\render\Traits\_Properties\actionProcessTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\actionStatusTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\agentTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\endTimeTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\errorTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\instrumentTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\locationTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\objectTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\participantTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\providerTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\resultTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\startTimeTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\targetTrait;
-
-if (!defined('ABSPATH')) {
-	exit;
-}
-
-class Action {
-	use actionProcessTrait, actionStatusTrait, agentTrait,
-		endTimeTrait, errorTrait, instrumentTrait, locationTrait,
-		objectTrait, participantTrait, providerTrait, resultTrait,
-		startTimeTrait, targetTrait;
-}
diff --git a/inc/managers/SEO/render/Thing/Action/Action.php b/inc/managers/SEO/render/Thing/Action/Action.php
new file mode 100644
index 0000000..318f89f
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Action/Action.php
@@ -0,0 +1,30 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Thing\Action;
+
+use JVBase\managers\SEO\render\Thing\Thing;
+use JVBase\managers\SEO\render\Traits\_Properties\actionProcessTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\actionStatusTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\agentTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\descriptionTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\endTimeTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\errorTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\instrumentTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\locationTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\nameTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\objectTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\participantTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\providerTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\resultTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\startTimeTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\targetTrait;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class Action extends Thing {
+	use  actionProcessTrait, actionStatusTrait, agentTrait,
+		endTimeTrait, errorTrait, instrumentTrait, locationTrait,
+		objectTrait, participantTrait, providerTrait, resultTrait,
+		startTimeTrait, targetTrait;
+}
diff --git a/inc/managers/SEO/render/Thing/Action/CommunicateAction.php b/inc/managers/SEO/render/Thing/Action/CommunicateAction.php
new file mode 100644
index 0000000..1f7c93b
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Action/CommunicateAction.php
@@ -0,0 +1,14 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Thing\Action;
+
+use JVBase\managers\SEO\render\Traits\_Properties\aboutTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\inLanguageTrait;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class CommunicateAction extends InteractAction {
+	use aboutTrait, inLanguageTrait;
+	//also has "recipient"
+}
diff --git a/inc/managers/SEO/render/Thing/Action/InteractAction.php b/inc/managers/SEO/render/Thing/Action/InteractAction.php
new file mode 100644
index 0000000..ac4caa4
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Action/InteractAction.php
@@ -0,0 +1,24 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Thing\Action;
+
+use JVBase\managers\SEO\render\Traits\_Properties\actionProcessTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\actionStatusTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\agentTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\endTimeTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\errorTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\instrumentTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\locationTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\objectTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\participantTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\providerTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\resultTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\startTimeTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\targetTrait;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class InteractAction extends Action {
+
+}
diff --git a/inc/managers/SEO/render/Thing/Action/OrganizeAction.php b/inc/managers/SEO/render/Thing/Action/OrganizeAction.php
new file mode 100644
index 0000000..9b03fbb
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Action/OrganizeAction.php
@@ -0,0 +1,10 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Thing\Action;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class OrganizeAction extends Action {
+
+}
diff --git a/inc/managers/SEO/render/Thing/Action/PlanAction.php b/inc/managers/SEO/render/Thing/Action/PlanAction.php
new file mode 100644
index 0000000..3bee51b
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Action/PlanAction.php
@@ -0,0 +1,13 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Thing\Action;
+
+use JVBase\inc\managers\SEO\render\Traits\_Properties\scheduledTimeTrait;
+
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class PlanAction extends OrganizeAction {
+	use scheduledTimeTrait;
+}
diff --git a/inc/managers/SEO/render/Thing/Action/ScheduleAction.php b/inc/managers/SEO/render/Thing/Action/ScheduleAction.php
new file mode 100644
index 0000000..8b0aa9e
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Action/ScheduleAction.php
@@ -0,0 +1,10 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Thing\Action;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class ScheduleAction extends PlanAction {
+
+}
diff --git a/inc/managers/SEO/render/Thing/Action/_setup.php b/inc/managers/SEO/render/Thing/Action/_setup.php
new file mode 100644
index 0000000..6a9379d
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Action/_setup.php
@@ -0,0 +1,9 @@
+<?php
+require(JVB_DIR . '/inc/managers/SEO/render/Thing/Action/Action.php');
+	require(JVB_DIR . '/inc/managers/SEO/render/Thing/Action/InteractAction.php');
+		require(JVB_DIR . '/inc/managers/SEO/render/Thing/Action/CommunicateAction.php');
+	require(JVB_DIR . '/inc/managers/SEO/render/Thing/Action/OrganizeAction.php');
+		require(JVB_DIR . '/inc/managers/SEO/render/Thing/Action/PlanAction.php');
+			require(JVB_DIR . '/inc/managers/SEO/render/Thing/Action/ScheduleAction.php');
+
+
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/Comment/Answer.php b/inc/managers/SEO/render/Thing/CreativeWork/Comment/Answer.php
new file mode 100644
index 0000000..a87e4ff
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/Comment/Answer.php
@@ -0,0 +1,14 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment;
+
+use JVBase\inc\managers\SEO\render\Traits\_Properties\answerExplanationTrait;
+use JVBase\inc\managers\SEO\render\Traits\_Properties\parentItemTrait;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class Answer extends Comment
+{
+	use parentItemTrait, answerExplanationTrait;
+}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/Comment/Comment.php b/inc/managers/SEO/render/Thing/CreativeWork/Comment/Comment.php
new file mode 100644
index 0000000..c56bd77
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/Comment/Comment.php
@@ -0,0 +1,17 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment;
+
+use JVBase\inc\managers\SEO\render\Traits\_Properties\downvoteCountTrait;
+use JVBase\inc\managers\SEO\render\Traits\_Properties\parentItemTrait;
+use JVBase\inc\managers\SEO\render\Traits\_Properties\sharedContentTrait;
+use JVBase\inc\managers\SEO\render\Traits\_Properties\upvoteCountTrait;
+use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class Comment extends CreativeWork
+{
+	use downvoteCountTrait, parentItemTrait, sharedContentTrait, upvoteCountTrait;
+}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/Comment/Question.php b/inc/managers/SEO/render/Thing/CreativeWork/Comment/Question.php
new file mode 100644
index 0000000..c0ddd02
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/Comment/Question.php
@@ -0,0 +1,16 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment;
+
+use JVBase\inc\managers\SEO\render\Traits\_Properties\acceptedAnswerTrait;
+use JVBase\inc\managers\SEO\render\Traits\_Properties\answerCountTrait;
+use JVBase\inc\managers\SEO\render\Traits\_Properties\parentItemTrait;
+use JVBase\inc\managers\SEO\render\Traits\_Properties\suggestedAnswerTrait;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class Question extends Comment
+{
+	use acceptedAnswerTrait, answerCountTrait, parentItemTrait, suggestedAnswerTrait;
+}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/Comment/_setup.php b/inc/managers/SEO/render/Thing/CreativeWork/Comment/_setup.php
new file mode 100644
index 0000000..ff52393
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/Comment/_setup.php
@@ -0,0 +1,4 @@
+<?php
+require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/Comment/Comment.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/Comment/Answer.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/Comment/Question.php');
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/MediaObject/MediaObject.php b/inc/managers/SEO/render/Thing/CreativeWork/MediaObject/MediaObject.php
index 11ee774..62ac343 100644
--- a/inc/managers/SEO/render/Thing/CreativeWork/MediaObject/MediaObject.php
+++ b/inc/managers/SEO/render/Thing/CreativeWork/MediaObject/MediaObject.php
@@ -55,4 +55,11 @@
 	 * @var Date|DateTime Date (including time if available) when this media object was uploaded to this site.
 	 */
 	protected Date|DateTime $uploadDate;
+	public function setUploadDate(string|DateTime $date):void
+	{
+		if (is_string($date)) {
+			$date = new DateTime($date);
+		}
+		$this->uploadDate = $date;
+	}
 }
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage.php b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage.php
deleted file mode 100644
index 43be137..0000000
--- a/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-namespace JVBase\managers\SEO\render\Thing\CreativeWork\WebPage;
-
-use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork;
-use JVBase\managers\SEO\render\Traits\_Properties\breadcrumbTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\lastReviewedTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\primaryImageOfPageTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\relatedLinkTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\reviewedByTrait;
-use JVBase\managers\SEO\render\Traits\_Properties\significantLinkTrait;
-
-if (!defined('ABSPATH')) {
-	exit;
-}
-
-
-class CollectionPage extends WebPage
-{
-
-}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/CollectionPage.php b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/CollectionPage.php
new file mode 100644
index 0000000..39ba979
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/CollectionPage.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage;
+
+use JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\WebPage;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+
+class CollectionPage extends WebPage
+{
+
+}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/ImageGallery.php b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/ImageGallery.php
new file mode 100644
index 0000000..77a871d
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/ImageGallery.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+
+class ImageGallery extends MediaGallery
+{
+
+}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/MediaGallery.php b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/MediaGallery.php
new file mode 100644
index 0000000..057ebdb
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/MediaGallery.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage;
+
+use JVBase\managers\SEO\render\Traits\_Properties\additionalPropertyTrait;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+
+class MediaGallery extends CollectionPage
+{
+	use additionalPropertyTrait;
+}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/WebPage/_setup.php b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/_setup.php
index 05e2f53..20f1c5b 100644
--- a/inc/managers/SEO/render/Thing/CreativeWork/WebPage/_setup.php
+++ b/inc/managers/SEO/render/Thing/CreativeWork/WebPage/_setup.php
@@ -2,7 +2,9 @@
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/WebPage.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/AboutPage.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CheckoutPage.php');
-require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/CollectionPage.php');
+	require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/MediaGallery.php');
+	require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/CollectionPage/ImageGallery.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/ContactPage.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/FAQPage.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/ItemPage.php');
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/_setup.php b/inc/managers/SEO/render/Thing/CreativeWork/_setup.php
index b7922b9..13850f9 100644
--- a/inc/managers/SEO/render/Thing/CreativeWork/_setup.php
+++ b/inc/managers/SEO/render/Thing/CreativeWork/_setup.php
@@ -18,3 +18,4 @@
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebSite.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/MediaObject/_setup.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/WebPage/_setup.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/Comment/_setup.php');
diff --git a/inc/managers/SEO/render/Thing/Intangible/Enumeration/DayOfWeek.php b/inc/managers/SEO/render/Thing/Intangible/Enumeration/DayOfWeek.php
index a14d568..4455b86 100644
--- a/inc/managers/SEO/render/Thing/Intangible/Enumeration/DayOfWeek.php
+++ b/inc/managers/SEO/render/Thing/Intangible/Enumeration/DayOfWeek.php
@@ -11,7 +11,7 @@
 }
 
 class DayOfWeek extends Enumeration {
-	protected string $day;
+	protected string|array $day;
 	protected array $allowedDay = [
 		'monday'	=> 'https://schema.org/Monday',
 		'tuesday'	=> 'https://schema.org/Tuesday',
@@ -22,4 +22,21 @@
 		'sunday'	=> 'https://schema.org/Sunday',
 		'publicHolidays' => 'https://schema.org/PublicHolidays'
 	];
+	public function setDay(array|string $day) {
+		if (!is_array($day)) {
+			$day = [$day];
+		}
+		$day = array_filter($day, function($d) {
+			if (!array_key_exists(strtolower($d), $this->allowedDay)) {
+				error_log('Invalid day attempted: '.$d);
+				return false;
+			}
+			return true;
+		});
+		if (empty($day)) {
+			return;
+		}
+		$this->day = count($day) === 1 ? $day[0] : $day;
+	}
+
 }
diff --git a/inc/managers/SEO/render/Thing/Intangible/OfferCatalog.php b/inc/managers/SEO/render/Thing/Intangible/ItemList/OfferCatalog.php
similarity index 69%
rename from inc/managers/SEO/render/Thing/Intangible/OfferCatalog.php
rename to inc/managers/SEO/render/Thing/Intangible/ItemList/OfferCatalog.php
index 926c607..f55514c 100644
--- a/inc/managers/SEO/render/Thing/Intangible/OfferCatalog.php
+++ b/inc/managers/SEO/render/Thing/Intangible/ItemList/OfferCatalog.php
@@ -1,5 +1,5 @@
 <?php
-namespace JVBase\managers\SEO\render\Thing\Intangible;
+namespace JVBase\inc\managers\SEO\render\Thing\Intangible\ItemList;
 
 use JVBase\managers\SEO\render\Thing\Intangible\ItemList\ItemList;
 
diff --git a/inc/managers/SEO/render/Thing/Intangible/ItemList/_setup.php b/inc/managers/SEO/render/Thing/Intangible/ItemList/_setup.php
index 22a789d..bea6a3f 100644
--- a/inc/managers/SEO/render/Thing/Intangible/ItemList/_setup.php
+++ b/inc/managers/SEO/render/Thing/Intangible/ItemList/_setup.php
@@ -1,4 +1,4 @@
 <?php
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/ItemList/ItemList.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/ItemList/BreadcrumbList.php');
-
+require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/ItemList/OfferCatalog.php');
diff --git a/inc/managers/SEO/render/Thing/Intangible/StructuredValue/InteractionCounter.php b/inc/managers/SEO/render/Thing/Intangible/StructuredValue/InteractionCounter.php
index 6f02323..160f9c3 100644
--- a/inc/managers/SEO/render/Thing/Intangible/StructuredValue/InteractionCounter.php
+++ b/inc/managers/SEO/render/Thing/Intangible/StructuredValue/InteractionCounter.php
@@ -2,13 +2,6 @@
 
 namespace JVBase\managers\SEO\render\Thing\Intangible\StructuredValue;
 
-use JVBase\managers\SEO\render\DataType\DateTime;
-use JVBase\managers\SEO\render\DataType\Time;
-use JVBase\managers\SEO\render\Thing\Action;
-use JVBase\managers\SEO\render\Thing\CreativeWork\WebSite;
-use JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress;
-use JVBase\managers\SEO\render\Thing\Intangible\VirtualLocation;
-use JVBase\managers\SEO\render\Thing\Place\Place;
 use JVBase\managers\SEO\render\Traits\_Properties\endTimeTrait;
 use JVBase\managers\SEO\render\Traits\_Properties\interactionServiceTrait;
 use JVBase\managers\SEO\render\Traits\_Properties\interactionTypeTrait;
diff --git a/inc/managers/SEO/render/Thing/Intangible/StructuredValue/PriceSpecification.php b/inc/managers/SEO/render/Thing/Intangible/StructuredValue/PriceSpecification.php
index f58293a..33c2750 100644
--- a/inc/managers/SEO/render/Thing/Intangible/StructuredValue/PriceSpecification.php
+++ b/inc/managers/SEO/render/Thing/Intangible/StructuredValue/PriceSpecification.php
@@ -8,6 +8,7 @@
 use JVBase\managers\SEO\render\Traits\_Properties\minPriceTrait;
 use JVBase\managers\SEO\render\Traits\_Properties\priceCurrencyTrait;
 use JVBase\managers\SEO\render\Traits\_Properties\priceTrait;
+use JVBase\managers\SEO\render\Traits\_Properties\unitTextTrait;
 use JVBase\managers\SEO\render\Traits\_Properties\validFromTrait;
 use JVBase\managers\SEO\render\Traits\_Properties\validThroughTrait;
 
@@ -18,5 +19,5 @@
 class PriceSpecification extends StructuredValue {
 	use eligibleQuantityTrait, eligibleTransactionVolumeTrait, maxPriceTrait,
 		membershipPointsEarnedTrait, minPriceTrait, priceTrait, priceCurrencyTrait,
-		validFromTrait, validThroughTrait;
+		validFromTrait, validThroughTrait, unitTextTrait;
 }
diff --git a/inc/managers/SEO/render/Thing/Intangible/StructuredValue/UnitPriceSpecification.php b/inc/managers/SEO/render/Thing/Intangible/StructuredValue/UnitPriceSpecification.php
new file mode 100644
index 0000000..a467229
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Intangible/StructuredValue/UnitPriceSpecification.php
@@ -0,0 +1,14 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Thing\Intangible\StructuredValue;
+
+use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\PriceSpecification;
+use JVBase\managers\SEO\render\Traits\_Properties\unitTextTrait;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class UnitPriceSpecification extends PriceSpecification {
+	use unitTextTrait;
+	//TODO And others.
+}
diff --git a/inc/managers/SEO/render/Thing/Intangible/StructuredValue/_setup.php b/inc/managers/SEO/render/Thing/Intangible/StructuredValue/_setup.php
index 0c50fcd..6e3d35e 100644
--- a/inc/managers/SEO/render/Thing/Intangible/StructuredValue/_setup.php
+++ b/inc/managers/SEO/render/Thing/Intangible/StructuredValue/_setup.php
@@ -12,6 +12,7 @@
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/StructuredValue/QuantitativeValue.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/StructuredValue/ServicePeriod.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/StructuredValue/TypeAndQuantityNode.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/StructuredValue/UnitPriceSpecification.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/StructuredValue/WarrantyPromise.php');
 
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/StructuredValue/LocationFeatureSpecification.php');
diff --git a/inc/managers/SEO/render/Thing/Intangible/_setup.php b/inc/managers/SEO/render/Thing/Intangible/_setup.php
index 3089337..0869361 100644
--- a/inc/managers/SEO/render/Thing/Intangible/_setup.php
+++ b/inc/managers/SEO/render/Thing/Intangible/_setup.php
@@ -25,7 +25,6 @@
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Offer.php');
 
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/AggregateOffer.php');
-require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/OfferCatalog.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/PaymentMethod.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Schedule.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Service.php');
diff --git a/inc/managers/SEO/render/Thing/_setup.php b/inc/managers/SEO/render/Thing/_setup.php
index 60065e3..e1f0ca8 100644
--- a/inc/managers/SEO/render/Thing/_setup.php
+++ b/inc/managers/SEO/render/Thing/_setup.php
@@ -1,6 +1,7 @@
 <?php
-require(JVB_DIR . '/inc/managers/SEO/render/Thing/Action.php');
+
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Thing.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Thing/Action/_setup.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/_setup.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Event/_setup.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/_setup.php');
diff --git a/inc/managers/SEO/render/Traits/ThingSchema.php b/inc/managers/SEO/render/Traits/ThingSchema.php
index c58861f..7124007 100644
--- a/inc/managers/SEO/render/Traits/ThingSchema.php
+++ b/inc/managers/SEO/render/Traits/ThingSchema.php
@@ -5,9 +5,12 @@
 	exit;
 }
 
+use JVBase\managers\Cache;
 use JVBase\managers\SEO\render\DataType\Date;
 use JVBase\managers\SEO\render\DataType\DateTime;
 use JVBase\managers\SEO\render\DataType\Time;
+use JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\ImageObject;
+use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\PropertyValue;
 use JVBase\managers\SEO\render\Thing\Thing;
 use JVBase\managers\SEO\render\Traits\_Properties\additionalTypeTrait;
 use JVBase\managers\SEO\render\Traits\_Properties\alternateNameTrait;
@@ -38,7 +41,7 @@
 	{
 		global $wp;
 		$current = home_url( add_query_arg( $_GET, $wp->request ) );
-		$id = (isset($this->id)) ? $this->id : $current.'#'.strtolower($this->getTypeName());
+		$id = (isset($this->id)) ? $this->id : $current.'/#'.strtolower($this->getTypeName());
 		$elements = array_map(
 			function ($value) {
 
@@ -61,8 +64,9 @@
 				} else if (is_a($value, Time::class)) {
 					$value = $value->getTime();
 				}else if (!is_string($value)) {
-
-					error_log('Normal value? '.print_r($value, true));
+					if (JVB_TESTING && !is_numeric($value)) {
+						error_log('Normal value? '.print_r($value, true));
+					}
 				}
 
 				return $value;
@@ -114,8 +118,8 @@
 	public function setId(string $id):void
 	{
 		if (!filter_var($id, FILTER_VALIDATE_URL)) {
-			error_log('[SEO]Could not set id: '.$id.'. Should be a valid URL');
-			return;
+			global $wp;
+			$id = home_url( add_query_arg( $_GET, $wp->request ) ).'/#'.sanitize_title($id);
 		}
 		$this->id = $id;
 	}
@@ -126,4 +130,35 @@
 			unset($this->$property);
 		}
 	}
+
+	public static function createImageFromID(int $ID):ImageObject|false
+	{
+		$cache = Cache::for('schemaImage')->connect('post');
+		return $cache->remember(
+			$ID,
+			function() use($ID) {
+				$imagePost = get_post($ID);
+				if (!$imagePost) {
+					return false;
+				}
+				$image = wp_get_attachment_image_src($ID,'full');
+				if (empty($image)) {
+					return false;
+				}
+				$imageObject = new ImageObject();
+				$imageObject->setUploadDate($imagePost->post_date);
+				$imageObject->setWidth($image[1]);
+				$imageObject->setHeight($image[2]);
+				$imageObject->setContentUrl($image[0]);
+
+				$caption = wp_get_attachment_caption($ID);
+				if (!empty($caption)) {
+					$imageObject->setCaption($caption);
+				}
+
+				return $imageObject;
+			}
+		);
+
+	}
 }
diff --git a/inc/managers/SEO/render/Traits/_Helpers/_setup.php b/inc/managers/SEO/render/Traits/_Helpers/_setup.php
index 4d0fd8c..53046a4 100644
--- a/inc/managers/SEO/render/Traits/_Helpers/_setup.php
+++ b/inc/managers/SEO/render/Traits/_Helpers/_setup.php
@@ -1,2 +1,3 @@
 <?php
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Helpers/arrayHelper.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Helpers/enumerationHelper.php');
diff --git a/inc/managers/SEO/render/Traits/_Helpers/arrayHelper.php b/inc/managers/SEO/render/Traits/_Helpers/arrayHelper.php
index 0c5a941..e02cd4a 100644
--- a/inc/managers/SEO/render/Traits/_Helpers/arrayHelper.php
+++ b/inc/managers/SEO/render/Traits/_Helpers/arrayHelper.php
@@ -1,13 +1,20 @@
 <?php
 namespace JVBase\managers\SEO\render\Traits\_Helpers;
 
+use JVBase\base\SchemaHelper;
+
 if (!defined('ABSPATH')) {
 	exit;
 }
 trait arrayHelper {
 	protected function classArray(string $property, array $array, string $className):array
 	{
-		return array_filter($array, function ($item) use ($property, $className) {
+		return array_filter(array_map(function ($item) {
+			if (is_array($item) && array_key_exists('type', $item)) {
+				$item = SchemaHelper::classFromConfig($item);
+			}
+			return $item;
+		},$array), function ($item) use ($property, $className) {
 			$test = class_exists($className) && is_a($item, $className);
 			if (!$test) {
 				error_log('[SEO]Item property '.$property.' is not an instance of '.$className);
@@ -17,7 +24,14 @@
 	}
 	protected function mixedArray(string $property, array $array, array $allowedTypes):array
 	{
-		return array_filter($array, function($item) use ($property, $allowedTypes) {
+
+		return array_filter(array_map(function($item) {
+			if (is_array($item) && array_key_exists('type', $item)) {
+				return JVB()->schemaHelper()::classFromConfig($item);
+			} else {
+				return $item;
+			}
+		},$array), function($item) use ($property, $allowedTypes) {
 			$test = in_array(gettype($item), $allowedTypes) || $this->testClasses($item, $allowedTypes);
 			if (!$test) {
 				error_log('[SEO]Item property '.$property.' is not an allowed type: '.print_r($item, true));
diff --git a/inc/managers/SEO/render/Traits/_Helpers/enumerationHelper.php b/inc/managers/SEO/render/Traits/_Helpers/enumerationHelper.php
new file mode 100644
index 0000000..3648e0f
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Helpers/enumerationHelper.php
@@ -0,0 +1,18 @@
+<?php
+namespace JVBase\managers\SEO\render\Traits\_Helpers;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait enumerationHelper {
+	public function checkEnumArray(array $toCheck, string $property, mixed $class):array
+	{
+		if (!property_exists($class, $property)) {
+			error_log('[enumHelper]::checkEnumArray - invalid property of class: '.$property.', '.print_r($class, true));
+			return [];
+		}
+		$toCheck = array_map(function($check) use ($property, $class){
+
+		}, $toCheck);
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/_setup.php b/inc/managers/SEO/render/Traits/_Properties/_setup.php
index 5359535..9a463f9 100644
--- a/inc/managers/SEO/render/Traits/_Properties/_setup.php
+++ b/inc/managers/SEO/render/Traits/_Properties/_setup.php
@@ -1,6 +1,7 @@
 <?php
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/aboutTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/abstractTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/acceptedAnswerTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/acceptedPaymentMethodTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/acceptsReservationsTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/accountablePersonTrait.php');
@@ -26,6 +27,8 @@
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/alternativeHeadlineTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/amenityFeatureTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/amountOfThisGoodTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/answerCountTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/answerExplanationTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/applicableCountryTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/areaServedTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/artEditionTrait.php');
@@ -125,6 +128,7 @@
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/displayLocationTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/dissolutionDateTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/doorTimeTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/downvoteCountTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/dunsTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/durationOfWarrantyTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/durationTrait.php');
@@ -297,6 +301,7 @@
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/ownsTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/parentOrganizationTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/parentTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/parentItemTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/participantTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/partySizeTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/patternTrait.php');
@@ -365,6 +370,7 @@
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/reviewTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/sameAsTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/saturatedFatContentTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/scheduledTimeTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/scheduleTimezoneTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/sdDatePublishedTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/sdLicenseTrait.php');
@@ -381,6 +387,7 @@
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/serviceTypeTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/serviceUrlTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/servingSizeTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/sharedContentTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/siblingTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/significantLinkTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/sizeTrait.php');
@@ -405,6 +412,7 @@
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/subOrganizationTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/successorOfTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/sugarContentTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/suggestedAnswerTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/suitableForDietTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/superEventTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/targetTrait.php');
@@ -429,6 +437,7 @@
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/unitCodeTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/unitTextTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/unsaturatedFatContentTrait.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/upvoteCountTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/urlTemplateTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/urlTrait.php');
 require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/userInteractionCountTrait.php');
diff --git a/inc/managers/SEO/render/Traits/_Properties/acceptedAnswerTrait.php b/inc/managers/SEO/render/Traits/_Properties/acceptedAnswerTrait.php
new file mode 100644
index 0000000..76ed64e
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/acceptedAnswerTrait.php
@@ -0,0 +1,24 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Traits\_Properties;
+
+use JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Answer;
+use JVBase\managers\SEO\render\Thing\Intangible\ItemList\ItemList;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait acceptedAnswerTrait {
+	/**
+	 * @var Answer|ItemList The answer(s) that has been accepted as best, typically on a Question/Answer site. Sites vary in their selection mechanisms, e.g. drawing on community opinion and/or the view of the Question author.
+	 */
+	protected Answer|ItemList $acceptedAnswer;
+
+	public function getAcceptedAnswer():Answer|ItemList|null
+	{
+		return $this->acceptedAnswer??null;
+	}
+	public function setAcceptedAnswer(Answer|ItemList $acceptedAnswer):void
+	{
+		$this->acceptedAnswer = $acceptedAnswer;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/additionalPropertyTrait.php b/inc/managers/SEO/render/Traits/_Properties/additionalPropertyTrait.php
index 8102e25..ab417a8 100644
--- a/inc/managers/SEO/render/Traits/_Properties/additionalPropertyTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/additionalPropertyTrait.php
@@ -23,7 +23,20 @@
 	public function setAdditionalProperty(PropertyValue|array $additionalProperty):void
 	{
 		if (is_array($additionalProperty)) {
+			if (!is_numeric(array_key_first($additionalProperty))) {
+				$additionalProperty = [$additionalProperty];
+			}
+			$additionalProperty = array_map(function($property) {
+				if (!array_key_exists('type', $property)) {
+					$property['type'] = 'JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\PropertyValue';
+				}
+				return $property;
+			}, $additionalProperty);
 			$additionalProperty = $this->classArray('additionalProperty', $additionalProperty, 'JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\PropertyValue');
+
+			$additionalProperty = array_filter($additionalProperty, function ($property) {
+				return property_exists($property, 'value') && !empty($property->value);
+			});
 		}
 		$this->additionalProperty = $additionalProperty;
 	}
diff --git a/inc/managers/SEO/render/Traits/_Properties/addressTrait.php b/inc/managers/SEO/render/Traits/_Properties/addressTrait.php
index 0ea5736..61d5433 100644
--- a/inc/managers/SEO/render/Traits/_Properties/addressTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/addressTrait.php
@@ -1,6 +1,7 @@
 <?php
 namespace JVBase\managers\SEO\render\Traits\_Properties;
 
+use JVBase\base\SchemaHelper;
 use JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress;
 
 if (!defined('ABSPATH')) {
@@ -19,7 +20,10 @@
 	public function setAddress(PostalAddress|array|string $address):void
 	{
 		if (is_array($address)){
-			$address = PostalAddress::fromArray($address);
+			if (!array_key_exists('type', $address)) {
+				$address['type'] = 'JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress';
+			}
+			$address = SchemaHelper::classFromConfig($address);
 		}
 		$this->address = $address;
 	}
@@ -47,7 +51,7 @@
 				'addressLocality'	=> [
 					'type'	=> 'text',
 					'label'	=> 'Address Locality',
-					'hint'	=> 'The locality in which the street address is, and which is in the region. For example, "Park Allen".'
+					'hint'	=> 'The locality in which the street address is, and which is in the region. For example, "Edmonton".'
 				],
 				'addressRegion'	=> [
 					'type'	=> 'text',
diff --git a/inc/managers/SEO/render/Traits/_Properties/aggregateRatingTrait.php b/inc/managers/SEO/render/Traits/_Properties/aggregateRatingTrait.php
index a32732f..314b685 100644
--- a/inc/managers/SEO/render/Traits/_Properties/aggregateRatingTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/aggregateRatingTrait.php
@@ -1,6 +1,7 @@
 <?php
 namespace JVBase\managers\SEO\render\Traits\_Properties;
 
+use JVBase\base\SchemaHelper;
 use JVBase\managers\SEO\render\Thing\Intangible\Rating\AggregateRating;
 
 if (!defined('ABSPATH')) {
@@ -16,10 +17,14 @@
 	{
 		return $this->aggregateRating??null;
 	}
+
 	public function setAggregateRating(array|AggregateRating $aggregateRating):void
 	{
 		if (is_array($aggregateRating)){
-			$aggregateRating =AggregateRating::fromArray($aggregateRating);
+			if (!array_key_exists('type', $aggregateRating)) {
+				$aggregateRating['type'] = 'JVBase\managers\SEO\render\Thing\Intangible\Rating\AggregateRating';
+			}
+			$aggregateRating = SchemaHelper::classFromConfig($aggregateRating);
 		}
 		$this->aggregateRating = $aggregateRating;
 	}
diff --git a/inc/managers/SEO/render/Traits/_Properties/answerCountTrait.php b/inc/managers/SEO/render/Traits/_Properties/answerCountTrait.php
new file mode 100644
index 0000000..f07d737
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/answerCountTrait.php
@@ -0,0 +1,21 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Traits\_Properties;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait answerCountTrait {
+	/**
+	 * @var int The number of answers this question, answer or comment has received from the community.
+	 */
+	protected int $answerCount;
+
+	public function getAnswerCount():?int
+	{
+		return $this->answerCount??null;
+	}
+	public function setAnswerCount(int $answerCount):void
+	{
+		$this->answerCount = $answerCount;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/answerExplanationTrait.php b/inc/managers/SEO/render/Traits/_Properties/answerExplanationTrait.php
new file mode 100644
index 0000000..1528f69
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/answerExplanationTrait.php
@@ -0,0 +1,24 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Traits\_Properties;
+
+use JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Comment;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait answerExplanationTrait {
+	/**
+	 * @var Comment A step-by-step or full explanation about Answer. Can outline how this Answer was achieved or contain more broad clarification or statement about it.
+	 * TODO Can also be "WebContent", not included yet
+	 */
+	protected Comment $answerExplanation;
+
+	public function getAnswerExplanation():?Comment
+	{
+		return $this->answerExplanation??null;
+	}
+	public function setAnswerExplanation(Comment $answerExplanation):void
+	{
+		$this->answerExplanation = $answerExplanation;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/closesTrait.php b/inc/managers/SEO/render/Traits/_Properties/closesTrait.php
index 9029118..d314184 100644
--- a/inc/managers/SEO/render/Traits/_Properties/closesTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/closesTrait.php
@@ -16,8 +16,11 @@
 	{
 		return $this->closes??null;
 	}
-	public function setCloses(Time $closes):void
+	public function setCloses(Time|string $closes):void
 	{
+		if (is_string($closes)) {
+			$closes = new Time($closes);
+		}
 		$this->closes = $closes;
 	}
 }
diff --git a/inc/managers/SEO/render/Traits/_Properties/dayOfWeekTrait.php b/inc/managers/SEO/render/Traits/_Properties/dayOfWeekTrait.php
index 73b6997..96c1dc0 100644
--- a/inc/managers/SEO/render/Traits/_Properties/dayOfWeekTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/dayOfWeekTrait.php
@@ -2,22 +2,27 @@
 namespace JVBase\managers\SEO\render\Traits\_Properties;
 
 use JVBase\managers\SEO\render\Thing\Intangible\Enumeration\DayOfWeek;
+use JVBase\managers\SEO\render\Traits\_Helpers\enumerationHelper;
 
 if (!defined('ABSPATH')) {
 	exit;
 }
 trait dayOfWeekTrait {
+	use enumerationHelper;
 	/**
-	 * @var DayOfWeek The day of the week from which these opening hours are valid
+	 * @var DayOfWeek|array The day of the week from which these opening hours are valid
 	 */
-	protected DayOfWeek $dayOfWeek;
+	protected DayOfWeek|array $dayOfWeek;
 
-	public function getDayOfWeek():?DayOfWeek
+	public function getDayOfWeek():DayOfWeek|array|null
 	{
 		return $this->dayOfWeek??null;
 	}
-	public function setDayOfWeek(DayOfWeek $dayOfWeek):void
+	public function setDayOfWeek(DayOfWeek|array $dayOfWeek):void
 	{
+		if (is_array($dayOfWeek)) {
+
+		}
 		$this->dayOfWeek = $dayOfWeek;
 	}
 }
diff --git a/inc/managers/SEO/render/Traits/_Properties/downvoteCountTrait.php b/inc/managers/SEO/render/Traits/_Properties/downvoteCountTrait.php
new file mode 100644
index 0000000..d8bb1c9
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/downvoteCountTrait.php
@@ -0,0 +1,21 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Traits\_Properties;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait downvoteCountTrait {
+	/**
+	 * @var int The number of downvotes this question, answer or comment has received from the community.
+	 */
+	protected int $downvoteCount;
+
+	public function getDownvoteCount():?int
+	{
+		return $this->downvoteCount??null;
+	}
+	public function setDownvoteCount(int $downvoteCount):void
+	{
+		$this->downvoteCount = $downvoteCount;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/founderTrait.php b/inc/managers/SEO/render/Traits/_Properties/founderTrait.php
index d42fc6e..49f08c1 100644
--- a/inc/managers/SEO/render/Traits/_Properties/founderTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/founderTrait.php
@@ -1,6 +1,5 @@
 <?php
 namespace JVBase\managers\SEO\render\Traits\_Properties;
-
 use JVBase\managers\SEO\render\Thing\Organization\Organization;
 use JVBase\managers\SEO\render\Thing\Person\Person;
 use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
diff --git a/inc/managers/SEO/render/Traits/_Properties/geoTrait.php b/inc/managers/SEO/render/Traits/_Properties/geoTrait.php
index c82429f..e4bdfd1 100644
--- a/inc/managers/SEO/render/Traits/_Properties/geoTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/geoTrait.php
@@ -1,6 +1,7 @@
 <?php
 namespace JVBase\managers\SEO\render\Traits\_Properties;
 
+use JVBase\base\SchemaHelper;
 use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\GeoCoordinates;
 use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\GeoShape;
 
@@ -17,8 +18,15 @@
 	{
 		return $this->geo??null;
 	}
-	public function setGeo(GeoCoordinates|GeoShape $geo):void
+
+	public function setGeo(GeoCoordinates|GeoShape|array $geo):void
 	{
+		if (is_array($geo)) {
+			if(!array_key_exists('type', $geo)) {
+				$geo['type'] = 'JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\GeoCoordinates';
+			}
+			$geo = SchemaHelper::classFromConfig($geo);
+		}
 		$this->geo = $geo;
 	}
 	public function getGeoFieldConfig():array
diff --git a/inc/managers/SEO/render/Traits/_Properties/hasOfferCatalogTrait.php b/inc/managers/SEO/render/Traits/_Properties/hasOfferCatalogTrait.php
index 90f336a..0230b99 100644
--- a/inc/managers/SEO/render/Traits/_Properties/hasOfferCatalogTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/hasOfferCatalogTrait.php
@@ -1,7 +1,8 @@
 <?php
 namespace JVBase\managers\SEO\render\Traits\_Properties;
 
-use JVBase\managers\SEO\render\Thing\Intangible\OfferCatalog;
+use JVBase\base\SchemaHelper;
+use JVBase\inc\managers\SEO\render\Thing\Intangible\ItemList\OfferCatalog;
 use JVBase\registrar\Registrar;
 
 if (!defined('ABSPATH')) {
@@ -17,8 +18,26 @@
 	{
 		return $this->hasOfferCatalog??null;
 	}
-	public function setHasOfferCatalog(OfferCatalog $hasOfferCatalog):void
+	public function setHasOfferCatalog(OfferCatalog|array $hasOfferCatalog):void
 	{
+		if (is_array($hasOfferCatalog)){
+			if (array_key_exists('generate', $hasOfferCatalog) && $hasOfferCatalog['generate']) {
+				$this->generateOfferCatalog($hasOfferCatalog);
+			} else {
+				if (!array_key_exists('type', $hasOfferCatalog)) {
+					$hasOfferCatalog['type'] = 'JVBase\inc\managers\SEO\render\Thing\Intangible\ItemList\OfferCatalog';
+				}
+				$items = [];
+				if (array_key_exists('items', $hasOfferCatalog)) {
+					$items = $hasOfferCatalog['items'];
+					unset($hasOfferCatalog['items']);
+				}
+				$hasOfferCatalog = SchemaHelper::classFromConfig($hasOfferCatalog);
+				if (!empty($items)) {
+					$hasOfferCatalog->setItemListElement($items);
+				}
+			}
+		}
 		$this->hasOfferCatalog = $hasOfferCatalog;
 	}
 
@@ -31,7 +50,8 @@
 			'fields'	=> [
 				'generate'	=> [
 					'type'	=> 'true_false',
-					'label'	=> 'Generate from Content type?'
+					'label'	=> 'Generate from Content type?',
+					'default'	=> false,
 				],
 				'content_type'	=> [
 					'type'	=> 'checkbox',
@@ -67,6 +87,10 @@
 						'description'	=> [
 							'type'	=> 'textarea',
 							'label'	=> 'Description'
+						],
+						'price'	=> [
+							'type'	=> 'number',
+							'label'	=> 'Price'
 						]
 					]
 				]
diff --git a/inc/managers/SEO/render/Traits/_Properties/heightTrait.php b/inc/managers/SEO/render/Traits/_Properties/heightTrait.php
index 30c0c01..12da8cb 100644
--- a/inc/managers/SEO/render/Traits/_Properties/heightTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/heightTrait.php
@@ -9,15 +9,15 @@
 }
 trait heightTrait {
 	/**
-	 * @var Distance|QuantitativeValue The height of the item.
+	 * @var Distance|QuantitativeValue|string The height of the item.
 	 */
-	protected Distance|QuantitativeValue $height;
+	protected Distance|QuantitativeValue|string $height;
 
-	public function getHeight():Distance|QuantitativeValue|null
+	public function getHeight():Distance|QuantitativeValue|string|null
 	{
 		return $this->height??null;
 	}
-	public function setHeight(Distance|QuantitativeValue $height):void
+	public function setHeight(Distance|QuantitativeValue|string $height):void
 	{
 		$this->height = $height;
 	}
diff --git a/inc/managers/SEO/render/Traits/_Properties/imageTrait.php b/inc/managers/SEO/render/Traits/_Properties/imageTrait.php
index 44e8d6f..c13600a 100644
--- a/inc/managers/SEO/render/Traits/_Properties/imageTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/imageTrait.php
@@ -2,22 +2,27 @@
 namespace JVBase\managers\SEO\render\Traits\_Properties;
 
 use JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\ImageObject;
+use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
 
 if (!defined('ABSPATH')) {
 	exit;
 }
 trait imageTrait {
+	use arrayHelper;
 	/**
-	 * @var string|ImageObject Can be a URL, or a fully described ImageObject
+	 * @var string|ImageObject|array Can be a URL, or a fully described ImageObject
 	 */
-	protected string|ImageObject $image;
+	protected string|ImageObject|array $image;
 
 	public function getImage():?string
 	{
 		return $this->image??null;
 	}
-	public function setImage(string|ImageObject $image):void
+	public function setImage(string|ImageObject|array $image):void
 	{
+		if (is_array($image)) {
+			$image = $this->classArray('image', $image, 'JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\ImageObject');
+		}
 		$this->image = $image;
 	}
 
diff --git a/inc/managers/SEO/render/Traits/_Properties/interactionTypeTrait.php b/inc/managers/SEO/render/Traits/_Properties/interactionTypeTrait.php
index 717e73e..4167948 100644
--- a/inc/managers/SEO/render/Traits/_Properties/interactionTypeTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/interactionTypeTrait.php
@@ -1,7 +1,7 @@
 <?php
 namespace JVBase\managers\SEO\render\Traits\_Properties;
 
-use JVBase\managers\SEO\render\Thing\Action;
+use JVBase\inc\managers\SEO\render\Thing\Action\Action;
 
 if (!defined('ABSPATH')) {
 	exit;
diff --git a/inc/managers/SEO/render/Traits/_Properties/logoTrait.php b/inc/managers/SEO/render/Traits/_Properties/logoTrait.php
index b38a0b8..e8ab818 100644
--- a/inc/managers/SEO/render/Traits/_Properties/logoTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/logoTrait.php
@@ -18,6 +18,9 @@
 	}
 	public function setLogo(string|ImageObject $logo):void
 	{
+		if (is_string($logo) && is_numeric($logo)) {
+			$logo = self::createImageFromID($logo);
+		}
 		$this->logo = $logo;
 	}
 
diff --git a/inc/managers/SEO/render/Traits/_Properties/mainEntityTrait.php b/inc/managers/SEO/render/Traits/_Properties/mainEntityTrait.php
index 9246787..678ff35 100644
--- a/inc/managers/SEO/render/Traits/_Properties/mainEntityTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/mainEntityTrait.php
@@ -2,23 +2,28 @@
 namespace JVBase\managers\SEO\render\Traits\_Properties;
 
 use JVBase\managers\SEO\render\Thing\Thing;
+use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
 
 if (!defined('ABSPATH')) {
 	exit;
 }
 trait mainEntityTrait {
+	use arrayHelper;
 	/**
-	 * @var Thing Indicates the primary entity described in some page or other CreativeWork.
+	 * @var Thing|array Indicates the primary entity described in some page or other CreativeWork.
 	 * Inverse property: mainEntityOfPage
 	 */
-	protected Thing $mainEntity;
+	protected Thing|array $mainEntity;
 
-	public function getMainEntity():?Thing
+	public function getMainEntity():Thing|array|null
 	{
 		return $this->mainEntity??null;
 	}
-	public function setMainEntity(Thing $mainEntity):void
+	public function setMainEntity(Thing|array $mainEntity):void
 	{
+		if (is_array($mainEntity)) {
+			$mainEntity = $this->classArray('mainEntity', $mainEntity, 'JVBase\managers\SEO\render\Thing\Thing');
+		}
 		$this->mainEntity = $mainEntity;
 	}
 }
diff --git a/inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php b/inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php
index 94f601c..f9aba4a 100644
--- a/inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/openingHoursSpecificationTrait.php
@@ -1,23 +1,43 @@
 <?php
 namespace JVBase\managers\SEO\render\Traits\_Properties;
 
+use JVBase\base\SchemaHelper;
 use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\OpeningHoursSpecification;
+use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
 
 if (!defined('ABSPATH')) {
 	exit;
 }
 trait openingHoursSpecificationTrait {
+	use arrayHelper;
 	/**
-	 * @var OpeningHoursSpecification The opening hours of a certain place.
+	 * @var OpeningHoursSpecification|array The opening hours of a certain place.
 	 */
-	protected OpeningHoursSpecification $openingHoursSpecification;
+	protected OpeningHoursSpecification|array $openingHoursSpecification;
 
-	public function getOpeningHoursSpecification():?OpeningHoursSpecification
+	public function getOpeningHoursSpecification():OpeningHoursSpecification|array|null
 	{
 		return $this->openingHoursSpecification??null;
 	}
-	public function setOpeningHoursSpecification(OpeningHoursSpecification $openingHoursSpecification):void
+	public function setOpeningHoursSpecification(OpeningHoursSpecification|array $openingHoursSpecification):void
 	{
+		if (is_array($openingHoursSpecification)) {
+			if (array_key_exists('dayOfWeek', $openingHoursSpecification)) {
+				if (!array_key_exists('type', $openingHoursSpecification)) {
+					$openingHoursSpecification['type'] = 'JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\OpeningHoursSpecification';
+				}
+				$openingHoursSpecification = SchemaHelper::classFromConfig($openingHoursSpecification);
+			} else {
+				$out = [];
+				foreach ($openingHoursSpecification as $hours) {
+					if (!array_key_exists('type', $hours)){
+						$hours['type'] = 'JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\OpeningHoursSpecification';
+					}
+					$out[] = SchemaHelper::classFromConfig($hours);
+				}
+				$openingHoursSpecification = $out;
+			}
+		}
 		$this->openingHoursSpecification = $openingHoursSpecification;
 	}
 
diff --git a/inc/managers/SEO/render/Traits/_Properties/opensTrait.php b/inc/managers/SEO/render/Traits/_Properties/opensTrait.php
index c27b70a..493b3f3 100644
--- a/inc/managers/SEO/render/Traits/_Properties/opensTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/opensTrait.php
@@ -16,8 +16,11 @@
 	{
 		return $this->opens??null;
 	}
-	public function setOpens(Time $opens):void
+	public function setOpens(Time|string $opens):void
 	{
+		if (is_string($opens)) {
+			$opens = new Time($opens);
+		}
 		$this->opens = $opens;
 	}
 }
diff --git a/inc/managers/SEO/render/Traits/_Properties/parentItemTrait.php b/inc/managers/SEO/render/Traits/_Properties/parentItemTrait.php
new file mode 100644
index 0000000..481846a
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/parentItemTrait.php
@@ -0,0 +1,32 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Traits\_Properties;
+
+use JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Comment;
+use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork;
+use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait parentItemTrait {
+	use arrayHelper;
+	/**
+	 * @var Comment|CreativeWork|array The parent of a question, answer or item in general. Typically used for Q/A discussion threads e.g. a chain of comments with the first comment being an Article or other CreativeWork. See also comment which points from something to a comment about it.
+	 */
+	protected Comment|CreativeWork|array $parentItem;
+
+	public function getParent():Comment|CreativeWork|array|null
+	{
+		return $this->parentItem??null;
+	}
+	public function setParent(Comment|CreativeWork|array $parentItem):void
+	{
+		if (is_array($parentItem)) {
+			$parentItem = $this->mixedArray('parentItem', $parentItem, [
+				'JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork',
+				'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Comment'
+			]);
+		}
+		$this->parentItem = $parentItem;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/potentialActionTrait.php b/inc/managers/SEO/render/Traits/_Properties/potentialActionTrait.php
index aa895db..5c82ad3 100644
--- a/inc/managers/SEO/render/Traits/_Properties/potentialActionTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/potentialActionTrait.php
@@ -1,7 +1,7 @@
 <?php
 namespace JVBase\managers\SEO\render\Traits\_Properties;
 
-use JVBase\managers\SEO\render\Thing\Action;
+use JVBase\inc\managers\SEO\render\Thing\Action\Action;
 use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
 
 if (!defined('ABSPATH')) {
@@ -21,7 +21,17 @@
 	public function setPotentialAction(Action|array $potentialAction):void
 	{
 		if (is_array($potentialAction)) {
-			$potentialAction = $this->classArray('potentialAction', $potentialAction, 'JVBase\managers\SEO\render\Thing\Action');
+			$potentialAction = $this->classArray('potentialAction', $potentialAction, 'JVBase\inc\managers\SEO\render\Thing\Action\Action');
+			$potentialAction = array_map(
+				function ($item) {
+					$target = $item->getTarget();
+					$url = $target->getUrlTemplate();
+					$target->setId($url.'#entrypoint');
+
+					return $item;
+				},
+				$potentialAction
+			);
 			if (empty($potentialAction)) {
 				return;
 			}
diff --git a/inc/managers/SEO/render/Traits/_Properties/scheduledTimeTrait.php b/inc/managers/SEO/render/Traits/_Properties/scheduledTimeTrait.php
new file mode 100644
index 0000000..0a4cde5
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/scheduledTimeTrait.php
@@ -0,0 +1,26 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Traits\_Properties;
+
+use JVBase\managers\SEO\render\DataType\Time;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait scheduledTimeTrait {
+	/**
+	 * @var Time The opening hour of the place or service on the given day(s) of the week
+	 */
+	protected Time $scheduledTime;
+
+	public function getScheduledTime():?Time
+	{
+		return $this->scheduledTime??null;
+	}
+	public function setScheduledTime(Time|string $scheduledTime):void
+	{
+		if (is_string($scheduledTime)) {
+			$scheduledTime = new Time($scheduledTime);
+		}
+		$this->scheduledTime = $scheduledTime;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/sharedContentTrait.php b/inc/managers/SEO/render/Traits/_Properties/sharedContentTrait.php
new file mode 100644
index 0000000..37b103a
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/sharedContentTrait.php
@@ -0,0 +1,27 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Traits\_Properties;
+
+use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
+use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork;
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait sharedContentTrait {
+	use arrayHelper;
+	/**
+	 * @var CreativeWork|array A CreativeWork such as an image, video, or audio clip shared as part of this posting.
+	 */
+	protected CreativeWork|array $shared_content;
+
+	public function getSharedContent():CreativeWork|array|null
+	{
+		return $this->shared_content??null;
+	}
+	public function setSharedContent(CreativeWork|array $sharedContent):void
+	{
+		if (is_array($sharedContent)) {
+			$sharedContent = $this->classArray('sharedContent', $sharedContent, 'JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork');
+		}
+		$this->shared_content = $sharedContent;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/suggestedAnswerTrait.php b/inc/managers/SEO/render/Traits/_Properties/suggestedAnswerTrait.php
new file mode 100644
index 0000000..f3b4c48
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/suggestedAnswerTrait.php
@@ -0,0 +1,24 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Traits\_Properties;
+
+use JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Answer;
+use JVBase\managers\SEO\render\Thing\Intangible\ItemList\ItemList;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait suggestedAnswerTrait {
+	/**
+	 * @var Answer|ItemList The answer(s) that has been suggested as best, typically on a Question/Answer site. Sites vary in their selection mechanisms, e.g. drawing on community opinion and/or the view of the Question author.
+	 */
+	protected string $suggestedAnswer;
+
+	public function getSuggestedAnswer():?string
+	{
+		return $this->suggestedAnswer??null;
+	}
+	public function setSuggestedAnswer(string $suggestedAnswer):void
+	{
+		$this->suggestedAnswer = $suggestedAnswer;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/targetTrait.php b/inc/managers/SEO/render/Traits/_Properties/targetTrait.php
index f9967fd..440a308 100644
--- a/inc/managers/SEO/render/Traits/_Properties/targetTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/targetTrait.php
@@ -1,6 +1,7 @@
 <?php
 namespace JVBase\managers\SEO\render\Traits\_Properties;
 
+use JVBase\base\SchemaHelper;
 use JVBase\managers\SEO\render\Thing\Intangible\EntryPoint;
 
 if (!defined('ABSPATH')) {
@@ -8,16 +9,26 @@
 }
 trait targetTrait {
 	/**
-	 * @var EntryPoint|string Indicates a target EntryPoint, or url, for an Action.
+	 * @var EntryPoint Indicates a target EntryPoint, or url, for an Action.
 	 */
-	protected EntryPoint|string $description;
+	protected EntryPoint $target;
 
-	public function getDescription():EntryPoint|string|null
+	public function getTarget():EntryPoint|null
 	{
-		return $this->description??null;
+		return $this->target??null;
 	}
-	public function setDescription(EntryPoint|string $description):void
+	public function setTarget(EntryPoint|string $target):void
 	{
-		$this->description = $description;
+		if (is_string($target)) {
+			if (str_starts_with($target, '/')) {
+				$target = home_url().$target;
+			}
+			$temp = [
+				'type'	=> 'JVBase\managers\SEO\render\Thing\Intangible\EntryPoint',
+				'urlTemplate'	=> $target
+			];
+			$target = SchemaHelper::classFromConfig($temp);
+		}
+		$this->target = $target;
 	}
 }
diff --git a/inc/managers/SEO/render/Traits/_Properties/upvoteCountTrait.php b/inc/managers/SEO/render/Traits/_Properties/upvoteCountTrait.php
new file mode 100644
index 0000000..818dcd2
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/upvoteCountTrait.php
@@ -0,0 +1,21 @@
+<?php
+namespace JVBase\inc\managers\SEO\render\Traits\_Properties;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+trait upvoteCountTrait {
+	/**
+	 * @var int The number of upvotes this question, answer or comment has received from the community.
+	 */
+	protected int $upvoteCount;
+
+	public function getUpvoteCount():?int
+	{
+		return $this->upvoteCount??null;
+	}
+	public function setUpvoteCount(int $upvoteCount):void
+	{
+		$this->upvoteCount = $upvoteCount;
+	}
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/widthTrait.php b/inc/managers/SEO/render/Traits/_Properties/widthTrait.php
index d23977b..c09c978 100644
--- a/inc/managers/SEO/render/Traits/_Properties/widthTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/widthTrait.php
@@ -9,15 +9,15 @@
 }
 trait widthTrait {
 	/**
-	 * @var Distance|QuantitativeValue The width of the item.
+	 * @var Distance|QuantitativeValue|string The width of the item.
 	 */
-	protected Distance|QuantitativeValue $width;
+	protected Distance|QuantitativeValue|string $width;
 
-	public function getWidth():Distance|QuantitativeValue|null
+	public function getWidth():Distance|QuantitativeValue|string|null
 	{
 		return $this->width??null;
 	}
-	public function setWidth(Distance|QuantitativeValue $width):void
+	public function setWidth(Distance|QuantitativeValue|string $width):void
 	{
 		$this->width = $width;
 	}
diff --git a/inc/managers/TaxonomyRelationships.php b/inc/managers/TaxonomyRelationships.php
index 2becd42..1f919bd 100644
--- a/inc/managers/TaxonomyRelationships.php
+++ b/inc/managers/TaxonomyRelationships.php
@@ -1,9 +1,10 @@
 <?php
 namespace JVBase\managers;
 
-use JVBase\managers\Cache;
+use Exception;
 use WP_Error;
 use WP_Post;
+use WP_Query;
 
 if (!defined('ABSPATH')) {
     exit; // Exit if accessed directly
@@ -11,15 +12,15 @@
 
 class TaxonomyRelationships
 {
-    private string $table_name;
-    protected object $cache;
-
+    protected Cache $cache;
+	protected CustomTable $relationships;
 
     public function __construct()
     {
-        global $wpdb;
-        $this->table_name = $wpdb->prefix . BASE.'taxonomy_relationships';
+		$this->defineTables();
+
         $this->cache = Cache::for('term_relationship', WEEK_IN_SECONDS);
+		$this->cache->connect('terms');
 
         // Ensure the table exists
 //        $this->create_table_if_not_exists();
@@ -28,13 +29,41 @@
         add_action('init', [$this, 'init']);
     }
 
-    /**
-     * @return string
-     */
-    public function getTableName():string
-    {
-        return $this->table_name;
-    }
+	protected function defineTables():void
+	{
+		$relationships = CustomTable::for('taxonomy_relationships');
+
+		$relationships->setColumns([
+			'id'				=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+			'term_id'			=> "{$relationships->getTermIDType()} NOT NULL",
+			'related_term_id'	=> "{$relationships->getTermIDType()} NOT NULL",
+			'taxonomy'			=> 'varchar(32) NOT NULL',
+			'related_taxonomy'	=> 'varchar(32) NOT NULL',
+			'post_count'		=> 'int(11) NOT NULL DEFAULT 0',
+			'is_direct'			=> 'tinyint(1) NOT NULL DEFAULT 1',
+			'is_hierarchical'	=> 'tinyint(1) NOT NULL DEFAULT 0',
+			'last_updated'		=> 'timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
+		]);
+
+		$relationships->setKeys([
+			['key'=>'PRIMARY', 'value' => '(`id`)'],
+			['key'=>'UNIQUE', 'value' => '`relation` (`term_id`, `related_term_id`, `taxonomy`, `related_taxonomy`)'],
+			'`term_id` (`term_id`)',
+			'`related_term_id` (`related_term_id`)',
+			'`taxonomy` (`taxonomy`)',
+			'`related_taxonomy` (`related_taxonomy`)'
+		]);
+		$base = BASE;
+		$relationships->setConstraints([
+			"CONSTRAINT {$base}tax_rel_term_id FOREIGN KEY (`term_id`)
+			REFERENCES `{$relationships->getTermTable()}` (`term_id`) ON DELETE CASCADE",
+			"CONSTRAINT `{$base}tax_rel_related_id` FOREIGN KEY (`related_term_id`)
+			REFERENCES `{$relationships->getTermTable()}` (`term_id`) ON DELETE CASCADE"
+		]);
+		$relationships->defineTable();
+
+		$this->relationships = $relationships;
+	}
 
     /**
      * Hook into term and post saves
@@ -59,6 +88,10 @@
      */
     public function updatePostRelationships(int $post_id, WP_Post $post):void
     {
+		// Skip autosaves and revisions
+		if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
+			return;
+		}
 		$post_type = $post->post_type;
 		if (in_array($post_type, jvbIgnoredPostTypes())) {
 			return;
@@ -119,8 +152,6 @@
      */
     public function updateRelationship(int $term_id, int $related_term_id, string $taxonomy, string $related_taxonomy):bool
     {
-        global $wpdb;
-
         // Make sure both terms exist
         $term = get_term($term_id, $taxonomy);
         $related_term = get_term($related_term_id, $related_taxonomy);
@@ -130,61 +161,27 @@
             return false;
         }
 
-        // Ensure term_id and related_term_id are integers
-        $term_id = (int)$term_id;
-        $related_term_id = (int)$related_term_id;
+		// Check if term is parent of related term
+		$is_hierarchical = 0;
+		if ($taxonomy === $related_taxonomy) {
+			$ancestors = get_ancestors($related_term_id, $taxonomy, 'taxonomy');
+			if (in_array($term_id, $ancestors)) {
+				$is_hierarchical = 1;
+			}
+		}
+		// Calculate number of shared posts
+		$shared_posts_count = $this->countSharedPosts($term_id, $related_term_id, $taxonomy, $related_taxonomy);
+		$updated = $this->relationships->findOrCreate([
+			'term_id'			=> $term_id,
+			'related_term_id'	=> $related_term_id,
+			'taxonomy'			=> $taxonomy,
+			'related_taxonomy'	=> $related_taxonomy
+		],[
+			'is_hierarchical'	=> $is_hierarchical,
+			'post_count'	=> $shared_posts_count
+		]);
 
-        // Check if relationship exists
-        $existing = $wpdb->get_row($wpdb->prepare(
-            "SELECT id, post_count FROM {$this->table_name}
-        WHERE term_id = %d AND related_term_id = %d
-        AND taxonomy = %s AND related_taxonomy = %s",
-            $term_id,
-            $related_term_id,
-            $taxonomy,
-            $related_taxonomy
-        ));
-
-        // Calculate number of shared posts
-        $shared_posts_count = $this->countSharedPosts($term_id, $related_term_id, $taxonomy, $related_taxonomy);
-
-        // Check if term is parent of related term
-        $is_hierarchical = 0;
-        if ($taxonomy === $related_taxonomy) {
-            $ancestors = get_ancestors($related_term_id, $taxonomy, 'taxonomy');
-            if (in_array($term_id, $ancestors)) {
-                $is_hierarchical = 1;
-            }
-        }
-
-        if ($existing) {
-            // Update existing relationship
-            $wpdb->update(
-                $this->table_name,
-                [
-                    'post_count' => $shared_posts_count,
-                    'is_hierarchical' => $is_hierarchical
-                ],
-                [
-                    'id' => $existing->id
-                ]
-            );
-        } else {
-            // Insert new relationship
-            $wpdb->insert(
-                $this->table_name,
-                [
-                    'term_id' => $term_id,
-                    'related_term_id' => $related_term_id,
-                    'taxonomy' => $taxonomy,
-                    'related_taxonomy' => $related_taxonomy,
-                    'post_count' => $shared_posts_count,
-                    'is_hierarchical' => $is_hierarchical
-                ]
-            );
-        }
-
-        return true;
+        return (bool)$updated;
     }
 
     /**
@@ -197,23 +194,35 @@
      */
     private function countSharedPosts(int $term_id, int $related_term_id, string $taxonomy, string $related_taxonomy):int
     {
-        global $wpdb;
 
-        $term_posts = $wpdb->get_col($wpdb->prepare(
-            "SELECT object_id FROM {$wpdb->term_relationships} tr
+        $term_posts = $this->cache->remember(
+			$this->cache->generateKey(['term' => $term_id, 'taxonomy' => $taxonomy]),
+			function () use ($term_id, $taxonomy) {
+				global $wpdb;
+				return $wpdb->get_col($wpdb->prepare(
+					"SELECT object_id FROM {$wpdb->term_relationships} tr
             JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
             WHERE tt.term_id = %d AND tt.taxonomy = %s",
-            $term_id,
-            $taxonomy
-        ));
+					$term_id,
+					$taxonomy
+				));
+			}
+		);
 
-        $related_term_posts = $wpdb->get_col($wpdb->prepare(
-            "SELECT object_id FROM {$wpdb->term_relationships} tr
+
+        $related_term_posts = $this->cache->remember(
+			$this->cache->generateKey(['term' => $related_term_id, 'taxonomy' => $related_taxonomy]),
+			function () use($related_term_id, $related_taxonomy) {
+				global $wpdb;
+				return $wpdb->get_col($wpdb->prepare(
+					"SELECT object_id FROM {$wpdb->term_relationships} tr
             JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
             WHERE tt.term_id = %d AND tt.taxonomy = %s",
-            $related_term_id,
-            $related_taxonomy
-        ));
+					$related_term_id,
+					$related_taxonomy
+				));
+			});
+
 
         return count(array_intersect($term_posts, $related_term_posts));
     }
@@ -230,48 +239,14 @@
     public function getRelatedTerms(int $term_id, string $desired_taxonomy, bool $include_hierarchical = true):array
     {
 
-        $key = sprintf(
-            '%d_to_%s',
-            $term_id,
-            $desired_taxonomy
-        );
-        if ($include_hierarchical) {
-            $key.='_hierarchy';
-        }
-        $cache = $this->cache->get($key);
-        if ($cache) {
-            return $cache;
-        }
-
-        global $wpdb;
-
-        $term = get_term($term_id);
-        if (is_wp_error($term) || empty($term)) {
-            return [];
-        }
-
-        $query = $wpdb->prepare(
-            "SELECT related_term_id
-            FROM {$this->table_name}
-            WHERE term_id = %d
-            AND related_taxonomy = %s",
-            $term_id,
-            $desired_taxonomy
-        );
-
-        // Optionally include hierarchical relationships
-        if (!$include_hierarchical) {
-            $query .= " AND is_hierarchical = 0";
-        }
-
-        // Order by post count for popularity
-        $query .= " ORDER BY post_count DESC";
-
-        $related_term_ids = $wpdb->get_col($query);
-
-        $this->cache->set($key, $related_term_ids);
-
-        return $related_term_ids;
+		$where = [
+			'term_id'	=> $term_id,
+			'related_taxonomy' => $desired_taxonomy
+		];
+		if (!$include_hierarchical) {
+			$where['is_hierarchical'] = 0;
+		}
+		return $this->relationships->pluck('related_term_id', $where, 'post_count', 'DESC');
     }
 
     // Get all related terms based on multiple source terms (for "any" match)
@@ -284,50 +259,9 @@
      */
     public function getAnyRelatedTerms(array $term_ids, string $taxonomy):array
     {
-        if (empty($term_ids)) {
-            return [];
-        }
+		$term_ids = array_filter(array_map('absint', $term_ids));
+		return array_unique($this->relationships->pluck('related_term_id', ['term_id' => ['IN' => $term_ids], 'related_taxonomy' => $taxonomy], 'post_count', 'DESC'));
 
-        $related_term_ids = [];
-        foreach ($term_ids as $term_id) {
-            $related = $this->getRelatedTerms($term_id, $taxonomy);
-            $related_term_ids = array_merge($related_term_ids, $related);
-        }
-
-        return array_unique($related_term_ids);
-    }
-
-    // Get related terms that are common to all source terms (for "all" match)
-
-    /**
-     * @param array $term_ids
-     * @param string $taxonomy
-     *
-     * @return array
-     */
-    public function getAllRelatedTerms(array $term_ids, string $taxonomy):array
-    {
-        if (empty($term_ids)) {
-            return [];
-        }
-
-        $related_sets = [];
-        foreach ($term_ids as $term_id) {
-            $related = $this->getRelatedTerms($term_id, $taxonomy);
-            if (!empty($related)) {
-                $related_sets[] = $related;
-            }
-        }
-
-        if (count($related_sets) === 1) {
-            return $related_sets[0];
-        } elseif (count($related_sets) > 1) {
-            // Find intersection of all sets
-            $result = array_intersect(...$related_sets);
-            return array_values($result);
-        }
-
-        return [];
     }
 
     // Rebuild all relationships (useful for initial setup)
@@ -341,31 +275,34 @@
         global $wpdb;
 
         // Clear existing relationships
-        $wpdb->query("TRUNCATE TABLE {$this->table_name}");
-        $total_posts = $wpdb->get_var("SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_status = 'publish'");
+        $wpdb->query("TRUNCATE TABLE {$this->relationships->getFullTableName()}");
 
-        // Calculate number of batches needed (50 posts per batch)
-        $batch_size = 50;
-        $total_batches = ceil($total_posts / $batch_size);
+		$posts = new WP_Query([
+			'post_status' 	=> 'publish',
+			'posts_per_page'=> -1,
+			'fields'		=> 'ids'
+		]);
+		if (!$posts->have_posts()) {
+			wp_reset_postdata();
+			return true;
+		}
+
+		$ids = $posts->posts;
+		wp_reset_postdata();
+
 
         // Queue the operation
         $queue = JVB()->queue();
-        $operation_id = 'taxonomy_rebuild_' . uniqid();
 
         $queue->queueOperation(
             'taxonomy_relationships',
             1,
             [
-                'action' => 'rebuild_all',
-                'offset' => 0,
-                'limit' => $batch_size,
-                'total_posts' => $total_posts,
-                'total_batches' => $total_batches
+                'posts' => $ids,
             ],
             [
-                'operation_id' => $operation_id,
-                'count' => $total_batches,
-                'priority' => 'normal',
+                'chunk_key' 	=> 'posts',
+				'chunk_size' 	=> 50
             ]
         );
 
@@ -387,47 +324,20 @@
             return $result;
         }
 
-        if (isset($data['action']) && $data['action'] === 'rebuild_all') {
-            // Clear existing relationships
-            global $wpdb;
-
-            // Get batch of posts to process
-            $offset = isset($data['offset']) ? $data['offset'] : 0;
-            $limit = isset($data['limit']) ? $data['limit'] : 50; // Process in batches
-
-            $posts = $wpdb->get_col($wpdb->prepare(
-                "SELECT ID FROM {$wpdb->posts}
-             WHERE post_status = 'publish'
-             ORDER BY ID
-             LIMIT %d OFFSET %d",
-                $limit,
-                $offset
-            ));
-
-            // Process each post
-            foreach ($posts as $post_id) {
-                $this->updatePostRelationships($post_id);
-            }
-
-            // Get total number of posts
-            $total_posts = $wpdb->get_var("SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_status = 'publish'");
-
-            // Return progress information
-            return [
-                'success' => true,
-				'result'	=> [
-					'processed' => count($posts),
-					'offset' => $offset,
-					'next_offset' => $offset + $limit,
-					'total' => $total_posts,
-					'completed' => ($offset + $limit >= $total_posts)
-				]
-            ];
-        }
-        return [
-            'success'   => false,
-            'message'   => __('Hmmm.', 'jvb')
-        ];
+		try {
+			foreach ($data['posts'] as $postID) {
+				$post = get_post($postID);
+				$this->updatePostRelationships($postID, $post);
+			}
+		} catch (Exception $e) {
+			return [
+				'success' => false,
+				'message' => $e->getMessage()
+			];
+		}
+		return [
+			'success' => true
+		];
     }
 
     // Hook this to term deletion
@@ -439,13 +349,7 @@
      */
     public function deleteTermRelationships(int $term_id):void
     {
-        global $wpdb;
-
-        $wpdb->query($wpdb->prepare(
-            "DELETE FROM {$this->table_name}
-            WHERE term_id = %d OR related_term_id = %d",
-            $term_id,
-            $term_id
-        ));
+		$termIDs = $this->relationships->delete(['term_id' => $term_id]);
+		$related = $this->relationships->delete(['related_term_id' => $term_id]);
     }
 }
diff --git a/inc/managers/UserTermsManager.php b/inc/managers/UserTermsManager.php
index 4673568..cf8d595 100644
--- a/inc/managers/UserTermsManager.php
+++ b/inc/managers/UserTermsManager.php
@@ -1,36 +1,65 @@
 <?php
 namespace JVBase\managers;
 
+use JVBase\registrar\Registrar;
 use WP_Post;
 use WP_Error;
 use Exception;
+use WP_Query;
 
 if (!defined('ABSPATH')) {
     exit; // Exit if accessed directly
 }
 class UserTermsManager
 {
-    private string $table_name;
     private Cache $cache;
-    private string $cacheGroup  = 'user_terms_';
-    private int $ttl = DAY_IN_SECONDS; // 1 day default
-	protected \wpdb $wpdb;
+
+	protected CustomTable $index;
 
     public function __construct()
     {
-        global $wpdb;
-		$this->wpdb = $wpdb;
-        $this->table_name = $this->wpdb->prefix . BASE . 'user_term_index';
+		$this->defineTables();
+		$this->cache = Cache::for('term_ids')->connect('user');
 
         // Register hooks
-        add_action('save_post', [$this, 'updatePostUserTerms'], 10, 3);
-        add_action('before_delete_post', [$this, 'removePostUserTerms']);
         add_action('set_object_terms', [$this, 'handleTermAssignment'], 10, 6);
 
         // Add filter for bulk operation handling
         add_filter(BASE . 'handle_bulk_operation', [ $this, 'processOperation' ], 10, 3);
     }
 
+	public function defineTables():void
+	{
+		$userIndex = CustomTable::for('user_term_index');
+		$userIndex->setColumns([
+			'id'			=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+			'user_id'		=> "{$userIndex->getUserIDType()} NOT NULL",
+			'term_id'		=> "{$userIndex->getTermIDType()} NOT NULL",
+			'taxonomy'		=> 'varchar(32) NOT NULL',
+			'post_count'	=> 'int(11) NOT NULL DEFAULT 1',
+			'parent_id'		=> "{$userIndex->getTermIDType()} NOT NULL DEFAULT 0",
+			'last_used'		=> 'timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
+		]);
+		$userIndex->setKeys([
+			['key' => 'PRIMARY', 'value' => '(`id`)'],
+			['key' => 'UNIQUE', 'value' => '`user_term` (`user_id`, `term_id`, `taxonomy`)'],
+			'`user_taxonomy` (`user_id`, `taxonomy`)',
+			'`taxonomy` (`taxonomy`)',
+			'`user_id` (`user_id`)',
+			'`term_id` (`term_id`)',
+			'`parent_id` (`parent_id`)'
+		]);
+		$base = BASE;
+		$userIndex->setConstraints([
+			"CONSTRAINT `{$base}user_term_user_fk` FOREIGN KEY (`user_id`)
+			REFERENCES `{$userIndex->getUserTable()}` (`ID`) ON DELETE CASCADE",
+			"CONSTRAINT `{$base}user_term_term_fk` FOREIGN KEY (`term_id`)
+			REFERENCES `{$userIndex->getTermTable()}` (`term_id`) ON DELETE CASCADE"
+		]);
+		$userIndex->defineTable();
+		$this->index = $userIndex;
+	}
+
     /**
      * @param int $user_id
      * @param string|null $taxonomy
@@ -43,79 +72,8 @@
 		$cache->flush();
     }
 
-    // Update term usage when a post is saved
-
     /**
-     * @param int $post_id
-     * @param WP_Post $post
-     * @param bool $update
-     *
-     * @return void
-     */
-    public function updatePostUserTerms(int $post_id, WP_Post $post, bool $update):void
-    {
-        // Skip autosaves and revisions
-        if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
-            return;
-        }
-		// SAFETY: Skip attachments and other non-content post types
-		if (in_array($post->post_type, jvbIgnoredPostTypes())) {
-			return;
-		}
-
-        // Skip non-custom post types
-        $post_type = get_post_type($post);
-        if (!str_starts_with($post_type, BASE)) {
-            return;
-        }
-
-        $user_id = $post->post_author;
-
-        // Get all taxonomies for this post type
-        $taxonomies = get_object_taxonomies($post_type);
-
-        foreach ($taxonomies as $taxonomy) {
-            $terms = wp_get_post_terms($post_id, $taxonomy, ['fields' => 'ids']);
-
-            if (!is_wp_error($terms) && !empty($terms)) {
-                // Add the direct terms
-                foreach ($terms as $term_id) {
-                    $this->updateUserTerm($user_id, $term_id, $taxonomy, false);
-
-                    // Check if taxonomy is hierarchical and add parent terms
-                    if (is_taxonomy_hierarchical($taxonomy)) {
-                        $this->addParentTerms($user_id, $term_id, $taxonomy);
-                    }
-                }
-            }
-			$this->clearUserCache($user_id, $taxonomy);
-        }
-    }
-
-    // Add all parent terms for a given term
-
-    /**
-     * @param int $user_id
-     * @param int $term_id
-     * @param string $taxonomy
-     *
-     * @return void
-     */
-    public function addParentTerms(int $user_id, int $term_id, string $taxonomy):void
-    {
-        // Get all ancestors (parent terms)
-        $ancestors = get_ancestors($term_id, $taxonomy, 'taxonomy');
-
-        if (!empty($ancestors)) {
-            foreach ($ancestors as $ancestor_id) {
-                $this->updateUserTerm($user_id, $ancestor_id, $taxonomy, true);
-            }
-        }
-    }
-
-    // Handle term assignment changes
-
-    /**
+	 * Handle term assignment changes
      * @param int $object_id
      * @param array $terms
      * @param array $tt_ids
@@ -135,174 +93,28 @@
         }
 
         $user_id = $post->post_author;
-        $is_hierarchical = is_taxonomy_hierarchical($taxonomy);
 
-        // Get added terms
-        $added_tt_ids = array_diff($tt_ids, $old_tt_ids);
-        $added_terms = [];
-
-        if (!empty($added_tt_ids)) {
-            foreach ($added_tt_ids as $tt_id) {
-                $term_id = $this->getTermIDFromTTID($tt_id);
-                if ($term_id) {
-                    $added_terms[] = $term_id;
-                }
-            }
-
-            // Add or increment the new terms
-            foreach ($added_terms as $term_id) {
-                $this->updateUserTerm($user_id, $term_id, $taxonomy, false);
-
-                // Add parent terms for hierarchical taxonomies
-                if ($is_hierarchical) {
-                    $this->addParentTerms($user_id, $term_id, $taxonomy);
-                }
-            }
-        }
-
-        // Handle removed terms
-        $removed_tt_ids = array_diff($old_tt_ids, $tt_ids);
-        $removed_terms = [];
-
-        if (!empty($removed_tt_ids)) {
-            foreach ($removed_tt_ids as $tt_id) {
-                $term_id = $this->getTermIDFromTTID($tt_id);
-                if ($term_id) {
-                    $removed_terms[] = $term_id;
-                }
-            }
-
-            // Decrement removed terms
-            foreach ($removed_terms as $term_id) {
-                $this->decreaseUserTerm($user_id, $term_id, $taxonomy, false);
-
-                // Handle parent terms for hierarchical taxonomies
-                if ($is_hierarchical) {
-                    $this->handleParentTermRemoval($user_id, $term_id, $taxonomy);
-                }
-            }
-        }
+		$termIDs = array_unique(array_merge($tt_ids, $old_tt_ids));
+       if (!empty($termIDs)) {
+		   foreach ($termIDs as $termID) {
+			   $term_id = $this->getTermIDFromTTID($termID);
+			   $this->updateUserTerm($user_id, $term_id, $taxonomy);
+		   }
+	   }
     }
 
-    // Handles parent term removal logic
-
     /**
+	 * Updates the entry for the user's term relationship
      * @param int $user_id
      * @param int $term_id
      * @param string $taxonomy
      *
-     * @return void
-     */
-    public function handleParentTermRemoval(int $user_id, int $term_id, string $taxonomy):void
-    {
-        // Get parent terms
-        $ancestors = get_ancestors($term_id, $taxonomy, 'taxonomy');
-
-        if (!empty($ancestors)) {
-            foreach ($ancestors as $ancestor_id) {
-                // Check if this parent is still used by other terms
-                $still_needed = $this->isParentNeeded($user_id, $ancestor_id, $taxonomy);
-
-                if (!$still_needed) {
-                    $this->decreaseUserTerm($user_id, $ancestor_id, $taxonomy, true);
-                }
-            }
-        }
-    }
-
-    // Check if a parent term is still needed by other terms
-
-    /**
-     * @param int $user_id
-     * @param int $parent_term_id
-     * @param string $taxonomy
-     *
      * @return bool
      */
-    private function isParentNeeded(int $user_id, int $parent_term_id, string $taxonomy):bool
+    public function updateUserTerm(int $user_id, int $term_id, string $taxonomy):bool
     {
 
-
-        // Get all direct terms the user has
-        $direct_terms = $this->wpdb->get_col($this->wpdb->prepare(
-            "SELECT term_id FROM {$this->table_name}
-            WHERE user_id = %d
-            AND taxonomy = %s
-            AND is_parent = 0
-            AND post_count > 0",
-            $user_id,
-            $taxonomy
-        ));
-
-        if (empty($direct_terms)) {
-            return false;
-        }
-
-        // Check if any of these terms have the parent as an ancestor
-        foreach ($direct_terms as $term_id) {
-            if ($term_id == $parent_term_id) {
-                continue; // Skip the term itself
-            }
-
-            $ancestors = get_ancestors($term_id, $taxonomy, 'taxonomy');
-
-            if (in_array($parent_term_id, $ancestors)) {
-                return true; // This parent is needed
-            }
-        }
-
-        return false;
-    }
-
-    // Remove all term usage records for a post
-
-    /**
-     * @param int $post_id
-     *
-     * @return void
-     */
-    public function removePostUserTerms(int $post_id):void
-    {
-        $post = get_post($post_id);
-
-        // Skip if not a valid post
-        if (!$post || !str_starts_with($post->post_type, BASE)) {
-            return;
-        }
-
-        $user_id = $post->post_author;
-        $taxonomies = get_object_taxonomies($post->post_type);
-
-        foreach ($taxonomies as $taxonomy) {
-            $terms = wp_get_post_terms($post_id, $taxonomy, ['fields' => 'ids']);
-            $is_hierarchical = is_taxonomy_hierarchical($taxonomy);
-
-            if (!is_wp_error($terms) && !empty($terms)) {
-                foreach ($terms as $term_id) {
-                    $this->decreaseUserTerm($user_id, $term_id, $taxonomy, false);
-
-                    // Handle parent terms for hierarchical taxonomies
-                    if ($is_hierarchical) {
-                        $this->handleParentTermRemoval($user_id, $term_id, $taxonomy);
-                    }
-                }
-            }
-        }
-    }
-
-    // Add or increment a user-term relationship
-
-    /**
-     * @param int $user_id
-     * @param int $term_id
-     * @param string $taxonomy
-     * @param bool $is_parent
-     *
-     * @return bool
-     */
-    public function updateUserTerm(int $user_id, int $term_id, string $taxonomy, bool $is_parent = false):bool
-    {
-
+		$taxonomy = jvbCheckBase($taxonomy);
 
         // Ensure the term exists
         $term = get_term($term_id, $taxonomy);
@@ -310,93 +122,53 @@
             return false;
         }
 
-        // Insert or update the record
-        $result = $this->wpdb->query($this->wpdb->prepare(
-            "INSERT INTO {$this->table_name}
-            (user_id, term_id, taxonomy, post_count, is_parent, last_used)
-            VALUES (%d, %d, %s, 1, %d, NOW())
-            ON DUPLICATE KEY UPDATE
-            post_count = post_count + 1,
-            is_parent = IF(%d = 1, 1, is_parent),
-            last_used = NOW()",
-            $user_id,
-            $term_id,
-            $taxonomy,
-            $is_parent ? 1 : 0,
-            $is_parent ? 1 : 0
-        ));
+		$posts = new WP_Query([
+			'post_author' 	=> $user_id,
+			'tax_query'		=> [
+				[
+					'taxonomy'	=> $taxonomy,
+					'terms'		=> $term_id,
+				]
+			],
+			'post_status'	=> 'publish',
+			'posts_per_page'=> -1,
+			'fields'		=> 'ids'
+		]);
+		$userPosts = count($posts->posts);
 
-        // Clear the cache for this user and taxonomy
-        $this->clearUserCache($user_id, $taxonomy);
+		$result = $this->index->findOrCreate([
+			'user_id'	=> $user_id,
+			'term_id'	=> $term_id,
+			'taxonomy'	=> $taxonomy
+		],[
+			'parent_id'	=> $term->parent,
+			'post_count'=> $userPosts
+		]);
 
-        return ($result !== false);
+		if ($term->parent > 0) {
+			$this->updateUserTerm($user_id, $term->parent, $taxonomy);
+		}
+
+        return (bool) $result;
     }
 
-    // Decrement a user-term relationship
-
     /**
-     * @param int $user_id
-     * @param int $term_id
-     * @param string $taxonomy
-     * @param bool $is_parent
-     *
-     * @return bool
-     */
-    public function decreaseUserTerm(int $user_id, int $term_id, string $taxonomy, bool $is_parent = false):bool
-    {
-
-
-        // Update the record, decrementing the counter
-        $this->wpdb->query($this->wpdb->prepare(
-            "UPDATE {$this->table_name}
-            SET post_count = GREATEST(post_count - 1, 0),
-                last_used = NOW()
-            WHERE user_id = %d
-            AND term_id = %d
-            AND taxonomy = %s",
-            $user_id,
-            $term_id,
-            $taxonomy
-        ));
-
-        // Clean up zero-count records
-        $this->wpdb->query($this->wpdb->prepare(
-            "DELETE FROM {$this->table_name}
-            WHERE user_id = %d
-            AND term_id = %d
-            AND taxonomy = %s
-            AND post_count = 0",
-            $user_id,
-            $term_id,
-            $taxonomy
-        ));
-
-        // Clear the cache for this user and taxonomy
-        $this->clearUserCache($user_id, $taxonomy);
-
-        return true;
-    }
-
-    // Helper function to get term_id from term_taxonomy_id
-
-    /**
+	 * Helper function to get term_id from term_taxonomy_id
      * @param int $tt_id
      *
      * @return int
      */
     private function getTermIDFromTTID(int $tt_id):int
     {
-
-
-        return $this->wpdb->get_var($this->wpdb->prepare(
-            "SELECT term_id FROM {$this->wpdb->term_taxonomy} WHERE term_taxonomy_id = %d",
-            $tt_id
-        ));
+		global $wpdb;
+		return $wpdb->get_var($wpdb->prepare(
+			"SELECT term_id FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id = %d",
+			$tt_id
+		));
     }
 
-    // Rebuild the user terms index for all users
-
     /**
+	 * Rebuild the user terms index for all users
      * @return array
      */
     public function rebuildAllUserIndex():array
@@ -404,16 +176,19 @@
 
 
         // Clear existing index
-        $this->wpdb->query("TRUNCATE TABLE {$this->table_name}");
+		global $wpdb;
+        $wpdb->query("TRUNCATE TABLE {$this->index->getFullTableName()}");
 
-        // Get all users with posts
-		$users = $this->wpdb->get_col($this->wpdb->prepare(
-			"SELECT DISTINCT post_author
-			FROM {$this->wpdb->posts}
-			WHERE post_status = 'publish'
-			AND post_type LIKE %s",
-			$this->wpdb->esc_like(BASE) . '%'
-		));
+        $users = get_users([
+			'has_published_posts'	=> [array_map('jvbCheckBase', Registrar::getRegistered('post'))],
+			'fields' => 'ID'
+		]);
+		if (empty($users)) {
+			return [
+				'success'	=> true,
+				'message'	=> 'No users found to update'
+			];
+		}
 
         JVB()->queue()->queueOperation(
             'rebuild_user_term_index',
@@ -422,7 +197,6 @@
                 'users' => $users
             ],
             [
-                'count'   => count($users),
 				'chunk_key' => 'users',
 				'chunk_size' => 5,
                 'operation_id'      => 'rebuild_user_terms_' . date('Y_m_d')
@@ -435,15 +209,22 @@
         ];
     }
 
-    // Rebuild the index for a specific user
-
     /**
+	 * Rebuild the index for a specific user
      * @param int $user_id
      *
      * @return array
      */
     public function rebuildUserIndex(int $user_id):array
     {
+		$user = get_userdata($user_id);
+		if (!$user) {
+			return  [
+				'success'	=> false,
+				'message'	=> 'User does not exist'
+			];
+		}
+
         JVB()->queue()->queueOperation(
             'rebuild_user_term_index',
             0,
@@ -452,7 +233,7 @@
             ],
             [
                 'count'   => 1,
-                'operation_id'      => 'rebuild_user_terms_' . date('Y_m_d')
+                'operation_id'      => 'rebuild_user_'.$user_id.'_terms_' . date('Y_m_d')
             ]
         );
 
@@ -478,63 +259,64 @@
         }
         try {
 
+			$results = [];
             foreach ($data['users'] as $user_id) {
-                // Clear existing user records
-                $this->wpdb->query($this->wpdb->prepare(
-                    "DELETE FROM {$this->table_name} WHERE user_id = %d",
-                    $user_id
-                ));
+				$this->index->delete(['user_id' => $user_id]);
 
                 // Clear all caches for this user
                 $this->clearUserCache($user_id);
 
                 // Get all the user's published posts
-				$posts = $this->wpdb->get_col($this->wpdb->prepare(
-					"SELECT ID FROM {$this->wpdb->posts}
-					WHERE post_author = %d
-					AND post_status = 'publish'
-					AND post_type LIKE %s",
-					$user_id,
-					$this->wpdb->esc_like(BASE) . '%'
-				));
+				$posts = new WP_Query([
+					'post_status'	=> 'publish',
+					'post_type'		=> array_map('jvbCheckBase', Registrar::getRegistered('post')),
+					'posts_per_page'=> -1,
+					'fields'		=> 'ids'
+				]);
 
-                $processed_terms = 0;
+				if (empty($posts->posts)) {
+					return [
+						'success'	=> true,
+						'result'	=> [
+							'user_id'	=> $user_id,
+							'processed_posts'=> 0,
+							'processed_terms'=> 0,
+						]
+					];
+				}
 
-                foreach ($posts as $post_id) {
-                    $post_type = get_post_type($post_id);
-                    $taxonomies = get_object_taxonomies($post_type);
+				$terms = [];
 
-                    foreach ($taxonomies as $taxonomy) {
-                        $terms = wp_get_post_terms($post_id, $taxonomy, ['fields' => 'ids']);
-                        $is_hierarchical = is_taxonomy_hierarchical($taxonomy);
+				$taxonomies = [];
+				$result=[
+					'user_id'	=> $user_id,
+					'posts'		=> count($posts->posts)
+				];
+				foreach ($posts->posts as $postID) {
+					$postType = get_post_type($postID);
+					if (!array_key_exists($postType, $taxonomies)) {
+						$taxonomies[$postType] = get_object_taxonomies($postType);
+					}
+					$tax = $taxonomies[$postType];
+					$terms = array_unique(array_merge($terms, wp_get_object_terms($postID, $tax, ['fields' => 'ids'])));
+				}
 
-                        if (!is_wp_error($terms) && !empty($terms)) {
-                            foreach ($terms as $term_id) {
-                                // Add direct term
-                                $this->updateUserTerm($user_id, $term_id, $taxonomy, false);
-                                $processed_terms++;
+				$result['terms'] = count($terms);
 
-                                // Add parent terms for hierarchical taxonomies
-                                if ($is_hierarchical) {
-                                    $ancestors = get_ancestors($term_id, $taxonomy, 'taxonomy');
-                                    foreach ($ancestors as $ancestor_id) {
-                                        $this->updateUserTerm($user_id, $ancestor_id, $taxonomy, true);
-                                        $processed_terms++;
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
+				foreach ($terms as $termID) {
+					$taxonomy = get_term($termID)->taxonomy??false;
+					if (!$taxonomy) {
+						continue;
+					}
+					$this->updateUserTerm($user_id, $termID, $taxonomy);
+				}
+
+				$results[] = $result;
             }
 
             return [
                 'success' => true,
-				'result'	=> [
-					'user_id' => $user_id,
-					'processed_posts' => count($posts),
-					'processed_terms' => $processed_terms
-				]
+				'result'	=> $results
             ];
         } catch (Exception $e) {
             JVB()->error()->log(
@@ -558,122 +340,29 @@
      *
      * @return array
      */
-    public function getUserTerms(int $user_id, string $taxonomy, array $args = []):array
+    public function fetchUserTerms(int $user_id, string $taxonomy, array $args = []):array
     {
-        // Default arguments
-        $defaults = [
-            'orderby' => 'count',        // 'count' or 'name' or 'last_used'
-            'order' => 'DESC',           // 'DESC' or 'ASC'
-            'limit' => 0,                // 0 for no limit
-            'min_count' => 1,            // Minimum usage count
-            'include_parents' => true,   // Whether to include parent terms
-            'only_direct' => false,      // If true, only returns directly-used terms
-            'skip_cache' => false        // Whether to skip cache lookup
-        ];
-        $args = wp_parse_args($args, $defaults);
-
-        // Skip cache and fetch directly
-        return $this->fetchUserTerms($user_id, $taxonomy, $args);
-    }
-
-    /**
-     * @param int $user_id
-     * @param string $taxonomy
-     * @param array $args
-     *
-     * @return array
-     */
-    private function fetchUserTerms(int $user_id, string $taxonomy, array $args):array
-    {
+		$taxonomy = jvbNoBase($taxonomy);
+		if (!in_array($taxonomy, Registrar::getRegistered('term'))){
+			return [];
+		}
         $taxonomy = jvbCheckBase($taxonomy);
-		$cache = Cache::for($user_id.'_term_relationships', DAY_IN_SECONDS)->connect('post', true)->connect('taxonomy', true);
-        $key = $cache->generateKey(array_merge(
-            [
-                'taxonomy'  => $taxonomy,
-            ],
-            $args
-        ));
-        if (!$args['skip_cache']) {
-            $cached = $cache->get($key);
-            if ($cached) {
-                return $cached;
-            }
-        }
 
-        // Build query
-        $query = "
-            SELECT ut.term_id, ut.post_count, ut.last_used, ut.is_parent, t.name, t.slug
-            FROM {$this->table_name} ut
-            JOIN {$this->wpdb->terms} t ON ut.term_id = t.term_id
-            WHERE ut.user_id = %d
-            AND ut.taxonomy = %s
-            AND ut.post_count >= %d
-        ";
+		$order = array_key_exists('order', $args) ? (!in_array(strtoupper($args['order']), ['ASC', 'DESC']) ? 'DESC' : $args['order']) : 'DESC';
+		$orderby = array_key_exists('orderby', $args) ? (!in_array(strtolower($args['orderby']), ['post_count', 'last_used']) ? 'post_count' : $args['orderby']) : 'post_count';
+		$limit = array_key_exists('limit', $args) ? absint($args['limit']) : 0;
+		$limit = $limit === 0 ? null : $limit;
 
-        $query_args = [
-            $user_id,
-            $taxonomy,
-            $args['min_count']
-        ];
-
-        // Add parent term filter if needed
-        if ($args['only_direct']) {
-            $query .= " AND ut.is_parent = 0";
-        } elseif (!$args['include_parents']) {
-            $query .= " AND ut.is_parent = 0";
-        }
-
-        // Add ordering
-        switch ($args['orderby']) {
-            case 'name':
-                $query .= " ORDER BY t.name " . ($args['order'] === 'DESC' ? 'DESC' : 'ASC');
-                break;
-            case 'last_used':
-                $query .= " ORDER BY ut.last_used " . ($args['order'] === 'DESC' ? 'DESC' : 'ASC');
-                break;
-            case 'count':
-            default:
-                $query .= " ORDER BY ut.post_count " . ($args['order'] === 'DESC' ? 'DESC' : 'ASC');
-                break;
-        }
-
-        // Add limit if specified
-        if ($args['limit'] > 0) {
-            $query .= " LIMIT %d";
-            $query_args[] = $args['limit'];
-        }
-
-        // Execute query
-        $results = $this->wpdb->get_results(
-            $this->wpdb->prepare($query, $query_args),
-            ARRAY_A
-        );
-        $cache->set($key, $results);
-
-        return $results;
-    }
-
-    // Simple function to get just term IDs for a user
-
-    /**
-     * @param int $user_id
-     * @param string $taxonomy
-     * @param array $args
-     *
-     * @return array
-     */
-    public function getUserTermIDs(int $user_id, string $taxonomy, array $args = []):array
-    {
-        // Skip cache
-        $terms = $this->getUserTerms($user_id, $taxonomy, array_merge($args, ['skip_cache' => true]));
-
-        if (empty($terms)) {
-            return [];
-        }
-
-        return array_map(function ($term) {
-            return (int)$term['term_id'];
-        }, $terms);
+		return $this->index->pluck(
+			'term_id',
+			[
+				'user_id'	=> $user_id,
+				'taxonomy'	=> $taxonomy
+			],
+			$orderby,
+			$order,
+			$limit
+		);
     }
 
 }
diff --git a/inc/managers/VerifyEntryManager.php b/inc/managers/VerifyEntryManager.php
new file mode 100644
index 0000000..afbee5d
--- /dev/null
+++ b/inc/managers/VerifyEntryManager.php
@@ -0,0 +1,163 @@
+<?php
+namespace JVBase\managers;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+use JVBase\registrar\Registrar;
+use JVBase\managers\CustomTable;
+class VerifyEntryManager {
+	protected CustomTable $table;
+	protected int $max = 3;
+	public function __construct()
+	{
+		$this->defineTables();
+	}
+
+	protected function defineTables():void
+	{
+		$types = implode(',', array_map(function($item) { return "`{$item}`"; },Registrar::getFeatured('verify_entry')));
+
+		$table = CustomTable::for('verify_entry');
+
+		$table->setColumns([
+			'id'			=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+			'user_id'		=> "{$table->getUserIDType()} NOT NULL",
+			'content_id'	=> "{$table->getPostIDType()} NOT NULL",
+			'content_type'	=> 'varchar(255) NOT NULL',
+			'term_id'		=> "{$table->getTermIDType()} NOT NULL",
+			'taxonomy'		=> "ENUM({$types}) NOT NULL",
+			'status'		=> "ENUM('requested', 'rejected','accepted') NOT NULL DEFAULT 'requested'",
+			'result'		=> 'JSON DEFAULT NULL',
+			'notes'			=> 'text DEFAULT NULL',
+			'created_date'	=> 'timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP',
+			'updated_date'	=> 'timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
+		]);
+
+		$table->setKeys([
+			['key' => 'PRIMARY', 'value' => '(`id`)'],
+			['key' => 'UNIQUE', 'value' => 'content_term (`content_id`, `term_id`)'],
+			'user_id (`user_id`)',
+			'term_id (`term_id`)',
+			'status (`term_id`, `status`)'
+		]);
+
+		$base = BASE;
+		$table->setConstraints([
+			"CONSTRAINT `{$base}verify_entry_user` FOREIGN KEY (`user_id`)
+			REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE",
+			"CONSTRAINT `{$base}verify_entry_term` FOREIGN KEY (`term_id`)
+			REFERENCES `{$table->getTermTable()}` (`term_id`) ON DELETE CASCADE",
+		]);
+
+		$table->defineTable();
+
+		$this->table = $table;
+	}
+	protected function response(?bool $success = null, string $message = ''):array
+	{
+		$response = [
+			'success'	=> is_null($success) ? 'partial' : $success,
+		];
+		if (!empty($message)) {
+			$response['message'] = $message;
+		}
+		return $response;
+	}
+
+	public function requestEntry(int $userID, int $termID, string $taxonomy):array
+	{
+		$user = get_userdata($userID);
+		if (!$user || is_wp_error($user)) {
+			return $this->response(false, 'User does not exist');
+		}
+		$term = get_term($termID, jvbCheckBase($taxonomy));
+		if (!$term || is_wp_error($term)) {
+			return $this->response(false, 'Term does not exist');
+		}
+
+		$profile = get_user_meta($userID, BASE.'link', true);
+		if (empty($profile)) {
+			return $this->response(false, 'No Profile found');
+		}
+
+		$managers = jvbGetTermOwners($termID);
+		if (in_array($userID, $managers)) {
+			wp_set_object_terms($profile, $termID, jvbCheckBase($taxonomy));
+			return $this->response(true, 'User is manager');
+		}
+
+		$total = $this->table->pluck('user_id', [
+			'user_id'	=> $userID,
+			'status'	=> 'requested'
+		]);
+		if (count($total) >= $this->max) {
+			return $this->response(false, 'User already has maximum requests');
+		}
+
+		$existing = $this->table->get([
+			'user_id'	=> $userID,
+			'term_id'	=> $termID
+		]);
+		if ($existing) {
+			return $this->response(false, 'Existing pending request found. Please wait for response.');
+		}
+
+		$request = $this->table->insert([
+			'user_id'	=> $userID,
+			'content_id'=> (int)$profile,
+			'content_type'=> jvbNoBase(get_post_type($profile)),
+			'term_id'	=> $termID,
+			'taxonomy'  => jvbNoBase($taxonomy)
+		]);
+
+		if ($request && !empty($owners)) {
+			JVB()->notification()->notify($owners,'entry_requested',$userID, [
+				'target_id' => $termID,
+				'target_type' => $taxonomy
+			]);
+		}
+		return $this->response(true, 'Request has been sent');
+	}
+
+	protected function actionEntry(bool $approve, int $userID, int $requestID, ?string $notes = null):array
+	{
+		$request = $this->table->get(['id' => $requestID]);
+		if (!$request) {
+			return $this->response(false, 'Request does not exist');
+		}
+
+		$termID = $request['term_id'];
+		$owners = jvbGetTermOwners($termID);
+		if (!user_can($userID, 'can_manage_'.$termID) || !in_array($userID, $owners)) {
+			return $this->response(false, 'User does not exist');
+		}
+		$result = $this->table->update([
+			'status'	=> ($approve ? 'accepted' : 'rejected'),
+			'notes'		=> $notes
+		], [
+			'id'	=> $requestID
+		]);
+		if ($approve) {
+			wp_set_object_terms($request['content_id'], $termID, jvbCheckBase($request['taxonomy']));
+		}
+
+		JVB()->notification()?->notify($request['user_id'],$approve ? 'entry_approved' : 'entry_denied',$userID);
+		//TODO: What if there are multiple managers, and one approved the request? Do we just delete the notification?
+		JVB()->notification()?->unnotify($owners, 'entry_requested', $request['user_id'], [
+			'target_id' => $termID,
+			'target_type' => $request['taxonomy']
+		]);
+		return $this->response(true, 'Request has been '. $approve ? 'approved' : 'denied');
+	}
+	public function approveEntry(int $userID, int $requestID, ?string $notes = null):array
+	{
+		return $this->actionEntry(true, $userID, $requestID, $notes);
+	}
+
+	public function denyEntry(int $userID, int $requestID, ?string $notes = null):array
+	{
+		return $this->actionEntry(false, $userID, $requestID, $notes);
+	}
+}
diff --git a/inc/managers/_setup.php b/inc/managers/_setup.php
index d29be0c..45b8f73 100644
--- a/inc/managers/_setup.php
+++ b/inc/managers/_setup.php
@@ -62,7 +62,12 @@
 //}
 
 if (Features::forSite()->has('notifications')) {
+	require(JVB_DIR . '/inc/managers/Notifications/Content.php');
+	require(JVB_DIR . '/inc/managers/Notifications/EmailDigests.php');
+	require(JVB_DIR . '/inc/managers/Notifications/Notifications.php');
+	require(JVB_DIR . '/inc/managers/Notifications/Preferences.php');
 	require(JVB_DIR . '/inc/managers/NotificationManager.php');
+
 }
 
 if (Features::forMembership()->has('forum') && !empty(Registrar::getFeatured('is_content', 'term'))) {
diff --git a/inc/managers/queue/Queue.php b/inc/managers/queue/Queue.php
index d93078b..95b7afb 100644
--- a/inc/managers/queue/Queue.php
+++ b/inc/managers/queue/Queue.php
@@ -78,7 +78,7 @@
 		]);
 
 		$queue->setKeys([
-			['key' => 'PRIMARY', 'value' => 'id'],
+			['key' => 'PRIMARY', 'value' => '(`id`)'],
 			'`idx_run_queue` (`state`, `priority`, `scheduled_at`)',
 			'`idx_user_ops` (`user_id`, `state`)',
 			'`idx_user_type_pending` (`user_id`, `type`, `state`)',
@@ -111,7 +111,7 @@
 		]);
 
 		$stats->setKeys([
-			['key' => 'PRIMARY', 'value' => 'id'],
+			['key' => 'PRIMARY', 'value' => '(`id`)'],
 			['key' => 'UNIQUE', 'value' => '(`date`, `type`)'],
 			'`date_idx` (`date`)',
 			'`type_idx` (`type`)'
diff --git a/inc/registrar/Fields.php b/inc/registrar/Fields.php
index 6887534..e1e4928 100644
--- a/inc/registrar/Fields.php
+++ b/inc/registrar/Fields.php
@@ -166,8 +166,9 @@
 
 	public function  modifyField(string $name, string $property, mixed $value):void
 	{
+		$property = 'set'.implode('',array_map('ucfirst',explode('_', $property)));
 		$field = $this->fields[$name];
-		$field->$property = $value;
+		$field->$property($value);
 	}
 
 	public function getFields():array
diff --git a/inc/registrar/Posts.php b/inc/registrar/Posts.php
index 416f24b..ac1d5e1 100644
--- a/inc/registrar/Posts.php
+++ b/inc/registrar/Posts.php
@@ -1,6 +1,8 @@
 <?php
 namespace JVBase\registrar;
 
+use JVBase\forms\TaxonomySelector;
+
 if (!defined('ABSPATH')) {
 	exit;
 }
@@ -208,9 +210,12 @@
 	 * @var string|false
 	 */
 	public string|false $template_lock;
+	protected string $unbased;
+	protected ?string $taxonomyRewrite = null;
 
 	public function __construct(string $postType, string $singular, string $plural)
 	{
+		$this->unbased = jvbNoBase($postType);
 		$this->postType = jvbCheckBase($postType);
 		$this->labels = $this->buildLabels($singular, $plural);
 	}
@@ -312,6 +317,29 @@
 		unset ($args['postType']);
 
 //		error_log('Registering PostType '.$this->postType.', with args: '.print_r($args, true));
+		$registrar = Registrar::getInstance($this->postType);
+		$rewrite = $args['rewrite']['slug']??'';
+		if ($registrar) {
+
+			$hasSlug = array_key_exists('rewrite', $args) && array_key_exists('slug', $args['rewrite']);
+
+			if ($registrar->rewrite_taxonomy && !str_contains($rewrite, '%')) {
+				if (!$hasSlug && !array_key_exists('rewrite', $args)) {
+					$args['rewrite'] = [];
+				}
+				$tax = is_array($registrar->rewrite_taxonomy) ? $registrar->rewrite_taxonomy[array_key_first($registrar->rewrite_taxonomy)] : $registrar->rewrite_taxonomy;
+				$args['rewrite']['slug'] = $hasSlug ? $rewrite."/%{$tax}%" : $this->unbased."/%{$tax}%";
+			}
+
+			if ($registrar->hasFeature('is_calendar')) {
+				if (!$hasSlug && !array_key_exists('rewrite', $args)) {
+					$args['rewrite'] = [];
+				}
+				$args['rewrite']['slug'] = $hasSlug ? $rewrite."/%eyear%/%emonth%/%eday%" : $this->unbased."/%eyear%/%emonth%/%eday%";
+			}
+		}
+
+
 		$registered = register_post_type($this->postType, $args);
 		if (is_wp_error($registered)) {
 			JVB()->error()->log('JVBase\registrar\Posts', 'Could not register post type', $registered->get_error_messages());
@@ -337,4 +365,93 @@
 			'not_found_in_trash' => "No {$plural} found in Trash.",
 		];
 	}
+
+	public function addTaxonomyRewrite(string $taxonomy):void
+	{
+		$exists = Registrar::getInstance($taxonomy);
+		if (!$exists) return;
+
+		$this->taxonomyRewrite = $taxonomy;
+
+		if (!isset($this->rewrite)) {
+			$this->rewrite = [];
+		}
+		if (array_key_exists('slug', $this->rewrite)) {
+			if (str_contains($this->rewrite['slug'], '%')) {
+				return;
+			}
+			$this->rewrite['slug'] = $this->rewrite['slug'].'/%'.$taxonomy.'%';
+		} else {
+			$this->rewrite['slug'] = $this->unbased.'/%'.$taxonomy.'%';
+		}
+
+		$this->addTaxonomyRewriteRules();
+		add_action('post_type_link', [$this, 'rewriteTaxonomySingle'], 15, 2);
+		add_action('post_type_archive_link', [$this, 'rewriteTaxonomyArchive'], 15, 2);
+	}
+	public function addTaxonomyRewriteRules(): void
+	{
+		if (!$this->taxonomyRewrite) return;
+		$tax = jvbCheckBase($this->taxonomyRewrite);
+
+		// Rule 1: Post type archive - /faq/
+		add_rewrite_rule(
+			"{$this->unbased}/?$",
+			"index.php?post_type={$this->postType}",
+			'top'
+		);
+
+		// Rule 2: Single posts with taxonomy - /faq/section/post/
+		add_rewrite_rule(
+			"{$this->unbased}/([^/]+)/([^/]+)/?$",
+			"index.php?post_type={$this->postType}&name=\$matches[2]&{$tax}=\$matches[1]",
+			'top'
+		);
+
+		// Rule 3: Un-sectioned posts - /faq/post/
+		// Use 'bottom' priority so taxonomy rules match first
+		add_rewrite_rule(
+			"{$this->unbased}/([^/]+)/?$",
+			"index.php?post_type={$this->postType}&name=\$matches[1]",
+			'bottom'
+		);
+	}
+
+	/**
+	 * Set $this->rewrite_taxonomy to a valid taxonomy
+	 * @param string $url
+	 * @param \WP_Post $post
+	 * @return string
+	 */
+	public function rewriteTaxonomySingle(string $url, \WP_Post $post): string
+	{
+		if ($post->post_type === $this->postType && !is_null($this->taxonomyRewrite)) {
+			$type = $this->taxonomyRewrite;
+			$taxonomy = jvbCheckBase($type);
+			$terms = wp_get_post_terms($post->ID, $taxonomy);
+			if (!empty($terms) && !is_wp_error($terms)) {
+				$path = TaxonomySelector::getTermPath($terms[0], true);
+				$path = implode('/', array_map(function($term) {
+					return sanitize_title($term);
+				}, $path));
+				return str_replace("%{$type}%", $path, $url);
+			}
+			return str_replace("/%{$type}%", '', $url);
+		}
+		return $url;
+	}
+
+	/**
+	 * Set $this->rewrite_taxonomy to a valid taxonomy
+	 * @param string $url
+	 * @param string $post_type
+	 * @return string
+	 */
+	public function rewriteTaxonomyArchive(string $url, string $post_type):string
+	{
+		if ($post_type === $this->postType && !is_null($this->taxonomyRewrite)) {
+			$url = get_home_url(null, "/{$this->postType}/");
+		}
+		return $url;
+	}
 }
diff --git a/inc/registrar/Registrar.php b/inc/registrar/Registrar.php
index a69a13f..727e3b3 100644
--- a/inc/registrar/Registrar.php
+++ b/inc/registrar/Registrar.php
@@ -1,9 +1,12 @@
 <?php
 namespace JVBase\registrar;
 
+use JVBase\forms\TaxonomySelector;
+use JVBase\inc\registrar\helpers\HideSingle;
 use JVBase\managers\Cache;
 use JVBase\managers\CRUD;
 use JVBase\managers\IconsManager;
+use JVBase\managers\KarmaManager;
 use JVBase\meta\Meta;
 use JVBase\registrar\config\Breadcrumbs;
 use JVBase\registrar\config\Dashboard;
@@ -14,9 +17,11 @@
 use JVBase\registrar\config\SEO;
 use JVBase\registrar\helpers\AddIntegrationFields;
 use JVBase\registrar\helpers\MakeCalendarType;
+use JVBase\registrar\helpers\MakeTimelineType;
 use JVBase\registrar\helpers\MakeTrackChanges;
 use JVBase\registrar\helpers\MakeVerification;
 use JVBase\utility\Features;
+use WP_Post;
 use WP_Query;
 
 if (!defined('ABSPATH')) {
@@ -41,11 +46,13 @@
 
 	protected int|false $page = false;
 
+	public ?string $rewrite_taxonomy = null;
+
 	protected static array $allFlags = [
 		//Shared Flags
 		'favouritable', 'karma', 'show_feed', 'show_directory', 'approve_new', 'has_responses', 'invitable',
 		//Post Flags
-		'hide_single', 'redirect_to_author', 'is_calendar', 'single_image', 'is_timeline', 'is_gallery',
+		'hide_single', 'redirect_to_author', 'is_calendar', 'single_image', 'is_timeline', 'is_gallery', 'is_faq', 'is_glossary', 'rewrite_taxonomy',
 		//Taxonomy Flags
 		'is_content', 'is_ownable', 'verify_entry', 'track_changes', 'associate_user_content',
 		//User Flags
@@ -62,6 +69,7 @@
 	 * @var bool Whether to setup karma for this content
 	 */
 	protected bool $karma = false;
+	protected ?KarmaManager $karmaManager = null;
 	/**
 	 * @var bool Whether this should be available in the feed block
 	 */
@@ -90,6 +98,7 @@
 	 * @var bool Whether single items of this content should be hidden
 	 */
 	protected bool $hide_single = false;
+	protected ?HideSingle $hideSingleHandler = null;
 
 	/**
 	 * @var bool Whether single items should just go to the author's page
@@ -98,11 +107,21 @@
 	/**
 	 * @var bool Whether to make this a calendar type (example: events)
 	 */
-	protected bool $is_calendar;
+	protected bool $is_calendar = false;
+	protected ?MakeCalendarType $isCalendarHandler = null;
 	/**
 	 * @var bool Whether this is a before/after post type
 	 */
 	protected bool $is_timeline = false;
+	protected ?MakeTimelineType $isTimelineHandler = null;
+	/**
+	 * @var bool Whether this is a defined term post type
+	 */
+	protected bool $is_glossary = false;
+	/**
+	 * @var bool Whether this is a faq post type
+	 */
+	protected bool $is_faq = false;
 	/**
 	 * @var bool Whether the uploader can group images prior to upload
 	 */
@@ -113,7 +132,7 @@
 	/**
 	 * @var bool For taxonomy types only. Treats the taxonomy as a content (ie: tattoo shops))
 	 */
-	protected bool $is_content;
+	protected bool $is_content = false;
 	/**
 	 * @var bool Whether this taxonomy can be owned/managed by specific people only
 	 */
@@ -122,10 +141,12 @@
 	 * @var bool Whether users/content need to request the owner for admission
 	 */
 	protected bool $verify_entry;
+	protected ?MakeVerification $verifyEntryHandler = null;
 	/**
 	 * @var bool Whether we should track post movements from term to term (ie. artists in tattoo shops)
 	 */
 	protected bool $track_changes;
+	protected ?MakeTrackChanges $trackChangesHandler = null;
 
 	/**
 	 * @var bool Whether any content by members in this taxonomy should show up in this taxonomy
@@ -158,7 +179,7 @@
 	/**
 	 * @var bool Whether to generate a profile for this user role
 	 */
-	protected bool $profile_link;
+	public bool $profile_link;
 	/**
 	 * @var array|string
 	 */
@@ -274,8 +295,16 @@
 			return $this;
 		}
 		$this->args = $args;
+
 		foreach ($args as $property => $value) {
-			$this->registrar->$property = $value;
+			if (property_exists($this->registrar, $property)) {
+				$this->registrar->$property = $value;
+			}
+
+			if (property_exists($this, $property)) {
+				$this->$property = $value;
+			}
+
 		}
 		if (isset($this->icon) && !str_contains($this->icon, 'dashicons')){
 			$this->registrar->menu_icon = IconsManager::for()->getCSSIcon($this->icon);
@@ -390,6 +419,30 @@
 		return $this->based;
 	}
 
+	public function setGlossary(bool $set):self
+	{
+		$this->is_glossary = $set;
+//		if ($set) {
+//			$this->timeline = new MakeTimelineType($this->slug, $this);
+//		}
+		return $this;
+	}
+	public function isGlossary():bool
+	{
+		return $this->is_glossary;
+	}
+	public function setFaq(bool $set):self
+	{
+		$this->is_faq = $set;
+//		if ($set) {
+//			$this->timeline = new MakeTimelineType($this->slug, $this);
+//		}
+		return $this;
+	}
+	public function isFaq():bool
+	{
+		return $this->is_faq;
+	}
 	public function setTimeline(bool $set):self
 	{
 		$this->is_timeline = $set;
@@ -422,6 +475,9 @@
 				case 'is_content':
 					add_action('init', [$this, 'setupContent'], 20);
 					break;
+				case 'is_glossary':
+					$this->hide_single = true;
+					break;
 			}
 		}
 		return $this;
@@ -447,13 +503,18 @@
 	}
 	public static function getFeatured(string $feature, ?string $type = null):array
 	{
+		self::ensureInstanced();
+
 		if (!in_array($feature, static::$allFlags)) {
 			error_log('Feature requested not found: '.$feature);
 			return [];
 		}
 
 		return array_map(function($inst) { return $inst->slug; },array_filter(self::$instances, function ($inst) use ($feature, $type){
-			return isset($inst->$feature) && $inst->$feature === true && (is_null($type) || $inst->type === $type);
+			if (!is_null($type) && $inst->type !== $type) {
+				return false;
+			}
+			return property_exists($inst, $feature) && isset($inst->$feature) && $inst->$feature === true;
 		}));
 	}
 
@@ -478,12 +539,15 @@
 	}
 		protected function getBreadcrumbs():Breadcrumbs
 		{
-			$this->breadcrumbs = new Breadcrumbs($this->slug, $this);
+			if (!isset($this->breadcrumbs)) {
+				$this->breadcrumbs = new Breadcrumbs($this->slug, $this);
+			}
+
 			return $this->breadcrumbs;
 		}
 		protected function getCalendar():MakeCalendarType|false
 		{
-			if ($this->is_calendar){
+			if ($this->is_calendar && !isset($this->calendar)){
 				$this->calendar = new MakeCalendarType($this->slug, $this);
 			} else {
 				$this->calendar = false;
@@ -493,26 +557,25 @@
 
 		protected function getDashboard():Dashboard
 		{
-			$this->dashboard = new Dashboard($this->plural, $this);
+			if (!isset($this->dashboard)) {
+				$this->dashboard = new Dashboard($this->plural, $this);
+			}
+
 			return $this->dashboard;
 		}
 
 		protected function getDirectory():Directory|false
 		{
-			if ($this->show_directory) {
-				$this->directory = new Directory($this->singular, $this);
-			} else {
-				$this->directory = false;
+			if (!isset($this->directory)) {
+				$this->directory = new Directory($this->singular);
 			}
 			return $this->directory;
 		}
 
 		protected function getFeed():Feed|false
 		{
-			if ($this->show_feed) {
-				$this->feed = new Feed($this->slug, $this);
-			} else {
-				$this->feed = false;
+			if (!isset($this->feed)) {
+				$this->feed = new Feed($this->slug);
 			}
 			return $this->feed;
 		}
@@ -520,7 +583,7 @@
 		public function getSEO():SEO
 		{
 			if (!isset($this->seo)){
-				$this->seo = new SEO($this->slug, $this);
+				$this->seo = new SEO($this->slug);
 			}
 			return $this->seo;
 		}
@@ -585,13 +648,48 @@
 
 	public function register():void
 	{
-		if ($this->registrar) {
-			$this->registrar->register();
+		if ($this->type === 'post') {
+			if ($this->hide_single) {
+				$this->hideSingleHandler = new HideSingle($this->slug, $this);
+			}
+			if ($this->is_timeline) {
+				$this->isTimelineHandler = new MakeTimelineType($this->slug, $this);
+				$this->registrar->hierarchical = true;
+			}
+			if ($this->is_calendar) {
+				$this->isCalendarHandler = new MakeCalendarType($this->slug, $this);
+			}
+
+			if (!is_null($this->rewrite_taxonomy)) {
+				$this->registrar->addTaxonomyRewrite($this->rewrite_taxonomy);
+			}
+
+			if ($this->registrar) {
+				$this->registrar->register();
+			}
+		} elseif ($this->type === 'term') {
+			if ($this->is_content) {
+				if ($this->verify_entry) {
+					$this->verifyEntryHandler = new MakeVerification();
+				}
+				if ($this->track_changes) {
+					$this->trackChangesHandler = new MakeTrackChanges($this->slug);
+				}
+			}
+
+			if ($this->registrar) {
+				$this->registrar->register();
+			}
 		}
 
+		if ($this->karma) {
+			$this->karmaManager = KarmaManager::for($this->slug);
+		}
 	}
 	public static function getInstance(string $slug):Registrar|false
 	{
+		self::ensureInstanced();
+
 		$slug = jvbNoBase($slug);
 		if (array_key_exists($slug, static::$instances)) {
 			return static::$instances[$slug];
@@ -601,6 +699,7 @@
 
 	public static function getFieldsFor(string $slug):array
 	{
+		self::ensureInstanced();
 		if (!array_key_exists($slug, static::$instances)) {
 			return [];
 		}
@@ -610,6 +709,7 @@
 
 	public static function getRegistered(?string $type = null):array
 	{
+		self::ensureInstanced();
 		$instances = ($type) ? array_filter(static::$instances, function($instance) use ($type) {
 			return $instance->type === $type;
 		}) : static::$instances;
@@ -618,6 +718,7 @@
 
 	public static function getLabels():array
 	{
+		self::ensureInstanced();
 		return array_map(function ($instance) {
 			return ['singular' => $instance->getSingular(),
 				'plural' => $instance->getPlural()];
@@ -797,4 +898,20 @@
 		error_log('Built the '.$this->slug.' page content.');
 		return $content . $out;
 	}
+
+	public static function ensureInstanced():void
+	{
+		if (empty(self::$instances)) {
+			do_action('jvbDefineRegistrar');
+			do_action('jvbDefineRegistrarFields');
+		}
+	}
+
+	/*****************************************************************
+	 * FLAGGED FEATURES
+	*****************************************************************/
+
+
+
+
 }
diff --git a/inc/registrar/_setup.php b/inc/registrar/_setup.php
index 897eb11..f9d9672 100644
--- a/inc/registrar/_setup.php
+++ b/inc/registrar/_setup.php
@@ -15,7 +15,10 @@
 require(JVB_DIR . '/inc/registrar/fields/SelectorField.php');
 require(JVB_DIR . '/inc/registrar/fields/UploadField.php');
 
+require(JVB_DIR . '/inc/registrar/helpers/AddIntegrationFields.php');
+require(JVB_DIR . '/inc/registrar/helpers/HideSingle.php');
 require(JVB_DIR . '/inc/registrar/helpers/MakeCalendarType.php');
+require(JVB_DIR . '/inc/registrar/helpers/MakeTimelineType.php');
 require(JVB_DIR . '/inc/registrar/helpers/MakeTrackChanges.php');
 require(JVB_DIR . '/inc/registrar/helpers/MakeVerification.php');
 
diff --git a/inc/registrar/config/Breadcrumbs.php b/inc/registrar/config/Breadcrumbs.php
index 9d7b8e5..69c0fe3 100644
--- a/inc/registrar/config/Breadcrumbs.php
+++ b/inc/registrar/config/Breadcrumbs.php
@@ -40,7 +40,7 @@
 		$config = [
 			'title'	=> $this->title,
 		];
-		if (isset($this->addCrumb)) {
+		if (!empty($this->addCrumb)) {
 			$config['addCrumb'] = $this->addCrumb;
 		}
 		return $config;
diff --git a/inc/registrar/config/SEO.php b/inc/registrar/config/SEO.php
index 4797cf9..5248bb7 100644
--- a/inc/registrar/config/SEO.php
+++ b/inc/registrar/config/SEO.php
@@ -38,7 +38,7 @@
 			if ($registrar) {
 				switch ($registrar->getType()) {
 					case 'term':
-						$this->config['schema']['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage';
+						$this->config['schema']['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage';
 						break;
 					case 'post':
 						$this->config['schema']['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork';
diff --git a/inc/registrar/config/seo/Resolver.php b/inc/registrar/config/seo/Resolver.php
index 3f18043..ab46dd4 100644
--- a/inc/registrar/config/seo/Resolver.php
+++ b/inc/registrar/config/seo/Resolver.php
@@ -16,6 +16,13 @@
 
 	public static function resolve(string $template, ?Meta $meta): string
 	{
+		if (!$meta) {
+			if (is_singular()){
+				$meta = Meta::forPost(get_the_ID());
+			}else if (is_tax()) {
+				$meta = Meta::forTerm(get_queried_object()->term_id);
+			}
+		}
 		self::$meta = $meta;
 		return preg_replace_callback(
 			'/\{\{([^}]+)}}/',
@@ -45,6 +52,9 @@
 			error_log('[SEO]Meta Resolver. Could not find meta configuration for variable: '.$variable);
 			return '';
 		}
+		if ($variable === 'post_content') {
+			return self::resolvePostContent($variable, $meta);
+		}
 		return match($config['type']) {
 			'upload', 'image', 'gallery' => self::resolveImage($variable, $meta),
 			'selector', 'taxonomy', 'user', 'post' => self::resolveObject($variable, $meta),
@@ -69,6 +79,8 @@
 			}
 
 		}
+		error_log('resolveRelation: '.print_r($path, true));
+
 		$ID = $meta->get($relation);
 		if (!$ID || $ID === '') {
 			return '';
@@ -78,14 +90,16 @@
 		if (!$config) {
 			return '';
 		}
+
 		$type = false;
 		if ($config['type'] === 'taxonomy' || array_key_exists('taxonomy', $config)) {
-			$type = 'taxonomy';
-		} elseif ($config['type'] === 'user'){
+			$type = 'term';
+		} elseif ($config['type'] === 'user' || array_key_exists('user', $config)){
 			$type = 'user';
-		} elseif ($config['type'] === 'post'){
+		} elseif ($config['type'] === 'post' || array_key_exists('post', $config)){
 			$type = 'post';
 		}
+
 		if (!$type) {
 			error_log('[SEO]Meta Resolver. Could not find type for relation: '.$relation.': '.$field);
 			return '';
@@ -95,6 +109,9 @@
 		return self::resolve($field, $newMeta);
 	}
 
+	protected static function resolvePostContent(string $variable, ?Meta $meta):string{
+		return wp_strip_all_tags(str_replace("\n", '', $meta->get('post_content')));
+	}
 	protected static function resolveImage(string $variable, ?Meta $meta, bool $returnID = false): string
 	{
 		$imgID = $meta->get($variable);
@@ -133,7 +150,11 @@
 			return $checkType;
 		}
 
-		error_log('[SEO]Resolver - No method found for '.$property.' with value: '.print_r($value, true).'. Defaulting to base Resolver');
+		$ignore = ['description', 'name'];
+		if (!in_array($property, $ignore)) {
+			error_log('[SEO]Resolver - No method found for '.$property.' with value: '.print_r($value, true).'. Defaulting to base Resolver');
+		}
+
 
 		return self::resolve($value, $meta);
 	}
diff --git a/inc/registrar/config/seo/Schema.php b/inc/registrar/config/seo/Schema.php
index 22e033c..15c256e 100644
--- a/inc/registrar/config/seo/Schema.php
+++ b/inc/registrar/config/seo/Schema.php
@@ -1,6 +1,7 @@
 <?php
 namespace JVBase\registrar\config\seo;
 
+use JVBase\base\SchemaHelper;
 use JVBase\managers\Cache;
 use JVBase\managers\SEO\render;
 use JVBase\meta\Meta;
@@ -35,7 +36,7 @@
 	];
 
 	protected array $defaultArchive = [
-		'type'	=> 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage',
+		'type'	=> 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage',
 		'title' => '{{registrar.plural}}'
 	];
 
@@ -58,7 +59,7 @@
 
 			switch ($registrar->getType()) {
 				case 'term':
-					$this->defaultSchema['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage';
+					$this->defaultSchema['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage';
 					break;
 				case 'user':
 					$this->defaultSchema['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\ProfilePage';
@@ -79,7 +80,7 @@
 
 		$registrar = Registrar::getInstance($this->slug);
 		$this->defaultArchive = [
-			'type'	=> 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage',
+			'type'	=> 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage',
 			'name'	=> $registrar->getPlural(),
 			'description' => $registrar->getDescription()
 		];
@@ -148,6 +149,7 @@
 	public function outputSchema():void
 	{
 		$registrar = Registrar::getInstance($this->slug);
+
 		if (is_singular()) {
 			$this->outputSingularSchema();
 		} elseif (is_post_type_archive(jvbCheckBase($this->slug) || is_tax(jvbCheckBase($this->slug)))) {
@@ -163,10 +165,19 @@
 				$this->cache->flush();
 			}
 
+			$registrar = Registrar::getInstance($this->slug);
+
 			return $this->cache->remember(
 				$ID,
-				function () use ($ID) {
+				function () use ($ID, $registrar) {
 					$meta = Meta::forPost($ID);
+					if ($registrar->hasFeature('is_faq')) {
+						return $this->outputQASchema($ID, $meta);
+					}
+					if ($registrar->hasFeature('is_timeline')) {
+						return $this->outputTimelineSchema($ID, $meta);
+					}
+
 					$config = $this->getConfig();
 
 					$class = JVB()->schemaHelper()::classFromConfig($config, $meta);
@@ -191,7 +202,7 @@
 					$config = JVB()->schemaHelper()::schema($action);
 
 					if (!array_key_exists('type', $config)) {
-						$config['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage';
+						$config['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage';
 					}
 					if (!class_exists($config['type'])) {
 						error_log('No class found for archive schema output: '.$config['type']);
@@ -238,12 +249,22 @@
 					$action = BASE.ucfirst($this->slug).'Archive';
 					$config = JVB()->schemaHelper()->archive($this->slug);
 					if (!array_key_exists('type', $config)) {
-						$config['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage';
+						$config['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage';
 					}
 					if (!class_exists($config['type'])) {
 						error_log('No class found for archive schema output: '.$config['type']);
 						return [];
 					}
+
+					$registrar = Registrar::getInstance($this->slug);
+					if ($registrar->hasFeature('is_glossary')) {
+						return $this->outputGlossarySchema();
+					} else if ($registrar->hasFeature('is_faq')) {
+						return $this->outputFAQSchema();
+					}
+					if ($registrar->hasFeature('is_timeline')) {
+						return $this->outputTimelineArchiveSchema();
+					}
 					$obj = get_queried_object();
 					$meta = (property_exists($obj, 'taxonomy')) ? Meta::forTerm($obj->term_id) : null;
 
@@ -262,6 +283,7 @@
 					foreach ($items->posts as $ID) {
 						$item = $this->outputReferenceSchema($ID, 'post',false);
 						$listItem = new render\Thing\Intangible\ListItem();
+						$listItem->setId('listitem-'.$ID);
 						$listItem->setPosition($pos);
 						$listItem->setItem($item);
 						$itemListItems[] = $listItem;
@@ -301,7 +323,7 @@
 						error_log('Invalid type used for reference: '.print_r($type, true));
 						$meta = null;
 				}
-				$config = $this->getConfig('archive');
+				$config = $this->getConfig();
 				$class = JVB()->schemaHelper()::classFromConfig($config, $meta);
 				$class->delete('about');
 
@@ -331,6 +353,28 @@
 		return JVB()->schemaHelper()::getConfig($this->slug, $type);
 	}
 
+	public function resolveMeta(array $config, Meta $meta):array
+	{
+		foreach ($config as $property => $value) {
+			if (is_array($value)) {
+				$config[$property] = $this->resolveMeta($value, $meta);
+				if ($property === 'additionalProperty' && (!array_key_exists('value', $config[$property]) || empty($config[$property]['value']))) {
+					unset($config[$property]);
+				}
+			}
+			if (is_string($value) && str_contains($value, '{{')) {
+				$value = Resolver::resolve($value, $meta);
+				if (empty($value)){
+					unset($config[$property]);
+				} else {
+					$config[$property] = $value;
+				}
+			}
+		}
+
+		return $config;
+	}
+
 	public function define(string $property, string $value):void
 	{
 		$class = $this->getConfig('schema')['type'];
@@ -392,7 +436,7 @@
 		if (is_singular($based)){
 			$config = $this->getConfig('meta');
 			$meta = Meta::forPost(get_the_ID());
-			$title = Resolver::resolve($config['name'], $meta);
+			$title = Resolver::resolve($config['name']??$config['title'], $meta);
 		} elseif (is_post_type_archive($based) ) {
 			$config = $this->getConfig('archive');
 			$title = $config['name'];
@@ -405,4 +449,210 @@
 		}
 		return $title;
 	}
+	public function outputQASchema(int $ID, Meta $meta):array
+	{
+		$registrar = Registrar::getInstance($this->slug);
+		global $wp;
+		$current = get_home_url(null, $wp->request).'/';
+		$config = $this->getConfig();
+
+		$config['type']	= 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\QAPage';
+		$page = SchemaHelper::classFromConfig($config, $meta);
+
+		$post = get_queried_object();
+		$question = [
+			'id'	=> $current.'#question-'.$post->post_name,
+			'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Question',
+			'name'	=> $meta->get('post_title'),
+			'acceptedAnswer' => [
+				'id'	=> $current.'#answer-'.$post->post_name,
+				'type'	=> 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Answer',
+				'text'	=> wp_strip_all_tags(str_replace("\n", '', $meta->get('post_content'))),
+			],
+		];
+		$question = SchemaHelper::classFromConfig($question);
+		$page->setMainEntity($question);
+		$page->setAuthor(JVB()->seo()->getCreator(true));
+		return $page->outputSchema();
+
+	}
+	public function outputTimelineSchema(int $ID, Meta $meta):array
+	{
+		$registrar = Registrar::getInstance($this->slug);
+		global $wp;
+		$current = get_home_url(null, $wp->request).'/';
+		$config = $this->getConfig();
+
+		$config['type']	= 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\ImageGallery';
+		$this->resolveMeta($config, $meta);
+		$page = SchemaHelper::classFromConfig($config, $meta);
+
+		$post = get_queried_object();
+		$images = [];
+		$children = new WP_Query([
+			'post_type'=> jvbCheckBase($this->slug),
+			'post_status'	=> 'publish',
+			'post_parent'	=> $post->ID,
+		]);
+		$children = $children->posts;
+		array_unshift($children, $post);
+
+		foreach ($children as $index => $child) {
+			$meta = Meta::forPost($child->ID);
+
+			$image = render\Thing\Thing::createImageFromID($meta->get('post_thumbnail'));
+			$image->setId(($index === 0) ? '#before-treatment' : '#treatment-'.$index);
+			$image->setPosition($index+1);
+			$image->setName(($index === 0) ? 'Before Laser Tattoo Removal' : 'After '.$index.' Laser Tattoo Removal Treatments');
+			if (!empty ($meta->get('post_excerpt'))){
+				$image->setDescription($meta->get('post_excerpt'));
+			}
+
+			$images[] = $image;
+		}
+
+		$page->setImage($images);
+		$page->setAuthor(JVB()->seo()->getCreator(true));
+		return $page->outputSchema();
+
+	}
+
+	/*********************************************
+	 * Archive Presets
+	*********************************************/
+	public function outputFAQSchema():array
+	{
+		$registrar = Registrar::getInstance($this->slug);
+		global $wp;
+		$current = get_home_url(null, $wp->request).'/';
+		$config = $this->getConfig('archive');
+		$page = [
+			'id'	=> $current.'#'.$registrar->getSlug(),
+			'type'	=> 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\FAQPage',
+			'name'	=> array_key_exists('name', $config) ? $config['name'] : $registrar->getPlural(),
+			'description' => array_key_exists('description', $config) ? $config['description'] : $registrar->getDescription(),
+			'url'	=> $current,
+		];
+
+		$page = SchemaHelper::classFromConfig($page);
+
+		$args = [
+			'post_type'		=> $registrar->getBased(),
+			'posts_per_page'=> -1,
+			'post_status' 	=> 'publish',
+		];
+		$obj = get_queried_object();
+
+		if (property_exists($obj, 'taxonomy')) {
+			$page->setName('FAQ on '.$obj->name);
+
+			$args['post_type'] = array_map('jvbCheckBase', $registrar->registrar->for);
+
+			$args['tax_query'] = [
+				[
+					'taxonomy'	=> $obj->taxonomy,
+					'terms'		=> $obj->term_id,
+				]
+			];
+		}
+
+		$questions = [];
+		$posts = new WP_Query($args);
+		foreach ($posts->posts as $post) {
+			$meta = Meta::forPost($post->ID);
+			$question = [
+				'id'	=> $current.'#question-'.$post->post_name,
+				'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Question',
+				'name'	=> $meta->get('post_title'),
+				'acceptedAnswer' => [
+					'id'	=> $current.'#answer-'.$post->post_name,
+					'type'	=> 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Answer',
+					'text'	=> $meta->get('post_excerpt'),
+				],
+				'url'	=> get_the_permalink($post->ID),
+			];
+			$questions[] = SchemaHelper::classFromConfig($question);
+		}
+		wp_reset_postdata();
+		$page->setMainEntity($questions);
+
+		return $page->outputSchema();
+
+	}
+	public function outputTimelineArchiveSchema():array
+	{
+		$registrar = Registrar::getInstance($this->slug);
+		global $wp;
+		$current = get_home_url(null, $wp->request).'/';
+		$config = $this->getConfig('archive');
+		$page = array_merge($config, [
+			'id'	=> $current.'#'.$registrar->getSlug(),
+			'type'	=> 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage',
+			'name'	=> array_key_exists('name', $config) ? $config['name'] : $registrar->getPlural(),
+			'description' => array_key_exists('description', $config) ? $config['description'] : $registrar->getDescription(),
+			'url'	=> $current,
+		]);
+		$page = SchemaHelper::classFromConfig($page);
+
+		$parts = [];
+		$timelines = new WP_Query([
+			'post_type'		=> $registrar->getBased(),
+			'posts_per_page'=> 50,
+			'post_status' 	=> 'publish',
+			'post_parent'	=> 0,
+		]);
+		foreach ($timelines->posts as $post) {
+			$item = $this->outputReferenceSchema($post->ID, 'post', false);
+			$item->setId($current.'#'.$post->post_name);
+			$item->setName($post->post_title);
+			$item->setUrl(get_the_permalink($post->ID));
+			$parts[] = $item;
+		}
+
+		$page->setHasPart($parts);
+		return $page->outputSchema();
+	}
+	public function outputGlossarySchema():array
+	{
+		$registrar = Registrar::getInstance($this->slug);
+		global $wp;
+		$current = get_home_url(null, $wp->request).'/';
+		$config = $this->getConfig('archive');
+		$page = [
+			'id'	=> $current.'#'.$registrar->getSlug(),
+			'type'	=> 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage',
+			'name'	=> array_key_exists('name', $config) ? $config['name'] : $registrar->getPlural(),
+			'description' => array_key_exists('description', $config) ? $config['description'] : $registrar->getDescription(),
+			'url'	=> $current,
+		];
+		$page = SchemaHelper::classFromConfig($page);
+
+
+		//Defined Termset
+		$termset = [
+			'type'	=> 'JVBase\managers\SEO\render\Thing\CreativeWork\DefinedTermSet',
+			'id'	=> $current.'#definedtermset',
+			'name'	=> $registrar->getPlural(),
+			'description' => $registrar->getDescription(),
+		];
+		$termset = SchemaHelper::classFromConfig($termset);
+
+		$terms = new WP_Query([
+			'post_type'	=> $registrar->getBased(),
+			'posts_per_page' => -1,
+			'post_status' => 'publish',
+		]);
+
+		$outputTerms = [];
+		foreach ($terms->posts as $post) {
+			$item = $this->outputReferenceSchema($post->ID, 'post', false);
+			$item->setId($current.'#'.$post->post_name);
+			$item->setName($post->post_title);
+			$outputTerms[] = $item;
+		}
+		$termset->setHasDefinedTerm($outputTerms);
+
+		$page->setMainEntity($termset);
+		return $page->outputSchema();
+	}
 }
diff --git a/inc/registrar/fields/Field.php b/inc/registrar/fields/Field.php
index add97be..7b902cb 100644
--- a/inc/registrar/fields/Field.php
+++ b/inc/registrar/fields/Field.php
@@ -19,6 +19,8 @@
 	protected bool $quickEdit = true;	// whether to show in quick edit table
 	protected bool $quill;				// whether to use quill
 	protected int $maxLength;			// of characters
+	protected string $subtype;
+	protected array $allowedSubtype = ['text', 'url','number','tel','email','number'];
 	/**
 	 * @var ?bool For timeline post types. Indicates whether all posts get this field, or just the parent
 	 */
@@ -50,7 +52,7 @@
 
 		foreach ($config as $key => $value) {
 			if (property_exists($class, $key)) {
-				$method = 'set' . ucfirst($key);
+				$method = 'set'.implode('',array_map('ucfirst',explode('_', $key)));;
 				$class->$method($value);
 			} else {
 				error_log('Instance: '.print_r($class, true));
@@ -149,4 +151,16 @@
 			}
 		}, $config);
 	}
+	public function setSubtype(string $subtype):void
+	{
+		if (!in_array($subtype, $this->allowedSubtype)) {
+			error_log('[SelectorField]Attempted subtype not allowed: '.$subtype);
+			return;
+		}
+		$this->subtype = $subtype;
+	}
+	public function getSubtype():string
+	{
+		return $this->subtype;
+	}
 }
diff --git a/inc/registrar/fields/UploadField.php b/inc/registrar/fields/UploadField.php
index dfc2f1b..1042fdc 100644
--- a/inc/registrar/fields/UploadField.php
+++ b/inc/registrar/fields/UploadField.php
@@ -12,26 +12,26 @@
 
 	protected int $maxUploads;
 
-	protected function setMultiple(bool $set):void
+	public function setMultiple(bool $set):void
 	{
 		$this->multiple = $set;
 	}
-	protected function getMultiple():bool
+	public function getMultiple():bool
 	{
 		return $this->multiple;
 	}
 
-	protected function setMaxUploads(int $maxUploads):void
+	public function setMaxUploads(int $maxUploads):void
 	{
 		$max = 20;
 		$this->maxUploads = min($maxUploads, $max);
 	}
-	protected function getMaxUploads():int
+	public function getMaxUploads():int
 	{
 		return $this->maxUploads;
 	}
 
-	protected function setSubtype(string $subtype):void
+	public function setSubtype(string $subtype):void
 	{
 		$allowed = ['document', 'video', 'image', 'all'];
 		if (!in_array($subtype, $allowed)) {
@@ -40,7 +40,7 @@
 		}
 		$this->subtype = $subtype;
 	}
-	protected function getSubtype():string
+	public function getSubtype():string
 	{
 		return $this->subtype;
 	}
diff --git a/inc/registrar/helpers/HideSingle.php b/inc/registrar/helpers/HideSingle.php
new file mode 100644
index 0000000..20ef320
--- /dev/null
+++ b/inc/registrar/helpers/HideSingle.php
@@ -0,0 +1,103 @@
+<?php
+namespace JVBase\inc\registrar\helpers;
+
+use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
+use WP_Post;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class HideSingle {
+	protected string $slug;
+	protected string $postType;
+	protected Registrar $registrar;
+	public function __construct(string $slug, Registrar $registrar) {
+		$this->slug = $slug;
+		$this->postType = jvbCheckBase($slug);
+		$this->registrar = $registrar;
+
+		if ($this->registrar->hasFeature('hide_single')) {
+			add_filter('is_post_type_viewable', [$this, 'hideFromPublic']);
+			if ($this->registrar->hasFeature('redirect_to_author')) {
+				add_filter('post_type_link', [$this, 'redirectSingleToAuthor'], 15, 2);
+				add_action('template_redirect', [$this, 'actuallyRedirectToAuthor']);
+			} else {
+				add_filter('post_type_link', [$this, 'redirectSingleToArchive'], 15, 2);
+				add_action('template_redirect', [$this, 'actuallyRedirectToArchive']);
+			}
+		}
+	}
+
+	/**
+	 * Use $this->hide_single
+	 * @param bool $is_viewable
+	 * @return bool
+	 */
+	public function hideFromPublic(bool $is_viewable):bool
+	{
+		if (!is_admin() && is_singular($this->postType)) {
+			return false;
+		}
+		return $is_viewable;
+	}
+
+	/**
+	 * Use $this->redirect_to_author for this method
+	 * @param string $url
+	 * @param \WP_Post $post
+	 * @return string
+	 */
+	public function redirectSingleToAuthor(string $url, \WP_Post $post): string
+	{
+		if ($post->post_type !== $this->postType) {
+			return $url;
+		}
+
+		// Redirect to author page or archive
+		$user_link = jvbUserProfileLink($post->post_author);
+		if ($user_link) {
+			$query_var = str_replace(BASE, '', $post->post_type);
+			return add_query_arg($query_var, $post->ID, get_permalink($user_link));
+		}
+
+		return get_post_type_archive_link($post->post_type);
+	}
+	public function actuallyRedirectToAuthor():void
+	{
+		if (is_singular($this->postType)) {
+			global $post;
+			$url = $this->redirectSingleToAuthor('', $post);
+			$url = add_query_arg($this->slug, $post->ID, $url);
+			wp_redirect($url, 301);
+			exit;
+		}
+	}
+
+	/**
+	 * Use $this->hide_single
+	 * @param string $url
+	 * @param \WP_Post $post
+	 * @return string
+	 */
+	public function redirectSingleToArchive(string $url, \WP_Post $post): string
+	{
+		if ($post->post_type !== $this->postType) {
+			return $url;
+		}
+
+		return get_post_type_archive_link($post->post_type).'#'.$post->post_name;
+	}
+
+
+	public function actuallyRedirectToArchive():void
+	{
+		if (is_singular($this->postType)) {
+			global $post;
+			$url = get_post_type_archive_link($this->postType).'#'.$post->post_name;
+			wp_redirect($url, 301);
+			exit;
+		}
+	}
+}
diff --git a/inc/registrar/helpers/MakeTimelineType.php b/inc/registrar/helpers/MakeTimelineType.php
new file mode 100644
index 0000000..84ea959
--- /dev/null
+++ b/inc/registrar/helpers/MakeTimelineType.php
@@ -0,0 +1,47 @@
+<?php
+namespace JVBase\registrar\helpers;
+
+use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
+use WP_Post;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class MakeTimelineType {
+	protected string $slug;
+	protected string $postType;
+	protected Registrar $registrar;
+	public function __construct(string $slug, Registrar $registrar) {
+		$this->slug = $slug;
+		$this->postType = jvbCheckBase($slug);
+		$this->registrar = $registrar;
+
+		add_action('template_redirect', [$this, 'redirectChildToParent']);
+	}
+
+	/**
+	 * Set $this->is_timeline
+	 * @return void
+	 */
+	public function redirectChildToParent(): void
+	{
+		if (!is_singular($this->postType)) {
+			return;
+		}
+
+		global $post;
+
+		// If this post has a parent, redirect to parent
+		if ($post->post_parent) {
+			$parent_url = get_permalink($post->post_parent);
+
+			// Add anchor or query param to indicate which child was accessed
+			$redirect_url = add_query_arg('update', $post->ID, $parent_url);
+
+			wp_redirect($redirect_url, 301);
+			exit;
+		}
+	}
+}
diff --git a/inc/registrar/helpers/MakeTrackChanges.php b/inc/registrar/helpers/MakeTrackChanges.php
index 4019f1f..3237e52 100644
--- a/inc/registrar/helpers/MakeTrackChanges.php
+++ b/inc/registrar/helpers/MakeTrackChanges.php
@@ -1,10 +1,131 @@
 <?php
 namespace JVBase\registrar\helpers;
 
+use JVBase\managers\CustomTable;
+
 if (!defined('ABSPATH')) {
 	exit;
 }
 
 class MakeTrackChanges {
+	protected string $slug;
+	protected string $based;
+	protected CustomTable $table;
+	public function __construct(string $slug) {
+		$this->slug = $slug;
+		$this->based = jvbCheckBase($slug);
+		$this->defineTables();
 
+		add_action('set_object_terms', [$this, 'trackHistory'], 10, 6);
+	}
+	public function defineTables():void
+	{
+		$table = CustomTable::for('history_'.$this->slug);
+
+		$table->setColumns([
+			'id'			=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+			'user_id'		=> "{$table->getUserIDType()} NOT NULL",
+			'profile_id'	=> "{$table->getPostIDType()} NOT NULL",
+			'role'			=> 'varchar(255) NOT NULL',
+			'term_id'		=> "{$table->getTermIDType()} NOT NULL",
+			'start_date'	=> 'date DEFAULT NULL',
+			'end_date'		=> 'date DEFAULT NULL',
+			'locked'		=> 'tinyint(1) NOT NULL DEFAULT 0',
+			'created_at'	=> 'timestamp DEFAULT CURRENT_TIMESTAMP',
+			'updated_at'	=> 'timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
+		]);
+
+		$table->setKeys([
+			['key' => 'PRIMARY', 'value' => '(`id`)'],
+			['key' => 'UNIQUE', 'value' => '(`profile_id`, `term_id`, `start_date`)'],
+			'content_role (`term_id`, `role`)',
+			'user_id (`user_id`)',
+			'profile_id (`profile_id`)',
+			'term_id (`term_id`)',
+		]);
+
+		$base = BASE;
+		$table->setConstraints([
+			"CONSTRAINT `{$base}{$this->slug}_history_user` FOREIGN KEY (`user_id`)
+			REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE",
+			"CONSTRAINT `{$base}{$this->slug}_history_profile` FOREIGN KEY (`profile_id`)
+			REFERENCES `{$table->getPostTable()}` (`ID`) ON DELETE CASCADE"
+		]);
+
+		$table->defineTable();
+		$this->table = $table;
+	}
+
+	public function trackHistory(int $object_id, array $terms, array $tt_ids, string $taxonomy, bool $append, array $old_tt_ids):void
+	{
+		if ($taxonomy !== $this->based) {
+			return;
+		}
+
+		$user = get_post_meta($object_id, BASE.'link', true);
+		if (empty($author)) {
+			$user = get_post($object_id)->post_author??false;
+			if (!$user) {
+				return;
+			}
+		}
+		$userObj = get_userdata($user);
+		if (!$userObj) {return;}
+
+		$role = jvbUserRole($user);
+
+		$new = array_diff($tt_ids, $old_tt_ids);
+		$old = array_diff($old_tt_ids, $new);
+
+		$this->table->transaction(
+			function() use ($new, $old, $role, $user, $object_id) {
+				foreach ($new as $newTerm) {
+					$termID = $this->getTermIDFromTTID($newTerm);
+
+					$this->table->findOrCreate([
+						'user_id'	=> $user,
+						'profile_id'=> $object_id,
+						'role'		=> $role,
+						'term_id'	=> $termID,
+						'locked'	=> 0
+					], [
+						'start_date'	=> date('Y-m-d')
+					]);
+				}
+
+				foreach ($old as $oldTerm) {
+					$termID = $this->getTermIDFromTTID($oldTerm);
+
+					$this->table->update(
+						[
+							'end_date'	=> date('Y-m-d'),
+							'locked'	=> 1
+						],
+						[
+							'user_id'	=> $user,
+							'profile_id'=> $object_id,
+							'term_id'	=> $termID,
+							'locked'	=> 0
+						]);
+				}
+			}
+		);
+
+
+	}
+
+	/**
+	 * Helper function to get term_id from term_taxonomy_id
+	 * @param int $tt_id
+	 *
+	 * @return int
+	 */
+	private function getTermIDFromTTID(int $tt_id):int
+	{
+		global $wpdb;
+		return $wpdb->get_var($wpdb->prepare(
+			"SELECT term_id FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id = %d",
+			$tt_id
+		));
+	}
 }
diff --git a/inc/registrar/helpers/MakeVerification.php b/inc/registrar/helpers/MakeVerification.php
index edf6d0e..6574bdb 100644
--- a/inc/registrar/helpers/MakeVerification.php
+++ b/inc/registrar/helpers/MakeVerification.php
@@ -5,6 +5,25 @@
 	exit;
 }
 
+use JVBase\registrar\Registrar;
+use JVBase\managers\CustomTable;
 class MakeVerification {
+	protected CustomTable $table;
+	public function __construct()
+	{
+		$this->defineTables();
+	}
 
+	protected function defineTables():void
+	{
+		$types = implode(',', array_map(function($item) { return "`{$item}`"; },Registrar::getFeatured('verify_entry')));
+
+		$table = CustomTable::for('verify_entry');
+
+		$table->setColumns([
+			'id'			=> 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
+			'user_id'		=> "{$table->getUserIDType()} NOT NULL",
+			'content'
+		])
+	}
 }
diff --git a/inc/registry/CheckCustomTables.php b/inc/registry/CheckCustomTables.php
index eaf4dbc..b04af0b 100644
--- a/inc/registry/CheckCustomTables.php
+++ b/inc/registry/CheckCustomTables.php
@@ -440,7 +440,7 @@
     /******************************************************
      * Table Definitions
      *****************************************************/
-
+// MOVED TO Queue.php
 //    protected function queueTables():array
 //    {
 //
@@ -514,30 +514,31 @@
 //        ];
 //    }
 
-	protected function errorLogTables():array
-	{
-		return [
-			'error_log'=> "(
-				`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-				`error_type` varchar(50) NOT NULL,
-				`component` varchar(100) NOT NULL,
-				`method` varchar(100) DEFAULT NULL,
-				`page_url` varchar(255) DEFAULT NULL,
-				`message` text NOT NULL,
-				`context` JSON,
-				`severity` varchar(20) NOT NULL,
-				`user_id` {$this->userIDType} DEFAULT NULL,
-				`user_was_logged_in` tinyint(1) NOT NULL,
-				`source` enum('frontend','backend') NOT NULL,
-				`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
-				PRIMARY KEY (`id`),
-				KEY `created_at` (`created_at`),
-				KEY `component_severity_date` (`component`, `severity`, `created_at`),
-				KEY `error_type_date` (`error_type`, `created_at`),
-				KEY `severity_date` (`severity`, `created_at`)
-			)"
-		];
-	}
+//MOVED TO ErrorHandler.php
+//	protected function errorLogTables():array
+//	{
+//		return [
+//			'error_log'=> "(
+//				`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//				`error_type` varchar(50) NOT NULL,
+//				`component` varchar(100) NOT NULL,
+//				`method` varchar(100) DEFAULT NULL,
+//				`page_url` varchar(255) DEFAULT NULL,
+//				`message` text NOT NULL,
+//				`context` JSON,
+//				`severity` varchar(20) NOT NULL,
+//				`user_id` {$this->userIDType} DEFAULT NULL,
+//				`user_was_logged_in` tinyint(1) NOT NULL,
+//				`source` enum('frontend','backend') NOT NULL,
+//				`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
+//				PRIMARY KEY (`id`),
+//				KEY `created_at` (`created_at`),
+//				KEY `component_severity_date` (`component`, `severity`, `created_at`),
+//				KEY `error_type_date` (`error_type`, `created_at`),
+//				KEY `severity_date` (`severity`, `created_at`)
+//			)"
+//		];
+//	}
 
 	protected function userIntegrationsTable():array
 	{
@@ -559,445 +560,449 @@
 			)"
 		];
 	}
+//MOVED TO NotificationManger.php
+//    protected function notificationTables():array
+//    {
+//
+//        return [
+//            // Main notifications table
+//            'notifications' => "(
+//            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//            `owner_id` {$this->userIDType} NOT NULL,
+//            `action_user_id` {$this->userIDType} NOT NULL,
+//            `target_id` bigint(20) DEFAULT NULL,
+//            `target_type` varchar(30) DEFAULT NULL,
+//            `type` enum('new_favourite','new_artist','artist_approved','artist_invitation',
+//                  'new_term','term_approved','term_rejected','list_shared',
+//                  'system_message','shop_invitation') NOT NULL,
+//            `status` enum('unread','read','actioned','dismissed') NOT NULL DEFAULT 'unread',
+//            `priority` enum('low','normal','high') NOT NULL DEFAULT 'normal',
+//            `message` varchar(255) DEFAULT NULL,
+//            `context` JSON DEFAULT NULL,
+//            `requires_action` tinyint(1) NOT NULL DEFAULT 0,
+//            `action_taken` tinyint(1) NOT NULL DEFAULT 0,
+//            `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+//            `read_at` datetime DEFAULT NULL,
+//            `actioned_at` datetime DEFAULT NULL,
+//            `emailed_at` datetime DEFAULT NULL,
+//            `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+//            PRIMARY KEY (`id`),
+//            KEY `user_status` (`owner_id`, `status`),
+//            KEY `target_lookup` (`target_id`, `target_type`),
+//            KEY `unread_notifications` (`owner_id`, `status`, `created_at`),
+//            KEY `requires_action` (`owner_id`, `requires_action`, `action_taken`),
+//            KEY `acting_user_lookup` (`owner_id`, `action_user_id`, `type`, `status`, `created_at`),
+//            CONSTRAINT `{$this->base}notify_owner` FOREIGN KEY (`owner_id`)
+//            REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
+//            CONSTRAINT `{$this->base}action_id` FOREIGN KEY (`action_user_id`)
+//            REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
+//        )",
+//
+//
+//            'notifications_content' => "(
+//            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//            `user_id` {$this->userIDType} NOT NULL,
+//            `date` date NOT NULL,
+//            `frequency` enum('daily','weekly','monthly') NOT NULL,
+//            `tattoo_count` int unsigned NOT NULL DEFAULT 0,
+//            `artwork_count` int unsigned NOT NULL DEFAULT 0,
+//            `piercing_count` int unsigned NOT NULL DEFAULT 0,
+//            `event_count` int unsigned NOT NULL DEFAULT 0,
+//            `news_count` int unsigned NOT NULL DEFAULT 0,
+//            `offer_count` int unsigned NOT NULL DEFAULT 0,
+//            `total_items` int unsigned NOT NULL DEFAULT 0,
+//            `has_profile_update` tinyint(1) NOT NULL DEFAULT 0,
+//            `new_items` JSON DEFAULT NULL,
+//            `updated_items` JSON DEFAULT NULL,
+//            `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+//            `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+//            PRIMARY KEY (`id`),
+//            UNIQUE KEY `artist_date_frequency` (`user_id`, `date`, `frequency`),
+//            KEY `recent_content` (`date`, `frequency`),
+//            KEY `artist_frequency` (`user_id`, `frequency`),
+//            CONSTRAINT `{$this->base}content_artist` FOREIGN KEY (`user_id`)
+//            REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
+//        )",
+//
+//            'notifications_user_seen' => "(
+//            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//            `user_id` {$this->userIDType} NOT NULL,
+//            `content_notification_id` bigint(20) unsigned NOT NULL,
+//            `status` enum('unread','read','dismissed') NOT NULL DEFAULT 'unread',
+//            `read_at` datetime DEFAULT NULL,
+//            `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+//            PRIMARY KEY (`id`),
+//            UNIQUE KEY `user_content_notif` (`user_id`, `content_notification_id`),
+//            KEY `user_status` (`user_id`, `status`),
+//            CONSTRAINT `{$this->base}user_content_user` FOREIGN KEY (`user_id`)
+//            REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
+//            CONSTRAINT `{$this->base}user_content_notification` FOREIGN KEY (`content_notification_id`)
+//            REFERENCES `{$this->prefixed}notifications_content` (`id`) ON DELETE CASCADE
+//        )",
+//
+//            // User notification preferences
+//            'notification_preferences' => "(
+//            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//            `user_id` {$this->userIDType} NOT NULL,
+//            `item_id` bigint(20) NOT NULL,
+//            `notification_type` varchar(50) NOT NULL,
+//            `frequency` enum('never','daily','weekly','monthly') DEFAULT 'never',
+//            `last_sent` datetime DEFAULT NULL,
+//            `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+//            `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+//            PRIMARY KEY (`id`),
+//            UNIQUE KEY `user_type` (`user_id`, `item_id`),
+//            KEY `user_frequency` (`user_id`, `frequency`),
+//            KEY `frequency_lookup` (`frequency`, `last_sent`),
+//            CONSTRAINT `{$this->base}notification_pref_user` FOREIGN KEY (`user_id`)
+//                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
+//        )",
+//
+//            // Notification digest scheduling and tracking
+//            'notification_digests' => "(
+//            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//            `user_id` {$this->userIDType} NOT NULL,
+//            `frequency` enum('daily','weekly','monthly') NOT NULL,
+//            `scheduled_at` datetime NOT NULL,
+//            `sent_at` datetime DEFAULT NULL,
+//            `status` enum('pending','processing','sent','failed') DEFAULT 'pending',
+//            `notification_count` int unsigned DEFAULT 0,
+//            `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+//            PRIMARY KEY (`id`),
+//            KEY `scheduled_digests` (`frequency`, `scheduled_at`, `status`),
+//            KEY `user_digests` (`user_id`, `frequency`),
+//            CONSTRAINT `{$this->base}digest_user` FOREIGN KEY (`user_id`)
+//                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
+//        )",
+//
+//            // Analytics on notification interactions
+//            'stats__notifications' => "(
+//            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//            `notification_id` bigint(20) unsigned NOT NULL,
+//            `user_id` {$this->userIDType} NOT NULL,
+//            `action` varchar(30) NOT NULL,
+//            `action_source` enum('web','email','app') DEFAULT 'web',
+//            `action_details` JSON DEFAULT NULL,
+//            `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+//            PRIMARY KEY (`id`),
+//            KEY `notification_lookup` (`notification_id`),
+//            KEY `user_actions` (`user_id`, `action`),
+//            KEY `action_analysis` (`action`, `action_source`),
+//            CONSTRAINT `{$this->base}metrics_user` FOREIGN KEY (`user_id`)
+//                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
+//            CONSTRAINT `{$this->base}metrics_notification` FOREIGN KEY (`notification_id`)
+//                REFERENCES `{$this->prefixed}notifications` (`id`) ON DELETE CASCADE
+//        )"
+//        ];
+//    }
 
-    protected function notificationTables():array
-    {
-
-        return [
-            // Main notifications table
-            'notifications' => "(
-            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-            `owner_id` {$this->userIDType} NOT NULL,
-            `action_user_id` {$this->userIDType} NOT NULL,
-            `target_id` bigint(20) DEFAULT NULL,
-            `target_type` varchar(30) DEFAULT NULL,
-            `type` enum('new_favourite','new_artist','artist_approved','artist_invitation',
-                  'new_term','term_approved','term_rejected','list_shared',
-                  'system_message','shop_invitation') NOT NULL,
-            `status` enum('unread','read','actioned','dismissed') NOT NULL DEFAULT 'unread',
-            `priority` enum('low','normal','high') NOT NULL DEFAULT 'normal',
-            `message` varchar(255) DEFAULT NULL,
-            `context` JSON DEFAULT NULL,
-            `requires_action` tinyint(1) NOT NULL DEFAULT 0,
-            `action_taken` tinyint(1) NOT NULL DEFAULT 0,
-            `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-            `read_at` datetime DEFAULT NULL,
-            `actioned_at` datetime DEFAULT NULL,
-            `emailed_at` datetime DEFAULT NULL,
-            `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-            PRIMARY KEY (`id`),
-            KEY `user_status` (`owner_id`, `status`),
-            KEY `target_lookup` (`target_id`, `target_type`),
-            KEY `unread_notifications` (`owner_id`, `status`, `created_at`),
-            KEY `requires_action` (`owner_id`, `requires_action`, `action_taken`),
-            KEY `acting_user_lookup` (`owner_id`, `action_user_id`, `type`, `status`, `created_at`),
-            CONSTRAINT `{$this->base}notify_owner` FOREIGN KEY (`owner_id`)
-            REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
-            CONSTRAINT `{$this->base}action_id` FOREIGN KEY (`action_user_id`)
-            REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
-        )",
+//MOVED TO ApprovalManager.php
+//    protected function approvalTables($types):array
+//    {
+//        $tables = [];
+//        $save = [];
+//
+//        foreach ($types as $type => $config) {
+//            $save[$type] = ($type === 'term') ? $config : 'user';
+//            $tables['approval_'.$type.'_requests'] = "(
+//                `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//                `user_id` {$this->userIDType} NOT NULL,
+//                `status` enum('pending','approved','rejected','appealed','expired') DEFAULT 'pending',
+//                `required_approvals` int unsigned DEFAULT 3,
+//                `current_approvals` int unsigned DEFAULT 0,
+//                `current_rejections` int unsigned DEFAULT 0,
+//                `expires_at` datetime DEFAULT NULL,
+//                `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+//                `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+//                `approved_by` json DEFAULT NULL,
+//                `rejected_by` json DEFAULT NULL,
+//                `created_item` json DEFAULT NULL,
+//                PRIMARY KEY (`id`),
+//                KEY `status` (`status`),
+//                KEY `expiring_requests` (`status`, `expires_at`),
+//                CONSTRAINT `{$this->base}{$type}_approval_requester` FOREIGN KEY (`user_id`)
+//                    REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
+//            )";
+//            $tables['approval_'.$type.'_votes'] = "(
+//                `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//                `request_id` bigint(20) unsigned NOT NULL,
+//                `user_id` {$this->userIDType} NOT NULL,
+//                `vote` enum('approve','reject','dismissed') NOT NULL,
+//                `notes` text DEFAULT NULL,
+//                `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+//                PRIMARY KEY (`id`),
+//                UNIQUE KEY `unique_vote` (`request_id`, `user_id`),
+//                KEY `user_votes` (`user_id`),
+//                CONSTRAINT `{$this->base}{$type}_user_approval_request` FOREIGN KEY (`request_id`)
+//                    REFERENCES `{$this->prefixed}approval_{$type}_requests` (`id`) ON DELETE CASCADE,
+//                CONSTRAINT `{$this->base}{$type}_user_approval_voter` FOREIGN KEY (`user_id`)
+//                    REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
+//            )";
+//        }
+//        if (!empty($save)) {
+//            update_option(BASE.'approvals_types', $save);
+//        }
+//        return $tables;
+//    }
 
 
-            'notifications_content' => "(
-            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-            `user_id` {$this->userIDType} NOT NULL,
-            `date` date NOT NULL,
-            `frequency` enum('daily','weekly','monthly') NOT NULL,
-            `tattoo_count` int unsigned NOT NULL DEFAULT 0,
-            `artwork_count` int unsigned NOT NULL DEFAULT 0,
-            `piercing_count` int unsigned NOT NULL DEFAULT 0,
-            `event_count` int unsigned NOT NULL DEFAULT 0,
-            `news_count` int unsigned NOT NULL DEFAULT 0,
-            `offer_count` int unsigned NOT NULL DEFAULT 0,
-            `total_items` int unsigned NOT NULL DEFAULT 0,
-            `has_profile_update` tinyint(1) NOT NULL DEFAULT 0,
-            `new_items` JSON DEFAULT NULL,
-            `updated_items` JSON DEFAULT NULL,
-            `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-            `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-            PRIMARY KEY (`id`),
-            UNIQUE KEY `artist_date_frequency` (`user_id`, `date`, `frequency`),
-            KEY `recent_content` (`date`, `frequency`),
-            KEY `artist_frequency` (`user_id`, `frequency`),
-            CONSTRAINT `{$this->base}content_artist` FOREIGN KEY (`user_id`)
-            REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
-        )",
+//	protected function taxonomyRelationshipsTables():array
+//	{
+//		$tables = [
+//			'taxonomy_relationships' => "(
+//		`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//		`term_id` {$this->termIDType} NOT NULL,
+//		`related_term_id` {$this->termIDType} NOT NULL,
+//		`taxonomy` varchar(32) NOT NULL,
+//		`related_taxonomy` varchar(32) NOT NULL,
+//		`post_count` int(11) NOT NULL DEFAULT 0,
+//		`is_direct` tinyint(1) NOT NULL DEFAULT 1,
+//		`is_hierarchical` tinyint(1) NOT NULL DEFAULT 0,
+//		`last_updated` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+//		PRIMARY KEY (`id`),
+//		KEY `term_id` (`term_id`),
+//		KEY `related_term_id` (`related_term_id`),
+//		KEY `taxonomy` (`taxonomy`),
+//		KEY `related_taxonomy` (`related_taxonomy`),
+//		UNIQUE KEY `term_relation` (`term_id`, `related_term_id`, `taxonomy`, `related_taxonomy`),
+//		CONSTRAINT `{$this->base}tax_rel_term_id` FOREIGN KEY (`term_id`)
+//			REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE,
+//		CONSTRAINT `{$this->base}tax_rel_related_id` FOREIGN KEY (`related_term_id`)
+//			REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE
+//	)"
+//		];
+//
+////		if ((array_key_exists('dashboard', $this->JVB_SITE) && $this->JVB_SITE['dashboard'] === true) || array_key_exists('use_feed_block', $this->JVB_SITE) && $this->JVB_SITE['use_feed_block']) {
+//			$tables['user_term_index'] = "(
+//		`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//		`user_id` {$this->userIDType} NOT NULL,
+//		`term_id` {$this->termIDType} NOT NULL,
+//		`taxonomy` varchar(32) NOT NULL,
+//		`post_count` int(11) NOT NULL DEFAULT 1,
+//		`is_parent` tinyint(1) NOT NULL DEFAULT 0,
+//		`last_used` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+//		PRIMARY KEY (`id`),
+//		UNIQUE KEY `user_term` (`user_id`, `term_id`, `taxonomy`),
+//		KEY `user_taxonomy` (`user_id`, `taxonomy`),
+//		KEY `taxonomy` (`taxonomy`),
+//		CONSTRAINT `{$this->base}user_term_user_fk` FOREIGN KEY (`user_id`)
+//			REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
+//		CONSTRAINT `{$this->base}user_term_term_fk` FOREIGN KEY (`term_id`)
+//			REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE
+//	)";
+////		}
+//
+//		return $tables;
+//	}
+//MOVED TO FavouritesManager.php
+//    protected function favouriteTables():array
+//    {
+//
+//        return [
+//            'favourites'    => "(
+//            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//            `user_id` {$this->userIDType} NOT NULL,
+//            `type` varchar(50) NOT NULL,
+//            `target_id` bigint(20) NOT NULL,
+//            `notes` text DEFAULT NULL,
+//            `date_added` datetime DEFAULT CURRENT_TIMESTAMP,
+//            PRIMARY KEY (`id`),
+//            UNIQUE KEY `unique_favourite` (`user_id`, `type`, `target_id`),
+//            KEY `user_type` (`user_id`, `type`),
+//            KEY `target_type` (`target_id`, `type`),
+//            CONSTRAINT `{$this->base}favourites_user` FOREIGN KEY (`user_id`)
+//                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
+//        )",
+//            'favourites_lists'  => "(
+//            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//            `user_id` {$this->userIDType} NOT NULL,
+//            `name` varchar(255) NOT NULL,
+//            `description` text,
+//            `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+//            `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+//            PRIMARY KEY (`id`),
+//            KEY `user_lists` (`user_id`),
+//            CONSTRAINT `{$this->base}list_user` FOREIGN KEY (`user_id`)
+//                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
+//        )",
+//            'favourites_list_items' => "(
+//            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//            `list_id` bigint(20) unsigned NOT NULL,
+//            `item_type` varchar(50) NOT NULL,
+//            `item_id` bigint(20) NOT NULL,
+//            `favourite_id` bigint(20) unsigned DEFAULT NULL,
+//            `added_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+//            PRIMARY KEY (`id`),
+//            UNIQUE KEY `unique_list_item` (`list_id`, `item_type`, `item_id`),
+//            KEY `list_items` (`list_id`),
+//            KEY `favourite_id` (`favourite_id`),
+//            CONSTRAINT `{$this->base}list_items` FOREIGN KEY (`list_id`)
+//                REFERENCES 	`{$this->prefixed}favourites_lists` (`id`) ON DELETE CASCADE,
+//            CONSTRAINT `{$this->base}list_favourite` FOREIGN KEY (`favourite_id`)
+//                REFERENCES `{$this->prefixed}favourites` (`id`) ON DELETE SET NULL
+//        )",
+//            'favourites_list_shares' => "(
+//            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//            `list_id` bigint(20) unsigned NOT NULL,
+//            `user_id` {$this->userIDType} NULL,
+//            `email` varchar(255) NOT NULL,
+//            `permission_type` enum('view', 'edit') NOT NULL DEFAULT 'view',
+//            `status` enum('pending', 'accepted', 'rejected', 'revoked') NOT NULL DEFAULT 'pending',
+//            `invitation_token` varchar(64) NULL,
+//            `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+//            `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+//            PRIMARY KEY (`id`),
+//            UNIQUE KEY `unique_share_user` (`list_id`, `user_id`, `status`),
+//            UNIQUE KEY `unique_share_email` (`list_id`, `email`, `status`),
+//            KEY `list_shares` (`list_id`),
+//            KEY `status_index` (`status`),
+//            CONSTRAINT `{$this->base}share_list` FOREIGN KEY (`list_id`)
+//                REFERENCES `{$this->prefixed}favourites_lists` (`id`) ON DELETE CASCADE,
+//            CONSTRAINT `{$this->base}share_user` FOREIGN KEY (`user_id`)
+//                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
+//        )",
+//            'favourites_list_stats'    => "(
+//            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//            `item_type` varchar(50) NOT NULL,
+//            `item_id` bigint(20) NOT NULL,
+//            `list_count` int NOT NULL DEFAULT 0,
+//            `last_added` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+//            PRIMARY KEY (`id`),
+//            UNIQUE KEY `unique_item_stat` (`item_type`, `item_id`),
+//            KEY `item_stats` (`item_type`, `item_id`)
+//        )",
+//        ];
+//    }
 
-            'notifications_user_seen' => "(
-            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-            `user_id` {$this->userIDType} NOT NULL,
-            `content_notification_id` bigint(20) unsigned NOT NULL,
-            `status` enum('unread','read','dismissed') NOT NULL DEFAULT 'unread',
-            `read_at` datetime DEFAULT NULL,
-            `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-            PRIMARY KEY (`id`),
-            UNIQUE KEY `user_content_notif` (`user_id`, `content_notification_id`),
-            KEY `user_status` (`user_id`, `status`),
-            CONSTRAINT `{$this->base}user_content_user` FOREIGN KEY (`user_id`)
-            REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
-            CONSTRAINT `{$this->base}user_content_notification` FOREIGN KEY (`content_notification_id`)
-            REFERENCES `{$this->prefixed}notifications_content` (`id`) ON DELETE CASCADE
-        )",
+//MOVED TO ForumManager.php
+//    protected function newsRelationshipsTable():array
+//    {
+//        return [
+//            'news_relationships'    => "(
+//                `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//                `shop_id` {$this->termIDType} NOT NULL,
+//                `user_id` {$this->userIDType} NOT NULL,
+//                `artist_id` {$this->postIDType} NOT NULL,
+//                `news_count` int(10) unsigned NOT NULL DEFAULT 0,
+//                `last_post_date` datetime DEFAULT NULL,
+//                `last_updated` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+//                PRIMARY KEY (`id`),
+//                UNIQUE KEY `shop_user` (`shop_id`, `user_id`),
+//                KEY `shop_id` (`shop_id`),
+//                KEY `user_id` (`user_id`),
+//                KEY `artist_id` (`artist_id`),
+//            CONSTRAINT `{$this->base}nr_shop_news_shop` FOREIGN KEY (`shop_id`)
+//                REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE,
+//            CONSTRAINT `{$this->base}nr_shop_news_user` FOREIGN KEY (`user_id`)
+//                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
+//            CONSTRAINT `{$this->base}nr_shop_news_artist` FOREIGN KEY (`artist_id`)
+//                REFERENCES `{$this->wpdb->posts}` (`ID`) ON DELETE SET NULL
+//        )"
+//        ];
+//    }
 
-            // User notification preferences
-            'notification_preferences' => "(
-            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-            `user_id` {$this->userIDType} NOT NULL,
-            `item_id` bigint(20) NOT NULL,
-            `notification_type` varchar(50) NOT NULL,
-            `frequency` enum('never','daily','weekly','monthly') DEFAULT 'never',
-            `last_sent` datetime DEFAULT NULL,
-            `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
-            `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-            PRIMARY KEY (`id`),
-            UNIQUE KEY `user_type` (`user_id`, `item_id`),
-            KEY `user_frequency` (`user_id`, `frequency`),
-            KEY `frequency_lookup` (`frequency`, `last_sent`),
-            CONSTRAINT `{$this->base}notification_pref_user` FOREIGN KEY (`user_id`)
-                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
-        )",
+//MOVED TO ResponseManager.php
+//    protected function responseTable():array
+//    {
+//        return [
+//            'responses' => "(
+//            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//            `item_id` {$this->postIDType} NOT NULL,
+//            `content` text NOT NULL,
+//            `user_id` {$this->userIDType} NOT NULL,
+//            `parent_id` bigint(20) unsigned DEFAULT NULL,
+//            `response` text NOT NULL,
+//            `status` enum('published','hidden','flagged','deleted') DEFAULT 'published',
+//            `is_user_deleted` tinyint(1) DEFAULT 0,
+//            `upvotes` int NOT NULL DEFAULT 0,
+//            `downvotes` int NOT NULL DEFAULT 0,
+//            `karma` int NOT NULL DEFAULT 0,
+//            `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+//            `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+//            PRIMARY KEY (`id`),
+//            KEY `item_lookup` (`item_id`, `status`),
+//            KEY `user_comments` (`user_id`),
+//            KEY `parent_child` (`parent_id`),
+//            KEY `karma_order` (`karma`),
+//            CONSTRAINT `{$this->base}re_responses_news` FOREIGN KEY (`item_id`)
+//                REFERENCES `{$this->wpdb->posts}` (`ID`) ON DELETE CASCADE,
+//            CONSTRAINT `{$this->base}re_responses_parent` FOREIGN KEY (`parent_id`)
+//                REFERENCES `{$this->prefixed}responses` (`id`) ON DELETE SET NULL
+//        )",
+//            'karma_response' => "(
+//                `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//                `item_id` bigint(20) NOT NULL,
+//                `user_id` {$this->userIDType} NOT NULL,
+//                `vote` enum('up','down') NOT NULL,
+//                `date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+//                PRIMARY KEY (`id`),
+//                UNIQUE KEY `user_post` (`user_id`,`item_id`),
+//                KEY `item_id` (`item_id`),
+//                KEY `user_id` (`user_id`),
+//            CONSTRAINT `{$this->base}_response_item_id` FOREIGN KEY (`item_id`)
+//                REFERENCES `{$this->prefixed}responses` (`id`) ON DELETE CASCADE,
+//            CONSTRAINT `{$this->base}_response_user_id` FOREIGN KEY (`user_id`)
+//                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
+//                )"
+//        ];
+//    }
 
-            // Notification digest scheduling and tracking
-            'notification_digests' => "(
-            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-            `user_id` {$this->userIDType} NOT NULL,
-            `frequency` enum('daily','weekly','monthly') NOT NULL,
-            `scheduled_at` datetime NOT NULL,
-            `sent_at` datetime DEFAULT NULL,
-            `status` enum('pending','processing','sent','failed') DEFAULT 'pending',
-            `notification_count` int unsigned DEFAULT 0,
-            `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
-            PRIMARY KEY (`id`),
-            KEY `scheduled_digests` (`frequency`, `scheduled_at`, `status`),
-            KEY `user_digests` (`user_id`, `frequency`),
-            CONSTRAINT `{$this->base}digest_user` FOREIGN KEY (`user_id`)
-                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
-        )",
-
-            // Analytics on notification interactions
-            'stats__notifications' => "(
-            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-            `notification_id` bigint(20) unsigned NOT NULL,
-            `user_id` {$this->userIDType} NOT NULL,
-            `action` varchar(30) NOT NULL,
-            `action_source` enum('web','email','app') DEFAULT 'web',
-            `action_details` JSON DEFAULT NULL,
-            `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
-            PRIMARY KEY (`id`),
-            KEY `notification_lookup` (`notification_id`),
-            KEY `user_actions` (`user_id`, `action`),
-            KEY `action_analysis` (`action`, `action_source`),
-            CONSTRAINT `{$this->base}metrics_user` FOREIGN KEY (`user_id`)
-                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
-            CONSTRAINT `{$this->base}metrics_notification` FOREIGN KEY (`notification_id`)
-                REFERENCES `{$this->prefixed}notifications` (`id`) ON DELETE CASCADE
-        )"
-        ];
-    }
-
-    protected function approvalTables($types):array
-    {
-        $tables = [];
-        $save = [];
-
-        foreach ($types as $type => $config) {
-            $save[$type] = ($type === 'term') ? $config : 'user';
-            $tables['approval_'.$type.'_requests'] = "(
-                `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-                `user_id` {$this->userIDType} NOT NULL,
-                `status` enum('pending','approved','rejected','appealed','expired') DEFAULT 'pending',
-                `required_approvals` int unsigned DEFAULT 3,
-                `current_approvals` int unsigned DEFAULT 0,
-                `current_rejections` int unsigned DEFAULT 0,
-                `expires_at` datetime DEFAULT NULL,
-                `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
-                `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-                `approved_by` json DEFAULT NULL,
-                `rejected_by` json DEFAULT NULL,
-                `created_item` json DEFAULT NULL,
-                PRIMARY KEY (`id`),
-                KEY `status` (`status`),
-                KEY `expiring_requests` (`status`, `expires_at`),
-                CONSTRAINT `{$this->base}{$type}_approval_requester` FOREIGN KEY (`user_id`)
-                    REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
-            )";
-            $tables['approval_'.$type.'_votes'] = "(
-                `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-                `request_id` bigint(20) unsigned NOT NULL,
-                `user_id` {$this->userIDType} NOT NULL,
-                `vote` enum('approve','reject','dismissed') NOT NULL,
-                `notes` text DEFAULT NULL,
-                `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
-                PRIMARY KEY (`id`),
-                UNIQUE KEY `unique_vote` (`request_id`, `user_id`),
-                KEY `user_votes` (`user_id`),
-                CONSTRAINT `{$this->base}{$type}_user_approval_request` FOREIGN KEY (`request_id`)
-                    REFERENCES `{$this->prefixed}approval_{$type}_requests` (`id`) ON DELETE CASCADE,
-                CONSTRAINT `{$this->base}{$type}_user_approval_voter` FOREIGN KEY (`user_id`)
-                    REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
-            )";
-        }
-        if (!empty($save)) {
-            update_option(BASE.'approvals_types', $save);
-        }
-        return $tables;
-    }
-
-
-	protected function taxonomyRelationshipsTables():array
-	{
-		$tables = [
-			'taxonomy_relationships' => "(
-		`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-		`term_id` {$this->termIDType} NOT NULL,
-		`related_term_id` {$this->termIDType} NOT NULL,
-		`taxonomy` varchar(32) NOT NULL,
-		`related_taxonomy` varchar(32) NOT NULL,
-		`post_count` int(11) NOT NULL DEFAULT 0,
-		`is_direct` tinyint(1) NOT NULL DEFAULT 1,
-		`is_hierarchical` tinyint(1) NOT NULL DEFAULT 0,
-		`last_updated` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-		PRIMARY KEY (`id`),
-		KEY `term_id` (`term_id`),
-		KEY `related_term_id` (`related_term_id`),
-		KEY `taxonomy` (`taxonomy`),
-		KEY `related_taxonomy` (`related_taxonomy`),
-		UNIQUE KEY `term_relation` (`term_id`, `related_term_id`, `taxonomy`, `related_taxonomy`),
-		CONSTRAINT `{$this->base}tax_rel_term_id` FOREIGN KEY (`term_id`)
-			REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE,
-		CONSTRAINT `{$this->base}tax_rel_related_id` FOREIGN KEY (`related_term_id`)
-			REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE
-	)"
-		];
-
-		if ((array_key_exists('dashboard', $this->JVB_SITE) && $this->JVB_SITE['dashboard'] === true) || array_key_exists('use_feed_block', $this->JVB_SITE) && $this->JVB_SITE['use_feed_block']) {
-			$tables['user_term_index'] = "(
-		`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-		`user_id` {$this->userIDType} NOT NULL,
-		`term_id` {$this->termIDType} NOT NULL,
-		`taxonomy` varchar(32) NOT NULL,
-		`post_count` int(11) NOT NULL DEFAULT 1,
-		`is_parent` tinyint(1) NOT NULL DEFAULT 0,
-		`last_used` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-		PRIMARY KEY (`id`),
-		UNIQUE KEY `user_term` (`user_id`, `term_id`, `taxonomy`),
-		KEY `user_taxonomy` (`user_id`, `taxonomy`),
-		KEY `taxonomy` (`taxonomy`),
-		CONSTRAINT `{$this->base}user_term_user_fk` FOREIGN KEY (`user_id`)
-			REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
-		CONSTRAINT `{$this->base}user_term_term_fk` FOREIGN KEY (`term_id`)
-			REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE
-	)";
-		}
-
-		return $tables;
-	}
-
-    protected function favouriteTables():array
-    {
-
-        return [
-            'favourites'    => "(
-            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-            `user_id` {$this->userIDType} NOT NULL,
-            `type` varchar(50) NOT NULL,
-            `target_id` bigint(20) NOT NULL,
-            `notes` text DEFAULT NULL,
-            `date_added` datetime DEFAULT CURRENT_TIMESTAMP,
-            PRIMARY KEY (`id`),
-            UNIQUE KEY `unique_favourite` (`user_id`, `type`, `target_id`),
-            KEY `user_type` (`user_id`, `type`),
-            KEY `target_type` (`target_id`, `type`),
-            CONSTRAINT `{$this->base}favourites_user` FOREIGN KEY (`user_id`)
-                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
-        )",
-            'favourites_lists'  => "(
-            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-            `user_id` {$this->userIDType} NOT NULL,
-            `name` varchar(255) NOT NULL,
-            `description` text,
-            `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-            `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-            PRIMARY KEY (`id`),
-            KEY `user_lists` (`user_id`),
-            CONSTRAINT `{$this->base}list_user` FOREIGN KEY (`user_id`)
-                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
-        )",
-            'favourites_list_items' => "(
-            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-            `list_id` bigint(20) unsigned NOT NULL,
-            `item_type` varchar(50) NOT NULL,
-            `item_id` bigint(20) NOT NULL,
-            `favourite_id` bigint(20) unsigned DEFAULT NULL,
-            `added_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-            PRIMARY KEY (`id`),
-            UNIQUE KEY `unique_list_item` (`list_id`, `item_type`, `item_id`),
-            KEY `list_items` (`list_id`),
-            KEY `favourite_id` (`favourite_id`),
-            CONSTRAINT `{$this->base}list_items` FOREIGN KEY (`list_id`)
-                REFERENCES 	`{$this->prefixed}favourites_lists` (`id`) ON DELETE CASCADE,
-            CONSTRAINT `{$this->base}list_favourite` FOREIGN KEY (`favourite_id`)
-                REFERENCES `{$this->prefixed}favourites` (`id`) ON DELETE SET NULL
-        )",
-            'favourites_list_shares' => "(
-            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-            `list_id` bigint(20) unsigned NOT NULL,
-            `user_id` {$this->userIDType} NOT NULL,
-            `email` varchar(255) NOT NULL,
-            `permission_type` enum('view', 'edit') NOT NULL DEFAULT 'view',
-            `status` enum('pending', 'accepted', 'rejected', 'revoked') NOT NULL DEFAULT 'pending',
-            `invitation_token` varchar(64) NULL,
-            `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-            `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-            PRIMARY KEY (`id`),
-            UNIQUE KEY `unique_share_user` (`list_id`, `user_id`, `status`),
-            UNIQUE KEY `unique_share_email` (`list_id`, `email`, `status`),
-            KEY `list_shares` (`list_id`),
-            KEY `status_index` (`status`),
-            CONSTRAINT `{$this->base}share_list` FOREIGN KEY (`list_id`)
-                REFERENCES `{$this->prefixed}favourites_lists` (`id`) ON DELETE CASCADE,
-            CONSTRAINT `{$this->base}share_user` FOREIGN KEY (`user_id`)
-                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
-        )",
-            'favourites_list_stats'    => "(
-            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-            `item_type` varchar(50) NOT NULL,
-            `item_id` bigint(20) NOT NULL,
-            `list_count` int NOT NULL DEFAULT 0,
-            `last_added` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-            PRIMARY KEY (`id`),
-            UNIQUE KEY `unique_item_stat` (`item_type`, `item_id`),
-            KEY `item_stats` (`item_type`, `item_id`)
-        )",
-        ];
-    }
-
-    protected function newsRelationshipsTable():array
-    {
-        return [
-            'news_relationships'    => "(
-                `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-                `shop_id` {$this->termIDType} NOT NULL,
-                `user_id` {$this->userIDType} NOT NULL,
-                `artist_id` {$this->postIDType} NOT NULL,
-                `news_count` int(10) unsigned NOT NULL DEFAULT 0,
-                `last_post_date` datetime DEFAULT NULL,
-                `last_updated` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-                PRIMARY KEY (`id`),
-                UNIQUE KEY `shop_user` (`shop_id`, `user_id`),
-                KEY `shop_id` (`shop_id`),
-                KEY `user_id` (`user_id`),
-                KEY `artist_id` (`artist_id`),
-            CONSTRAINT `{$this->base}nr_shop_news_shop` FOREIGN KEY (`shop_id`)
-                REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE,
-            CONSTRAINT `{$this->base}nr_shop_news_user` FOREIGN KEY (`user_id`)
-                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
-            CONSTRAINT `{$this->base}nr_shop_news_artist` FOREIGN KEY (`artist_id`)
-                REFERENCES `{$this->wpdb->posts}` (`ID`) ON DELETE SET NULL
-        )"
-        ];
-    }
-
-    protected function responseTable():array
-    {
-        return [
-            'responses' => "(
-            `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-            `item_id` {$this->postIDType} NOT NULL,
-            `content` text NOT NULL,
-            `user_id` {$this->userIDType} NOT NULL,
-            `parent_id` bigint(20) unsigned DEFAULT NULL,
-            `response` text NOT NULL,
-            `status` enum('published','hidden','flagged','deleted') DEFAULT 'published',
-            `is_user_deleted` tinyint(1) DEFAULT 0,
-            `upvotes` int NOT NULL DEFAULT 0,
-            `downvotes` int NOT NULL DEFAULT 0,
-            `karma` int NOT NULL DEFAULT 0,
-            `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
-            `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-            PRIMARY KEY (`id`),
-            KEY `item_lookup` (`item_id`, `status`),
-            KEY `user_comments` (`user_id`),
-            KEY `parent_child` (`parent_id`),
-            KEY `karma_order` (`karma`),
-            CONSTRAINT `{$this->base}re_responses_news` FOREIGN KEY (`item_id`)
-                REFERENCES `{$this->wpdb->posts}` (`ID`) ON DELETE CASCADE,
-            CONSTRAINT `{$this->base}re_responses_parent` FOREIGN KEY (`parent_id`)
-                REFERENCES `{$this->prefixed}responses` (`id`) ON DELETE SET NULL
-        )",
-            'karma_response' => "(
-                `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-                `item_id` bigint(20) NOT NULL,
-                `user_id` {$this->userIDType} NOT NULL,
-                `vote` enum('up','down') NOT NULL,
-                `date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-                PRIMARY KEY (`id`),
-                UNIQUE KEY `user_post` (`user_id`,`item_id`),
-                KEY `item_id` (`item_id`),
-                KEY `user_id` (`user_id`),
-            CONSTRAINT `{$this->base}_response_item_id` FOREIGN KEY (`item_id`)
-                REFERENCES `{$this->prefixed}responses` (`id`) ON DELETE CASCADE,
-            CONSTRAINT `{$this->base}_response_user_id` FOREIGN KEY (`user_id`)
-                REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
-                )"
-        ];
-    }
-
-    protected function karmaTables(array $types):array
-    {
-        $tables = [];
-        foreach ($types as $type => $config) {
-            $t = false;
-            if (array_key_exists($type, $this->JVB_CONTENT)) {
-                $t = 'posts';
-            } elseif (array_key_exists($type, $this->JVB_TAXONOMY)) {
-                $t = 'terms';
-            } elseif (array_key_exists($type, $this->JVB_USER)) {
-                $t = 'users';
-            }
-
-            if (!$t) {
-                continue;
-            }
-
-            switch ($t) {
-                case 'posts':
-					$referenceType = $this->postIDType;
-                    $reference_table = $this->wpdb->posts;
-                    $reference_column = 'ID';
-                    break;
-                case 'terms':
-					$referenceType = $this->termIDType;
-                    $reference_table = $this->wpdb->terms;
-                    $reference_column = 'term_id';
-                    break;
-                case 'users':
-					$referenceType = $this->userIDType;
-                    $reference_table = $this->userTable;
-                    $reference_column = 'ID';
-                    break;
-            }
-
-            $tables['karma_'.$type] = "(
-                `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-                `item_id` {$referenceType} NOT NULL,
-                `user_id` {$this->userIDType} NOT NULL,
-                `vote` enum('up','down') NOT NULL,
-                `date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-                PRIMARY KEY (`id`),
-                UNIQUE KEY `user_post` (`user_id`,`item_id`),
-                KEY `item_id` (`item_id`),
-                KEY `user_id` (`user_id`),
-                CONSTRAINT `{$this->base}kt_{$type}_item_id` FOREIGN KEY (`item_id`)
-                    REFERENCES `{$reference_table}` (`{$reference_column}`) ON DELETE CASCADE,
-                CONSTRAINT `{$this->base}kt_{$type}_user_id` FOREIGN KEY (`user_id`)
-                    REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
-            )";
-
-        }
-
-        return $tables;
-    }
+	//MOVED TO KarmaManager.php
+//    protected function karmaTables(array $types):array
+//    {
+//        $tables = [];
+//        foreach ($types as $type => $config) {
+//            $t = false;
+//            if (array_key_exists($type, $this->JVB_CONTENT)) {
+//                $t = 'posts';
+//            } elseif (array_key_exists($type, $this->JVB_TAXONOMY)) {
+//                $t = 'terms';
+//            } elseif (array_key_exists($type, $this->JVB_USER)) {
+//                $t = 'users';
+//            }
+//
+//            if (!$t) {
+//                continue;
+//            }
+//
+//            switch ($t) {
+//                case 'posts':
+//					$referenceType = $this->postIDType;
+//                    $reference_table = $this->wpdb->posts;
+//                    $reference_column = 'ID';
+//                    break;
+//                case 'terms':
+//					$referenceType = $this->termIDType;
+//                    $reference_table = $this->wpdb->terms;
+//                    $reference_column = 'term_id';
+//                    break;
+//                case 'users':
+//					$referenceType = $this->userIDType;
+//                    $reference_table = $this->userTable;
+//                    $reference_column = 'ID';
+//                    break;
+//            }
+//
+//            $tables['karma_'.$type] = "(
+//                `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//                `item_id` {$referenceType} NOT NULL,
+//                `user_id` {$this->userIDType} NOT NULL,
+//                `vote` enum('up','down') NOT NULL,
+//                `date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+//                PRIMARY KEY (`id`),
+//                UNIQUE KEY `user_post` (`user_id`,`item_id`),
+//                KEY `item_id` (`item_id`),
+//                KEY `user_id` (`user_id`),
+//                CONSTRAINT `{$this->base}kt_{$type}_item_id` FOREIGN KEY (`item_id`)
+//                    REFERENCES `{$reference_table}` (`{$reference_column}`) ON DELETE CASCADE,
+//                CONSTRAINT `{$this->base}kt_{$type}_user_id` FOREIGN KEY (`user_id`)
+//                    REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
+//            )";
+//
+//        }
+//
+//        return $tables;
+//    }
 
     protected function calendarTables(array $types):array
     {
@@ -1159,7 +1164,7 @@
         ];
     }
 
-	protected function invitationTables(array $config): array
+	/*protected function invitationTables(array $config): array
 	{
 		if (empty($config['roles']) && empty($config['terms'])) {
 			return [];
@@ -1212,177 +1217,180 @@
 		$definitions .= ")";
 
 		return ['invitations' => $definitions];
-	}
+	}*/
 
-    protected function trackChangesTables($types)
-    {
-        $tables = [];
-        foreach ($types as $type => $config) {
-            $contents = $config['for_content'];
-            foreach ($contents as $content) {
-                $tables['history_'.$content.'_'.$type] = "(
-                    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-                    `user_id` {$this->userIDType} NOT NULL,
-                    `content_id` bigint(20) NOT NULL,
-                    `term_id` {$this->termIDType} NOT NULL,
-                    `role` varchar(50) DEFAULT 'artist',
-                    `is_primary` tinyint(1) DEFAULT 0,
-                    `start_date` date DEFAULT NULL,
-                    `end_date` date DEFAULT NULL,
-                    `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
-                    PRIMARY KEY (`id`),
-                    UNIQUE KEY `content_term` (`content_id`, `term_id`),
-                    KEY content_role (`term_id`, `role`),
-                    CONSTRAINT `{$this->base}{$content}_{$type}_history_user` FOREIGN KEY (`user_id`)
-                        REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
-                    CONSTRAINT `{$this->base}{$content}_{$type}_history_term` FOREIGN KEY (`term_id`)
-                        REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE
-                )";
-            }
-        }
+	//MOVED TO MakeTrackChanges.php
+//    protected function trackChangesTables($types)
+//    {
+//        $tables = [];
+//        foreach ($types as $type => $config) {
+//            $contents = $config['for_content'];
+//            foreach ($contents as $content) {
+//                $tables['history_'.$content.'_'.$type] = "(
+//                    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//                    `user_id` {$this->userIDType} NOT NULL,
+//                    `content_id` bigint(20) NOT NULL,
+//                    `term_id` {$this->termIDType} NOT NULL,
+//                    `role` varchar(50) DEFAULT 'artist',
+//                    `is_primary` tinyint(1) DEFAULT 0,
+//                    `start_date` date DEFAULT NULL,
+//                    `end_date` date DEFAULT NULL,
+//                    `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
+//                    PRIMARY KEY (`id`),
+//                    UNIQUE KEY `content_term` (`content_id`, `term_id`),
+//                    KEY content_role (`term_id`, `role`),
+//                    CONSTRAINT `{$this->base}{$content}_{$type}_history_user` FOREIGN KEY (`user_id`)
+//                        REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
+//                    CONSTRAINT `{$this->base}{$content}_{$type}_history_term` FOREIGN KEY (`term_id`)
+//                        REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE
+//                )";
+//            }
+//        }
+//
+//        return $tables;
+//    }
 
-        return $tables;
-    }
-    protected function requestEntryTables($types)
-    {
-        $tables = [];
-        foreach ($types as $type => $config) {
-            $contents = $config['for_content'];
-            foreach ($contents as $content) {
-                $tables[$content.'_'.$type.'_requests'] = "(
-                    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-                    `user_id` {$this->userIDType} NOT NULL,
-                    `content_id` bigint(20) NOT NULL,
-                    `term_id` {$this->termIDType} NOT NULL,
-                    `managers` json DEFAULT NULL,
-                    `status` ENUM('requested', 'rejected', 'accepted') DEFAULT 'requested',
-                    `dismissed` smallint(1) unsigned DEFAULT NULL,
-                    `created_date` timestamp DEFAULT CURRENT_TIMESTAMP,
-                    `updated_date` timestamp DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
-                    `notes` text DEFAULT NULL,
-                    PRIMARY KEY (`id`),
-                    UNIQUE KEY `{$this->base}content_term` (`content_id`, `term_id`),
-                    CONSTRAINT `{$this->base}{$content}_{$type}_request_user` FOREIGN KEY (`user_id`)
-                        REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
-                    CONSTRAINT `{$this->base}{$content}_{$type}_request_term` FOREIGN KEY (`term_id`)
-                        REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE
-                )";
-            }
-        }
+//MOVED TO VerifyEntryManager.php
+//    protected function requestEntryTables($types)
+//    {
+//        $tables = [];
+//        foreach ($types as $type => $config) {
+//            $contents = $config['for_content'];
+//            foreach ($contents as $content) {
+//                $tables[$content.'_'.$type.'_requests'] = "(
+//                    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//                    `user_id` {$this->userIDType} NOT NULL,
+//                    `content_id` bigint(20) NOT NULL,
+//                    `term_id` {$this->termIDType} NOT NULL,
+//                    `managers` json DEFAULT NULL,
+//                    `status` ENUM('requested', 'rejected', 'accepted') DEFAULT 'requested',
+//                    `dismissed` smallint(1) unsigned DEFAULT NULL,
+//                    `created_date` timestamp DEFAULT CURRENT_TIMESTAMP,
+//                    `updated_date` timestamp DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
+//                    `notes` text DEFAULT NULL,
+//                    PRIMARY KEY (`id`),
+//                    UNIQUE KEY `{$this->base}content_term` (`content_id`, `term_id`),
+//                    CONSTRAINT `{$this->base}{$content}_{$type}_request_user` FOREIGN KEY (`user_id`)
+//                        REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
+//                    CONSTRAINT `{$this->base}{$content}_{$type}_request_term` FOREIGN KEY (`term_id`)
+//                        REFERENCES `{$this->wpdb->terms}` (`term_id`) ON DELETE CASCADE
+//                )";
+//            }
+//        }
+//
+//        return $tables;
+//    }
 
-        return $tables;
-    }
-
-
+// MOVED TO ReferralManager.php
 	/**
 	 * Create referral tracking tables
 	 *
 	 * Call this from the main table creation method in CheckCustomTables.php:
 	 * $tables = array_merge($tables, $this->referralTables());
 	 */
-	protected function referralTables(): array
-	{
-		// Create tables in dependency order
-		// First: referrals (depends only on wp_users)
-		$mainTable['referrals'] = "(
-	`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-	`referrer_id` {$this->userIDType} NOT NULL,
-	`referee_id` {$this->userIDType} NOT NULL,
-	`referee_name` varchar(255) NOT NULL,
-	`referee_email` varchar(255) NOT NULL,
-	`referee_phone` varchar(50) DEFAULT NULL,
-	`referral_code` varchar(50) NOT NULL,
-	`status` enum('pending', 'consulted', 'treated', 'cancelled') DEFAULT 'pending',
-	`referred_at` datetime NOT NULL,
-	`consulted_at` datetime DEFAULT NULL,
-	`treated_at` datetime DEFAULT NULL,
-	`treatment_count` int DEFAULT 0,
-	`notes` text DEFAULT NULL,
-	PRIMARY KEY (`id`),
-	UNIQUE KEY `referee_unique` (`referee_id`),
-	KEY `referrer_idx` (`referrer_id`),
-	KEY `status_idx` (`status`),
-	KEY `code_idx` (`referral_code`),
-	KEY `date_idx` (`referred_at`),
-	KEY `consult_idx` (`consulted_at`),
-	CONSTRAINT `{$this->base}referral_referrer_fk` FOREIGN KEY (`referrer_id`)
-		REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
-	CONSTRAINT `{$this->base}referral_referee_fk` FOREIGN KEY (`referee_id`)
-		REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
-)";
-
-		// Create the main referrals table first
-		$this->createTables($mainTable);
-
-		// Now create dependent tables
-		$dependentTables = [];
-
-		// Second: jane_clients (depends only on wp_users)
-		$dependentTables['jane_clients'] = "(
-	`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-	`patient_guid` varchar(50) NOT NULL,
-	`user_id` {$this->userIDType} NOT NULL,
-	`first_name` varchar(100) NOT NULL,
-	`last_name` varchar(100) NOT NULL,
-	`email` varchar(255) NOT NULL,
-	`imported_at` datetime DEFAULT CURRENT_TIMESTAMP,
-	`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-	PRIMARY KEY (`id`),
-	UNIQUE KEY `patient_guid_unique` (`patient_guid`),
-	KEY `user_idx` (`user_id`),
-	KEY `email_idx` (`email`),
-	CONSTRAINT `{$this->base}jane_client_user_fk` FOREIGN KEY (`user_id`)
-		REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
-)";
-
-		// Third: referral_treatments (depends on referrals AND wp_users)
-		$dependentTables['referral_treatments'] = "(
-	`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-	`referral_id` bigint(20) unsigned NOT NULL,
-	`user_id` {$this->userIDType} NOT NULL,
-	`treatment_type` varchar(100) NOT NULL COMMENT 'Tier 1-6, Brows, etc',
-	`treatment_date` datetime NOT NULL,
-	`invoice_number` varchar(50) DEFAULT NULL,
-	`amount` decimal(10,2) DEFAULT NULL,
-	`status` enum('completed', 'no_show', 'cancelled') DEFAULT 'completed',
-	`imported_at` datetime DEFAULT CURRENT_TIMESTAMP,
-	PRIMARY KEY (`id`),
-	KEY `referral_idx` (`referral_id`),
-	KEY `user_idx` (`user_id`),
-	KEY `date_idx` (`treatment_date`),
-	KEY `type_idx` (`treatment_type`),
-	CONSTRAINT `{$this->base}treatment_referral_fk` FOREIGN KEY (`referral_id`)
-		REFERENCES `{$this->prefixed}referrals` (`id`) ON DELETE CASCADE,
-	CONSTRAINT `{$this->base}treatment_user_fk` FOREIGN KEY (`user_id`)
-		REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
-)";
-
-		// Fourth: referral_rewards (depends on referrals AND wp_users)
-		$dependentTables['referral_rewards'] = "(
-	`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
-	`referral_id` bigint(20) unsigned NOT NULL,
-	`user_id` {$this->userIDType} NOT NULL,
-	`reward_type` enum('referrer', 'referee') NOT NULL,
-	`amount` decimal(10,2) NOT NULL,
-	`reward_calculation` varchar(20) DEFAULT NULL COMMENT 'percentage or fixed',
-	`status` enum('available', 'redeemed', 'expired', 'cancelled') DEFAULT 'available',
-	`created_at` datetime NOT NULL,
-	`redeemed_at` datetime DEFAULT NULL,
-	`expires_at` datetime DEFAULT NULL,
-	`notes` text DEFAULT NULL,
-	PRIMARY KEY (`id`),
-	KEY `referral_idx` (`referral_id`),
-	KEY `user_idx` (`user_id`),
-	KEY `status_idx` (`status`),
-	KEY `type_idx` (`reward_type`),
-	CONSTRAINT `{$this->base}reward_referral_fk` FOREIGN KEY (`referral_id`)
-		REFERENCES `{$this->prefixed}referrals` (`id`) ON DELETE CASCADE,
-	CONSTRAINT `{$this->base}reward_user_fk` FOREIGN KEY (`user_id`)
-		REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
-)";
-
-		return $dependentTables;
-	}
+//	protected function referralTables(): array
+//	{
+//		// Create tables in dependency order
+//		// First: referrals (depends only on wp_users)
+//		$mainTable['referrals'] = "(
+//	`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//	`referrer_id` {$this->userIDType} NOT NULL,
+//	`referee_id` {$this->userIDType} NOT NULL,
+//	`referee_name` varchar(255) NOT NULL,
+//	`referee_email` varchar(255) NOT NULL,
+//	`referee_phone` varchar(50) DEFAULT NULL,
+//	`referral_code` varchar(50) NOT NULL,
+//	`status` enum('pending', 'consulted', 'treated', 'cancelled') DEFAULT 'pending',
+//	`referred_at` datetime NOT NULL,
+//	`consulted_at` datetime DEFAULT NULL,
+//	`treated_at` datetime DEFAULT NULL,
+//	`treatment_count` int DEFAULT 0,
+//	`notes` text DEFAULT NULL,
+//	PRIMARY KEY (`id`),
+//	UNIQUE KEY `referee_unique` (`referee_id`, `referral_code`),
+//	KEY `referrer_idx` (`referrer_id`),
+//	KEY `status_idx` (`status`),
+//	KEY `code_idx` (`referral_code`),
+//	KEY `date_idx` (`referred_at`),
+//	KEY `consult_idx` (`consulted_at`),
+//	CONSTRAINT `{$this->base}referral_referrer_fk` FOREIGN KEY (`referrer_id`)
+//		REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE,
+//	CONSTRAINT `{$this->base}referral_referee_fk` FOREIGN KEY (`referee_id`)
+//		REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
+//)";
+//
+//		// Create the main referrals table first
+//		$this->createTables($mainTable);
+//
+//		// Now create dependent tables
+//		$dependentTables = [];
+//
+//		// Second: jane_clients (depends only on wp_users)
+//		$dependentTables['jane_clients'] = "(
+//	`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//	`patient_guid` varchar(50) NOT NULL,
+//	`user_id` {$this->userIDType} NOT NULL,
+//	`first_name` varchar(100) NOT NULL,
+//	`last_name` varchar(100) NOT NULL,
+//	`email` varchar(255) NOT NULL,
+//	`imported_at` datetime DEFAULT CURRENT_TIMESTAMP,
+//	`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+//	PRIMARY KEY (`id`),
+//	UNIQUE KEY `patient_guid_unique` (`patient_guid`),
+//	KEY `user_idx` (`user_id`),
+//	KEY `email_idx` (`email`),
+//	CONSTRAINT `{$this->base}jane_client_user_fk` FOREIGN KEY (`user_id`)
+//		REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
+//)";
+//
+//		// Third: referral_treatments (depends on referrals AND wp_users)
+//		$dependentTables['referral_treatments'] = "(
+//	`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//	`referral_id` bigint(20) unsigned NOT NULL,
+//	`user_id` {$this->userIDType} NOT NULL,
+//	`treatment_type` varchar(100) NOT NULL COMMENT 'Tier 1-6, Brows, etc',
+//	`treatment_date` datetime NOT NULL,
+//	`invoice_number` varchar(50) DEFAULT NULL,
+//	`amount` decimal(10,2) DEFAULT NULL,
+//	`status` enum('completed', 'no_show', 'cancelled') DEFAULT 'completed',
+//	`imported_at` datetime DEFAULT CURRENT_TIMESTAMP,
+//	PRIMARY KEY (`id`),
+//	KEY `referral_idx` (`referral_id`),
+//	KEY `user_idx` (`user_id`),
+//	KEY `date_idx` (`treatment_date`),
+//	KEY `type_idx` (`treatment_type`),
+//	CONSTRAINT `{$this->base}treatment_referral_fk` FOREIGN KEY (`referral_id`)
+//		REFERENCES `{$this->prefixed}referrals` (`id`) ON DELETE CASCADE,
+//	CONSTRAINT `{$this->base}treatment_user_fk` FOREIGN KEY (`user_id`)
+//		REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
+//)";
+//
+//		// Fourth: referral_rewards (depends on referrals AND wp_users)
+//		$dependentTables['referral_rewards'] = "(
+//	`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+//	`referral_id` bigint(20) unsigned NOT NULL,
+//	`user_id` {$this->userIDType} NOT NULL,
+//	`reward_type` enum('referrer', 'referee') NOT NULL,
+//	`amount` decimal(10,2) NOT NULL,
+//	`reward_calculation` varchar(20) DEFAULT NULL COMMENT 'percentage or fixed',
+//	`status` enum('available', 'redeemed', 'expired', 'cancelled') DEFAULT 'available',
+//	`created_at` datetime NOT NULL,
+//	`redeemed_at` datetime DEFAULT NULL,
+//	`expires_at` datetime DEFAULT NULL,
+//	`notes` text DEFAULT NULL,
+//	PRIMARY KEY (`id`),
+//	KEY `referral_idx` (`referral_id`),
+//	KEY `user_idx` (`user_id`),
+//	KEY `status_idx` (`status`),
+//	KEY `type_idx` (`reward_type`),
+//	CONSTRAINT `{$this->base}reward_referral_fk` FOREIGN KEY (`referral_id`)
+//		REFERENCES `{$this->prefixed}referrals` (`id`) ON DELETE CASCADE,
+//	CONSTRAINT `{$this->base}reward_user_fk` FOREIGN KEY (`user_id`)
+//		REFERENCES `{$this->userTable}` (`ID`) ON DELETE CASCADE
+//)";
+//
+//		return $dependentTables;
+//	}
 
     /*******************************************************************************************
      * These methods help create a content-type taxonomy, like the tattoo shops in edmonton.ink
diff --git a/inc/rest/routes/FavouritesRoutes.php b/inc/rest/routes/FavouritesRoutes.php
index ae49baf..3494f5b 100644
--- a/inc/rest/routes/FavouritesRoutes.php
+++ b/inc/rest/routes/FavouritesRoutes.php
@@ -505,7 +505,7 @@
 			'target_id' => $target_id
 		]);
 
-		if ($result['created']) {
+		if ((bool)$result) {
 			$this->updateFavouriteCount($type, $target_id);
 			$this->maybeNotifyOwner($type, $target_id, $user_id);
 		}
@@ -582,13 +582,13 @@
 							'type' => $type,
 							'target_id' => $target_id
 						]);
-						if ($result['created']) $results['added']++;
+						if ((bool) $result) $results['added']++;
 					} else {
-						$deleted = $table->where([
+						$deleted = $table->delete([
 							'user_id' => $user_id,
 							'type' => $type,
 							'target_id' => $target_id
-						])->deleteResults();
+						]);
 						if ($deleted) $results['removed']++;
 					}
 
diff --git a/inc/rest/routes/TermRoutes.php b/inc/rest/routes/TermRoutes.php
index 431c7b5..f536bba 100644
--- a/inc/rest/routes/TermRoutes.php
+++ b/inc/rest/routes/TermRoutes.php
@@ -1,9 +1,8 @@
 <?php
 namespace JVBase\rest\routes;
 
-use JVBase\JVB;
+use JVBase\registrar\Registrar;
 use JVBase\rest\Rest;
-use JVBase\managers\TaxonomyRelationships;
 use JVBase\managers\UserTermsManager;
 use JVBase\rest\Route;
 use JVBase\utility\Features;
@@ -89,7 +88,7 @@
 
         // Add relationship data if requested
         if ($request->get_param('include_relationships')) {
-            $relationship_manager = new TaxonomyRelationships();
+            $relationship_manager = JVB()->termRelationships();
             $related_taxonomies = $request->get_param('related_taxonomies') ?: ['jvb_style', 'jvb_theme', 'jvb_city'];
 
             $data['relationships'] = [];
@@ -194,8 +193,8 @@
 		if (array_key_exists('content', $data)) {
 			// If content_type is provided, use the specialized endpoint
 			$content_type = $request->get_param('content');
-			global $feed_types;
-			if (taxIsJVBContentTax($content_type)) {
+			$registrar = Registrar::getInstance($content_type);
+			if ($registrar->hasFeature('is_content')) {
 				$response = $this->getTermsForContentType($request);
 				return $this->addCacheHeaders($response);
 			}
@@ -239,7 +238,7 @@
             $main_context = json_decode($request->get_param('main_context'), true);
             $userID = get_post_meta($main_context['id'], BASE.'link', true);
             $manager = new UserTermsManager();
-            $related = $manager->getUserTermIDs($userID, $taxonomy);
+            $related = $manager->fetchUserTerms($userID, $taxonomy);
 
             if (empty($related)) {
                 $response = $this->emptyResult();
@@ -252,7 +251,7 @@
             $main_context = json_decode($request->get_param('main_context'), true);
             $thisTaxonomy = str_replace('taxonomy:', '', $main_context['context']);
             $ID = (int)$main_context['id'];
-            $manager = new TaxonomyRelationships();
+            $manager = JVB()->termRelationships();
             $related = $manager->getRelatedTerms($ID, BASE.$request->get_param('taxonomy'));
 
             if (empty($related)) {
@@ -270,7 +269,7 @@
             $match = $request->get_param('match') ?? 'any';
             $context = json_decode($request['context'], true);
 
-            $relationshipManager = new TaxonomyRelationships();
+            $relationshipManager = JVB()->termRelationships();
             // Prepare array to collect term IDs that match the context
             $related_term_ids = [];
 
@@ -584,7 +583,7 @@
      */
     public function getTermsForContentType(WP_REST_Request $request):WP_REST_Response
     {
-        $manager = new TaxonomyRelationships();
+        $manager = JVB()->termRelationships();
         $content_type = BASE . $request->get_param('content');
         $taxonomy = BASE . $request->get_param('taxonomy');
         $search = $request->get_param('search');
diff --git a/inc/rest/routes/VoteRoutes.php b/inc/rest/routes/VoteRoutes.php
index 7f55f1d..c0298ec 100644
--- a/inc/rest/routes/VoteRoutes.php
+++ b/inc/rest/routes/VoteRoutes.php
@@ -2,6 +2,7 @@
 namespace JVBase\rest\routes;
 
 use JVBase\managers\CustomTable;
+use JVBase\managers\KarmaManager;
 use JVBase\registrar\Registrar;
 use JVBase\rest\Response;
 use JVBase\rest\Rest;
@@ -65,7 +66,7 @@
 
         $vote = sanitize_text_field($request->get_param('vote')??'');
 		$itemID = absint($request->get_param('item_id')??0);
-        if ($itemID === 0 || !in_array($vote, ['up', 'down'])) {
+        if ($itemID === 0 || !in_array($vote, [true, false, null])) {
 			return Response::validationError(['message' => __('Invalid item or vote attempt', 'jvb')]);
         }
 
@@ -74,194 +75,27 @@
 			return Response::validationError(['message' => __('User doesn\'t match. Bot?', 'jvb')]);
 		}
 
-        $operation = sanitize_text_field($request->get_param('id'));
-
-		$type = Registrar::getInstance($content)->getType()??false;
+		$type = $registrar->getType()??false;
 		if (!$type) {
 			return Response::validationError(['message' => __('Invalid content type', 'jvb')]);
 		}
 
-        $data = [
-            'user'      => $user,
-            'item_id'   => $itemID,
-            'content'   => $content,
-			'type'		=> $type,
-            'vote'      => $vote,
-        ];
 
-        error_log('Final Vote Data: '.print_r($data, true));
-        error_log('Operation: '.print_r($operation, true));
-        $operationID = JVB()->queue()->add(
-            'karmic',
-            $user,
-            $data,
-            [
-                'priority'      => 'high',
-                'operation_id'  => $operation,
-            ]
-        );
-		return $this->queued($operationID['operation_id']);
-    }
-
-	/**
-	 * @param WP_Error|array $result
-	 * @param object $operation
-	 * @param array $data
-	 *
-	 * @return WP_Error|array
-	 * @throws Exception
-	 */
-    public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array
-    {
-        if ($operation->type !== 'karmic') {
-            return $result;
-        }
-
-        //Check if item exists
-		$item = match ($data['type']) {
-			'post' 	=> get_post($data['item_id']),
-			'term'	=> get_term($data['item_id'], jvbCheckBase($data['content'])),
-			'user'	=> get_userdata($data['item_id']),
-			default => false
-		};
-        if (!$item || is_wp_error($item)) {
-            return [
-                'success' => false,
-                'result'   => __('Invalid item', 'jvb')
-            ];
-        }
-
-		$table = CustomTable::for('karma_' . $data['content']);
-
-		return $table->transaction(function($table) use ($data) {
-			// Check existing vote
-			$existing = $table->where([
-				'item_id' => $data['item_id'],
-				'user_id' => $data['user']
-			])->first();
-
-			$existing_vote = $existing->vote ?? null;
-			$new_vote = $data['vote'];
-
-			// No previous vote - insert new
-			if ($existing_vote === null) {
-				$inserted = $table->create([
-					'item_id' => $data['item_id'],
-					'user_id' => $data['user'],
-					'vote' => $new_vote,
-				]);
-
-				if (!$inserted) {
-					throw new Exception('Failed to record vote');
-				}
-
-				$this->updateVoteCount($data['content'], $data['type'], $data['item_id'], $new_vote, 1);
-				$this->cache->invalidate($data['user']);
-
-				return [
-					'success' => true,
-					'result' => __('Vote recorded', 'jvb'),
-				];
-			}
-
-			// Changing vote
-			if ($existing_vote !== $new_vote) {
-				$updated = $table->where([
-					'item_id' => $data['item_id'],
-					'user_id' => $data['user']
-				])->updateResults(['vote' => $new_vote]);
-
-				if (!$updated) {
-					throw new Exception('Failed to update vote');
-				}
-
-				// Decrement old, increment new
-				$this->updateVoteCount($data['content'], $data['type'], $data['item_id'], $existing_vote, -1);
-				$this->updateVoteCount($data['content'], $data['type'], $data['item_id'], $new_vote, 1);
-				$this->cache->invalidate($data['user']);
-
-				return [
-					'success' => true,
-					'result' => __('Vote updated', 'jvb'),
-				];
-			}
-
-			// Toggle off - remove vote
-			$deleted = $table->where([
-				'item_id' => $data['item_id'],
-				'user_id' => $data['user']
-			])->deleteResults();
-
-			if (!$deleted) {
-				throw new Exception('Failed to remove vote');
-			}
-
-			$this->updateVoteCount($data['content'], $data['type'], $data['item_id'], $existing_vote, -1);
-			$this->cache->delete($data['user']);
-
-			return [
-				'success' => true,
-				'result' => __('Vote removed', 'jvb'),
-			];
-		});
-    }
-
-    /**
-     * @param string $content
-     * @param int $ID
-     * @param string $vote
-     * @param int $value
-     *
-     * @return void
-     */
-    protected function updateVoteCount(string $content, string $type, int $ID, string $vote, int $value):void
-    {
-		$key = ($vote === 'down') ? BASE.'downvotes' : BASE.'upvotes';
-
-		switch ($type) {
-			case 'post':
-				$old = (int) get_post_meta($ID, $key, true);
-				$new = max(0, $old + $value);
-				update_post_meta($ID, $key, $new);
-
-				$up = (int) get_post_meta($ID, BASE.'upvotes', true);
-				$down = (int) get_post_meta($ID, BASE.'downvotes', true);
-				update_post_meta($ID, BASE.'karma', $up - $down);
-				break;
-
-			case 'term':
-				$old = (int) get_term_meta($ID, $key, true);
-				$new = max(0, $old + $value);
-				update_term_meta($ID, $key, $new);
-
-				$up = (int) get_term_meta($ID, BASE.'upvotes', true);
-				$down = (int) get_term_meta($ID, BASE.'downvotes', true);
-				update_term_meta($ID, BASE.'karma', $up - $down);
-				break;
-
-			case 'user':
-				$old = (int) get_user_meta($ID, $key, true);
-				$new = max(0, $old + $value);
-				update_user_meta($ID, $key, $new);
-
-				$up = (int) get_user_meta($ID, BASE.'upvotes', true);
-				$down = (int) get_user_meta($ID, BASE.'downvotes', true);
-				update_user_meta($ID, BASE.'karma', $up - $down);
-				break;
-
-			case 'response':
-				$field = str_replace(BASE, '', $key);
-				CustomTable::for('responses')->query(
-					"UPDATE {table}
-					 SET $field = GREATEST(0, $field + %d),
-					     karma = (upvotes - downvotes)
-					 WHERE id = %d",
-					[$value, $ID]
-				);
-				break;
+		$man = KarmaManager::getInstance($type);
+		if (!$man) {
+			return Response::validationError(['message' => __('Karma not set up', 'jvb')]);
 		}
+
+		[$success, $message] = $man->vote($user, $itemID, $content, $vote);
+
+		return match ($success) {
+			true, 'partial' => Response::success(['message' => $message]),
+			default => Response::error($message),
+		};
     }
 
+
+
     /**
      * @param WP_REST_Request $request
      *
@@ -276,25 +110,7 @@
 			return Response::success($cache);
 		}
 
-		$votes = [];
-
-		foreach (Registrar::getFeatured('has_karma') as $type) {
-			$table = CustomTable::for('karma_' . $type);
-
-			// Skip if table doesn't exist
-			global $wpdb;
-			if ($wpdb->get_var("SHOW TABLES LIKE '{$table->getFullTableName()}'") != $table->getFullTableName()) {
-				continue;
-			}
-
-			$results = $table->where(['user_id' => $user])->getResults();
-
-			if (!empty($results)) {
-				foreach ($results as $vote) {
-					$votes[$type][$vote->item_id] = $vote->vote;
-				}
-			}
-		}
+		$votes = KarmaManager::getUserVotes($user);
 
 		$this->cache->set($user, $votes);
 
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
index a5d0dc8..4e6d8ba 100644
--- a/inc/ui/CRUDSkeleton.php
+++ b/inc/ui/CRUDSkeleton.php
@@ -901,7 +901,7 @@
 		if ($limit) {
 			if ($limit === 'user') {
 				$manager = new UserTermsManager();
-				return $manager->getUserTerms($this->user_id, $taxonomy);
+				return $manager->fetchUserTerms($this->user_id, $taxonomy);
 			} else {
 				$limit = (int)$limit;
 			}
diff --git a/jvb.php b/jvb.php
index b326bb1..3f2b76e 100644
--- a/jvb.php
+++ b/jvb.php
@@ -245,6 +245,8 @@
 
 
 
+
+
 require(JVB_DIR . '/inc/blocks/_setup.php');
 
 

--
Gitblit v1.10.0