mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: add ability to link courses to products
This commit is contained in:
parent
3f96f1ec9f
commit
f0aeb4605c
8 changed files with 446 additions and 2 deletions
|
|
@ -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.payments.payments import PaymentsConfig, PaymentsConfigCreate, PaymentsConfigRead, PaymentsConfigUpdate
|
||||||
from src.db.users import PublicUser
|
from src.db.users import PublicUser
|
||||||
from src.security.auth import get_current_user
|
from src.security.auth import get_current_user
|
||||||
from src.services.payments.payments import (
|
from src.services.payments.payments_config import (
|
||||||
create_payments_config,
|
create_payments_config,
|
||||||
get_payments_config,
|
get_payments_config,
|
||||||
update_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.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_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()
|
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)
|
await delete_payments_product(request, org_id, product_id, current_user, db_session)
|
||||||
return {"message": "Payments product deleted successfully"}
|
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
|
||||||
|
)
|
||||||
|
|
|
||||||
124
apps/api/src/services/payments/payments_courses.py
Normal file
124
apps/api/src/services/payments/payments_courses.py
Normal file
|
|
@ -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
|
||||||
|
|
@ -3,7 +3,7 @@ from sqlmodel import Session
|
||||||
import stripe
|
import stripe
|
||||||
from src.db.payments.payments_products import PaymentPriceTypeEnum, PaymentProductTypeEnum, PaymentsProduct
|
from src.db.payments.payments_products import PaymentPriceTypeEnum, PaymentProductTypeEnum, PaymentsProduct
|
||||||
from src.db.users import AnonymousUser, PublicUser
|
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(
|
async def get_stripe_credentials(
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import * as Yup from 'yup';
|
||||||
import { Label } from '@components/ui/label';
|
import { Label } from '@components/ui/label';
|
||||||
import { Badge } from '@components/ui/badge';
|
import { Badge } from '@components/ui/badge';
|
||||||
import { getPaymentConfigs } from '@services/payments/payments';
|
import { getPaymentConfigs } from '@services/payments/payments';
|
||||||
|
import ProductLinkedCourses from './SubComponents/ProductLinkedCourses';
|
||||||
|
|
||||||
const validationSchema = Yup.object().shape({
|
const validationSchema = Yup.object().shape({
|
||||||
name: Yup.string().required('Name is required'),
|
name: Yup.string().required('Name is required'),
|
||||||
|
|
@ -162,6 +163,7 @@ function PaymentsProductPage() {
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<ProductLinkedCourses productId={product.id} />
|
||||||
<div className="mt-2 flex items-center justify-between bg-gray-100 rounded-md p-2">
|
<div className="mt-2 flex items-center justify-between bg-gray-100 rounded-md p-2">
|
||||||
<span className="text-sm text-gray-600">Price:</span>
|
<span className="text-sm text-gray-600">Price:</span>
|
||||||
<span className="font-semibold text-lg">
|
<span className="font-semibold text-lg">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext';
|
||||||
|
import { useLHSession } from '@components/Contexts/LHSessionContext';
|
||||||
|
import { linkCourseToProduct } from '@services/payments/products';
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Search } from 'lucide-react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { mutate } from 'swr';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { getOrgCourses } from '@services/courses/courses';
|
||||||
|
import { getCoursesLinkedToProduct } from '@services/payments/products';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
|
||||||
|
import { getUriWithOrg } from '@services/config/config';
|
||||||
|
|
||||||
|
interface LinkCourseModalProps {
|
||||||
|
productId: string;
|
||||||
|
onSuccess: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CoursePreviewProps {
|
||||||
|
course: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
thumbnail_image: string;
|
||||||
|
course_uuid: string;
|
||||||
|
};
|
||||||
|
orgslug: string;
|
||||||
|
onLink: (courseId: string) => void;
|
||||||
|
isLinked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CoursePreview = ({ course, orgslug, onLink, isLinked }: CoursePreviewProps) => {
|
||||||
|
const org = useOrg() as any;
|
||||||
|
|
||||||
|
const thumbnailImage = course.thumbnail_image
|
||||||
|
? getCourseThumbnailMediaDirectory(org?.org_uuid, course.course_uuid, course.thumbnail_image)
|
||||||
|
: '../empty_thumbnail.png';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-4 p-4 bg-white rounded-lg border border-gray-100 hover:border-gray-200 transition-colors">
|
||||||
|
{/* Thumbnail */}
|
||||||
|
<div
|
||||||
|
className="flex-shrink-0 w-[120px] h-[68px] rounded-md bg-cover bg-center ring-1 ring-inset ring-black/10"
|
||||||
|
style={{ backgroundImage: `url(${thumbnailImage})` }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-grow space-y-1">
|
||||||
|
<h3 className="font-medium text-gray-900 line-clamp-1">
|
||||||
|
{course.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-500 line-clamp-2">
|
||||||
|
{course.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Button */}
|
||||||
|
<div className="flex-shrink-0 flex items-center">
|
||||||
|
{isLinked ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled
|
||||||
|
className="text-gray-500"
|
||||||
|
>
|
||||||
|
Already Linked
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={() => onLink(course.id)}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Link Course
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LinkCourseModal({ productId, onSuccess }: LinkCourseModalProps) {
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const org = useOrg() as any;
|
||||||
|
const session = useLHSession() as any;
|
||||||
|
|
||||||
|
const { data: courses } = useSWR(
|
||||||
|
() => org && session ? [org.slug, searchTerm, session.data?.tokens?.access_token] : null,
|
||||||
|
([orgSlug, search, token]) => getOrgCourses(orgSlug, null, token)
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: linkedCourses } = useSWR(
|
||||||
|
() => org && session ? [`/payments/${org.id}/products/${productId}/courses`, session.data?.tokens?.access_token] : null,
|
||||||
|
([_, token]) => getCoursesLinkedToProduct(org.id, productId, token)
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLinkCourse = async (courseId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await linkCourseToProduct(org.id, productId, courseId, session.data?.tokens?.access_token);
|
||||||
|
if (response.success) {
|
||||||
|
mutate([`/payments/${org.id}/products`, session.data?.tokens?.access_token]);
|
||||||
|
toast.success('Course linked successfully');
|
||||||
|
onSuccess();
|
||||||
|
} else {
|
||||||
|
toast.error(response.data?.detail || 'Failed to link course');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to link course');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLinked = (courseId: string) => {
|
||||||
|
return linkedCourses?.data?.some((course: any) => course.id === courseId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
|
||||||
|
|
||||||
|
{/* Course List */}
|
||||||
|
<div className="max-h-[400px] overflow-y-auto space-y-2 px-3">
|
||||||
|
{courses?.map((course: any) => (
|
||||||
|
<CoursePreview
|
||||||
|
key={course.course_uuid}
|
||||||
|
course={course}
|
||||||
|
orgslug={org.slug}
|
||||||
|
onLink={handleLinkCourse}
|
||||||
|
isLinked={isLinked(course.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Empty State */}
|
||||||
|
{(!courses || courses.length === 0) && (
|
||||||
|
<div className="text-center py-6 text-gray-500">
|
||||||
|
No courses found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { getCoursesLinkedToProduct, unlinkCourseFromProduct } from '@services/payments/products';
|
||||||
|
import { useLHSession } from '@components/Contexts/LHSessionContext';
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext';
|
||||||
|
import { Trash2, Plus, BookOpen } from 'lucide-react';
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { mutate } from 'swr';
|
||||||
|
import Modal from '@components/StyledElements/Modal/Modal';
|
||||||
|
import LinkCourseModal from './LinkCourseModal';
|
||||||
|
|
||||||
|
interface ProductLinkedCoursesProps {
|
||||||
|
productId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProductLinkedCourses({ productId }: ProductLinkedCoursesProps) {
|
||||||
|
const [linkedCourses, setLinkedCourses] = useState<any[]>([]);
|
||||||
|
const [isLinkModalOpen, setIsLinkModalOpen] = useState(false);
|
||||||
|
const session = useLHSession() as any;
|
||||||
|
const org = useOrg() as any;
|
||||||
|
|
||||||
|
const fetchLinkedCourses = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getCoursesLinkedToProduct(org.id, productId, session.data?.tokens?.access_token);
|
||||||
|
setLinkedCourses(response.data || []);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to fetch linked courses');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnlinkCourse = async (courseId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await unlinkCourseFromProduct(org.id, productId, courseId, session.data?.tokens?.access_token);
|
||||||
|
if (response.success) {
|
||||||
|
await fetchLinkedCourses();
|
||||||
|
mutate([`/payments/${org.id}/products`, session.data?.tokens?.access_token]);
|
||||||
|
toast.success('Course unlinked successfully');
|
||||||
|
} else {
|
||||||
|
toast.error(response.data?.detail || 'Failed to unlink course');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to unlink course');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (org && session && productId) {
|
||||||
|
fetchLinkedCourses();
|
||||||
|
}
|
||||||
|
}, [org, session, productId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-700">Linked Courses</h3>
|
||||||
|
<Modal
|
||||||
|
isDialogOpen={isLinkModalOpen}
|
||||||
|
onOpenChange={setIsLinkModalOpen}
|
||||||
|
dialogTitle="Link Course to Product"
|
||||||
|
dialogDescription="Select a course to link to this product"
|
||||||
|
dialogContent={
|
||||||
|
<LinkCourseModal
|
||||||
|
productId={productId}
|
||||||
|
onSuccess={() => {
|
||||||
|
setIsLinkModalOpen(false);
|
||||||
|
fetchLinkedCourses();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
dialogTrigger={
|
||||||
|
<Button variant="outline" size="sm" className="flex items-center gap-2">
|
||||||
|
<Plus size={16} />
|
||||||
|
<span>Link Course</span>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{linkedCourses.length === 0 ? (
|
||||||
|
<div className="text-sm text-gray-500 flex items-center gap-2">
|
||||||
|
<BookOpen size={16} />
|
||||||
|
<span>No courses linked yet</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
linkedCourses.map((course) => (
|
||||||
|
<div
|
||||||
|
key={course.id}
|
||||||
|
className="flex items-center justify-between p-2 bg-gray-50 rounded-md"
|
||||||
|
>
|
||||||
|
<span className="text-sm font-medium">{course.name}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleUnlinkCourse(course.id)}
|
||||||
|
className="text-red-500 hover:text-red-700"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -46,5 +46,31 @@ export async function getProductDetails(orgId: number, productId: string, access
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function linkCourseToProduct(orgId: number, productId: string, courseId: string, access_token: string) {
|
||||||
|
const result = await fetch(
|
||||||
|
`${getAPIUrl()}payments/${orgId}/products/${productId}/courses/${courseId}`,
|
||||||
|
RequestBodyWithAuthHeader('POST', null, null, access_token)
|
||||||
|
);
|
||||||
|
const res = await getResponseMetadata(result);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unlinkCourseFromProduct(orgId: number, productId: string, courseId: string, access_token: string) {
|
||||||
|
const result = await fetch(
|
||||||
|
`${getAPIUrl()}payments/${orgId}/products/${productId}/courses/${courseId}`,
|
||||||
|
RequestBodyWithAuthHeader('DELETE', null, null, access_token)
|
||||||
|
);
|
||||||
|
const res = await getResponseMetadata(result);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCoursesLinkedToProduct(orgId: number, productId: string, access_token: string) {
|
||||||
|
const result = await fetch(
|
||||||
|
`${getAPIUrl()}payments/${orgId}/products/${productId}/courses`,
|
||||||
|
RequestBodyWithAuthHeader('GET', null, null, access_token)
|
||||||
|
);
|
||||||
|
const res = await getResponseMetadata(result);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue