mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: prevent access removal if user has paid for a product
This commit is contained in:
parent
0e97580747
commit
cdd893ca6f
7 changed files with 40 additions and 17 deletions
|
|
@ -9,6 +9,7 @@ from src.db.payments.payments_products import (
|
||||||
PaymentsProductUpdate,
|
PaymentsProductUpdate,
|
||||||
PaymentsProductRead,
|
PaymentsProductRead,
|
||||||
)
|
)
|
||||||
|
from src.db.payments.payments_users import PaymentStatusEnum, PaymentsUser
|
||||||
from src.db.users import PublicUser, AnonymousUser
|
from src.db.users import PublicUser, AnonymousUser
|
||||||
from src.db.organizations import Organization
|
from src.db.organizations import Organization
|
||||||
from src.services.orgs.orgs import rbac_check
|
from src.services.orgs.orgs import rbac_check
|
||||||
|
|
@ -138,6 +139,18 @@ async def delete_payments_product(
|
||||||
if not product:
|
if not product:
|
||||||
raise HTTPException(status_code=404, detail="Payments product not found")
|
raise HTTPException(status_code=404, detail="Payments product not found")
|
||||||
|
|
||||||
|
# Check if there are any payment users linked to this product
|
||||||
|
statement = select(PaymentsUser).where(
|
||||||
|
PaymentsUser.payment_product_id == product_id,
|
||||||
|
PaymentsUser.status.in_([PaymentStatusEnum.ACTIVE, PaymentStatusEnum.COMPLETED]) # type: ignore
|
||||||
|
)
|
||||||
|
payment_users = db_session.exec(statement).all()
|
||||||
|
if payment_users:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Cannot delete product because users have paid access to it."
|
||||||
|
)
|
||||||
|
|
||||||
# Archive product in Stripe
|
# Archive product in Stripe
|
||||||
await archive_stripe_product(request, org_id, product.provider_product_id, current_user, db_session)
|
await archive_stripe_product(request, org_id, product.provider_product_id, current_user, db_session)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ async def create_payment_user(
|
||||||
stripe_customer=provider_data if provider_data else None,
|
stripe_customer=provider_data if provider_data else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if user already has a payment user
|
# Check if user already has a payment user for this product
|
||||||
statement = select(PaymentsUser).where(
|
statement = select(PaymentsUser).where(
|
||||||
PaymentsUser.user_id == user_id,
|
PaymentsUser.user_id == user_id,
|
||||||
PaymentsUser.org_id == org_id,
|
PaymentsUser.org_id == org_id,
|
||||||
|
|
@ -52,8 +52,12 @@ async def create_payment_user(
|
||||||
existing_payment_user = db_session.exec(statement).first()
|
existing_payment_user = db_session.exec(statement).first()
|
||||||
|
|
||||||
if existing_payment_user:
|
if existing_payment_user:
|
||||||
if existing_payment_user.status == PaymentStatusEnum.PENDING:
|
# If status is PENDING, CANCELLED, or FAILED, delete the existing record
|
||||||
# Delete existing pending payment
|
if existing_payment_user.status in [
|
||||||
|
PaymentStatusEnum.PENDING,
|
||||||
|
PaymentStatusEnum.CANCELLED,
|
||||||
|
PaymentStatusEnum.FAILED
|
||||||
|
]:
|
||||||
db_session.delete(existing_payment_user)
|
db_session.delete(existing_payment_user)
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ async def create_checkout_session(
|
||||||
product_id=product_id,
|
product_id=product_id,
|
||||||
status=PaymentStatusEnum.PENDING,
|
status=PaymentStatusEnum.PENDING,
|
||||||
provider_data=customer,
|
provider_data=customer,
|
||||||
current_user=current_user,
|
current_user=InternalUser(),
|
||||||
db_session=db_session
|
db_session=db_session
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import useSWR from 'swr'
|
||||||
import { getOwnedCourses } from '@services/payments/payments'
|
import { getOwnedCourses } from '@services/payments/payments'
|
||||||
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail'
|
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail'
|
||||||
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
import { BookOpen } from 'lucide-react'
|
import { BookOpen, Package2 } from 'lucide-react'
|
||||||
|
|
||||||
function OwnedCoursesPage() {
|
function OwnedCoursesPage() {
|
||||||
const org = useOrg() as any
|
const org = useOrg() as any
|
||||||
|
|
@ -24,10 +24,15 @@ function OwnedCoursesPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full bg-[#f8f8f8] pl-10 pr-10 pt-5 ">
|
<div className="h-full w-full bg-[#f8f8f8] pl-10 pr-10 pt-5 ">
|
||||||
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-6">
|
<div className="flex flex-col bg-white nice-shadow px-5 py-3 rounded-md mb-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Package2 className="w-8 h-8 text-gray-800" />
|
||||||
|
<div className="flex flex-col -space-y-1">
|
||||||
<h1 className="font-bold text-xl text-gray-800">My Courses</h1>
|
<h1 className="font-bold text-xl text-gray-800">My Courses</h1>
|
||||||
<h2 className="text-gray-500 text-md">Courses you have purchased or subscribed to</h2>
|
<h2 className="text-gray-500 text-md">Courses you have purchased or subscribed to</h2>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 w-full">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 w-full">
|
||||||
{ownedCourses?.map((course: any) => (
|
{ownedCourses?.map((course: any) => (
|
||||||
|
|
|
||||||
|
|
@ -55,12 +55,12 @@ function PaymentsProductPage() {
|
||||||
}, [paymentConfigs]);
|
}, [paymentConfigs]);
|
||||||
|
|
||||||
const handleArchiveProduct = async (productId: string) => {
|
const handleArchiveProduct = async (productId: string) => {
|
||||||
try {
|
const res = await archiveProduct(org.id, productId, session.data?.tokens?.access_token);
|
||||||
await archiveProduct(org.id, productId, session.data?.tokens?.access_token);
|
|
||||||
mutate([`/payments/${org.id}/products`, session.data?.tokens?.access_token]);
|
mutate([`/payments/${org.id}/products`, session.data?.tokens?.access_token]);
|
||||||
|
if (res.status === 200) {
|
||||||
toast.success('Product archived successfully');
|
toast.success('Product archived successfully');
|
||||||
} catch (error) {
|
} else {
|
||||||
toast.error('Failed to archive product');
|
toast.error(res.data.detail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -185,8 +185,7 @@ function PaymentsProductPage() {
|
||||||
<div className="flex justify-center items-center py-10">
|
<div className="flex justify-center items-center py-10">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsCreateModalOpen(true)}
|
onClick={() => setIsCreateModalOpen(true)}
|
||||||
className={`mb-4 flex items-center space-x-2 px-3 py-1.5 rounded-lg bg-gradient-to-bl text-white font-medium from-gray-700 to-gray-900 border border-gray-600 shadow-gray-900/20 nice-shadow transition duration-300 ${
|
className={`mb-4 flex items-center space-x-2 px-3 py-1.5 rounded-lg bg-gradient-to-bl text-white font-medium from-gray-700 to-gray-900 border border-gray-600 shadow-gray-900/20 nice-shadow transition duration-300 ${isStripeEnabled ? 'hover:from-gray-600 hover:to-gray-800' : 'opacity-50 cursor-not-allowed'
|
||||||
isStripeEnabled ? 'hover:from-gray-600 hover:to-gray-800' : 'opacity-50 cursor-not-allowed'
|
|
||||||
}`}
|
}`}
|
||||||
disabled={!isStripeEnabled}
|
disabled={!isStripeEnabled}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
'use server';
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config';
|
||||||
import { RequestBodyWithAuthHeader, errorHandling } from '@services/utils/ts/requests';
|
import { RequestBodyWithAuthHeader, errorHandling } from '@services/utils/ts/requests';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
'use server';
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config';
|
||||||
import { RequestBodyWithAuthHeader, getResponseMetadata } from '@services/utils/ts/requests';
|
import { RequestBodyWithAuthHeader, getResponseMetadata } from '@services/utils/ts/requests';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue