Skip to content

Commit 3b7b873

Browse files
author
Rajat Saxena
committed
WIP: Migration for payment plans; checkout screen now shows included products, theme is still off; new graph query for getting included products details
1 parent 4429bc4 commit 3b7b873

15 files changed

Lines changed: 1248 additions & 419 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import mongoose from "mongoose";
2+
import { nanoid } from "nanoid";
3+
4+
function generateUniqueId() {
5+
return nanoid();
6+
}
7+
8+
mongoose.connect(process.env.DB_CONNECTION_STRING, {
9+
useNewUrlParser: true,
10+
useUnifiedTopology: true,
11+
});
12+
13+
const PaymentPlanSchema = new mongoose.Schema(
14+
{
15+
domain: { type: mongoose.Schema.Types.ObjectId, required: true },
16+
planId: {
17+
type: String,
18+
required: true,
19+
unique: true,
20+
default: generateUniqueId,
21+
},
22+
entityId: { type: String, required: true },
23+
entityType: {
24+
type: String,
25+
required: true,
26+
enum: ["course", "community"],
27+
},
28+
},
29+
{
30+
timestamps: true,
31+
},
32+
);
33+
const PaymentPlan = mongoose.model("PaymentPlan", PaymentPlanSchema);
34+
35+
const CourseSchema = new mongoose.Schema(
36+
{
37+
domain: { type: mongoose.Schema.Types.ObjectId, required: true },
38+
courseId: { type: String, required: true, default: generateUniqueId },
39+
paymentPlans: [String],
40+
defaultPaymentPlan: { type: String },
41+
},
42+
{
43+
timestamps: true,
44+
},
45+
);
46+
const Course = mongoose.model("Course", CourseSchema);
47+
48+
const CommunitySchema = new mongoose.Schema(
49+
{
50+
domain: { type: mongoose.Schema.Types.ObjectId, required: true },
51+
communityId: {
52+
type: String,
53+
required: true,
54+
unique: true,
55+
default: generateUniqueId,
56+
},
57+
paymentPlans: [String],
58+
defaultPaymentPlan: { type: String },
59+
},
60+
{
61+
timestamps: true,
62+
},
63+
);
64+
const Community = mongoose.model("Community", CommunitySchema);
65+
66+
const migratePaymentPlansOfProducts = async () => {
67+
console.log("🏁 Migrating payment plans of products");
68+
const courses = await Course.find({});
69+
for (const course of courses) {
70+
const paymentPlans = await PaymentPlan.find({
71+
domain: course.domain,
72+
planId: { $in: course.paymentPlans },
73+
});
74+
75+
for (const paymentPlan of paymentPlans) {
76+
paymentPlan.entityId = course.courseId;
77+
paymentPlan.entityType = "course";
78+
console.log(
79+
`Updating payment plan ${paymentPlan.planId} for product ${course.courseId}`,
80+
);
81+
await paymentPlan.save();
82+
}
83+
84+
// delete paymentPlans property from course
85+
course.paymentPlans = undefined;
86+
await course.save();
87+
}
88+
console.log("✅ Migrating payment plans of products completed\n");
89+
};
90+
91+
const migratePaymentPlansOfCommunities = async () => {
92+
console.log("🏁 Migrating payment plans of communities");
93+
const communities = await Community.find({});
94+
for (const community of communities) {
95+
const paymentPlans = await PaymentPlan.find({
96+
domain: community.domain,
97+
planId: { $in: community.paymentPlans },
98+
});
99+
100+
for (const paymentPlan of paymentPlans) {
101+
paymentPlan.entityId = community.communityId;
102+
paymentPlan.entityType = "community";
103+
console.log(
104+
`Updating payment plan ${paymentPlan.planId} for community ${community.communityId}`,
105+
);
106+
await paymentPlan.save();
107+
}
108+
109+
// delete paymentPlans property from community
110+
community.paymentPlans = undefined;
111+
await community.save();
112+
}
113+
console.log("✅ Migrating payment plans of communities completed\n");
114+
};
115+
116+
const migratePaymentPlans = async () => {
117+
await migratePaymentPlansOfProducts();
118+
await migratePaymentPlansOfCommunities();
119+
};
120+
121+
(async () => {
122+
await migratePaymentPlans();
123+
mongoose.connection.close();
124+
})();

apps/web/app/(with-contexts)/(with-layout)/checkout/product.tsx

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { AddressContext } from "@components/contexts";
44
import Checkout, { Product } from "@components/public/payments/checkout";
5-
import { Constants, PaymentPlan } from "@courselit/common-models";
5+
import { Constants, PaymentPlan, Course } from "@courselit/common-models";
66
import { useToast } from "@courselit/components-library";
77
import { FetchBuilder } from "@courselit/utils";
88
import { TOAST_TITLE_ERROR } from "@ui-config/strings";
@@ -20,6 +20,54 @@ export default function ProductCheckout() {
2020

2121
const [product, setProduct] = useState<Product | null>(null);
2222
const [paymentPlans, setPaymentPlans] = useState<PaymentPlan[]>([]);
23+
const [includedProducts, setIncludedProducts] = useState<Course[]>([]);
24+
25+
const getIncludedProducts = useCallback(async () => {
26+
const query = `
27+
query ($entityId: String!, $entityType: MembershipEntityType!) {
28+
includedProducts: getIncludedProducts(entityId: $entityId, entityType: $entityType) {
29+
courseId
30+
title
31+
slug
32+
featuredImage {
33+
thumbnail
34+
file
35+
}
36+
}
37+
}
38+
`;
39+
const fetch = new FetchBuilder()
40+
.setUrl(`${address.backend}/api/graph`)
41+
.setPayload({
42+
query,
43+
variables: {
44+
entityId,
45+
entityType: (entityType === MembershipEntityType.COURSE
46+
? MembershipEntityType.COURSE
47+
: MembershipEntityType.COMMUNITY
48+
).toUpperCase(),
49+
},
50+
})
51+
.setIsGraphQLEndpoint(true)
52+
.build();
53+
try {
54+
const response = await fetch.exec();
55+
if (response.includedProducts) {
56+
setIncludedProducts([...response.includedProducts]);
57+
} else {
58+
toast({
59+
title: TOAST_TITLE_ERROR,
60+
description: "Course not found",
61+
});
62+
}
63+
} catch (err: any) {
64+
toast({
65+
title: TOAST_TITLE_ERROR,
66+
description: err.message,
67+
});
68+
} finally {
69+
}
70+
}, [address.backend, entityId, entityType, toast]);
2371

2472
const getProduct = useCallback(async () => {
2573
const query = `
@@ -40,7 +88,9 @@ export default function ProductCheckout() {
4088
emiAmount
4189
emiTotalInstallments
4290
subscriptionMonthlyAmount
43-
subscriptionYearlyAmount
91+
subscriptionYearlyAmont
92+
description
93+
includedProducts
4494
}
4595
defaultPaymentPlan
4696
}
@@ -60,6 +110,7 @@ export default function ProductCheckout() {
60110
slug: response.course.slug,
61111
featuredImage: response.course.featuredImage?.file,
62112
type: MembershipEntityType.COURSE,
113+
defaultPaymentPlanId: response.course.defaultPaymentPlan,
63114
});
64115
setPaymentPlans([...response.course.paymentPlans]);
65116
} else {
@@ -92,13 +143,16 @@ export default function ProductCheckout() {
92143
emiTotalInstallments
93144
subscriptionMonthlyAmount
94145
subscriptionYearlyAmount
146+
description
147+
includedProducts
95148
}
96149
featuredImage {
97150
thumbnail
98151
file
99152
}
100153
autoAcceptMembers
101154
joiningReasonText
155+
defaultPaymentPlan
102156
}
103157
}
104158
`;
@@ -117,6 +171,7 @@ export default function ProductCheckout() {
117171
featuredImage: response.community.featuredImage?.file,
118172
joiningReasonText: response.community.joiningReasonText,
119173
autoAcceptMembers: response.community.autoAcceptMembers,
174+
defaultPaymentPlanId: response.community.defaultPaymentPlan,
120175
});
121176
setPaymentPlans([...response.community.paymentPlans]);
122177
} else {
@@ -144,9 +199,21 @@ export default function ProductCheckout() {
144199
}
145200
}, [entityId, entityType, getProduct, getCommunity]);
146201

202+
useEffect(() => {
203+
if (paymentPlans.length > 0) {
204+
getIncludedProducts();
205+
}
206+
}, [paymentPlans, getIncludedProducts]);
207+
147208
if (!product) {
148209
return null;
149210
}
150211

151-
return <Checkout product={product} paymentPlans={paymentPlans} />;
212+
return (
213+
<Checkout
214+
product={product}
215+
paymentPlans={paymentPlans}
216+
includedProducts={includedProducts}
217+
/>
218+
);
152219
}

apps/web/app/(with-contexts)/dashboard/(sidebar)/paymentplan/[type]/[id]/edit/[planid]/page.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
import { useParams, useRouter } from "next/navigation";
44
import { useEffect } from "react";
5-
import { Loader2 } from "lucide-react";
65
import { Constants, MembershipEntityType } from "@courselit/common-models";
76
import DashboardContent from "@/components/admin/dashboard-content";
8-
import { PaymentPlanForm } from "@/components/admin/payments/payment-plan-form";
7+
import {
8+
PaymentPlanForm,
9+
PaymentPlanFormSkeleton,
10+
} from "@/components/admin/payments/payment-plan-form";
11+
import { Skeleton } from "@/components/ui/skeleton";
912
import {
1013
COMMUNITY_SETTINGS,
1114
EDIT_PAYMENT_PLAN_HEADER,
@@ -43,7 +46,6 @@ export default function EditPaymentPlanPage() {
4346
},
4447
{ label: EDIT_PAYMENT_PLAN_HEADER, href: "#" },
4548
];
46-
4749
const { paymentPlan, loaded: paymentPlanLoaded } = usePaymentPlan(
4850
planId,
4951
entityId,
@@ -61,9 +63,17 @@ export default function EditPaymentPlanPage() {
6163
if (!paymentPlanLoaded) {
6264
return (
6365
<DashboardContent breadcrumbs={breadcrumbs}>
64-
<div className="flex items-center justify-center h-64">
65-
<Loader2 className="h-8 w-8 animate-spin" />
66+
<div className="space-y-2">
67+
<div className="flex flex-col lg:flex-row gap-4 lg:items-center justify-between mb-8">
68+
<div>
69+
<h1 className="text-4xl font-semibold">
70+
{EDIT_PAYMENT_PLAN_HEADER}
71+
</h1>
72+
<Skeleton className="h-5 w-64 mt-2" />
73+
</div>
74+
</div>
6675
</div>
76+
<PaymentPlanFormSkeleton />
6777
</DashboardContent>
6878
);
6979
}
@@ -87,6 +97,7 @@ export default function EditPaymentPlanPage() {
8797
initialData={
8898
paymentPlan
8999
? {
100+
planId: paymentPlan.planId,
90101
name: paymentPlan.name,
91102
description: paymentPlan.description,
92103
type: paymentPlan.type,

0 commit comments

Comments
 (0)