diff --git a/apps/api/src/routers/ee/payments.py b/apps/api/src/routers/ee/payments.py
index 6cd8c3ce..526e4311 100644
--- a/apps/api/src/routers/ee/payments.py
+++ b/apps/api/src/routers/ee/payments.py
@@ -4,7 +4,7 @@ from src.core.events.database import get_db_session
from src.db.payments.payments import PaymentsConfig, PaymentsConfigCreate, PaymentsConfigRead, PaymentsConfigUpdate
from src.db.users import PublicUser
from src.security.auth import get_current_user
-from src.services.payments.payments import (
+from src.services.payments.payments_config import (
create_payments_config,
get_payments_config,
update_payments_config,
@@ -12,6 +12,11 @@ from src.services.payments.payments import (
)
from src.db.payments.payments_products import PaymentsProductCreate, PaymentsProductRead, PaymentsProductUpdate
from src.services.payments.payments_products import create_payments_product, delete_payments_product, get_payments_product, list_payments_products, update_payments_product
+from src.services.payments.payments_courses import (
+ link_course_to_product,
+ unlink_course_from_product,
+ get_courses_by_product
+)
router = APIRouter()
@@ -105,3 +110,41 @@ async def api_delete_payments_product(
):
await delete_payments_product(request, org_id, product_id, current_user, db_session)
return {"message": "Payments product deleted successfully"}
+
+@router.post("/{org_id}/products/{product_id}/courses/{course_id}")
+async def api_link_course_to_product(
+ request: Request,
+ org_id: int,
+ product_id: int,
+ course_id: int,
+ current_user: PublicUser = Depends(get_current_user),
+ db_session: Session = Depends(get_db_session),
+):
+ return await link_course_to_product(
+ request, org_id, course_id, product_id, current_user, db_session
+ )
+
+@router.delete("/{org_id}/products/{product_id}/courses/{course_id}")
+async def api_unlink_course_from_product(
+ request: Request,
+ org_id: int,
+ product_id: int,
+ course_id: int,
+ current_user: PublicUser = Depends(get_current_user),
+ db_session: Session = Depends(get_db_session),
+):
+ return await unlink_course_from_product(
+ request, org_id, course_id, current_user, db_session
+ )
+
+@router.get("/{org_id}/products/{product_id}/courses")
+async def api_get_courses_by_product(
+ request: Request,
+ org_id: int,
+ product_id: int,
+ current_user: PublicUser = Depends(get_current_user),
+ db_session: Session = Depends(get_db_session),
+):
+ return await get_courses_by_product(
+ request, org_id, product_id, current_user, db_session
+ )
diff --git a/apps/api/src/services/payments/payments.py b/apps/api/src/services/payments/payments_config.py
similarity index 100%
rename from apps/api/src/services/payments/payments.py
rename to apps/api/src/services/payments/payments_config.py
diff --git a/apps/api/src/services/payments/payments_courses.py b/apps/api/src/services/payments/payments_courses.py
new file mode 100644
index 00000000..e83aa10a
--- /dev/null
+++ b/apps/api/src/services/payments/payments_courses.py
@@ -0,0 +1,124 @@
+from datetime import datetime
+from fastapi import HTTPException, Request
+from sqlmodel import Session, select
+from src.db.payments.payments_courses import PaymentCourse
+from src.db.payments.payments_products import PaymentsProduct
+from src.db.courses.courses import Course
+from src.db.users import PublicUser, AnonymousUser
+from src.services.courses.courses import rbac_check
+
+async def link_course_to_product(
+ request: Request,
+ org_id: int,
+ course_id: int,
+ product_id: int,
+ current_user: PublicUser | AnonymousUser,
+ db_session: Session,
+):
+ # Check if course exists and user has permission
+ statement = select(Course).where(Course.id == course_id)
+ course = db_session.exec(statement).first()
+
+ if not course:
+ raise HTTPException(status_code=404, detail="Course not found")
+
+ # RBAC check
+ await rbac_check(request, course.course_uuid, current_user, "update", db_session)
+
+ # Check if product exists
+ statement = select(PaymentsProduct).where(
+ PaymentsProduct.id == product_id,
+ PaymentsProduct.org_id == org_id
+ )
+ product = db_session.exec(statement).first()
+
+ if not product:
+ raise HTTPException(status_code=404, detail="Product not found")
+
+ # Check if course is already linked to another product
+ statement = select(PaymentCourse).where(PaymentCourse.course_id == course.id)
+ existing_link = db_session.exec(statement).first()
+
+ if existing_link:
+ raise HTTPException(
+ status_code=400,
+ detail="Course is already linked to a product"
+ )
+
+ # Create new payment course link
+ payment_course = PaymentCourse(
+ course_id=course.id, # type: ignore
+ payment_product_id=product_id,
+ org_id=org_id,
+ )
+
+ db_session.add(payment_course)
+ db_session.commit()
+
+ return {"message": "Course linked to product successfully"}
+
+async def unlink_course_from_product(
+ request: Request,
+ org_id: int,
+ course_id: int,
+ current_user: PublicUser | AnonymousUser,
+ db_session: Session,
+):
+ # Check if course exists and user has permission
+ statement = select(Course).where(Course.id == course_id)
+ course = db_session.exec(statement).first()
+
+ if not course:
+ raise HTTPException(status_code=404, detail="Course not found")
+
+ # RBAC check
+ await rbac_check(request, course.course_uuid, current_user, "update", db_session)
+
+ # Find and delete the payment course link
+ statement = select(PaymentCourse).where(
+ PaymentCourse.course_id == course.id,
+ PaymentCourse.org_id == org_id
+ )
+ payment_course = db_session.exec(statement).first()
+
+ if not payment_course:
+ raise HTTPException(
+ status_code=404,
+ detail="Course is not linked to any product"
+ )
+
+ db_session.delete(payment_course)
+ db_session.commit()
+
+ return {"message": "Course unlinked from product successfully"}
+
+async def get_courses_by_product(
+ request: Request,
+ org_id: int,
+ product_id: int,
+ current_user: PublicUser | AnonymousUser,
+ db_session: Session,
+):
+ # Check if product exists
+ statement = select(PaymentsProduct).where(
+ PaymentsProduct.id == product_id,
+ PaymentsProduct.org_id == org_id
+ )
+ product = db_session.exec(statement).first()
+
+ if not product:
+ raise HTTPException(status_code=404, detail="Product not found")
+
+ # Get all courses linked to this product with explicit join
+ statement = (
+ select(Course)
+ .select_from(Course)
+ .join(PaymentCourse, Course.id == PaymentCourse.course_id) # type: ignore
+ .where(
+ PaymentCourse.payment_product_id == product_id,
+ PaymentCourse.org_id == org_id
+ )
+ )
+ courses = db_session.exec(statement).all()
+
+ return courses
diff --git a/apps/api/src/services/payments/stripe.py b/apps/api/src/services/payments/stripe.py
index 052d2618..f351d0bc 100644
--- a/apps/api/src/services/payments/stripe.py
+++ b/apps/api/src/services/payments/stripe.py
@@ -3,7 +3,7 @@ from sqlmodel import Session
import stripe
from src.db.payments.payments_products import PaymentPriceTypeEnum, PaymentProductTypeEnum, PaymentsProduct
from src.db.users import AnonymousUser, PublicUser
-from src.services.payments.payments import get_payments_config
+from src.services.payments.payments_config import get_payments_config
async def get_stripe_credentials(
diff --git a/apps/web/components/Dashboard/Payments/PaymentsProductPage.tsx b/apps/web/components/Dashboard/Payments/PaymentsProductPage.tsx
index 667334d5..240fe73f 100644
--- a/apps/web/components/Dashboard/Payments/PaymentsProductPage.tsx
+++ b/apps/web/components/Dashboard/Payments/PaymentsProductPage.tsx
@@ -19,6 +19,7 @@ import * as Yup from 'yup';
import { Label } from '@components/ui/label';
import { Badge } from '@components/ui/badge';
import { getPaymentConfigs } from '@services/payments/payments';
+import ProductLinkedCourses from './SubComponents/ProductLinkedCourses';
const validationSchema = Yup.object().shape({
name: Yup.string().required('Name is required'),
@@ -162,6 +163,7 @@ function PaymentsProductPage() {
)}
+
+ {course.description} +
+