diff --git a/worklenz-backend/database/sql/4_functions.sql b/worklenz-backend/database/sql/4_functions.sql
index 56bae8ba..9c9cc820 100644
--- a/worklenz-backend/database/sql/4_functions.sql
+++ b/worklenz-backend/database/sql/4_functions.sql
@@ -6156,3 +6156,219 @@ BEGIN
RETURN v_new_id;
END;
$$;
+
+CREATE OR REPLACE FUNCTION transfer_team_ownership(_team_id UUID, _new_owner_id UUID) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _old_owner_id UUID;
+ _owner_role_id UUID;
+ _admin_role_id UUID;
+ _old_org_id UUID;
+ _new_org_id UUID;
+ _has_license BOOLEAN;
+ _old_owner_role_id UUID;
+ _new_owner_role_id UUID;
+ _has_active_coupon BOOLEAN;
+ _other_teams_count INTEGER;
+ _new_owner_org_id UUID;
+ _license_type_id UUID;
+ _has_valid_license BOOLEAN;
+BEGIN
+ -- Get the current owner's ID and organization
+ SELECT t.user_id, t.organization_id
+ INTO _old_owner_id, _old_org_id
+ FROM teams t
+ WHERE t.id = _team_id;
+
+ IF _old_owner_id IS NULL THEN
+ RAISE EXCEPTION 'Team not found';
+ END IF;
+
+ -- Get the new owner's organization
+ SELECT organization_id INTO _new_owner_org_id
+ FROM organizations
+ WHERE user_id = _new_owner_id;
+
+ -- Get the old organization
+ SELECT id INTO _old_org_id
+ FROM organizations
+ WHERE id = _old_org_id;
+
+ IF _old_org_id IS NULL THEN
+ RAISE EXCEPTION 'Organization not found';
+ END IF;
+
+ -- Check if new owner has any valid license type
+ SELECT EXISTS (
+ SELECT 1
+ FROM (
+ -- Check regular subscriptions
+ SELECT lus.user_id, lus.status, lus.active
+ FROM licensing_user_subscriptions lus
+ WHERE lus.user_id = _new_owner_id
+ AND lus.active = TRUE
+ AND lus.status IN ('active', 'trialing')
+
+ UNION ALL
+
+ -- Check custom subscriptions
+ SELECT lcs.user_id, lcs.subscription_status as status, TRUE as active
+ FROM licensing_custom_subs lcs
+ WHERE lcs.user_id = _new_owner_id
+ AND lcs.end_date > CURRENT_DATE
+
+ UNION ALL
+
+ -- Check trial status in organizations
+ SELECT o.user_id, o.subscription_status as status, TRUE as active
+ FROM organizations o
+ WHERE o.user_id = _new_owner_id
+ AND o.trial_in_progress = TRUE
+ AND o.trial_expire_date > CURRENT_DATE
+ ) valid_licenses
+ ) INTO _has_valid_license;
+
+ IF NOT _has_valid_license THEN
+ RAISE EXCEPTION 'New owner does not have a valid license (subscription, custom subscription, or trial)';
+ END IF;
+
+ -- Check if new owner has any active coupon codes
+ SELECT EXISTS (
+ SELECT 1
+ FROM licensing_coupon_codes lcc
+ WHERE lcc.redeemed_by = _new_owner_id
+ AND lcc.is_redeemed = TRUE
+ AND lcc.is_refunded = FALSE
+ ) INTO _has_active_coupon;
+
+ IF _has_active_coupon THEN
+ RAISE EXCEPTION 'New owner has active coupon codes that need to be handled before transfer';
+ END IF;
+
+ -- Count other teams in the organization for information purposes
+ SELECT COUNT(*) INTO _other_teams_count
+ FROM teams
+ WHERE organization_id = _old_org_id
+ AND id != _team_id;
+
+ -- If new owner has their own organization, move the team to their organization
+ IF _new_owner_org_id IS NOT NULL THEN
+ -- Update the team to use the new owner's organization
+ UPDATE teams
+ SET user_id = _new_owner_id,
+ organization_id = _new_owner_org_id
+ WHERE id = _team_id;
+
+ -- Create notification about organization change
+ PERFORM create_notification(
+ _old_owner_id,
+ _team_id,
+ NULL,
+ NULL,
+ CONCAT('Team ', (SELECT name FROM teams WHERE id = _team_id), ' has been moved to a different organization')
+ );
+
+ PERFORM create_notification(
+ _new_owner_id,
+ _team_id,
+ NULL,
+ NULL,
+ CONCAT('Team ', (SELECT name FROM teams WHERE id = _team_id), ' has been moved to your organization')
+ );
+ ELSE
+ -- If new owner doesn't have an organization, transfer the old organization to them
+ UPDATE organizations
+ SET user_id = _new_owner_id
+ WHERE id = _old_org_id;
+
+ -- Update the team to use the same organization
+ UPDATE teams
+ SET user_id = _new_owner_id,
+ organization_id = _old_org_id
+ WHERE id = _team_id;
+
+ -- Notify both users about organization ownership transfer
+ PERFORM create_notification(
+ _old_owner_id,
+ NULL,
+ NULL,
+ NULL,
+ CONCAT('You are no longer the owner of organization ', (SELECT organization_name FROM organizations WHERE id = _old_org_id), '')
+ );
+
+ PERFORM create_notification(
+ _new_owner_id,
+ NULL,
+ NULL,
+ NULL,
+ CONCAT('You are now the owner of organization ', (SELECT organization_name FROM organizations WHERE id = _old_org_id), '')
+ );
+ END IF;
+
+ -- Get the owner and admin role IDs
+ SELECT id INTO _owner_role_id FROM roles WHERE team_id = _team_id AND owner = TRUE;
+ SELECT id INTO _admin_role_id FROM roles WHERE team_id = _team_id AND admin_role = TRUE;
+
+ -- Get current role IDs for both users
+ SELECT role_id INTO _old_owner_role_id
+ FROM team_members
+ WHERE team_id = _team_id AND user_id = _old_owner_id;
+
+ SELECT role_id INTO _new_owner_role_id
+ FROM team_members
+ WHERE team_id = _team_id AND user_id = _new_owner_id;
+
+ -- Update the old owner's role to admin if they want to stay in the team
+ IF _old_owner_role_id IS NOT NULL THEN
+ UPDATE team_members
+ SET role_id = _admin_role_id
+ WHERE team_id = _team_id AND user_id = _old_owner_id;
+ END IF;
+
+ -- Update the new owner's role to owner
+ IF _new_owner_role_id IS NOT NULL THEN
+ UPDATE team_members
+ SET role_id = _owner_role_id
+ WHERE team_id = _team_id AND user_id = _new_owner_id;
+ ELSE
+ -- If new owner is not a team member yet, add them
+ INSERT INTO team_members (user_id, team_id, role_id)
+ VALUES (_new_owner_id, _team_id, _owner_role_id);
+ END IF;
+
+ -- Create notification for both users about team ownership
+ PERFORM create_notification(
+ _old_owner_id,
+ _team_id,
+ NULL,
+ NULL,
+ CONCAT('You are no longer the owner of team ', (SELECT name FROM teams WHERE id = _team_id), '')
+ );
+
+ PERFORM create_notification(
+ _new_owner_id,
+ _team_id,
+ NULL,
+ NULL,
+ CONCAT('You are now the owner of team ', (SELECT name FROM teams WHERE id = _team_id), '')
+ );
+
+ RETURN json_build_object(
+ 'success', TRUE,
+ 'old_owner_id', _old_owner_id,
+ 'new_owner_id', _new_owner_id,
+ 'team_id', _team_id,
+ 'old_org_id', _old_org_id,
+ 'new_org_id', COALESCE(_new_owner_org_id, _old_org_id),
+ 'old_role_id', _old_owner_role_id,
+ 'new_role_id', _new_owner_role_id,
+ 'has_valid_license', _has_valid_license,
+ 'has_active_coupon', _has_active_coupon,
+ 'other_teams_count', _other_teams_count,
+ 'org_ownership_transferred', _new_owner_org_id IS NULL,
+ 'team_moved_to_new_org', _new_owner_org_id IS NOT NULL
+ );
+END;
+$$;
diff --git a/worklenz-backend/worklenz-email-templates/password-changed-notification.html b/worklenz-backend/worklenz-email-templates/password-changed-notification.html
index f734d8a8..2c8e2d3a 100644
--- a/worklenz-backend/worklenz-email-templates/password-changed-notification.html
+++ b/worklenz-backend/worklenz-email-templates/password-changed-notification.html
@@ -2,31 +2,30 @@
-
+ Password Changed | Worklenz
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+ Password Changed Successfully
+
+
+
Hi,
+
This is a confirmation that your Worklenz
+ account password was changed.