From 12e5205559c8def89659f9922a91b8b628289664 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 30 Sep 2025 20:18:30 +0000
Subject: [PATCH] Square update and initial Referral System started

---
 assets/js/dash/SquareCheckout.js   |   17 
 assets/js/min/square.min.js        |    2 
 JVBase.php                         |    6 
 inc/managers/ReferralManager.php   | 1036 +++++++++++++++++++++
 inc/integrations/Square.php        |  428 ++------
 inc/managers/ReferralManager2.php  | 1006 ++++++++++++++++++++
 inc/rest/_setup.php                |    3 
 inc/managers/EmailManager.php      |    4 
 inc/managers/_setup.php            |    4 
 inc/registry/CheckCustomTables.php |   70 +
 inc/rest/routes/ReferralRoutes.php |  315 ++++++
 11 files changed, 2,559 insertions(+), 332 deletions(-)

diff --git a/JVBase.php b/JVBase.php
index 6dd39df..f88c372 100644
--- a/JVBase.php
+++ b/JVBase.php
@@ -5,6 +5,7 @@
 use JVBase\managers\ErrorHandler;
 use JVBase\managers\OperationQueue;
 use JVBase\managers\DashboardManager;
+use JVBase\managers\ReferralManager;
 use JVBase\managers\RoleManager;
 use JVBase\managers\SchemaManager;
 use JVBase\managers\AdminPages;
@@ -81,6 +82,11 @@
 			'userTerms'		=> new UserTermsManager(),
         ];
 
+		if (Features::forSite()->has('referrals')) {
+			$this->managers['referral'] = new ReferralManager();
+			$this->routes['referral'] = new ReferralRoutes();
+		}
+
 		if (Features::forSite()->has('dashboard')) {
 			$this->managers['dash'] = new DashboardManager();
 		}
diff --git a/assets/js/dash/SquareCheckout.js b/assets/js/dash/SquareCheckout.js
index d7adab8..1a1dda3 100644
--- a/assets/js/dash/SquareCheckout.js
+++ b/assets/js/dash/SquareCheckout.js
@@ -507,29 +507,30 @@
 		if (squareConfig.isOpen !== '1') {
 			return;
 		}
-		const response = await fetch(this.config.api_url + 'checkout', {
+
+		// Square Web Payments SDK handles EVERYTHING
+		// We just need to track the order for status updates
+		const response = await fetch(this.config.api_url + 'save-order', {
 			method: 'POST',
 			headers: {
 				'Content-Type': 'application/json',
 				'X-WP-Nonce': this.config.nonce
 			},
 			body: JSON.stringify({
-				source_id: token,
-				amount: orderData.total,
-				items: orderData.items,
+				order_id: token.orderId,  // From Square SDK response
+				payment_id: token.paymentId,  // From Square SDK response
 				customer: orderData.customer,
-				note: orderData.note,
-				currency: this.config.currency,
+				items: orderData.items,
 				action: 'jvb_integration_action',
 				service: 'square',
-				integration_action: 'process_order'
+				integration_action: 'save_order'
 			})
 		});
 
 		const result = await response.json();
 
 		if (!response.ok) {
-			throw new Error(result.message || 'Payment failed');
+			throw new Error(result.message || 'Failed to save order');
 		}
 
 		return result;
diff --git a/assets/js/min/square.min.js b/assets/js/min/square.min.js
index 531aaa4..550b94e 100644
--- a/assets/js/min/square.min.js
+++ b/assets/js/min/square.min.js
@@ -1 +1 @@
-(()=>{class t{constructor(t={}){this.checkout=document.querySelector("aside#cart"),this.checkout&&(this.config=Object.assign({application_id:squareConfig.application_id,location_id:squareConfig.location_id,api_url:squareConfig.api_url,nonce:squareConfig.nonce,currency:squareConfig.currency||"CAD"},t),this.stepMultiplier=1,this.cache=new window.jvbCache("cart",{TTL:864e5}),this.a11y=window.jvbA11y,this.initCart(),this.payments=null,this.card=null,this.isInitialized=!1,this.clickHandler=this.handleClick.bind(this),this.keyHandler=this.handleEscape.bind(this),this.changeHandler=this.handleChange.bind(this),this.initElements(),this.bindEvents(),this.init(),this.toggle.hidden=!1)}async initCart(){this.cartItems=await this.cache.get("cart")??new Map,console.log("cart",this.cartItems),this.cartItems.size>0&&this.notifyRestoredCart()}handleClick(t){if(window.targetCheck(t,".toggle-cart")){window.targetCheck(t,".toggle-cart");console.log("Toggle found. Toggling cart"),this.toggleCart()}else if(window.targetCheck(t,"button")&&window.targetCheck(t,"div.quantity")){let e=window.targetCheck(t,"div.quantity");this.handleNumberClick(t,e)}else if(window.targetCheck(t,"[data-add-to-cart]")){let e=window.targetCheck(t,"[data-add-to-cart]");this.handleAddToCart(e)}else if(window.targetCheck(t,"[data-remove-from-cart]")){let e=window.targetCheck(t,"[data-remove-from-cart]");this.handleRemoveFromCart(e)}else window.targetCheck(t,"[data-clear-cart]")?this.clearCart():this.checkout.classList.contains("expanded")&&!this.checkout.contains(t.target)&&t.target!==this.toggle&&this.closeCart()}handleChange(t,e){console.log("Checkout change");let a=window.targetCheck(t,".quantity-input");if(a){let e=t.target.closest(".quantity"),i=a.value;if(window.targetCheck(t,".cart-items")){let t=document.querySelector(`.menu-section [data-id="${e.dataset.id}"] input`);t&&(t.value=a.value)}i>0?this.handleAddToCart(e):this.handleRemoveFromCart(e)}}handleNumberClick(t,e){console.log(e),t.preventDefault();let a=0;if(t.target.closest(".increase")?a+=1:t.target.closest(".decrease")&&(a-=1),0!==a){let[t,i]=[parseInt(e.dataset.step),e.querySelector("input")],r=""===i.value?0:parseInt(i.value);i.value=r+t*a*this.stepMultiplier,i.dispatchEvent(new Event("change",{bubbles:!0})),this.handleNumberLimits(e)}}handleNumberLimits(t){let[e,a,i,r,s]=[t.dataset.min,t.dataset.max,t.querySelector("input"),t.querySelector(".increase"),t.querySelector(".decrease")],o=parseInt(i.value);o<e?(i.value=e,s.disabled=!0):o>a?(i.value=a,r.disabled=!1):r.disabled?r.disabled=!1:s.disabled&&(s.disabled=!1)}toggleCart(){this.checkout.classList.contains("expanded")?this.closeCart():this.openCart()}openCart(t="Opened Cart"){this.checkout.classList.add("expanded"),this.toggle.title="Hide cart",this.toggle.ariaExpanded=!0,this.toggle.querySelector("span").textContent="Close Cart",this.a11y.announce(t),this.maybeAddEmptyState(),document.addEventListener("keydown",this.keyHandler)}maybeAddEmptyState(){let t=this.itemsList.querySelector(".empty");if(t&&t.remove(),0===this.cartItems.size){this.checkoutPanel.disabled=!0,this.checkoutPanel.title="Add some things to your cart first!";let t=window.getTemplate("emptyCart");this.itemsList.append(t),this.table.closest("table").hidden=!0,this.total.hidden=!0,this.a11y.announce("Nothing in Cart")}else this.checkoutPanel.disabled=!1,this.table.closest("table").hidden=!1,this.total.hidden=!1,this.checkoutPanel.title="Checkout"}closeCart(t="Closed Cart"){this.checkout.classList.remove("expanded"),this.toggle.title="Show Cart",this.toggle.ariaExpanded=!1,this.toggle.querySelector("span").textContent="",this.a11y.announce(t),document.removeEventListener("keydown",this.keyHandler)}handleEscape(t){"Escape"===t.key?(this.closeCart("Closed Cart with escape key"),this.stepMultiplier=1):t.ctrlKey&&t.shiftKey?this.stepMultiplier=Math.max(100*parseInt(this.stepMultiplier),1e3):t.shiftKey&&(this.stepMultiplier=Math.max(10*parseInt(this.stepMultiplier),1e3))}handleAddToCart(t){let e=t.dataset.id;this.createItemElement(t);let a=parseFloat(t.dataset.price),i=parseInt(t.querySelector(".quantity-input")?.value)??1,r=parseFloat(a*i);this.cartItems.set(e,{post_id:e,name:t.dataset.name,price:a,quantity:i,total:r,square_catalog_id:t.dataset.squareCatalogId}),this.saveCart()}notifyRestoredCart(){let t=window.getTemplate("restoredCart");this.checkout.querySelector(".tab-content[data-tab=cartItems]").insertBefore(t,this.itemsList),this.cartItems.forEach((t=>{console.log(t);let e=window.getTemplate("cartItem"),a=e.querySelector(".quantity"),i=t.price,r=t.quantity;[a.dataset.id,e.querySelector("label").textContent,e.querySelector(".price").textContent,a.dataset.price,a.dataset.squareCatalogId,e.querySelector('[name="quantity"]').value,e.querySelector(".total").textContent]=[t.post_id,t.name,window.formatPrice(i),i,t.square_catalog_id,r,window.formatPrice(r*i)],this.table.append(e)})),this.updateTotal()}handleRemoveFromCart(t){if(confirm("This will remove this item from the cart. Continue?")){t.querySelector("[data-id]")||(t=t.closest(".item")?.querySelector(".quantity.field"));let e=t.dataset.id;this.cartItems.delete(e),this.table.querySelector(`[data-id="${e}"]`)?.closest("tr").remove();let a=document.querySelector(`[data-id="${e}"] input`);a&&(a.value=0),this.maybeAddEmptyState(),this.saveCart()}}clearCart(){this.cartItems.clear(),window.removeChildren(this.table),this.saveCart()}saveCart(){this.updateTotal(),this.cache.set("cart",this.cartItems)}updateTotal(){let t=0;this.cartItems.forEach((e=>{console.log(e),t+=e.total}));let e=.05*t;t=window.formatPrice(t+e),e=window.formatPrice(e),window.eraseText(this.totalTax),window.eraseText(this.grandTotal),window.typeText(this.totalTax,e),window.typeText(this.grandTotal,t),this.totalTax.classList.remove("typeText")}createItemElement(t){let e=this.itemsList.querySelector(`[data-id="${t.dataset.id}"]`),a=!1,i=t.dataset.price,r=t.querySelector('[name="quantity"]')?.value??1;if(e)e=e.closest("tr");else{a=!0,e=window.getTemplate("cartItem");let r=e.querySelector(".quantity");[r.dataset.id,e.querySelector("label").textContent,e.querySelector(".price").textContent,r.dataset.price,r.dataset.squareCatalogId]=[t.dataset.id,t.dataset.name,window.formatPrice(i),i,t.dataset.squareCatalogId]}[e.querySelector('[name="quantity"]').value,e.querySelector(".total").textContent]=[r,window.formatPrice(r*i)],a&&(e.classList.add("adding"),this.table.append(e),setTimeout((()=>{e.classList.remove("adding")}),500))}async init(){if(window.Square)try{this.payments=window.Square.payments(this.config.application_id,this.config.location_id),await this.initializePaymentMethods(),this.isInitialized=!0,document.dispatchEvent(new CustomEvent("squareCheckoutReady",{detail:{checkout:this}}))}catch(t){console.error("Failed to initialize Square payments:",t),this.handleError(t)}else console.error("Square Web Payments SDK not loaded")}initElements(){this.toggle=document.querySelector(".toggle-cart"),"1"!==squareConfig.isOpen&&(this.toggle.disabled=!0,this.toggle.title="Currently closed for online ordering"),this.checkoutPanel=this.checkout.querySelector('button[data-tab="checkout"]'),this.itemsList=this.checkout.querySelector(".cart-items"),this.table=this.checkout.querySelector(".cart-items tbody"),this.total=this.checkout.querySelector(".cart-total"),this.totalTax=this.total.querySelector(".tax span"),this.grandTotal=this.total.querySelector(".total span"),this.checkoutForm=this.checkout.querySelector("form"),this.tabs=new window.jvbTabs(this.checkoutForm,{updateURL:!1}),console.log("Initialized Checkout")}bindEvents(){this.checkoutForm.addEventListener("submit",(t=>this.handleFormSubmit(t))),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}async initializePaymentMethods(){if(document.getElementById("square-card-container"))try{this.card=await this.payments.card({style:this.getCardStyle()}),await this.card.attach("#square-card-container")}catch(t){throw console.error("Failed to initialize card:",t),t}}getCardStyle(){return{input:{fontSize:"16px",fontFamily:"inherit",color:"#333",backgroundColor:"#fff"},".input-container":{borderColor:"#ccc",borderRadius:"4px"},".input-container.is-focus":{borderColor:"#007cba"},".input-container.is-error":{borderColor:"#d63638"}}}async handleFormSubmit(t){if("1"!==squareConfig.isOpen)return;if(t.preventDefault(),!this.isInitialized)return void this.handleError("Checkout not initialized");const e=t.target,a=this.extractOrderData(e);try{window.jvbLoading.showLoading("Processing payment...");const t=await this.processPayment(a);this.handleSuccess(t,e)}catch(t){this.handleError(t)}finally{window.jvbLoading.hideLoading()}}extractOrderData(t){const e=Array.from(this.cartItems.values()).map((t=>({post_id:t.post_id,quantity:t.quantity,price:t.price,name:t.name})));return{total:100*e.reduce(((t,e)=>t+e.price*e.quantity),0),items:e,customer:{email:t.querySelector('[name="email"]')?.value||"",name:t.querySelector('[name="name"]')?.value||"",phone:t.querySelector('[name="phone"]')?.value||""},note:t.querySelector('[name="special_instructions"]')?.value||"",pickup_time:t.querySelector('[name="pickup_time"]')?.value||""}}async processPayment(t){try{const e=await this.card.tokenize();if("OK"===e.status)return await this.submitToServer(e.token,t);throw new Error("Card tokenization failed: "+(e.errors?.join(", ")||"Unknown error"))}catch(t){throw console.error("Payment processing failed:",t),t}}async submitToServer(t,e){if("1"!==squareConfig.isOpen)return;const a=await fetch(this.config.api_url+"checkout",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":this.config.nonce},body:JSON.stringify({source_id:t,amount:e.total,items:e.items,customer:e.customer,note:e.note,currency:this.config.currency,action:"jvb_integration_action",service:"square",integration_action:"process_order"})}),i=await a.json();if(!a.ok)throw new Error(i.message||"Payment failed");return i}trackOrder(t){this.orderId=t,this.scheduleOrderCheck(),this.checkout.querySelector("button[data-tab=order]").hidden=!1}scheduleOrderCheck(){window.debouncer.schedule("order",(()=>{this.checkOrderStatus()}),3e4)}async checkOrderStatus(){const t=await fetch(`/wp-json/jvb/v1/square/order-status/${this.orderId}`),e=await t.json();"ready"!==e.status&&this.scheduleOrderCheck(),this.updateOrderStatus(e)}updateOrderStatus(t){this.checkout.querySelectorAll(".status-item").forEach((e=>{e.dataset.status===t.status&&e.classList.add("active")})),this.checkout.querySelector("#eta").textContent=t.eta||"In progress"}async loadCustomerProfile(t){const e=await fetch("/wp-json/jvb/v1/square/customer",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":this.config.nonce},body:JSON.stringify({email:t})}),a=await e.json();a&&(this.displaySavedCards(a.cards),this.fillCustomerInfo(a.customer))}displaySavedCards(t){const e=document.getElementById("saved-cards");t.length&&(e.innerHTML=`\n            <h3>Saved Payment Methods</h3>\n            ${t.map((t=>`\n                <label>\n                    <input type="radio" name="payment_method" value="${t.id}">\n                    •••• ${t.last_4} (${t.card_brand})\n                </label>\n            `)).join("")}\n            <label>\n                <input type="radio" name="payment_method" value="new" checked>\n                Use new card\n            </label>\n        `)}handleSuccess(t,e){document.dispatchEvent(new CustomEvent("squareCheckoutSuccess",{detail:{result:t,form:e}}));const a=e.dataset.successUrl||`/order-confirmation/?order=${t.wp_order_id}`;window.location.href=a}handleError(t){console.error("Square checkout error:",t),document.dispatchEvent(new CustomEvent("squareCheckoutError",{detail:{error:t}})),window.jvbNotifications?.show?.(t.message||"Payment failed","error")}}document.addEventListener("DOMContentLoaded",(()=>{window.squareCheckout=new t}))})();
\ No newline at end of file
+(()=>{class t{constructor(t={}){this.checkout=document.querySelector("aside#cart"),this.checkout&&(this.config=Object.assign({application_id:squareConfig.application_id,location_id:squareConfig.location_id,api_url:squareConfig.api_url,nonce:squareConfig.nonce,currency:squareConfig.currency||"CAD"},t),this.stepMultiplier=1,this.cache=new window.jvbCache("cart",{TTL:864e5}),this.a11y=window.jvbA11y,this.initCart(),this.payments=null,this.card=null,this.isInitialized=!1,this.clickHandler=this.handleClick.bind(this),this.keyHandler=this.handleEscape.bind(this),this.changeHandler=this.handleChange.bind(this),this.initElements(),this.bindEvents(),this.init(),this.toggle.hidden=!1)}async initCart(){this.cartItems=await this.cache.get("cart")??new Map,console.log("cart",this.cartItems),this.cartItems.size>0&&this.notifyRestoredCart()}handleClick(t){if(window.targetCheck(t,".toggle-cart")){window.targetCheck(t,".toggle-cart");console.log("Toggle found. Toggling cart"),this.toggleCart()}else if(window.targetCheck(t,"button")&&window.targetCheck(t,"div.quantity")){let e=window.targetCheck(t,"div.quantity");this.handleNumberClick(t,e)}else if(window.targetCheck(t,"[data-add-to-cart]")){let e=window.targetCheck(t,"[data-add-to-cart]");this.handleAddToCart(e)}else if(window.targetCheck(t,"[data-remove-from-cart]")){let e=window.targetCheck(t,"[data-remove-from-cart]");this.handleRemoveFromCart(e)}else window.targetCheck(t,"[data-clear-cart]")?this.clearCart():this.checkout.classList.contains("expanded")&&!this.checkout.contains(t.target)&&t.target!==this.toggle&&this.closeCart()}handleChange(t,e){console.log("Checkout change");let a=window.targetCheck(t,".quantity-input");if(a){let e=t.target.closest(".quantity"),i=a.value;if(window.targetCheck(t,".cart-items")){let t=document.querySelector(`.menu-section [data-id="${e.dataset.id}"] input`);t&&(t.value=a.value)}i>0?this.handleAddToCart(e):this.handleRemoveFromCart(e)}}handleNumberClick(t,e){console.log(e),t.preventDefault();let a=0;if(t.target.closest(".increase")?a+=1:t.target.closest(".decrease")&&(a-=1),0!==a){let[t,i]=[parseInt(e.dataset.step),e.querySelector("input")],r=""===i.value?0:parseInt(i.value);i.value=r+t*a*this.stepMultiplier,i.dispatchEvent(new Event("change",{bubbles:!0})),this.handleNumberLimits(e)}}handleNumberLimits(t){let[e,a,i,r,s]=[t.dataset.min,t.dataset.max,t.querySelector("input"),t.querySelector(".increase"),t.querySelector(".decrease")],o=parseInt(i.value);o<e?(i.value=e,s.disabled=!0):o>a?(i.value=a,r.disabled=!1):r.disabled?r.disabled=!1:s.disabled&&(s.disabled=!1)}toggleCart(){this.checkout.classList.contains("expanded")?this.closeCart():this.openCart()}openCart(t="Opened Cart"){this.checkout.classList.add("expanded"),this.toggle.title="Hide cart",this.toggle.ariaExpanded=!0,this.toggle.querySelector("span").textContent="Close Cart",this.a11y.announce(t),this.maybeAddEmptyState(),document.addEventListener("keydown",this.keyHandler)}maybeAddEmptyState(){let t=this.itemsList.querySelector(".empty");if(t&&t.remove(),0===this.cartItems.size){this.checkoutPanel.disabled=!0,this.checkoutPanel.title="Add some things to your cart first!";let t=window.getTemplate("emptyCart");this.itemsList.append(t),this.table.closest("table").hidden=!0,this.total.hidden=!0,this.a11y.announce("Nothing in Cart")}else this.checkoutPanel.disabled=!1,this.table.closest("table").hidden=!1,this.total.hidden=!1,this.checkoutPanel.title="Checkout"}closeCart(t="Closed Cart"){this.checkout.classList.remove("expanded"),this.toggle.title="Show Cart",this.toggle.ariaExpanded=!1,this.toggle.querySelector("span").textContent="",this.a11y.announce(t),document.removeEventListener("keydown",this.keyHandler)}handleEscape(t){"Escape"===t.key?(this.closeCart("Closed Cart with escape key"),this.stepMultiplier=1):t.ctrlKey&&t.shiftKey?this.stepMultiplier=Math.max(100*parseInt(this.stepMultiplier),1e3):t.shiftKey&&(this.stepMultiplier=Math.max(10*parseInt(this.stepMultiplier),1e3))}handleAddToCart(t){let e=t.dataset.id;this.createItemElement(t);let a=parseFloat(t.dataset.price),i=parseInt(t.querySelector(".quantity-input")?.value)??1,r=parseFloat(a*i);this.cartItems.set(e,{post_id:e,name:t.dataset.name,price:a,quantity:i,total:r,square_catalog_id:t.dataset.squareCatalogId}),this.saveCart()}notifyRestoredCart(){let t=window.getTemplate("restoredCart");this.checkout.querySelector(".tab-content[data-tab=cartItems]").insertBefore(t,this.itemsList),this.cartItems.forEach((t=>{console.log(t);let e=window.getTemplate("cartItem"),a=e.querySelector(".quantity"),i=t.price,r=t.quantity;[a.dataset.id,e.querySelector("label").textContent,e.querySelector(".price").textContent,a.dataset.price,a.dataset.squareCatalogId,e.querySelector('[name="quantity"]').value,e.querySelector(".total").textContent]=[t.post_id,t.name,window.formatPrice(i),i,t.square_catalog_id,r,window.formatPrice(r*i)],this.table.append(e)})),this.updateTotal()}handleRemoveFromCart(t){if(confirm("This will remove this item from the cart. Continue?")){t.querySelector("[data-id]")||(t=t.closest(".item")?.querySelector(".quantity.field"));let e=t.dataset.id;this.cartItems.delete(e),this.table.querySelector(`[data-id="${e}"]`)?.closest("tr").remove();let a=document.querySelector(`[data-id="${e}"] input`);a&&(a.value=0),this.maybeAddEmptyState(),this.saveCart()}}clearCart(){this.cartItems.clear(),window.removeChildren(this.table),this.saveCart()}saveCart(){this.updateTotal(),this.cache.set("cart",this.cartItems)}updateTotal(){let t=0;this.cartItems.forEach((e=>{console.log(e),t+=e.total}));let e=.05*t;t=window.formatPrice(t+e),e=window.formatPrice(e),window.eraseText(this.totalTax),window.eraseText(this.grandTotal),window.typeText(this.totalTax,e),window.typeText(this.grandTotal,t),this.totalTax.classList.remove("typeText")}createItemElement(t){let e=this.itemsList.querySelector(`[data-id="${t.dataset.id}"]`),a=!1,i=t.dataset.price,r=t.querySelector('[name="quantity"]')?.value??1;if(e)e=e.closest("tr");else{a=!0,e=window.getTemplate("cartItem");let r=e.querySelector(".quantity");[r.dataset.id,e.querySelector("label").textContent,e.querySelector(".price").textContent,r.dataset.price,r.dataset.squareCatalogId]=[t.dataset.id,t.dataset.name,window.formatPrice(i),i,t.dataset.squareCatalogId]}[e.querySelector('[name="quantity"]').value,e.querySelector(".total").textContent]=[r,window.formatPrice(r*i)],a&&(e.classList.add("adding"),this.table.append(e),setTimeout((()=>{e.classList.remove("adding")}),500))}async init(){if(window.Square)try{this.payments=window.Square.payments(this.config.application_id,this.config.location_id),await this.initializePaymentMethods(),this.isInitialized=!0,document.dispatchEvent(new CustomEvent("squareCheckoutReady",{detail:{checkout:this}}))}catch(t){console.error("Failed to initialize Square payments:",t),this.handleError(t)}else console.error("Square Web Payments SDK not loaded")}initElements(){this.toggle=document.querySelector(".toggle-cart"),"1"!==squareConfig.isOpen&&(this.toggle.disabled=!0,this.toggle.title="Currently closed for online ordering"),this.checkoutPanel=this.checkout.querySelector('button[data-tab="checkout"]'),this.itemsList=this.checkout.querySelector(".cart-items"),this.table=this.checkout.querySelector(".cart-items tbody"),this.total=this.checkout.querySelector(".cart-total"),this.totalTax=this.total.querySelector(".tax span"),this.grandTotal=this.total.querySelector(".total span"),this.checkoutForm=this.checkout.querySelector("form"),this.tabs=new window.jvbTabs(this.checkoutForm,{updateURL:!1}),console.log("Initialized Checkout")}bindEvents(){this.checkoutForm.addEventListener("submit",(t=>this.handleFormSubmit(t))),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler)}async initializePaymentMethods(){if(document.getElementById("square-card-container"))try{this.card=await this.payments.card({style:this.getCardStyle()}),await this.card.attach("#square-card-container")}catch(t){throw console.error("Failed to initialize card:",t),t}}getCardStyle(){return{input:{fontSize:"16px",fontFamily:"inherit",color:"#333",backgroundColor:"#fff"},".input-container":{borderColor:"#ccc",borderRadius:"4px"},".input-container.is-focus":{borderColor:"#007cba"},".input-container.is-error":{borderColor:"#d63638"}}}async handleFormSubmit(t){if("1"!==squareConfig.isOpen)return;if(t.preventDefault(),!this.isInitialized)return void this.handleError("Checkout not initialized");const e=t.target,a=this.extractOrderData(e);try{window.jvbLoading.showLoading("Processing payment...");const t=await this.processPayment(a);this.handleSuccess(t,e)}catch(t){this.handleError(t)}finally{window.jvbLoading.hideLoading()}}extractOrderData(t){const e=Array.from(this.cartItems.values()).map((t=>({post_id:t.post_id,quantity:t.quantity,price:t.price,name:t.name})));return{total:100*e.reduce(((t,e)=>t+e.price*e.quantity),0),items:e,customer:{email:t.querySelector('[name="email"]')?.value||"",name:t.querySelector('[name="name"]')?.value||"",phone:t.querySelector('[name="phone"]')?.value||""},note:t.querySelector('[name="special_instructions"]')?.value||"",pickup_time:t.querySelector('[name="pickup_time"]')?.value||""}}async processPayment(t){try{const e=await this.card.tokenize();if("OK"===e.status)return await this.submitToServer(e.token,t);throw new Error("Card tokenization failed: "+(e.errors?.join(", ")||"Unknown error"))}catch(t){throw console.error("Payment processing failed:",t),t}}async submitToServer(t,e){if("1"!==squareConfig.isOpen)return;const a=await fetch(this.config.api_url+"save-order",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":this.config.nonce},body:JSON.stringify({order_id:t.orderId,payment_id:t.paymentId,customer:e.customer,items:e.items,action:"jvb_integration_action",service:"square",integration_action:"save_order"})}),i=await a.json();if(!a.ok)throw new Error(i.message||"Failed to save order");return i}trackOrder(t){this.orderId=t,this.scheduleOrderCheck(),this.checkout.querySelector("button[data-tab=order]").hidden=!1}scheduleOrderCheck(){window.debouncer.schedule("order",(()=>{this.checkOrderStatus()}),3e4)}async checkOrderStatus(){const t=await fetch(`/wp-json/jvb/v1/square/order-status/${this.orderId}`),e=await t.json();"ready"!==e.status&&this.scheduleOrderCheck(),this.updateOrderStatus(e)}updateOrderStatus(t){this.checkout.querySelectorAll(".status-item").forEach((e=>{e.dataset.status===t.status&&e.classList.add("active")})),this.checkout.querySelector("#eta").textContent=t.eta||"In progress"}async loadCustomerProfile(t){const e=await fetch("/wp-json/jvb/v1/square/customer",{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":this.config.nonce},body:JSON.stringify({email:t})}),a=await e.json();a&&(this.displaySavedCards(a.cards),this.fillCustomerInfo(a.customer))}displaySavedCards(t){const e=document.getElementById("saved-cards");t.length&&(e.innerHTML=`\n            <h3>Saved Payment Methods</h3>\n            ${t.map((t=>`\n                <label>\n                    <input type="radio" name="payment_method" value="${t.id}">\n                    •••• ${t.last_4} (${t.card_brand})\n                </label>\n            `)).join("")}\n            <label>\n                <input type="radio" name="payment_method" value="new" checked>\n                Use new card\n            </label>\n        `)}handleSuccess(t,e){document.dispatchEvent(new CustomEvent("squareCheckoutSuccess",{detail:{result:t,form:e}}));const a=e.dataset.successUrl||`/order-confirmation/?order=${t.wp_order_id}`;window.location.href=a}handleError(t){console.error("Square checkout error:",t),document.dispatchEvent(new CustomEvent("squareCheckoutError",{detail:{error:t}})),window.jvbNotifications?.show?.(t.message||"Payment failed","error")}}document.addEventListener("DOMContentLoaded",(()=>{window.squareCheckout=new t}))})();
\ No newline at end of file
diff --git a/inc/integrations/Square.php b/inc/integrations/Square.php
index d82ea73..a7b5451 100644
--- a/inc/integrations/Square.php
+++ b/inc/integrations/Square.php
@@ -1195,7 +1195,7 @@
 			$price = floatval($meta->getValue('price') ?: 0);
 			$catalog_object['item_data']['variations'][] = [
 				'type' => 'ITEM_VARIATION',
-				'id' => $existing_square_id ? null : '#neb_menu_item_' . $postID . '_var_default',
+				'id' => $existing_square_id ? null : '#'.BASE.'menu_item_' . $postID . '_var_default',
 				'item_variation_data' => [
 					'name' => 'Regular',
 					'ordinal' => 0,
@@ -1213,7 +1213,7 @@
 				$existing_var_id = get_post_meta($postID, BASE . '_square_variation_' . $index . '_id', true);
 				$catalog_object['item_data']['variations'][] = [
 					'type' => 'ITEM_VARIATION',
-					'id' => $existing_var_id ?: '#neb_menu_item_' . $postID . '_var_' . $index,
+					'id' => $existing_var_id ?: '#'.BASE.'menu_item_' . $postID . '_var_' . $index,
 					'item_variation_data' => [
 						'name' => $variation['name'] ?? 'Variation ' . ($index + 1),
 						'ordinal' => $index,
@@ -1755,172 +1755,6 @@
 	}
 
 	/******************************************************************
-	 * ORDER PROCESSING
-	 ******************************************************************/
-
-	/**
-	 * Process checkout order
-	 */
-	public function processOrder($data):WP_Error|array
-	{
-		check_ajax_referer('square_checkout_nonce', 'nonce');
-
-		$cart_items = json_decode(stripslashes($data['cart'] ?? '[]'), true);
-		$customer_info = [
-			'name' => sanitize_text_field($data['name'] ?? ''),
-			'email' => sanitize_email($data['email'] ?? ''),
-			'phone' => sanitize_text_field($data['phone'] ?? ''),
-		];
-		$payment_token = sanitize_text_field($data['payment_token'] ?? '');
-
-		if (empty($cart_items) || empty($payment_token)) {
-			return new WP_Error('error', 'Invalid order data');
-		}
-
-		// Calculate order total
-		$order_total = $this->calculateOrderTotal($cart_items);
-
-		// Create Square order
-		$order_response = $this->createSquareOrder($cart_items, $customer_info, $order_total);
-
-		if (is_wp_error($order_response)) {
-			return new WP_Error('error', $order_response->get_error_message());
-		}
-
-		// Process payment
-		$payment_response = $this->processSquarePayment($payment_token, $order_response['order']['id'], $order_total);
-
-		if (is_wp_error($payment_response)) {
-			return new WP_Error('error', $order_response->get_error_message());
-		}
-
-		// Save order to user if logged in
-		if (is_user_logged_in()) {
-			$this->saveOrderToUser(get_current_user_id(), $order_response['order']['id']);
-		}
-
-		return [
-			'success'	=> true,
-			'order_id' => $order_response['order']['id'],
-			'receipt_url' => $payment_response['payment']['receipt_url'] ?? '',
-			'message' => 'Order placed successfully!'
-		];
-	}
-
-	/**
-	 * Calculate order total from cart items
-	 */
-	private function calculateOrderTotal(array $cart_items): int
-	{
-		$total = 0;
-
-		foreach ($cart_items as $item) {
-			$post_id = intval($item['id'] ?? 0);
-			if (!$post_id) continue;
-
-			$meta = new MetaManager($post_id, 'post');
-			$price = floatval($meta->getValue('price'));
-			$quantity = intval($item['quantity'] ?? 1);
-
-			$total += ($price * $quantity * 100); // Convert to cents
-		}
-
-		// Add tax (simplified - you'd want more complex tax calculation)
-		$tax_rate = floatval(get_option(BASE . 'square_tax_rate', 0.05));
-		$tax = intval($total * $tax_rate);
-
-		return $total + $tax;
-	}
-
-	/**
-	 * Create Square order
-	 */
-	private function createSquareOrder(array $cart_items, array $customer_info, int $total): array|WP_Error
-	{
-		$line_items = [];
-
-		foreach ($cart_items as $item) {
-			$post_id = intval($item['id'] ?? 0);
-			$variation_index = $item['variation'] ?? null;
-
-			if (!$post_id) continue;
-
-			$post = get_post($post_id);
-			$square_catalog_id = get_post_meta($post_id, BASE . '_square_catalog_id', true);
-
-			$line_item = [
-				'quantity' => strval($item['quantity'] ?? 1),
-				'name' => $post->post_title
-			];
-
-			// If variation specified, get variation ID
-			if ($variation_index !== null && $square_catalog_id) {
-				$variation_id = get_post_meta($post_id, BASE . '_square_variation_' . $variation_index . '_id', true);
-				if ($variation_id) {
-					$line_item['catalog_object_id'] = $variation_id;
-
-					// Add variation name to line item
-					$meta = new MetaManager($post_id, 'post');
-					$variations = $meta->getValue('product_variations');
-					if (!empty($variations[$variation_index]['name'])) {
-						$line_item['name'] .= ' - ' . $variations[$variation_index]['name'];
-					}
-				}
-			} elseif ($square_catalog_id) {
-				// Use default variation if no specific variation
-				$default_variation_id = get_post_meta($post_id, BASE . '_square_variation_0_id', true);
-				if ($default_variation_id) {
-					$line_item['catalog_object_id'] = $default_variation_id;
-				}
-			}
-
-			// If no catalog ID, create ad-hoc line item
-			if (empty($line_item['catalog_object_id'])) {
-				$meta = new MetaManager($post_id, 'post');
-				$variations = $meta->getValue('product_variations');
-
-				if ($variation_index !== null && !empty($variations[$variation_index]['price'])) {
-					$price = floatval($variations[$variation_index]['price']);
-				} else {
-					$price = floatval($meta->getValue('price'));
-				}
-
-				$line_item['base_price_money'] = [
-					'amount' => intval($price * 100),
-					'currency' => 'CAD'
-				];
-			}
-
-			$line_items[] = $line_item;
-		}
-
-		return $this->postRequest('orders', [
-			'order' => [
-				'location_id' => $this->locationId,
-				'line_items' => $line_items,
-				'customer_id' => $this->getOrCreateSquareCustomer($customer_info)
-			]
-		]);
-	}
-
-	/**
-	 * Process Square payment
-	 */
-	private function processSquarePayment(string $payment_token, string $order_id, int $amount): array|WP_Error
-	{
-		return $this->postRequest('payments', [
-			'source_id' => $payment_token,
-			'idempotency_key' => wp_generate_uuid4(),
-			'amount_money' => [
-				'amount' => $amount,
-				'currency' => 'CAD'
-			],
-			'order_id' => $order_id,
-			'location_id' => $this->locationId
-		]);
-	}
-
-	/******************************************************************
 	 * WEBHOOK HANDLING
 	 ******************************************************************/
 
@@ -2121,7 +1955,12 @@
 			'squareConfig',
 			[
 				'isOpen'		=> jvbIsOpen(),
-//				'currency' => get_option('jvb_currency', 'CAD')
+				'application_id' => $this->credentials['client_id'] ?? '',
+				'location_id' => $this->locationId,
+				'environment' => $this->environment,
+				'api_url' => rest_url('jvb/v1/square/'),
+				'nonce' => wp_create_nonce('wp_rest'),
+				'currency' => get_option(BASE . 'currency', 'CAD')
 			]
 		);
 	}
@@ -2167,6 +2006,43 @@
 	}
 
 	/**
+	 * Save order reference for status tracking
+	 */
+	public function saveOrderReference($data): array
+	{
+		$order_id = sanitize_text_field($data['order_id'] ?? '');
+		$payment_id = sanitize_text_field($data['payment_id'] ?? '');
+
+		if (!$order_id) {
+			return ['success' => false, 'message' => 'Invalid order data'];
+		}
+
+		// Save to user if logged in
+		if (is_user_logged_in()) {
+			$user_id = get_current_user_id();
+			$orders = get_user_meta($user_id, BASE . '_square_orders', true) ?: [];
+			$orders[] = [
+				'order_id' => $order_id,
+				'payment_id' => $payment_id,
+				'date' => current_time('mysql'),
+				'customer' => $data['customer'] ?? []
+			];
+
+			// Keep last 50 orders
+			if (count($orders) > 50) {
+				$orders = array_slice($orders, -50);
+			}
+
+			update_user_meta($user_id, BASE . '_square_orders', $orders);
+		}
+
+		return [
+			'success' => true,
+			'order_id' => $order_id,
+			'message' => 'Order saved'
+		];
+	}
+	/**
 	 * Save order to user meta
 	 */
 	private function saveOrderToUser(int $user_id, string $order_id): void
@@ -2186,9 +2062,9 @@
 	}
 
 	/**
-	 * Get order status
+	 * Get order status (for customer feedback)
 	 */
-	public function getOrderStatus($data):WP_Error|array
+	public function getOrderStatus($data): WP_Error|array
 	{
 		$order_id = sanitize_text_field($data['order_id'] ?? '');
 
@@ -2196,26 +2072,24 @@
 			return new WP_Error('error', 'Order ID required');
 		}
 
-		// Check cache first
-		$cached_status = get_transient(BASE . 'square_order_' . $order_id);
-
-		if ($cached_status !== false) {
-			return $cached_status;
-		}
-
 		// Fetch from Square
-		$response = $this->getRequest('orders/' . $order_id);
+		$response = $this->getRequest('v2/orders/' . $order_id);
 
 		if (is_wp_error($response)) {
 			return new WP_Error('error', 'Could not fetch order status');
 		}
 
-		$status = $response['order']['state'] ?? 'UNKNOWN';
-		set_transient(BASE . 'square_order_' . $order_id, $status, 5 * MINUTE_IN_SECONDS);
+		$order = $response['order'] ?? [];
+		$status_data = [
+			'state' => $order['state'] ?? 'UNKNOWN',
+			'fulfillment_eta' => $order['fulfillments'][0]['pickup_details']['pickup_at'] ?? null
+		];
 
-		return array_merge([
-			'success'	=> true,
-		], $status);
+		return [
+			'success' => true,
+			'status' => $status_data['state'],
+			'eta' => $status_data['fulfillment_eta']
+		];
 	}
 
 	/**
@@ -3026,137 +2900,60 @@
 	IMAGE PROCESSING
 	 *********************************************************/
 	/**
-	 * Upload featured image to Square catalog
-	 *
-	 * @param int $postID WordPress post ID
-	 * @return array|WP_Error Result of the upload operation
-	 */
-	public function uploadImageToSquare(int $imgID): array|WP_Error
-	{
-
-		if ($imgID === 0) {
-			return new WP_Error('no_image', 'No image found for post');
-		}
-
-		try {
-			// Get the supported image (converts WebP if needed)
-			$supported_image_id = $this->getSupportedImage($imgID);
-
-			// Check if we've already uploaded this image
-			$existing_square_image_id = $this->getSquareImageId($supported_image_id);
-			if ($existing_square_image_id) {
-				return [
-					'success' => true,
-					'image_id' => $existing_square_image_id,
-					'message' => 'Image already exists in Square catalog'
-				];
-			}
-
-			// Step 1: Create image object in catalog
-			$image_object = $this->createSquareImageObject($supported_image_id);
-			if (is_wp_error($image_object)) {
-				return $image_object;
-			}
-
-			// Step 2: Upload the actual image file
-			$upload_result = $this->uploadImageFileToSquare(
-				$supported_image_id,
-				$image_object['id']
-			);
-
-			if (is_wp_error($upload_result)) {
-				return $upload_result;
-			}
-
-			// Store the Square image ID for future reference
-			$this->setSquareImageId($supported_image_id, $image_object['id']);
-
-			return [
-				'success' => true,
-				'image_id' => $image_object['id'],
-				'message' => 'Image successfully uploaded to Square'
-			];
-
-		} catch (\Exception $e) {
-			$this->logError('Failed to upload image to Square', [
-				'method' => 'uploadImageToSquare',
-				'error' => $e->getMessage()
-			]);
-
-			return new WP_Error('upload_failed', $e->getMessage());
-		}
-	}
-
-	/**
-	 * Create image object in Square catalog
-	 *
-	 * @param int $attachment_id WordPress attachment ID
-	 * @return array|WP_Error Square image object or error
-	 */
-	protected function createSquareImageObject(int $attachment_id): array|WP_Error
-	{
-		$image_title = get_the_title($attachment_id);
-		$alt_text = get_post_meta($attachment_id, '_wp_attachment_image_alt', true);
-
-		$image_object = [
-			'type' => 'IMAGE',
-			'id' => '#IMAGE_' . $attachment_id . '_' . time(),
-			'image_data' => [
-				'name' => $image_title ?: 'Image',
-				'caption' => $alt_text ?: ''
-			]
-		];
-
-		$response = $this->postRequest('catalog/batch-upsert', [
-			'idempotency_key' => wp_generate_uuid4(),
-			'batches' => [[
-				'objects' => [$image_object]
-			]]
-		]);
-
-		if (is_wp_error($response)) {
-			return $response;
-		}
-
-		if (!empty($response['objects'][0])) {
-			return $response['objects'][0];
-		}
-
-		return new WP_Error('creation_failed', 'Failed to create image object in Square');
-	}
-
-	/**
 	 * Upload image file to Square
 	 *
-	 * @param int $attachment_id WordPress attachment ID
-	 * @param string $square_image_id Square catalog image ID
+	 * @param int $imgID WordPress attachment ID
 	 * @return array|WP_Error Upload result or error
 	 */
-	protected function uploadImageFileToSquare(int $attachment_id, string $square_image_id): array|WP_Error
+	protected function uploadImageToSquare(int $imgID): array|WP_Error
 	{
-		$file_path = get_attached_file($attachment_id);
+		$supported_image_id = $this->getSupportedImage($imgID);
 
+		// Check if already uploaded
+		$existing_square_image_id = $this->getSquareImageId($supported_image_id);
+		if ($existing_square_image_id) {
+			return [
+				'success' => true,
+				'image_id' => $existing_square_image_id
+			];
+		}
+
+		$file_path = get_attached_file($supported_image_id);
 		if (!file_exists($file_path)) {
 			return new WP_Error('file_not_found', 'Image file not found');
 		}
 
 		// Verify file type
-		$mime_type = get_post_mime_type($attachment_id);
-		$allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
-
-		if (!in_array($mime_type, $allowed_types)) {
+		$mime_type = get_post_mime_type($supported_image_id);
+		if (!in_array($mime_type, ['image/jpeg', 'image/png', 'image/gif'])) {
 			return new WP_Error('invalid_type', 'Square only supports JPEG, PNG, and GIF images');
 		}
 
-		// Prepare the multipart form data
+		$image_title = get_the_title($supported_image_id);
+		$alt_text = get_post_meta($supported_image_id, '_wp_attachment_image_alt', true);
+
+		// Build multipart request - SINGLE STEP
 		$boundary = wp_generate_password(24);
 		$headers = $this->getRequestHeaders();
 		$headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
 
-		$body = $this->buildMultipartBody($file_path, $square_image_id, $boundary);
+		// Request JSON part
+		$request_json = [
+			'idempotency_key' => wp_generate_uuid4(),
+			'image' => [
+				'type' => 'IMAGE',
+				'id' => '#IMAGE_' . $supported_image_id . '_' . time(),
+				'image_data' => [
+					'name' => $image_title ?: 'Image',
+					'caption' => $alt_text ?: ''
+				]
+			]
+		];
+
+		$body = $this->buildMultipartBody($file_path, $request_json, $boundary);
 
 		$response = wp_remote_post(
-			$this->getApiUrl('catalog/images'),
+			$this->getApiUrl('v2/catalog/images'),
 			[
 				'headers' => $headers,
 				'body' => $body,
@@ -3168,56 +2965,43 @@
 			return $response;
 		}
 
-		$body = wp_remote_retrieve_body($response);
-		$data = json_decode($body, true);
+		$data = json_decode(wp_remote_retrieve_body($response), true);
 
 		if (!empty($data['errors'])) {
-			$error_message = $data['errors'][0]['detail'] ?? 'Unknown error';
-			return new WP_Error('upload_error', $error_message);
+			return new WP_Error('upload_error', $data['errors'][0]['detail'] ?? 'Unknown error');
 		}
 
-		if (!empty($data['image'])) {
-			return $data;
+		if (!empty($data['image']['id'])) {
+			$this->setSquareImageId($supported_image_id, $data['image']['id']);
+			return [
+				'success' => true,
+				'image_id' => $data['image']['id']
+			];
 		}
 
-		return new WP_Error('upload_failed', 'Failed to upload image to Square');
+		return new WP_Error('upload_failed', 'Failed to upload image');
 	}
 
-	/**
-	 * Build multipart form data for image upload
-	 *
-	 * @param string $file_path Path to image file
-	 * @param string $square_image_id Square catalog image ID
-	 * @param string $boundary Multipart boundary
-	 * @return string Multipart body
-	 */
-	protected function buildMultipartBody(string $file_path, string $square_image_id, string $boundary): string
+	protected function buildMultipartBody(string $file_path, array $request_json, string $boundary): string
 	{
 		$eol = "\r\n";
 		$body = '';
 
-		// Add request JSON
-		$request_data = [
-			'idempotency_key' => wp_generate_uuid4(),
-			'object_id' => $square_image_id
-		];
-
+		// Add request JSON part
 		$body .= '--' . $boundary . $eol;
 		$body .= 'Content-Disposition: form-data; name="request"' . $eol;
 		$body .= 'Content-Type: application/json' . $eol . $eol;
-		$body .= json_encode($request_data) . $eol;
+		$body .= json_encode($request_json) . $eol;
 
-		// Add image file
+		// Add image file part
 		$filename = basename($file_path);
 		$file_contents = file_get_contents($file_path);
 		$mime_type = mime_content_type($file_path);
 
 		$body .= '--' . $boundary . $eol;
-		$body .= 'Content-Disposition: form-data; name="image"; filename="' . $filename . '"' . $eol;
+		$body .= 'Content-Disposition: form-data; name="file"; filename="' . $filename . '"' . $eol;
 		$body .= 'Content-Type: ' . $mime_type . $eol . $eol;
 		$body .= $file_contents . $eol;
-
-		// End boundary
 		$body .= '--' . $boundary . '--' . $eol;
 
 		return $body;
diff --git a/inc/managers/EmailManager.php b/inc/managers/EmailManager.php
index d7d0368..e351a99 100644
--- a/inc/managers/EmailManager.php
+++ b/inc/managers/EmailManager.php
@@ -369,7 +369,9 @@
                     <p><strong>Note:</strong>Even after approval by admin, your ability to publish depends on your karmic standing by artists. Artists each have a vote they can cast (UP or DOWN) - if your karmic score dips too far in the negative, you account is subject to reconsideration or even a ban.</p>';
             }
             $message .= '<div class="divider"></div>';
-            $message .= '<p>If you didn\'t create this account, please ignore this email and the link will expire.</p>';
+            $message = apply_filters(BASE.'new_user_email_content', $message, $user);
+
+			$message .= '<p>If you didn\'t create this account, please ignore this email and the link will expire.</p>';
             $message .= sprintf('<p>Ink on, %s</p>', $user->first_name);
             $message .= $this->signature;
             $wp_new_user_notification_email['message'] = $this->getEmailTemplate($message, 'Welcome to edmonton.ink');
diff --git a/inc/managers/ReferralManager.php b/inc/managers/ReferralManager.php
new file mode 100644
index 0000000..90671f2
--- /dev/null
+++ b/inc/managers/ReferralManager.php
@@ -0,0 +1,1036 @@
+<?php
+namespace JVBase\managers;
+
+use WP_User;
+use WP_Error;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Referral Tracking Manager
+ *
+ * Handles user referral codes, tracking, rewards, and reporting.
+ * Keeps referrals separate from invitations system.
+ */
+class ReferralManager
+{
+	protected $wpdb;
+	protected CacheManager $cache;
+	protected string $referrals_table;
+	protected string $rewards_table;
+
+	// Default reward settings
+	protected array $default_settings = [
+		'referrer_reward_applies_to' => 'per_user',  // 'per_user' or 'flat_total'
+		'referrer_reward_amount' => 25.00,
+		'referrer_reward_type'	=> 'fixed',
+		'referee_reward_type' => 'percentage',  // 'percentage' or 'fixed'
+		'referee_reward_amount' => 20,  // 20% or $20
+		'referee_reward_applies_to' => 'first_order'  // 'first_order' or 'all_orders'
+	];
+
+	public function __construct()
+	{
+		global $wpdb;
+		$this->wpdb = $wpdb;
+		$this->cache = new CacheManager('referrals');
+		$this->referrals_table = BASE . 'referrals';
+		$this->rewards_table = BASE . 'referral_rewards';
+
+		// Hook into user registration to track referrals
+		add_action('user_register', [$this, 'processReferral'], 10, 1);
+
+		// Add meta boxes for admin to manage referrals
+		add_action('show_user_profile', [$this, 'displayUserReferralInfo']);
+		add_action('edit_user_profile', [$this, 'displayUserReferralInfo']);
+
+		// Save referral code changes
+		add_action('personal_options_update', [$this, 'saveUserReferralCode']);
+		add_action('edit_user_profile_update', [$this, 'saveUserReferralCode']);
+
+		add_filter(BASE.'new_user_email_content', [$this, 'addReferralToWelcomeEmail'], 99, 2);
+
+		if (is_user_logged_in()) {
+			add_action('wp_footer', [$this, 'outputShareWidget']);
+		}
+
+		add_action('template_redirect', [$this, 'trackReferralCode']);;
+		// Schedule cron jobs for reports
+		$this->registerCronJobs();
+	}
+
+	/**
+	 * Register cron jobs for automated reporting
+	 */
+	protected function registerCronJobs(): void
+	{
+		add_action(BASE . 'referral_daily_report', [$this, 'sendDailyReport']);
+		add_action(BASE . 'referral_weekly_report', [$this, 'sendWeeklyReport']);
+
+		// Schedule if not already scheduled
+		if (!wp_next_scheduled(BASE . 'referral_daily_report')) {
+			wp_schedule_event(strtotime('tomorrow 9:00'), 'daily', BASE . 'referral_daily_report');
+		}
+
+		if (!wp_next_scheduled(BASE . 'referral_weekly_report')) {
+			wp_schedule_event(strtotime('next Monday 9:00'), 'weekly', BASE . 'referral_weekly_report');
+		}
+	}
+
+	/**
+	 * Generate or get existing referral code for a user
+	 *
+	 * @param int $user_id
+	 * @param string|null $custom_code Optional custom code
+	 * @return string|WP_Error
+	 */
+	public function getUserReferralCode(int $user_id, ?string $custom_code = null)
+	{
+		$user = get_user_by('ID', $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;
+		}
+
+		// Generate new code if custom provided or none exists
+		$code = $custom_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');
+		}
+
+		// Save the code
+		update_user_meta($user_id, BASE . 'referral_code', $code);
+
+		return $code;
+	}
+
+	/**
+	 * Generate a referral code based on user info
+	 *
+	 * @param WP_User $user
+	 * @return string
+	 */
+	protected function generateReferralCode(WP_User $user): string
+	{
+		// Create code from first and last name
+		$first = sanitize_title($user->first_name ?: 'user');
+		$last = sanitize_title($user->last_name ?: wp_generate_password(4, false));
+
+		$base_code = strtoupper(substr($first, 0, 4) . substr($last, 0, 4));
+
+		// Ensure uniqueness
+		$code = $base_code;
+		$suffix = 1;
+
+		while ($this->isCodeTaken($code)) {
+			$code = $base_code . $suffix;
+			$suffix++;
+		}
+
+		return $code;
+	}
+
+	/**
+	 * 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
+	{
+		$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);
+	}
+
+	/**
+	 * Track a new referral when user registers
+	 *
+	 * @param int $user_id
+	 */
+	public function processReferral(int $user_id): void
+	{
+		// Check if there's a referral code in the session/cookie
+		$referral_code = $this->getReferralCodeFromSession();
+
+		if (!$referral_code) {
+			return;
+		}
+
+		// Find the referrer
+		$referrer = $this->getUserByReferralCode($referral_code);
+
+		if (!$referrer) {
+			return;
+		}
+
+		// Check if this user was already referred (prevent duplicates)
+		$existing = $this->getReferralByReferee($user_id);
+		if ($existing) {
+			return;
+		}
+
+		// Create referral record
+		$this->createReferral($referrer->ID, $user_id, $referral_code);
+
+		// Clear the session
+		$this->clearReferralSession();
+	}
+
+	/**
+	 * Create a referral record in the database
+	 *
+	 * @param int $referrer_id
+	 * @param int $referee_id
+	 * @param string $code
+	 * @return int|false
+	 */
+	protected function createReferral(int $referrer_id, int $referee_id, string $code)
+	{
+		$user = get_user_by('ID', $referee_id);
+
+		return $this->wpdb->insert(
+			$this->referrals_table,
+			[
+				'referrer_id' => $referrer_id,
+				'referee_id' => $referee_id,
+				'referee_name' => $user->display_name,
+				'referee_email' => $user->user_email,
+				'referee_phone' => get_user_meta($referee_id, BASE . 'phone', true) ?: '',
+				'referral_code' => $code,
+				'status' => 'pending',
+				'referred_at' => current_time('mysql')
+			],
+			['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s']
+		);
+	}
+
+	/**
+	 * Get user by referral code
+	 *
+	 * @param string $code
+	 * @return WP_User|null
+	 */
+	protected function getUserByReferralCode(string $code): ?WP_User
+	{
+		$users = get_users([
+			'meta_key' => BASE . 'referral_code',
+			'meta_value' => $code,
+			'number' => 1
+		]);
+
+		return $users[0] ?? null;
+	}
+
+	/**
+	 * Get referral record by referee ID
+	 *
+	 * @param int $referee_id
+	 * @return object|null
+	 */
+	public function getReferralByReferee(int $referee_id): ?object
+	{
+		$result = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT * FROM {$this->referrals_table} WHERE referee_id = %d",
+			$referee_id
+		));
+
+		return $result ?: null;
+	}
+
+	/**
+	 * Mark a referral as treated/rewarded
+	 *
+	 * @param int $referral_id
+	 * @param bool $treated
+	 * @return bool
+	 */
+	public function markAsTreated(int $referral_id, bool $treated = true): bool
+	{
+		$status = $treated ? 'treated' : 'pending';
+
+		$result = $this->wpdb->update(
+			$this->referrals_table,
+			[
+				'status' => $status,
+				'treated_at' => $treated ? current_time('mysql') : null
+			],
+			['id' => $referral_id],
+			['%s', '%s'],
+			['%d']
+		);
+
+		if ($result && $treated) {
+			// Create reward records when marking as treated
+			$this->createRewardRecords($referral_id);
+		}
+
+		return $result !== false;
+	}
+
+	/**
+	 * Create reward records for both referrer and referee
+	 *
+	 * @param int $referral_id
+	 */
+	protected function createRewardRecords(int $referral_id): void
+	{
+		$referral = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT * FROM {$this->referrals_table} WHERE id = %d",
+			$referral_id
+		));
+
+		if (!$referral) {
+			return;
+		}
+
+		$settings = $this->getRewardSettings();
+
+		// Create referrer reward
+		$this->wpdb->insert(
+			$this->rewards_table,
+			[
+				'referral_id' => $referral_id,
+				'user_id' => $referral->referrer_id,
+				'reward_type' => 'referrer',
+				'amount' => $settings['referrer_reward_amount'],
+				'status' => 'available',
+				'created_at' => current_time('mysql')
+			],
+			['%d', '%d', '%s', '%f', '%s', '%s']
+		);
+
+		// Create referee reward
+		$referee_amount = $settings['referee_reward_type'] === 'percentage'
+			? $settings['referee_reward_amount']  // Store as percentage
+			: $settings['referee_reward_amount'];  // Store as fixed amount
+
+		$this->wpdb->insert(
+			$this->rewards_table,
+			[
+				'referral_id' => $referral_id,
+				'user_id' => $referral->referee_id,
+				'reward_type' => 'referee',
+				'amount' => $referee_amount,
+				'reward_calculation' => $settings['referee_reward_type'],
+				'status' => 'available',
+				'created_at' => current_time('mysql')
+			],
+			['%d', '%d', '%s', '%f', '%s', '%s', '%s']
+		);
+	}
+
+	/**
+	 * Get referrals for a user
+	 *
+	 * @param int $user_id
+	 * @param array $args
+	 * @return array
+	 */
+	public function getUserReferrals(int $user_id, array $args = []): array
+	{
+		$defaults = [
+			'status' => 'all',
+			'limit' => 100,
+			'offset' => 0,
+			'orderby' => 'referred_at',
+			'order' => 'DESC'
+		];
+
+		$args = wp_parse_args($args, $defaults);
+
+		$where = $this->wpdb->prepare("WHERE referrer_id = %d", $user_id);
+
+		if ($args['status'] !== 'all') {
+			$where .= $this->wpdb->prepare(" AND status = %s", $args['status']);
+		}
+
+		$query = "SELECT * FROM {$this->referrals_table}
+                  {$where}
+                  ORDER BY {$args['orderby']} {$args['order']}
+                  LIMIT {$args['limit']} OFFSET {$args['offset']}";
+
+		return $this->wpdb->get_results($query);
+	}
+
+	/**
+	 * Get referral statistics for a user
+	 *
+	 * @param int $user_id
+	 * @return array
+	 */
+	public function getUserStats(int $user_id): array
+	{
+		$cache_key = 'stats_' . $user_id;
+		$cached = $this->cache->get($cache_key);
+
+		if ($cached !== false) {
+			return $cached;
+		}
+
+		$stats = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT
+                COUNT(*) as total_referrals,
+                SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treated_count,
+                SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count
+            FROM {$this->referrals_table}
+            WHERE referrer_id = %d",
+			$user_id
+		), ARRAY_A);
+
+		// Get total rewards
+		$rewards = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT
+                SUM(CASE WHEN status = 'available' THEN amount ELSE 0 END) as available_rewards,
+                SUM(CASE WHEN status = 'redeemed' THEN amount ELSE 0 END) as redeemed_rewards
+            FROM {$this->rewards_table}
+            WHERE user_id = %d AND reward_type = 'referrer'",
+			$user_id
+		), ARRAY_A);
+
+		$stats = array_merge($stats, $rewards);
+
+		$this->cache->set($cache_key, $stats, HOUR_IN_SECONDS);
+
+		return $stats;
+	}
+
+	/**
+	 * Get top referrers for a time period
+	 *
+	 * @param int $limit
+	 * @param string $period 'day'|'week'|'month'|'all'
+	 * @return array
+	 */
+	public function getTopReferrers(int $limit = 10, string $period = 'all'): array
+	{
+		$where = '';
+
+		if ($period !== 'all') {
+			$date_where = match($period) {
+				'day' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)",
+				'week' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)",
+				'month' => "referred_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)",
+				default => "1=1"
+			};
+
+			$where = "WHERE {$date_where}";
+		}
+
+		$query = "SELECT
+                    referrer_id,
+                    COUNT(*) as referral_count,
+                    SUM(CASE WHEN status = 'treated' THEN 1 ELSE 0 END) as treated_count
+                  FROM {$this->referrals_table}
+                  {$where}
+                  GROUP BY referrer_id
+                  ORDER BY referral_count DESC
+                  LIMIT {$limit}";
+
+		$results = $this->wpdb->get_results($query);
+
+		// Enrich with user data
+		foreach ($results as &$result) {
+			$user = get_user_by('ID', $result->referrer_id);
+			$result->user_name = $user ? $user->display_name : 'Unknown';
+			$result->user_email = $user ? $user->user_email : '';
+		}
+
+		return $results;
+	}
+
+	/**
+	 * Send daily report if there are new referrals
+	 */
+	public function sendDailyReport(): void
+	{
+		// Get referrals from the last 24 hours
+		$referrals = $this->wpdb->get_results(
+			"SELECT r.*, u.display_name as referrer_name, u.user_email as referrer_email
+             FROM {$this->referrals_table} r
+             LEFT JOIN {$this->wpdb->users} u ON r.referrer_id = u.ID
+             WHERE r.referred_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)
+             ORDER BY r.referred_at DESC"
+		);
+
+		if (empty($referrals)) {
+			return;  // No referrals, no email
+		}
+
+		// Generate CSV
+		$csv_content = $this->generateCSV($referrals);
+		$csv_filename = 'referrals-' . date('Y-m-d') . '.csv';
+
+		// Save CSV temporarily
+		$upload_dir = wp_upload_dir();
+		$csv_path = $upload_dir['basedir'] . '/' . $csv_filename;
+		file_put_contents($csv_path, $csv_content);
+
+		// Send email with attachment
+		$to = get_option('admin_email');
+		$subject = '[' . get_bloginfo('name') . '] Daily Referral Report - ' . date('F j, Y');
+
+		$message = $this->generateReportEmail($referrals, 'daily');
+
+		$attachments = [$csv_path];
+
+		wp_mail($to, $subject, $message, ['Content-Type: text/html; charset=UTF-8'], $attachments);
+
+		// Clean up temporary file
+		unlink($csv_path);
+	}
+
+	/**
+	 * Send weekly report with top referrers
+	 */
+	public function sendWeeklyReport(): void
+	{
+		$top_referrers = $this->getTopReferrers(10, 'week');
+		$total_referrals = $this->wpdb->get_var(
+			"SELECT COUNT(*) FROM {$this->referrals_table}
+             WHERE referred_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)"
+		);
+
+		if ($total_referrals == 0) {
+			return;
+		}
+
+		$to = get_option('admin_email');
+		$subject = '[' . get_bloginfo('name') . '] Weekly Referral Summary - ' . date('F j, Y');
+
+		$message = $this->generateWeeklyReportEmail($top_referrers, $total_referrals);
+
+		wp_mail($to, $subject, $message, ['Content-Type: text/html; charset=UTF-8']);
+	}
+
+	/**
+	 * Generate CSV content from referrals
+	 *
+	 * @param array $referrals
+	 * @return string
+	 */
+	protected function generateCSV(array $referrals): string
+	{
+		$csv = "Referred By,Referee Name,Referee Email,Referee Phone,Referral Code,Status,Referred At,Treated At\n";
+
+		foreach ($referrals as $referral) {
+			$csv .= sprintf(
+				'"%s","%s","%s","%s","%s","%s","%s","%s"' . "\n",
+				$referral->referrer_name ?? 'Unknown',
+				$referral->referee_name,
+				$referral->referee_email,
+				$referral->referee_phone,
+				$referral->referral_code,
+				$referral->status,
+				$referral->referred_at,
+				$referral->treated_at ?? 'Not yet'
+			);
+		}
+
+		return $csv;
+	}
+
+	/**
+	 * Generate HTML email for daily report
+	 *
+	 * @param array $referrals
+	 * @param string $period
+	 * @return string
+	 */
+	protected function generateReportEmail(array $referrals, string $period): string
+	{
+		$count = count($referrals);
+
+		$content = sprintf('<p>You have <strong>%d new referral%s</strong> today.</p>',
+			$count,
+			$count !== 1 ? 's' : ''
+		);
+
+		$content .= '<table style="width:100%; border-collapse: collapse; margin: 20px 0;">';
+		$content .= '<thead><tr style="background: #f5f5f5; text-align: left;">';
+		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">Referred By</th>';
+		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">New User</th>';
+		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">Email</th>';
+		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">Status</th>';
+		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">Time</th>';
+		$content .= '</tr></thead><tbody>';
+
+		foreach ($referrals as $referral) {
+			$content .= '<tr>';
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
+				esc_html($referral->referrer_name ?? 'Unknown'));
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
+				esc_html($referral->referee_name));
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
+				esc_html($referral->referee_email));
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
+				esc_html(ucfirst($referral->status)));
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
+				esc_html(date('g:i A', strtotime($referral->referred_at))));
+			$content .= '</tr>';
+		}
+
+		$content .= '</tbody></table>';
+		$content .= '<p><small>See attached CSV for full details.</small></p>';
+
+		return jvbGetEmailTemplate($content, 'Daily Referral Report');
+	}
+
+	/**
+	 * Generate HTML email for weekly report
+	 *
+	 * @param array $top_referrers
+	 * @param int $total_referrals
+	 * @return string
+	 */
+	protected function generateWeeklyReportEmail(array $top_referrers, int $total_referrals): string
+	{
+		$content = sprintf(
+			'<p>This week you had <strong>%d total referral%s</strong>.</p>',
+			$total_referrals,
+			$total_referrals !== 1 ? 's' : ''
+		);
+
+		$content .= '<h3>Top 10 Referrers This Week</h3>';
+		$content .= '<table style="width:100%; border-collapse: collapse; margin: 20px 0;">';
+		$content .= '<thead><tr style="background: #f5f5f5; text-align: left;">';
+		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">Rank</th>';
+		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">User</th>';
+		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">Total Referrals</th>';
+		$content .= '<th style="padding: 10px; border: 1px solid #ddd;">Treated</th>';
+		$content .= '</tr></thead><tbody>';
+
+		$rank = 1;
+		foreach ($top_referrers as $referrer) {
+			$content .= '<tr>';
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%d</td>', $rank++);
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%s</td>',
+				esc_html($referrer->user_name));
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%d</td>',
+				$referrer->referral_count);
+			$content .= sprintf('<td style="padding: 10px; border: 1px solid #ddd;">%d</td>',
+				$referrer->treated_count);
+			$content .= '</tr>';
+		}
+
+		$content .= '</tbody></table>';
+
+		return jvbGetEmailTemplate($content, 'Weekly Referral Summary');
+	}
+
+	/**
+	 * Get reward settings
+	 *
+	 * @return array
+	 */
+	protected function getRewardSettings(): array
+	{
+		$saved = get_option(BASE . 'referral_settings', []);
+		return wp_parse_args($saved, $this->default_settings);
+	}
+
+	/**
+	 * Session/Cookie handling for referral codes
+	 */
+	protected function getReferralCodeFromSession(): ?string
+	{
+		if (session_status() === PHP_SESSION_NONE) {
+			session_start();
+		}
+
+		return $_SESSION[BASE . 'referral_code'] ?? $_COOKIE[BASE . 'referral_code'] ?? null;
+	}
+
+	protected function clearReferralSession(): void
+	{
+		if (session_status() === PHP_SESSION_NONE) {
+			session_start();
+		}
+
+		unset($_SESSION[BASE . 'referral_code']);
+		setcookie(BASE . 'referral_code', '', time() - 3600, '/');
+	}
+
+	/**
+	 * Display referral info in user profile
+	 *
+	 * @param WP_User $user
+	 */
+	public function displayUserReferralInfo(WP_User $user): void
+	{
+		if (!current_user_can('edit_user', $user->ID)) {
+			return;
+		}
+
+		$referral_code = get_user_meta($user->ID, BASE . 'referral_code', true);
+		$stats = $this->getUserStats($user->ID);
+		$referrals = $this->getUserReferrals($user->ID, ['limit' => 10]);
+
+		?>
+		<h2>Referral Information</h2>
+		<table class="form-table">
+			<tr>
+				<th><label for="referral_code">Referral Code</label></th>
+				<td>
+					<input type="text"
+						   name="referral_code"
+						   id="referral_code"
+						   value="<?php echo esc_attr($referral_code); ?>"
+						   class="regular-text" />
+					<p class="description">
+						Users can sign up with this code.
+						<?php if ($referral_code): ?>
+							Share link: <?php echo home_url('/?ref=' . $referral_code); ?>
+						<?php endif; ?>
+					</p>
+				</td>
+			</tr>
+		</table>
+
+		<h3>Referral Statistics</h3>
+		<table class="form-table">
+			<tr>
+				<th>Total Referrals:</th>
+				<td><?php echo $stats['total_referrals'] ?? 0; ?></td>
+			</tr>
+			<tr>
+				<th>Treated:</th>
+				<td><?php echo $stats['treated_count'] ?? 0; ?></td>
+			</tr>
+			<tr>
+				<th>Pending:</th>
+				<td><?php echo $stats['pending_count'] ?? 0; ?></td>
+			</tr>
+			<tr>
+				<th>Available Rewards:</th>
+				<td>$<?php echo number_format($stats['available_rewards'] ?? 0, 2); ?></td>
+			</tr>
+			<tr>
+				<th>Redeemed Rewards:</th>
+				<td>$<?php echo number_format($stats['redeemed_rewards'] ?? 0, 2); ?></td>
+			</tr>
+		</table>
+
+		<?php if (!empty($referrals)): ?>
+		<h3>Recent Referrals</h3>
+		<table class="widefat">
+			<thead>
+			<tr>
+				<th>Name</th>
+				<th>Email</th>
+				<th>Status</th>
+				<th>Referred At</th>
+				<th>Actions</th>
+			</tr>
+			</thead>
+			<tbody>
+			<?php foreach ($referrals as $referral): ?>
+				<tr>
+					<td><?php echo esc_html($referral->referee_name); ?></td>
+					<td><?php echo esc_html($referral->referee_email); ?></td>
+					<td><?php echo esc_html(ucfirst($referral->status)); ?></td>
+					<td><?php echo esc_html($referral->referred_at); ?></td>
+					<td>
+						<?php if ($referral->status === 'pending'): ?>
+							<button type="button"
+									onclick="markReferralTreated(<?php echo $referral->id; ?>)">
+								Mark as Treated
+							</button>
+						<?php endif; ?>
+					</td>
+				</tr>
+			<?php endforeach; ?>
+			</tbody>
+		</table>
+	<?php endif; ?>
+
+		<script>
+			function markReferralTreated(referralId) {
+				if (!confirm('Mark this referral as treated? This will create reward records.')) {
+					return;
+				}
+
+				fetch('<?php echo rest_url(BASE . '/v1/referrals/' ); ?>' + referralId + '/treat', {
+					method: 'POST',
+					headers: {
+						'X-WP-Nonce': '<?php echo wp_create_nonce('wp_rest'); ?>'
+					}
+				})
+					.then(response => response.json())
+					.then(data => {
+						if (data.success) {
+							alert('Referral marked as treated!');
+							location.reload();
+						} else {
+							alert('Error: ' + (data.message || 'Unknown error'));
+						}
+					});
+			}
+		</script>
+		<?php
+	}
+
+	/**
+	 * Save custom referral code
+	 *
+	 * @param int $user_id
+	 */
+	public function saveUserReferralCode(int $user_id): void
+	{
+		if (!current_user_can('edit_user', $user_id)) {
+			return;
+		}
+
+		if (!isset($_POST['referral_code'])) {
+			return;
+		}
+
+		$code = sanitize_text_field($_POST['referral_code']);
+
+		if (empty($code)) {
+			delete_user_meta($user_id, BASE . 'referral_code');
+			return;
+		}
+
+		// Validate code format (alphanumeric only)
+		if (!preg_match('/^[A-Z0-9]+$/i', $code)) {
+			add_action('user_profile_update_errors', function($errors) {
+				$errors->add('invalid_referral_code',
+					'Referral code can only contain letters and numbers.');
+			});
+			return;
+		}
+
+		// Check if code is unique
+		if ($this->isCodeTaken($code, $user_id)) {
+			add_action('user_profile_update_errors', function($errors) {
+				$errors->add('referral_code_taken',
+					'This referral code is already in use.');
+			});
+			return;
+		}
+
+		update_user_meta($user_id, BASE . 'referral_code', strtoupper($code));
+	}
+
+	public function trackReferralCode(): void
+	{
+		if (!isset($_GET['ref'])) {
+			return;
+		}
+
+		$referral_code = strtoupper(sanitize_text_field($_GET['ref']));
+
+		// Start session if not already started
+		if (session_status() === PHP_SESSION_NONE) {
+			session_start();
+		}
+
+		// Store in both session and cookie (30 day expiry)
+		$_SESSION[BASE . 'referral_code'] = $referral_code;
+		setcookie(BASE . 'referral_code', $referral_code, time() + (30 * DAY_IN_SECONDS), '/');
+
+		// Optional: Redirect to clean URL (removes ?ref= from address bar)
+		$clean_url = remove_query_arg('ref');
+		wp_safe_redirect($clean_url);
+		exit;
+	}
+
+	/**
+	 * Display user's referral code and share options
+	 * Use in templates or dashboard with: echo jvbReferralShareWidget();
+	 *
+	 * @return string HTML output
+	 */
+	public function outputShareWidget(): string
+	{
+		$user_id = get_current_user_id();
+
+		if (!$user_id) {
+			return '';
+		}
+
+		$referral_code = get_user_meta($user_id, BASE . 'referral_code', true);
+
+		// Generate code if user doesn't have one
+		if (empty($referral_code)) {
+			$manager = new \JVBase\managers\ReferralManager();
+			$referral_code = $manager->getUserReferralCode($user_id);
+		}
+
+		$share_url = home_url('/?ref=' . $referral_code);
+		$encoded_url = urlencode($share_url);
+		$site_name = get_bloginfo('name');
+
+		ob_start();
+		?>
+		<div class="jvb-referral-widget" style="background: #f9f9f9; padding: 20px; border-radius: 8px; margin: 20px 0;">
+			<h3 style="margin-top: 0;">Share & Earn Rewards</h3>
+			<p>Share your unique referral code with friends and earn rewards when they book!</p>
+
+			<div class="referral-code-display" style="background: white; padding: 15px; border-radius: 4px; margin: 15px 0; text-align: center;">
+				<label style="display: block; font-size: 12px; color: #666; margin-bottom: 5px;">Your Referral Code</label>
+				<div style="font-size: 24px; font-weight: bold; letter-spacing: 2px; color: #2271b1;">
+					<?php echo esc_html($referral_code); ?>
+				</div>
+			</div>
+
+			<div class="referral-url" style="margin: 15px 0;">
+				<label style="display: block; font-size: 12px; color: #666; margin-bottom: 5px;">Share Link</label>
+				<div style="display: flex; gap: 10px;">
+					<input type="text"
+						   readonly
+						   value="<?php echo esc_url($share_url); ?>"
+						   id="referral-url-<?php echo $user_id; ?>"
+						   style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-family: monospace; font-size: 14px;">
+					<button type="button"
+							onclick="jvbCopyReferralUrl('referral-url-<?php echo $user_id; ?>')"
+							style="padding: 8px 16px; background: #2271b1; color: white; border: none; border-radius: 4px; cursor: pointer;">
+						Copy
+					</button>
+				</div>
+			</div>
+
+			<div class="referral-share-buttons" style="display: flex; gap: 10px; margin-top: 15px; flex-wrap: wrap;">
+				<a href="mailto:?subject=Check out <?php echo esc_attr($site_name); ?>&body=I thought you might like <?php echo esc_url($share_url); ?>"
+				   class="share-button"
+				   style="padding: 10px 20px; background: #666; color: white; text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;">
+					📧 Email
+				</a>
+				<a href="sms:?&body=Check out <?php echo esc_attr($site_name); ?>: <?php echo esc_url($share_url); ?>"
+				   class="share-button"
+				   style="padding: 10px 20px; background: #25D366; color: white; text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;">
+					💬 Text
+				</a>
+				<a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo $encoded_url; ?>"
+				   target="_blank"
+				   class="share-button"
+				   style="padding: 10px 20px; background: #1877f2; color: white; text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;">
+					f Facebook
+				</a>
+				<a href="https://twitter.com/intent/tweet?url=<?php echo $encoded_url; ?>&text=Check out <?php echo esc_attr($site_name); ?>"
+				   target="_blank"
+				   class="share-button"
+				   style="padding: 10px 20px; background: #1da1f2; color: white; text-decoration: none; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;">
+					𝕏 Twitter
+				</a>
+			</div>
+
+			<div id="referral-stats-<?php echo $user_id; ?>" class="referral-stats" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;">
+				<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 15px; text-align: center;">
+					<div>
+						<div style="font-size: 24px; font-weight: bold; color: #2271b1;" data-stat="total">-</div>
+						<div style="font-size: 12px; color: #666;">Total Referrals</div>
+					</div>
+					<div>
+						<div style="font-size: 24px; font-weight: bold; color: #00a32a;" data-stat="treated">-</div>
+						<div style="font-size: 12px; color: #666;">Completed</div>
+					</div>
+					<div>
+						<div style="font-size: 24px; font-weight: bold; color: #dba617;" data-stat="pending">-</div>
+						<div style="font-size: 12px; color: #666;">Pending</div>
+					</div>
+					<div>
+						<div style="font-size: 24px; font-weight: bold; color: #2271b1;" data-stat="rewards">$0</div>
+						<div style="font-size: 12px; color: #666;">Earned</div>
+					</div>
+				</div>
+			</div>
+		</div>
+
+		<script>
+			function jvbCopyReferralUrl(elementId) {
+				const input = document.getElementById(elementId);
+				input.select();
+				document.execCommand('copy');
+
+				// Visual feedback
+				const button = input.nextElementSibling;
+				const originalText = button.textContent;
+				button.textContent = 'Copied!';
+				button.style.background = '#00a32a';
+
+				setTimeout(() => {
+					button.textContent = originalText;
+					button.style.background = '#2271b1';
+				}, 2000);
+			}
+
+			// Load stats via AJAX
+			(function() {
+				fetch('<?php echo rest_url(BASE . '/v1/referrals/stats'); ?>', {
+					headers: {
+						'X-WP-Nonce': '<?php echo wp_create_nonce('wp_rest'); ?>'
+					}
+				})
+					.then(response => response.json())
+					.then(data => {
+						if (data.success && data.stats) {
+							const container = document.getElementById('referral-stats-<?php echo $user_id; ?>');
+							container.querySelector('[data-stat="total"]').textContent = data.stats.total_referrals || 0;
+							container.querySelector('[data-stat="treated"]').textContent = data.stats.treated_count || 0;
+							container.querySelector('[data-stat="pending"]').textContent = data.stats.pending_count || 0;
+							container.querySelector('[data-stat="rewards"]').textContent =
+								'$' + parseFloat(data.stats.available_rewards || 0).toFixed(2);
+						}
+					})
+					.catch(error => console.error('Error loading referral stats:', error));
+			})();
+		</script>
+		<?php
+		return ob_get_clean();
+	}
+
+	function addReferralToWelcomeEmail(string $content, WP_User $user): string
+	{
+		$referral = $this->getReferralByReferee($user->ID);
+
+		if (!$referral) {
+			return $content;
+		}
+
+		$settings = get_option(BASE . 'referral_settings', []);
+		$reward_amount = $settings['referee_reward_amount'] ?? 20;
+		$reward_type = $settings['referee_reward_type'] ?? 'percentage';
+
+		$reward_text = $reward_type === 'percentage'
+			? $reward_amount . '% off'
+			: '$' . number_format($reward_amount, 2) . ' off';
+
+		$bonus_content = '<div style="background: #e7f5ff; padding: 20px; border-radius: 8px; margin: 20px 0;">';
+		$bonus_content .= '<h3 style="margin-top: 0; color: #2271b1;">🎉 Welcome Bonus!</h3>';
+		$bonus_content .= '<p>Since you were referred by a friend, you\'ve earned <strong>' . $reward_text . '</strong> your first booking!</p>';
+		$bonus_content .= '<p>Your reward will be automatically applied when you book.</p>';
+		$bonus_content .= '</div>';
+
+		// Insert bonus content after the first paragraph
+		$parts = explode('</p>', $content, 2);
+		if (count($parts) === 2) {
+			return $parts[0] . '</p>' . $bonus_content . $parts[1];
+		}
+
+		return $content . $bonus_content;
+	}
+}
+
diff --git a/inc/managers/ReferralManager2.php b/inc/managers/ReferralManager2.php
new file mode 100644
index 0000000..3f299fc
--- /dev/null
+++ b/inc/managers/ReferralManager2.php
@@ -0,0 +1,1006 @@
+<?php
+namespace JVBase\managers;
+
+use JVBase\JVB;
+use WP_Error;
+use WP_REST_Response;
+use Exception;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Referral Tracking System
+ *
+ * Manages user referral codes, tracking, and rewards
+ * Uses existing infrastructure: MetaManager, CacheManager, NotificationManager
+ */
+class ReferralManager
+{
+	protected $wpdb;
+	protected $cache;
+	protected $table_codes;
+	protected $table_usage;
+	protected $table_rewards;
+
+	// Default reward settings
+	const DEFAULT_REFERRER_REWARD_TYPE = 'per_user'; // or 'flat_total'
+	const DEFAULT_REFERRER_REWARD_AMOUNT = 25.00;
+	const DEFAULT_REFERRED_REWARD_TYPE = 'percentage'; // or 'fixed'
+	const DEFAULT_REFERRED_REWARD_AMOUNT = 20; // 20% or $20
+
+	public function __construct()
+	{
+		global $wpdb;
+		$this->wpdb = $wpdb;
+		$this->cache = JVB()->cache();
+
+		$this->table_codes = $wpdb->prefix . BASE . 'referral_codes';
+		$this->table_usage = $wpdb->prefix . BASE . 'referral_usage';
+		$this->table_rewards = $wpdb->prefix . BASE . 'referral_rewards';
+
+		$this->registerHooks();
+	}
+
+	/**
+	 * Register WordPress hooks
+	 */
+	protected function registerHooks(): void
+	{
+		// Track new user registrations with referral codes
+		add_action('user_register', [$this, 'trackReferralRegistration'], 10, 2);
+
+		// Monthly report cron
+		add_action(BASE . 'referral_monthly_report', [$this, 'generateMonthlyReports']);
+
+		// Cleanup expired codes
+		add_action(BASE . 'cleanup_referrals', [$this, 'cleanupExpiredCodes']);
+
+		// Handle bulk operations
+		add_filter(BASE . 'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
+	}
+
+	/************************************************************
+	 * Referral Code Management
+	 ************************************************************/
+
+	/**
+	 * Create or update a user's referral code
+	 *
+	 * @param int $user_id User ID
+	 * @param string|null $custom_code Optional custom code (must be unique)
+	 * @return array|WP_Error
+	 */
+	public function createReferralCode(int $user_id, ?string $custom_code = null): array|WP_Error
+	{
+		// Validate user
+		if (!$this->validateUser($user_id)) {
+			return new WP_Error('invalid_user', 'Invalid user ID');
+		}
+
+		// Check if user already has a code
+		$existing = $this->getUserReferralCode($user_id);
+
+		if ($existing && !$custom_code) {
+			return $existing; // Return existing code if no custom code requested
+		}
+
+		// Generate or validate custom code
+		$code = $custom_code ? $this->sanitizeCode($custom_code) : $this->generateUniqueCode($user_id);
+
+		// Check if code is already taken
+		if ($this->isCodeTaken($code, $user_id)) {
+			return new WP_Error('code_taken', 'This referral code is already in use');
+		}
+
+		// Validate code format
+		if (!$this->validateCodeFormat($code)) {
+			return new WP_Error('invalid_format', 'Code must be 4-20 alphanumeric characters');
+		}
+
+		$data = [
+			'user_id' => $user_id,
+			'code' => $code,
+			'is_active' => 1,
+			'created_at' => current_time('mysql'),
+			'updated_at' => current_time('mysql')
+		];
+
+		if ($existing) {
+			// Update existing code
+			$result = $this->wpdb->update(
+				$this->table_codes,
+				['code' => $code, 'updated_at' => current_time('mysql')],
+				['user_id' => $user_id]
+			);
+		} else {
+			// Insert new code
+			$result = $this->wpdb->insert($this->table_codes, $data);
+		}
+
+		if ($result === false) {
+			return new WP_Error('db_error', 'Failed to save referral code');
+		}
+
+		// Clear cache
+		$this->cache->delete('referral_code_' . $user_id);
+		$this->cache->delete('referral_user_' . $code);
+
+		return [
+			'success' => true,
+			'code' => $code,
+			'url' => $this->getReferralUrl($code)
+		];
+	}
+
+	/**
+	 * Get user's referral code
+	 *
+	 * @param int $user_id User ID
+	 * @return array|null
+	 */
+	public function getUserReferralCode(int $user_id): ?array
+	{
+		$cache_key = 'referral_code_' . $user_id;
+		$cached = $this->cache->get($cache_key);
+
+		if ($cached !== false) {
+			return $cached;
+		}
+
+		$result = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT * FROM {$this->table_codes} WHERE user_id = %d",
+			$user_id
+		), ARRAY_A);
+
+		if ($result) {
+			$result['url'] = $this->getReferralUrl($result['code']);
+			$result['stats'] = $this->getCodeStats($result['code']);
+			$this->cache->set($cache_key, $result, 3600);
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Get referral code statistics
+	 *
+	 * @param string $code Referral code
+	 * @return array
+	 */
+	public function getCodeStats(string $code): array
+	{
+		$cache_key = 'referral_stats_' . $code;
+		$cached = $this->cache->get($cache_key);
+
+		if ($cached !== false) {
+			return $cached;
+		}
+
+		$stats = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT
+                COUNT(*) as total_uses,
+                COUNT(CASE WHEN registered_at IS NOT NULL THEN 1 END) as completed_registrations,
+                COUNT(CASE WHEN first_order_at IS NOT NULL THEN 1 END) as converted_orders
+             FROM {$this->table_usage}
+             WHERE referral_code = %s",
+			$code
+		), ARRAY_A);
+
+		$this->cache->set($cache_key, $stats, 1800);
+		return $stats ?: ['total_uses' => 0, 'completed_registrations' => 0, 'converted_orders' => 0];
+	}
+
+	/**
+	 * Get user ID from referral code
+	 *
+	 * @param string $code Referral code
+	 * @return int|null User ID or null
+	 */
+	public function getUserFromCode(string $code): ?int
+	{
+		$cache_key = 'referral_user_' . $code;
+		$cached = $this->cache->get($cache_key);
+
+		if ($cached !== false) {
+			return $cached;
+		}
+
+		$user_id = $this->wpdb->get_var($this->wpdb->prepare(
+			"SELECT user_id FROM {$this->table_codes}
+             WHERE code = %s AND is_active = 1",
+			$code
+		));
+
+		if ($user_id) {
+			$this->cache->set($cache_key, (int)$user_id, 3600);
+			return (int)$user_id;
+		}
+
+		return null;
+	}
+
+	/************************************************************
+	 * Referral Tracking
+	 ************************************************************/
+
+	/**
+	 * Track when someone clicks a referral link
+	 *
+	 * @param string $code Referral code
+	 * @param string|null $email Optional email if user provides it
+	 * @return array|WP_Error
+	 */
+	public function trackReferralClick(string $code, ?string $email = null): array|WP_Error
+	{
+		$user_id = $this->getUserFromCode($code);
+
+		if (!$user_id) {
+			return new WP_Error('invalid_code', 'Invalid referral code');
+		}
+
+		// Check if this email/IP already used this code recently (prevent duplicate tracking)
+		$ip_address = $this->getClientIp();
+
+		$existing = $this->wpdb->get_var($this->wpdb->prepare(
+			"SELECT id FROM {$this->table_usage}
+             WHERE referral_code = %s
+             AND (email = %s OR ip_address = %s)
+             AND clicked_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)",
+			$code,
+			$email ?: '',
+			$ip_address
+		));
+
+		if ($existing) {
+			return ['success' => true, 'message' => 'Already tracked'];
+		}
+
+		// Track the click
+		$data = [
+			'referral_code' => $code,
+			'referrer_user_id' => $user_id,
+			'email' => $email,
+			'ip_address' => $ip_address,
+			'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
+			'clicked_at' => current_time('mysql')
+		];
+
+		$result = $this->wpdb->insert($this->table_usage, $data);
+
+		if ($result === false) {
+			return new WP_Error('db_error', 'Failed to track referral');
+		}
+
+		// Store in cookie for 30 days
+		setcookie('jvb_referral', $code, time() + (86400 * 30), '/');
+
+		return [
+			'success' => true,
+			'tracking_id' => $this->wpdb->insert_id
+		];
+	}
+
+	/**
+	 * Track referral when user registers
+	 *
+	 * @param int $new_user_id Newly registered user ID
+	 * @param array $userdata User data
+	 * @return void
+	 */
+	public function trackReferralRegistration(int $new_user_id, array $userdata = []): void
+	{
+		// Check for referral code in cookie or GET parameter
+		$code = $_COOKIE['jvb_referral'] ?? $_GET['ref'] ?? null;
+
+		if (!$code) {
+			return;
+		}
+
+		$user = get_userdata($new_user_id);
+		if (!$user) {
+			return;
+		}
+
+		// Update or create usage record
+		$usage = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT * FROM {$this->table_usage}
+             WHERE referral_code = %s
+             AND (email = %s OR ip_address = %s)
+             ORDER BY clicked_at DESC LIMIT 1",
+			$code,
+			$user->user_email,
+			$this->getClientIp()
+		), ARRAY_A);
+
+		if ($usage) {
+			// Update existing record
+			$this->wpdb->update(
+				$this->table_usage,
+				[
+					'referred_user_id' => $new_user_id,
+					'email' => $user->user_email,
+					'registered_at' => current_time('mysql')
+				],
+				['id' => $usage['id']]
+			);
+		} else {
+			// Create new record (direct registration with code)
+			$referrer_id = $this->getUserFromCode($code);
+
+			if ($referrer_id) {
+				$this->wpdb->insert($this->table_usage, [
+					'referral_code' => $code,
+					'referrer_user_id' => $referrer_id,
+					'referred_user_id' => $new_user_id,
+					'email' => $user->user_email,
+					'ip_address' => $this->getClientIp(),
+					'clicked_at' => current_time('mysql'),
+					'registered_at' => current_time('mysql')
+				]);
+			}
+		}
+
+		// Clear cache
+		$this->cache->delete('referral_stats_' . $code);
+
+		// Notify referrer
+		if (isset($referrer_id) && $referrer_id) {
+			$this->notifyReferrer($referrer_id, $new_user_id);
+		}
+	}
+
+	/**
+	 * Track when referred user makes first order
+	 *
+	 * @param int $user_id User who made order
+	 * @param float $order_amount Order amount
+	 * @return void
+	 */
+	public function trackFirstOrder(int $user_id, float $order_amount): void
+	{
+		$usage = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT * FROM {$this->table_usage}
+             WHERE referred_user_id = %d
+             AND first_order_at IS NULL",
+			$user_id
+		), ARRAY_A);
+
+		if (!$usage) {
+			return;
+		}
+
+		// Update usage record
+		$this->wpdb->update(
+			$this->table_usage,
+			[
+				'first_order_at' => current_time('mysql'),
+				'first_order_amount' => $order_amount
+			],
+			['id' => $usage['id']]
+		);
+
+		// Process rewards
+		$this->processRewards($usage['referrer_user_id'], $user_id, $order_amount);
+
+		// Clear cache
+		$this->cache->delete('referral_stats_' . $usage['referral_code']);
+	}
+
+	/************************************************************
+	 * Reward Management
+	 ************************************************************/
+
+	/**
+	 * Process referral rewards
+	 *
+	 * @param int $referrer_id User who referred
+	 * @param int $referred_id User who was referred
+	 * @param float $order_amount First order amount
+	 * @return void
+	 */
+	protected function processRewards(int $referrer_id, int $referred_id, float $order_amount): void
+	{
+		// Get reward settings
+		$settings = $this->getRewardSettings();
+
+		// Calculate referrer reward
+		$referrer_amount = $this->calculateReferrerReward($referrer_id, $settings);
+
+		if ($referrer_amount > 0) {
+			$this->addReward($referrer_id, 'referrer', $referrer_amount, $referred_id);
+		}
+
+		// Calculate referred user reward (already applied at checkout)
+		$referred_amount = $this->calculateReferredReward($order_amount, $settings);
+
+		if ($referred_amount > 0) {
+			$this->addReward($referred_id, 'referred', $referred_amount, $referrer_id);
+		}
+	}
+
+	/**
+	 * Calculate referrer reward amount
+	 *
+	 * @param int $referrer_id Referrer user ID
+	 * @param array $settings Reward settings
+	 * @return float Reward amount
+	 */
+	protected function calculateReferrerReward(int $referrer_id, array $settings): float
+	{
+		$type = $settings['referrer_reward_type'];
+		$amount = floatval($settings['referrer_reward_amount']);
+
+		if ($type === 'per_user') {
+			return $amount;
+		}
+
+		// For 'flat_total', check if total reward cap reached
+		$total_earned = $this->getTotalRewardsEarned($referrer_id, 'referrer');
+
+		if ($total_earned >= $amount) {
+			return 0; // Cap reached
+		}
+
+		return min($settings['referrer_reward_per_user'] ?? 25.00, $amount - $total_earned);
+	}
+
+	/**
+	 * Calculate referred user reward
+	 *
+	 * @param float $order_amount Order amount
+	 * @param array $settings Reward settings
+	 * @return float Discount amount
+	 */
+	protected function calculateReferredReward(float $order_amount, array $settings): float
+	{
+		$type = $settings['referred_reward_type'];
+		$amount = floatval($settings['referred_reward_amount']);
+
+		if ($type === 'percentage') {
+			return $order_amount * ($amount / 100);
+		}
+
+		return min($amount, $order_amount); // Fixed amount, but not more than order
+	}
+
+	/**
+	 * Add reward to user's account
+	 *
+	 * @param int $user_id User receiving reward
+	 * @param string $type 'referrer' or 'referred'
+	 * @param float $amount Reward amount
+	 * @param int $related_user_id Related user ID
+	 * @return bool
+	 */
+	protected function addReward(int $user_id, string $type, float $amount, int $related_user_id): bool
+	{
+		$data = [
+			'user_id' => $user_id,
+			'reward_type' => $type,
+			'amount' => $amount,
+			'related_user_id' => $related_user_id,
+			'status' => 'pending',
+			'created_at' => current_time('mysql')
+		];
+
+		$result = $this->wpdb->insert($this->table_rewards, $data);
+
+		if ($result) {
+			// Notify user
+			$notification_type = $type === 'referrer' ? 'referral_reward_earned' : 'referral_reward_received';
+			JVB()->notification()->addNotification(
+				$user_id,
+				$notification_type,
+				null,
+				sprintf('You earned $%.2f in referral rewards!', $amount)
+			);
+
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Get total rewards earned by user
+	 *
+	 * @param int $user_id User ID
+	 * @param string|null $type Optional reward type filter
+	 * @return float Total amount
+	 */
+	public function getTotalRewardsEarned(int $user_id, ?string $type = null): float
+	{
+		$sql = "SELECT SUM(amount) FROM {$this->table_rewards} WHERE user_id = %d";
+		$params = [$user_id];
+
+		if ($type) {
+			$sql .= " AND reward_type = %s";
+			$params[] = $type;
+		}
+
+		$total = $this->wpdb->get_var($this->wpdb->prepare($sql, $params));
+		return floatval($total);
+	}
+
+	/**
+	 * Get user's available reward balance
+	 *
+	 * @param int $user_id User ID
+	 * @return float Available balance
+	 */
+	public function getAvailableBalance(int $user_id): float
+	{
+		$total = $this->wpdb->get_var($this->wpdb->prepare(
+			"SELECT SUM(amount) FROM {$this->table_rewards}
+             WHERE user_id = %d AND status IN ('pending', 'available')",
+			$user_id
+		));
+
+		return floatval($total);
+	}
+
+	/************************************************************
+	 * Monthly Reports
+	 ************************************************************/
+
+	/**
+	 * Generate monthly reports for all users with referrals
+	 *
+	 * @return void
+	 */
+	public function generateMonthlyReports(): void
+	{
+		$first_day = date('Y-m-01', strtotime('last month'));
+		$last_day = date('Y-m-t', strtotime('last month'));
+
+		// Get all users who had referral activity last month
+		$users = $this->wpdb->get_col($this->wpdb->prepare(
+			"SELECT DISTINCT referrer_user_id
+             FROM {$this->table_usage}
+             WHERE registered_at BETWEEN %s AND %s
+             OR first_order_at BETWEEN %s AND %s",
+			$first_day, $last_day, $first_day, $last_day
+		));
+
+		if (empty($users)) {
+			return;
+		}
+
+		// Queue report generation
+		$queue = JVB()->queue();
+		$queue->queueOperation(
+			'generate_referral_report',
+			0,
+			[
+				'users' => $users,
+				'period_start' => $first_day,
+				'period_end' => $last_day
+			],
+			[
+				'count' => count($users),
+				'chunk_key' => 'users',
+				'chunk_size' => 10,
+				'priority' => 'low'
+			]
+		);
+	}
+
+	/**
+	 * Generate report for a single user
+	 *
+	 * @param int $user_id User ID
+	 * @param string $period_start Start date
+	 * @param string $period_end End date
+	 * @return array|WP_Error
+	 */
+	public function generateUserReport(int $user_id, string $period_start, string $period_end): array|WP_Error
+	{
+		$user = get_userdata($user_id);
+		if (!$user) {
+			return new WP_Error('invalid_user', 'Invalid user');
+		}
+
+		$code = $this->getUserReferralCode($user_id);
+		if (!$code) {
+			return new WP_Error('no_code', 'User has no referral code');
+		}
+
+		// Get activity for period
+		$activity = $this->wpdb->get_results($this->wpdb->prepare(
+			"SELECT * FROM {$this->table_usage}
+             WHERE referrer_user_id = %d
+             AND (
+                (registered_at BETWEEN %s AND %s)
+                OR (first_order_at BETWEEN %s AND %s)
+             )
+             ORDER BY registered_at DESC",
+			$user_id, $period_start, $period_end, $period_start, $period_end
+		), ARRAY_A);
+
+		// Generate CSV
+		$csv_path = $this->generateActivityCSV($user_id, $activity, $period_start, $period_end);
+
+		// Send email with CSV attachment
+		$this->sendMonthlyReportEmail($user, $activity, $csv_path, $period_start, $period_end);
+
+		return [
+			'success' => true,
+			'user_id' => $user_id,
+			'activity_count' => count($activity)
+		];
+	}
+
+	/**
+	 * Generate CSV file for activity
+	 *
+	 * @param int $user_id User ID
+	 * @param array $activity Activity records
+	 * @param string $period_start Start date
+	 * @param string $period_end End date
+	 * @return string File path
+	 */
+	protected function generateActivityCSV(int $user_id, array $activity, string $period_start, string $period_end): string
+	{
+		$upload_dir = wp_upload_dir();
+		$filename = sprintf(
+			'referral-report-%d-%s-to-%s.csv',
+			$user_id,
+			$period_start,
+			$period_end
+		);
+		$filepath = $upload_dir['basedir'] . '/referral-reports/' . $filename;
+
+		// Create directory if needed
+		wp_mkdir_p(dirname($filepath));
+
+		$fp = fopen($filepath, 'w');
+
+		// Headers
+		fputcsv($fp, [
+			'Date',
+			'Type',
+			'Email',
+			'User ID',
+			'Status',
+			'Order Amount',
+			'Reward Earned'
+		]);
+
+		// Data rows
+		foreach ($activity as $record) {
+			$type = $record['registered_at'] ? 'Registration' : 'Click';
+			if ($record['first_order_at']) {
+				$type = 'First Order';
+			}
+
+			fputcsv($fp, [
+				$record['registered_at'] ?? $record['clicked_at'],
+				$type,
+				$record['email'],
+				$record['referred_user_id'] ?? 'N/A',
+				$record['first_order_at'] ? 'Converted' : ($record['registered_at'] ? 'Registered' : 'Pending'),
+				$record['first_order_amount'] ?? 'N/A',
+				$this->getRewardForUsage($record['id'])
+			]);
+		}
+
+		fclose($fp);
+		return $filepath;
+	}
+
+	/**
+	 * Get reward amount for usage record
+	 *
+	 * @param int $usage_id Usage ID
+	 * @return string Formatted amount or N/A
+	 */
+	protected function getRewardForUsage(int $usage_id): string
+	{
+		$usage = $this->wpdb->get_row($this->wpdb->prepare(
+			"SELECT * FROM {$this->table_usage} WHERE id = %d",
+			$usage_id
+		), ARRAY_A);
+
+		if (!$usage || !$usage['referred_user_id']) {
+			return 'N/A';
+		}
+
+		$reward = $this->wpdb->get_var($this->wpdb->prepare(
+			"SELECT amount FROM {$this->table_rewards}
+             WHERE user_id = %d AND related_user_id = %d
+             AND reward_type = 'referrer'",
+			$usage['referrer_user_id'],
+			$usage['referred_user_id']
+		));
+
+		return $reward ? '$' . number_format($reward, 2) : 'N/A';
+	}
+
+	/**
+	 * Send monthly report email
+	 *
+	 * @param object $user User object
+	 * @param array $activity Activity records
+	 * @param string $csv_path Path to CSV file
+	 * @param string $period_start Start date
+	 * @param string $period_end End date
+	 * @return bool
+	 */
+	protected function sendMonthlyReportEmail($user, array $activity, string $csv_path, string $period_start, string $period_end): bool
+	{
+		$total_clicks = count(array_filter($activity, fn($a) => !empty($a['clicked_at'])));
+		$total_registrations = count(array_filter($activity, fn($a) => !empty($a['registered_at'])));
+		$total_orders = count(array_filter($activity, fn($a) => !empty($a['first_order_at'])));
+
+		$total_earned = $this->wpdb->get_var($this->wpdb->prepare(
+			"SELECT SUM(r.amount) FROM {$this->table_rewards} r
+             INNER JOIN {$this->table_usage} u ON r.related_user_id = u.referred_user_id
+             WHERE r.user_id = %d
+             AND r.reward_type = 'referrer'
+             AND r.created_at BETWEEN %s AND %s",
+			$user->ID, $period_start, $period_end
+		));
+
+		$subject = sprintf(
+			'Your Referral Report for %s',
+			date('F Y', strtotime($period_start))
+		);
+
+		$message = sprintf(
+			"Hi %s,\n\n" .
+			"Here's your referral activity summary for %s:\n\n" .
+			"📊 Activity Overview:\n" .
+			"- Clicks: %d\n" .
+			"- New Registrations: %d\n" .
+			"- First Orders: %d\n" .
+			"- Total Earned: $%.2f\n\n" .
+			"Your current reward balance: $%.2f\n\n" .
+			"Detailed activity is attached as a CSV file.\n\n" .
+			"Keep sharing your referral link to earn more rewards!\n" .
+			"Your link: %s\n\n" .
+			"Thanks,\n%s",
+			$user->display_name,
+			date('F Y', strtotime($period_start)),
+			$total_clicks,
+			$total_registrations,
+			$total_orders,
+			floatval($total_earned),
+			$this->getAvailableBalance($user->ID),
+			$this->getReferralUrl($this->getUserReferralCode($user->ID)['code']),
+			get_bloginfo('name')
+		);
+
+		return wp_mail(
+			$user->user_email,
+			$subject,
+			$message,
+			['Content-Type: text/plain; charset=UTF-8'],
+			[$csv_path]
+		);
+	}
+
+	/************************************************************
+	 * Settings & Configuration
+	 ************************************************************/
+
+	/**
+	 * Get referral reward settings
+	 *
+	 * @return array Settings
+	 */
+	public function getRewardSettings(): array
+	{
+		$defaults = [
+			'referrer_reward_type' => self::DEFAULT_REFERRER_REWARD_TYPE,
+			'referrer_reward_amount' => self::DEFAULT_REFERRER_REWARD_AMOUNT,
+			'referrer_reward_per_user' => self::DEFAULT_REFERRER_REWARD_AMOUNT,
+			'referred_reward_type' => self::DEFAULT_REFERRED_REWARD_TYPE,
+			'referred_reward_amount' => self::DEFAULT_REFERRED_REWARD_AMOUNT
+		];
+
+		// Get from options (can be customized in admin settings)
+		$saved = get_option(BASE . 'referral_settings', []);
+
+		return array_merge($defaults, $saved);
+	}
+
+	/**
+	 * Update referral reward settings
+	 *
+	 * @param array $settings New settings
+	 * @return bool
+	 */
+	public function updateRewardSettings(array $settings): bool
+	{
+		$valid_settings = [];
+
+		if (isset($settings['referrer_reward_type'])) {
+			$valid_settings['referrer_reward_type'] = in_array($settings['referrer_reward_type'], ['per_user', 'flat_total'])
+				? $settings['referrer_reward_type']
+				: self::DEFAULT_REFERRER_REWARD_TYPE;
+		}
+
+		if (isset($settings['referrer_reward_amount'])) {
+			$valid_settings['referrer_reward_amount'] = max(0, floatval($settings['referrer_reward_amount']));
+		}
+
+		if (isset($settings['referrer_reward_per_user'])) {
+			$valid_settings['referrer_reward_per_user'] = max(0, floatval($settings['referrer_reward_per_user']));
+		}
+
+		if (isset($settings['referred_reward_type'])) {
+			$valid_settings['referred_reward_type'] = in_array($settings['referred_reward_type'], ['percentage', 'fixed'])
+				? $settings['referred_reward_type']
+				: self::DEFAULT_REFERRED_REWARD_TYPE;
+		}
+
+		if (isset($settings['referred_reward_amount'])) {
+			$valid_settings['referred_reward_amount'] = max(0, floatval($settings['referred_reward_amount']));
+		}
+
+		return update_option(BASE . 'referral_settings', $valid_settings);
+	}
+
+	/************************************************************
+	 * Helper Methods
+	 ************************************************************/
+
+	/**
+	 * Generate unique referral code
+	 *
+	 * @param int $user_id User ID
+	 * @return string Unique code
+	 */
+	protected function generateUniqueCode(int $user_id): string
+	{
+		$user = get_userdata($user_id);
+		$base = strtoupper(substr($user->user_login, 0, 6));
+		$code = $base . rand(1000, 9999);
+
+		// Ensure uniqueness
+		while ($this->isCodeTaken($code)) {
+			$code = $base . rand(1000, 9999);
+		}
+
+		return $code;
+	}
+
+	/**
+	 * Sanitize referral code
+	 *
+	 * @param string $code Raw code
+	 * @return string Sanitized code
+	 */
+	protected function sanitizeCode(string $code): string
+	{
+		return strtoupper(preg_replace('/[^A-Z0-9]/', '', strtoupper($code)));
+	}
+
+	/**
+	 * Check if code is already taken
+	 *
+	 * @param string $code Code to check
+	 * @param int|null $exclude_user_id User ID to exclude
+	 * @return bool
+	 */
+	protected function isCodeTaken(string $code, ?int $exclude_user_id = null): bool
+	{
+		$sql = "SELECT COUNT(*) FROM {$this->table_codes} WHERE code = %s";
+		$params = [$code];
+
+		if ($exclude_user_id) {
+			$sql .= " AND user_id != %d";
+			$params[] = $exclude_user_id;
+		}
+
+		$count = $this->wpdb->get_var($this->wpdb->prepare($sql, $params));
+		return $count > 0;
+	}
+
+	/**
+	 * Validate code format
+	 *
+	 * @param string $code Code to validate
+	 * @return bool
+	 */
+	protected function validateCodeFormat(string $code): bool
+	{
+		return preg_match('/^[A-Z0-9]{4,20}$/', $code);
+	}
+
+	/**
+	 * Validate user ID
+	 *
+	 * @param int $user_id User ID
+	 * @return bool
+	 */
+	protected function validateUser(int $user_id): bool
+	{
+		return get_userdata($user_id) !== false;
+	}
+
+	/**
+	 * Get referral URL for code
+	 *
+	 * @param string $code Referral code
+	 * @return string Full URL
+	 */
+	protected function getReferralUrl(string $code): string
+	{
+		return add_query_arg('ref', $code, home_url('/register'));
+	}
+
+	/**
+	 * Get client IP address
+	 *
+	 * @return string IP address
+	 */
+	protected function getClientIp(): string
+	{
+		$ip = $_SERVER['HTTP_CF_CONNECTING_IP'] ??
+			$_SERVER['HTTP_X_FORWARDED_FOR'] ??
+			$_SERVER['REMOTE_ADDR'] ??
+			'0.0.0.0';
+
+		return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '0.0.0.0';
+	}
+
+	/**
+	 * Notify referrer of new registration
+	 *
+	 * @param int $referrer_id Referrer user ID
+	 * @param int $referred_id Referred user ID
+	 * @return void
+	 */
+	protected function notifyReferrer(int $referrer_id, int $referred_id): void
+	{
+		JVB()->notification()->addNotification(
+			$referrer_id,
+			'referral_signup',
+			$referred_id,
+			'Someone signed up using your referral code!'
+		);
+	}
+
+	/**
+	 * Cleanup expired/old records
+	 *
+	 * @return void
+	 */
+	public function cleanupExpiredCodes(): void
+	{
+		// Delete clicks older than 90 days with no registration
+		$this->wpdb->query(
+			"DELETE FROM {$this->table_usage}
+             WHERE clicked_at < DATE_SUB(NOW(), INTERVAL 90 DAY)
+             AND registered_at IS NULL"
+		);
+	}
+
+	/**
+	 * Handle bulk operations
+	 *
+	 * @param mixed $result Default result
+	 * @param object $operation Operation object
+	 * @param array $data Operation data
+	 * @return mixed
+	 */
+	public function processOperation($result, object $operation, array $data)
+	{
+		if ($operation->type === 'generate_referral_report') {
+			$user_id = $data['users'][$operation->progress_count] ?? null;
+
+			if ($user_id) {
+				return $this->generateUserReport(
+					$user_id,
+					$data['period_start'],
+					$data['period_end']
+				);
+			}
+		}
+
+		return $result;
+	}
+}
diff --git a/inc/managers/_setup.php b/inc/managers/_setup.php
index 2d32697..77dd5f4 100644
--- a/inc/managers/_setup.php
+++ b/inc/managers/_setup.php
@@ -50,3 +50,7 @@
 if (Features::forSite()->has('dashboard')) {
 	require(JVB_DIR . '/inc/managers/DashboardManager.php');
 }
+
+if (Features::forSite()->has('referrals')) {
+	require(JVB_DIR . '/inc/managers/ReferralManager.php');
+}
diff --git a/inc/registry/CheckCustomTables.php b/inc/registry/CheckCustomTables.php
index 1d4bcaa..1dc7235 100644
--- a/inc/registry/CheckCustomTables.php
+++ b/inc/registry/CheckCustomTables.php
@@ -72,6 +72,13 @@
 				error_log("JVB: Error in dashboard section: " . $e->getMessage());
 			}
 
+			try {
+				if (array_key_exists('referrals', $this->JVB_SITE) && $this->JVB_SITE['referrals']) {
+					$tables = array_merge($tables, $this->referralTables());
+				}
+			} catch (Exception $e) {
+				error_log("JVB: Error in referrals section: " . $e->getMessage());
+			}
 			// RE-ENABLE favourites tables
 			try {
 				if ($this->JVB_SITE['favourites']) {
@@ -1146,6 +1153,68 @@
     }
 
 
+	/**
+	 * 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
+	{
+		$tables = [];
+
+		// Main referrals table
+		$tables[BASE . 'referrals'] = "(
+        `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+        `referrer_id` bigint(20) unsigned NOT NULL,
+        `referee_id` bigint(20) unsigned 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', 'treated', 'cancelled') DEFAULT 'pending',
+        `referred_at` datetime NOT NULL,
+        `treated_at` datetime DEFAULT NULL,
+        `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`),
+        CONSTRAINT `{$this->base}referral_referrer_fk` FOREIGN KEY (`referrer_id`)
+            REFERENCES {$this->wpdb->users} (`ID`) ON DELETE CASCADE,
+        CONSTRAINT `{$this->base}referral_referee_fk` FOREIGN KEY (`referee_id`)
+            REFERENCES {$this->wpdb->users} (`ID`) ON DELETE CASCADE
+    )";
+
+		// Rewards table
+		$tables[BASE . 'referral_rewards'] = "(
+        `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+        `referral_id` bigint(20) unsigned NOT NULL,
+        `user_id` bigint(20) unsigned 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->wpdb->prefix}" . BASE . "referrals (`id`) ON DELETE CASCADE,
+        CONSTRAINT `{$this->base}reward_user_fk` FOREIGN KEY (`user_id`)
+            REFERENCES {$this->wpdb->users} (`ID`) ON DELETE CASCADE
+    )";
+
+		return $tables;
+	}
+
     /*******************************************************************************************
      * These methods help create a content-type taxonomy, like the tattoo shops in edmonton.ink
      *     To set up, ensure that some fields in the registered taxonomy include 'content_table' => true
@@ -1538,4 +1607,5 @@
 
         return "(\n    " . implode(",\n    ", $allDefinitions) . "\n)";
     }
+
 }
diff --git a/inc/rest/_setup.php b/inc/rest/_setup.php
index 9f554cc..c9cf18a 100644
--- a/inc/rest/_setup.php
+++ b/inc/rest/_setup.php
@@ -33,6 +33,9 @@
 if (Features::forMembership()->has('forum')) {
 	require(JVB_DIR . '/inc/rest/routes/NewsRoutes.php');
 }
+if (Features::forSite()->has('referrals')) {
+	require(JVB_DIR . '/inc/rest/routes/ReferralRoutes.php');
+}
 if (Features::anyContentHas('response') || Features::anyTaxonomyHas('response')) {
 	require(JVB_DIR . '/inc/rest/routes/ResponseRoutes.php');
 }
diff --git a/inc/rest/routes/ReferralRoutes.php b/inc/rest/routes/ReferralRoutes.php
new file mode 100644
index 0000000..a447a16
--- /dev/null
+++ b/inc/rest/routes/ReferralRoutes.php
@@ -0,0 +1,315 @@
+<?php
+namespace JVBase\rest;
+
+use WP_REST_Request;
+use WP_REST_Response;
+use WP_Error;
+use JVBase\managers\ReferralManager;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * REST API routes for referral system
+ */
+class ReferralRoutes extends RestRouteManager
+{
+	protected ReferralManager $manager;
+
+	public function __construct()
+	{
+		$this->route = 'referrals';
+		$this->manager = new ReferralManager();
+		parent::__construct();
+	}
+
+	public function registerRoutes(): void
+	{
+		// Get user's referrals
+		register_rest_route($this->namespace, "/{$this->route}", [
+			'methods' => 'GET',
+			'callback' => [$this, 'getUserReferrals'],
+			'permission_callback' => [$this, 'checkPermission']
+		]);
+
+		// Get or create referral code
+		register_rest_route($this->namespace, "/{$this->route}/code", [
+			'methods' => 'GET',
+			'callback' => [$this, 'getReferralCode'],
+			'permission_callback' => [$this, 'checkPermission']
+		]);
+
+		// Update referral code
+		register_rest_route($this->namespace, "/{$this->route}/code", [
+			'methods' => 'POST',
+			'callback' => [$this, 'updateReferralCode'],
+			'permission_callback' => [$this, 'checkPermission'],
+			'args' => [
+				'code' => [
+					'required' => true,
+					'type' => 'string',
+					'validate_callback' => function($param) {
+						return preg_match('/^[A-Z0-9]+$/i', $param);
+					}
+				]
+			]
+		]);
+
+		// Track referral click (public endpoint)
+		register_rest_route($this->namespace, "/{$this->route}/track", [
+			'methods' => 'POST',
+			'callback' => [$this, 'trackReferralClick'],
+			'permission_callback' => '__return_true',
+			'args' => [
+				'code' => [
+					'required' => true,
+					'type' => 'string'
+				]
+			]
+		]);
+
+		// Mark referral as treated
+		register_rest_route($this->namespace, "/{$this->route}/(?P<id>\d+)/treat", [
+			'methods' => 'POST',
+			'callback' => [$this, 'markAsTreated'],
+			'permission_callback' => function() {
+				return current_user_can('manage_options');
+			},
+			'args' => [
+				'id' => [
+					'required' => true,
+					'validate_callback' => function($param) {
+						return is_numeric($param);
+					}
+				]
+			]
+		]);
+
+		// Get user stats
+		register_rest_route($this->namespace, "/{$this->route}/stats", [
+			'methods' => 'GET',
+			'callback' => [$this, 'getUserStats'],
+			'permission_callback' => [$this, 'checkPermission']
+		]);
+
+		// Get top referrers (admin only)
+		register_rest_route($this->namespace, "/{$this->route}/leaderboard", [
+			'methods' => 'GET',
+			'callback' => [$this, 'getTopReferrers'],
+			'permission_callback' => function() {
+				return current_user_can('manage_options');
+			},
+			'args' => [
+				'period' => [
+					'default' => 'week',
+					'enum' => ['day', 'week', 'month', 'all']
+				],
+				'limit' => [
+					'default' => 10,
+					'type' => 'integer'
+				]
+			]
+		]);
+
+		// Get/Update referral settings (admin only)
+		register_rest_route($this->namespace, "/{$this->route}/settings", [
+			[
+				'methods' => 'GET',
+				'callback' => [$this, 'getSettings'],
+				'permission_callback' => function() {
+					return current_user_can('manage_options');
+				}
+			],
+			[
+				'methods' => 'POST',
+				'callback' => [$this, 'updateSettings'],
+				'permission_callback' => function() {
+					return current_user_can('manage_options');
+				}
+			]
+		]);
+	}
+
+	public function checkPermission(WP_REST_Request $request): bool
+	{
+		return is_user_logged_in();
+	}
+
+	/**
+	 * Get user's referrals
+	 */
+	public function getUserReferrals(WP_REST_Request $request): WP_REST_Response
+	{
+		$user_id = get_current_user_id();
+
+		$args = [
+			'status' => $request->get_param('status') ?? 'all',
+			'limit' => $request->get_param('limit') ?? 50,
+			'offset' => $request->get_param('offset') ?? 0
+		];
+
+		$referrals = $this->manager->getUserReferrals($user_id, $args);
+
+		return new WP_REST_Response([
+			'success' => true,
+			'referrals' => $referrals
+		]);
+	}
+
+	/**
+	 * Get user's referral code
+	 */
+	public function getReferralCode(WP_REST_Request $request): WP_REST_Response
+	{
+		$user_id = get_current_user_id();
+		$code = $this->manager->getUserReferralCode($user_id);
+
+		if (is_wp_error($code)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => $code->get_error_message()
+			], 400);
+		}
+
+		return new WP_REST_Response([
+			'success' => true,
+			'code' => $code,
+			'share_url' => home_url('/?ref=' . $code)
+		]);
+	}
+
+	/**
+	 * Update user's referral code
+	 */
+	public function updateReferralCode(WP_REST_Request $request): WP_REST_Response
+	{
+		$user_id = get_current_user_id();
+		$new_code = strtoupper(sanitize_text_field($request->get_param('code')));
+
+		$result = $this->manager->getUserReferralCode($user_id, $new_code);
+
+		if (is_wp_error($result)) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => $result->get_error_message()
+			], 400);
+		}
+
+		return new WP_REST_Response([
+			'success' => true,
+			'code' => $result,
+			'message' => 'Referral code updated successfully'
+		]);
+	}
+
+	/**
+	 * Track referral click and store in session
+	 */
+	public function trackReferralClick(WP_REST_Request $request): WP_REST_Response
+	{
+		$code = strtoupper(sanitize_text_field($request->get_param('code')));
+
+		// Start session if not already started
+		if (session_status() === PHP_SESSION_NONE) {
+			session_start();
+		}
+
+		// Store referral code in both session and cookie (30 day expiry)
+		$_SESSION[BASE . 'referral_code'] = $code;
+		setcookie(BASE . 'referral_code', $code, time() + (30 * DAY_IN_SECONDS), '/');
+
+		return new WP_REST_Response([
+			'success' => true,
+			'message' => 'Referral tracked'
+		]);
+	}
+
+	/**
+	 * Mark referral as treated
+	 */
+	public function markAsTreated(WP_REST_Request $request): WP_REST_Response
+	{
+		$referral_id = intval($request->get_param('id'));
+
+		$result = $this->manager->markAsTreated($referral_id, true);
+
+		if (!$result) {
+			return new WP_REST_Response([
+				'success' => false,
+				'message' => 'Failed to update referral'
+			], 400);
+		}
+
+		return new WP_REST_Response([
+			'success' => true,
+			'message' => 'Referral marked as treated and rewards created'
+		]);
+	}
+
+	/**
+	 * Get user stats
+	 */
+	public function getUserStats(WP_REST_Request $request): WP_REST_Response
+	{
+		$user_id = get_current_user_id();
+		$stats = $this->manager->getUserStats($user_id);
+
+		return new WP_REST_Response([
+			'success' => true,
+			'stats' => $stats
+		]);
+	}
+
+	/**
+	 * Get top referrers
+	 */
+	public function getTopReferrers(WP_REST_Request $request): WP_REST_Response
+	{
+		$period = $request->get_param('period') ?? 'week';
+		$limit = $request->get_param('limit') ?? 10;
+
+		$top_referrers = $this->manager->getTopReferrers($limit, $period);
+
+		return new WP_REST_Response([
+			'success' => true,
+			'period' => $period,
+			'referrers' => $top_referrers
+		]);
+	}
+
+	/**
+	 * Get referral settings
+	 */
+	public function getSettings(WP_REST_Request $request): WP_REST_Response
+	{
+		$settings = get_option(BASE . 'referral_settings', []);
+
+		return new WP_REST_Response([
+			'success' => true,
+			'settings' => $settings
+		]);
+	}
+
+	/**
+	 * Update referral settings
+	 */
+	public function updateSettings(WP_REST_Request $request): WP_REST_Response
+	{
+		$settings = [
+			'referrer_reward_type' => $request->get_param('referrer_reward_type') ?? 'per_user',
+			'referrer_reward_amount' => floatval($request->get_param('referrer_reward_amount') ?? 25),
+			'referee_reward_type' => $request->get_param('referee_reward_type') ?? 'percentage',
+			'referee_reward_amount' => floatval($request->get_param('referee_reward_amount') ?? 20),
+			'referee_reward_applies_to' => $request->get_param('referee_reward_applies_to') ?? 'first_order'
+		];
+
+		update_option(BASE . 'referral_settings', $settings);
+
+		return new WP_REST_Response([
+			'success' => true,
+			'message' => 'Settings updated successfully',
+			'settings' => $settings
+		]);
+	}
+}

--
Gitblit v1.10.0