diff --git a/apps/api/src/routers/ee/payments.py b/apps/api/src/routers/ee/payments.py
index 43d04038..aaec689c 100644
--- a/apps/api/src/routers/ee/payments.py
+++ b/apps/api/src/routers/ee/payments.py
@@ -17,6 +17,7 @@ from src.services.payments.payments_courses import (
unlink_course_from_product,
get_courses_by_product,
)
+from src.services.payments.payments_users import get_owned_courses
from src.services.payments.payments_webhook import handle_stripe_webhook
from src.services.payments.stripe import create_checkout_session
from src.services.payments.payments_access import check_course_paid_access
@@ -218,3 +219,12 @@ async def api_get_customers(
Get list of customers and their subscriptions for an organization
"""
return await get_customers(request, org_id, current_user, db_session)
+
+@router.get("/{org_id}/courses/owned")
+async def api_get_owned_courses(
+ request: Request,
+ org_id: int,
+ current_user: PublicUser = Depends(get_current_user),
+ db_session: Session = Depends(get_db_session),
+):
+ return await get_owned_courses(request, current_user, db_session)
\ No newline at end of file
diff --git a/apps/api/src/services/payments/payments_customers.py b/apps/api/src/services/payments/payments_customers.py
index d87892c0..3922bb0e 100644
--- a/apps/api/src/services/payments/payments_customers.py
+++ b/apps/api/src/services/payments/payments_customers.py
@@ -1,9 +1,8 @@
from fastapi import HTTPException, Request
from sqlmodel import Session, select
from src.db.organizations import Organization
-from src.db.users import PublicUser, AnonymousUser, User
+from src.db.users import PublicUser, AnonymousUser
from src.db.payments.payments_users import PaymentsUser
-from src.db.payments.payments_products import PaymentsProduct
from src.services.orgs.orgs import rbac_check
from src.services.payments.payments_products import get_payments_product
from src.services.users.users import read_user_by_id
diff --git a/apps/api/src/services/payments/payments_users.py b/apps/api/src/services/payments/payments_users.py
index 5b34e8c6..077ab713 100644
--- a/apps/api/src/services/payments/payments_users.py
+++ b/apps/api/src/services/payments/payments_users.py
@@ -1,9 +1,12 @@
from fastapi import HTTPException, Request
from sqlmodel import Session, select
from typing import Any
+from src.db.courses.courses import Course, CourseRead
+from src.db.payments.payments_courses import PaymentsCourse
from src.db.payments.payments_users import PaymentsUser, PaymentStatusEnum, ProviderSpecificData
from src.db.payments.payments_products import PaymentsProduct
-from src.db.users import InternalUser, PublicUser, AnonymousUser
+from src.db.resource_authors import ResourceAuthor
+from src.db.users import InternalUser, PublicUser, AnonymousUser, User, UserRead
from src.db.organizations import Organization
from src.services.orgs.orgs import rbac_check
from datetime import datetime
@@ -185,3 +188,59 @@ async def delete_payment_user(
# Delete payment user
db_session.delete(payment_user)
db_session.commit()
+
+
+async def get_owned_courses(
+ request: Request,
+ current_user: PublicUser | AnonymousUser,
+ db_session: Session,
+) -> list[CourseRead]:
+ # Anonymous users don't own any courses
+ if isinstance(current_user, AnonymousUser):
+ return []
+
+ # Get all active/completed payment users for the current user
+ statement = select(PaymentsUser).where(
+ PaymentsUser.user_id == current_user.id,
+ PaymentsUser.status.in_([PaymentStatusEnum.ACTIVE, PaymentStatusEnum.COMPLETED]) # type: ignore
+ )
+ payment_users = db_session.exec(statement).all()
+
+ # Get all product IDs from payment users
+ product_ids = [pu.payment_product_id for pu in payment_users]
+
+ # Get all courses linked to these products
+ courses = []
+ for product_id in product_ids:
+ # Get courses linked to this product through PaymentsCourse
+ statement = (
+ select(Course)
+ .join(PaymentsCourse, Course.id == PaymentsCourse.course_id) # type: ignore
+ .where(PaymentsCourse.payment_product_id == product_id)
+ )
+ product_courses = db_session.exec(statement).all()
+ courses.extend(product_courses)
+
+ # Remove duplicates by converting to set and back to list
+ unique_courses = list({course.id: course for course in courses}.values())
+
+ # Get authors for each course and convert to CourseRead
+ course_reads = []
+ for course in unique_courses:
+ # Get course authors
+ authors_statement = (
+ select(User)
+ .join(ResourceAuthor)
+ .where(ResourceAuthor.resource_uuid == course.course_uuid)
+ )
+ authors = db_session.exec(authors_statement).all()
+
+ # Convert authors to UserRead
+ author_reads = [UserRead.model_validate(author) for author in authors]
+
+ # Create CourseRead object
+ course_read = CourseRead(**course.model_dump(), authors=author_reads)
+ course_reads.append(course_read)
+
+ return course_reads
+
diff --git a/apps/web/app/orgs/[orgslug]/dash/user-account/owned/page.tsx b/apps/web/app/orgs/[orgslug]/dash/user-account/owned/page.tsx
new file mode 100644
index 00000000..f2b83c14
--- /dev/null
+++ b/apps/web/app/orgs/[orgslug]/dash/user-account/owned/page.tsx
@@ -0,0 +1,59 @@
+'use client'
+
+import React from 'react'
+import { useOrg } from '@components/Contexts/OrgContext'
+import { useLHSession } from '@components/Contexts/LHSessionContext'
+import useSWR from 'swr'
+import { getOwnedCourses } from '@services/payments/payments'
+import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail'
+import PageLoading from '@components/Objects/Loaders/PageLoading'
+import { BookOpen } from 'lucide-react'
+
+function OwnedCoursesPage() {
+ const org = useOrg() as any
+ const session = useLHSession() as any
+ const access_token = session?.data?.tokens?.access_token
+
+ const { data: ownedCourses, error, isLoading } = useSWR(
+ org ? [`/payments/${org.id}/courses/owned`, access_token] : null,
+ ([url, token]) => getOwnedCourses(org.id, token)
+ )
+
+ if (isLoading) return
+ You haven't purchased any courses yet +
+{session.data.user.username}
{isUserAdmin.isAdmin &&