Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: Grant Access to Additional Products for Members
description: Guide to grant access to additional products for community members
layout: ../../../layouts/MainLayout.astro
---

## Steps to Grant Access to Additional Products

1. Navigate to the **Memberships** section in your community dashboard.
2. Click on the member's name/email you want to manage.
3. You will be taken to user's edit screen.

## FAQ

1. When a members gets access to additional products, are these enrollments be tracked?
Yes, these enrollments are tracked and will show up in the product's enrollment metric. However, they will not count towards the product's sales metrics.

2. Will the included product's email sequences be triggered?
Yes, the included product's email sequences will be triggered.

3. Can I add unpublished products to the additional products?
No, only published and visible products can be added as additional products. Additionally, only published products will be added to the user's account upon joining the community.
124 changes: 124 additions & 0 deletions apps/web/.migrations/23-08-25_10-18-migrate-payment-plans.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import mongoose from "mongoose";
import { nanoid } from "nanoid";

function generateUniqueId() {
return nanoid();
}

mongoose.connect(process.env.DB_CONNECTION_STRING, {
useNewUrlParser: true,
useUnifiedTopology: true,
});

const PaymentPlanSchema = new mongoose.Schema(
{
domain: { type: mongoose.Schema.Types.ObjectId, required: true },
planId: {
type: String,
required: true,
unique: true,
default: generateUniqueId,
},
entityId: { type: String, required: true },
entityType: {
type: String,
required: true,
enum: ["course", "community"],
},
},
{
timestamps: true,
},
);
const PaymentPlan = mongoose.model("PaymentPlan", PaymentPlanSchema);

const CourseSchema = new mongoose.Schema(
{
domain: { type: mongoose.Schema.Types.ObjectId, required: true },
courseId: { type: String, required: true, default: generateUniqueId },
paymentPlans: [String],
defaultPaymentPlan: { type: String },
},
{
timestamps: true,
},
);
const Course = mongoose.model("Course", CourseSchema);

const CommunitySchema = new mongoose.Schema(
{
domain: { type: mongoose.Schema.Types.ObjectId, required: true },
communityId: {
type: String,
required: true,
unique: true,
default: generateUniqueId,
},
paymentPlans: [String],
defaultPaymentPlan: { type: String },
},
{
timestamps: true,
},
);
const Community = mongoose.model("Community", CommunitySchema);

const migratePaymentPlansOfProducts = async () => {
console.log("🏁 Migrating payment plans of products");
const courses = await Course.find({});
for (const course of courses) {
const paymentPlans = await PaymentPlan.find({
domain: course.domain,
planId: { $in: course.paymentPlans },
});

for (const paymentPlan of paymentPlans) {
paymentPlan.entityId = course.courseId;
paymentPlan.entityType = "course";
console.log(
`Updating payment plan ${paymentPlan.planId} for product ${course.courseId}`,
);
await paymentPlan.save();
}

// delete paymentPlans property from course
course.paymentPlans = undefined;
await course.save();
}
console.log("✅ Migrating payment plans of products completed\n");
};

const migratePaymentPlansOfCommunities = async () => {
console.log("🏁 Migrating payment plans of communities");
const communities = await Community.find({});
for (const community of communities) {
const paymentPlans = await PaymentPlan.find({
domain: community.domain,
planId: { $in: community.paymentPlans },
});

for (const paymentPlan of paymentPlans) {
paymentPlan.entityId = community.communityId;
paymentPlan.entityType = "community";
console.log(
`Updating payment plan ${paymentPlan.planId} for community ${community.communityId}`,
);
await paymentPlan.save();
}

// delete paymentPlans property from community
community.paymentPlans = undefined;
await community.save();
}
console.log("✅ Migrating payment plans of communities completed\n");
};

const migratePaymentPlans = async () => {
await migratePaymentPlansOfProducts();
await migratePaymentPlansOfCommunities();
};

(async () => {
await migratePaymentPlans();
mongoose.connection.close();
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import mongoose from "mongoose";
import { nanoid } from "nanoid";

function generateUniqueId() {
return nanoid();
}

mongoose.connect(process.env.DB_CONNECTION_STRING, {
useNewUrlParser: true,
useUnifiedTopology: true,
});

const PaymentPlanSchema = new mongoose.Schema(
{
domain: { type: mongoose.Schema.Types.ObjectId, required: true },
planId: {
type: String,
required: true,
unique: true,
default: generateUniqueId,
},
entityId: { type: String, required: true },
internal: { type: Boolean, default: false },
},
{
timestamps: true,
},
);
const PaymentPlan = mongoose.model("PaymentPlan", PaymentPlanSchema);

export const MembershipSchema = new mongoose.Schema(
{
domain: { type: mongoose.Schema.Types.ObjectId, required: true },
membershipId: {
type: String,
required: true,
unique: true,
default: generateUniqueId,
},
paymentPlanId: { type: String, required: true },
joiningReason: { type: String },
},
{
timestamps: true,
},
);
const Membership = mongoose.model("Membership", MembershipSchema);

const migrateInternalPaymentPlans = async () => {
console.log("🏁 Migrating internal payment plans");
const paymentPlans = await PaymentPlan.find({
internal: true,
});
for (const paymentPlan of paymentPlans) {
if (paymentPlan.entityId) continue;

paymentPlan.entityId = "internal";
await paymentPlan.save();
console.log(`Updated payment plan ${paymentPlan.planId}`);
}
console.log("✅ Migrating internal payment plans completed\n");
};

const migrateMembershipOfCommunityCreators = async () => {
console.log("🏁 Migrating membership of community creators");
const paymentPlans = await PaymentPlan.find({
internal: true,
});
for (const paymentPlan of paymentPlans) {
const memberships = await Membership.find({
domain: paymentPlan.domain,
joiningReason: "Joined as creator",
});
for (const membership of memberships) {
if (membership.paymentPlanId) continue;

membership.paymentPlanId = paymentPlan.planId;
await membership.save();
console.log(
`Updated membership ${membership.membershipId} with internal payment plan ${paymentPlan.planId}`,
);
}
}
console.log("✅ Migrating membership of community creators completed\n");
};

const migratePaymentPlansWithNoInternalProperty = async () => {
console.log("🏁 Migrating payment plans with no internal property");
const paymentPlans = await PaymentPlan.find({
internal: { $exists: false },
});
for (const paymentPlan of paymentPlans) {
paymentPlan.internal = false;
await paymentPlan.save();
console.log(`Updated payment plan ${paymentPlan.planId}`);
}
console.log(
"✅ Migrating payment plans with no internal property completed\n",
);
};

(async () => {
await migrateInternalPaymentPlans();
await migrateMembershipOfCommunityCreators();
await migratePaymentPlansWithNoInternalProperty();
mongoose.connection.close();
})();
71 changes: 69 additions & 2 deletions apps/web/app/(with-contexts)/(with-layout)/checkout/product.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

const [product, setProduct] = useState<Product | null>(null);
const [paymentPlans, setPaymentPlans] = useState<PaymentPlan[]>([]);
const [includedProducts, setIncludedProducts] = useState<Course[]>([]);

const getIncludedProducts = useCallback(async () => {
const query = `
query ($entityId: String!, $entityType: MembershipEntityType!) {
includedProducts: getIncludedProducts(entityId: $entityId, entityType: $entityType) {
courseId
title
slug
featuredImage {
thumbnail
file
}
}
}
`;
const fetch = new FetchBuilder()
.setUrl(`${address.backend}/api/graph`)
.setPayload({
query,
variables: {
entityId,
entityType: (entityType === MembershipEntityType.COURSE
? MembershipEntityType.COURSE
: MembershipEntityType.COMMUNITY
).toUpperCase(),
},
})
.setIsGraphQLEndpoint(true)
.build();
try {
const response = await fetch.exec();
if (response.includedProducts) {
setIncludedProducts([...response.includedProducts]);
} else {
toast({
title: TOAST_TITLE_ERROR,
description: "Course not found",
});
}
} catch (err: any) {
toast({
title: TOAST_TITLE_ERROR,
description: err.message,
});
} finally {
}
}, [address.backend, entityId, entityType, toast]);

const getProduct = useCallback(async () => {
const query = `
Expand All @@ -41,6 +89,8 @@ export default function ProductCheckout() {
emiTotalInstallments
subscriptionMonthlyAmount
subscriptionYearlyAmount
description
includedProducts
}
defaultPaymentPlan
}
Expand All @@ -60,6 +110,7 @@ export default function ProductCheckout() {
slug: response.course.slug,
featuredImage: response.course.featuredImage?.file,
type: MembershipEntityType.COURSE,
defaultPaymentPlanId: response.course.defaultPaymentPlan,
});
setPaymentPlans([...response.course.paymentPlans]);
} else {
Expand Down Expand Up @@ -92,13 +143,16 @@ export default function ProductCheckout() {
emiTotalInstallments
subscriptionMonthlyAmount
subscriptionYearlyAmount
description
includedProducts
}
featuredImage {
thumbnail
file
}
autoAcceptMembers
joiningReasonText
defaultPaymentPlan
}
}
`;
Expand All @@ -117,6 +171,7 @@ export default function ProductCheckout() {
featuredImage: response.community.featuredImage?.file,
joiningReasonText: response.community.joiningReasonText,
autoAcceptMembers: response.community.autoAcceptMembers,
defaultPaymentPlanId: response.community.defaultPaymentPlan,
});
setPaymentPlans([...response.community.paymentPlans]);
} else {
Expand Down Expand Up @@ -144,9 +199,21 @@ export default function ProductCheckout() {
}
}, [entityId, entityType, getProduct, getCommunity]);

useEffect(() => {
if (paymentPlans.length > 0) {
getIncludedProducts();
}
}, [paymentPlans, getIncludedProducts]);

if (!product) {
return null;
}

return <Checkout product={product} paymentPlans={paymentPlans} />;
return (
<Checkout
product={product}
paymentPlans={paymentPlans}
includedProducts={includedProducts}
/>
);
}
Loading
Loading