Add payment to cron endpoint for recurring subscribers, set cron to nce a day, set expiration to 31 days

This commit is contained in:
austinkelsay 2024-09-01 13:36:53 -05:00
parent e69d974ad7
commit b9c2d04ed4
5 changed files with 127 additions and 88 deletions

View File

@ -1,102 +1,106 @@
import prisma from "../prisma"; import prisma from "../prisma";
import { webln } from "@getalby/sdk";
import { LightningAddress } from "@getalby/sdk";
const lnAddress = process.env.NEXT_PUBLIC_LIGHTNING_ADDRESS;
export const getAllUsers = async () => { export const getAllUsers = async () => {
return await prisma.user.findMany({ return await prisma.user.findMany({
include: {
role: true, // Include related role
purchased: {
include: { include: {
role: true, // Include related role course: true, // Include course details in purchases
purchased: { resource: true, // Include resource details in purchases
include: {
course: true, // Include course details in purchases
resource: true, // Include resource details in purchases
},
},
}, },
}); },
},
});
}; };
export const getUserById = async (id) => { export const getUserById = async (id) => {
return await prisma.user.findUnique({ return await prisma.user.findUnique({
where: { id }, where: { id },
include: {
role: true, // Include related role
purchased: {
include: { include: {
role: true, // Include related role course: true, // Include course details in purchases
purchased: { resource: true, // Include resource details in purchases
include: {
course: true, // Include course details in purchases
resource: true, // Include resource details in purchases
},
},
}, },
}); },
},
});
}; };
export const getUserByPubkey = async (pubkey) => { export const getUserByPubkey = async (pubkey) => {
return await prisma.user.findUnique({ return await prisma.user.findUnique({
where: { pubkey }, where: { pubkey },
include: {
role: true, // Include related role
purchased: {
include: { include: {
role: true, // Include related role course: true, // Include course details in purchases
purchased: { resource: true, // Include resource details in purchases
include: {
course: true, // Include course details in purchases
resource: true, // Include resource details in purchases
},
},
}, },
}); },
},
});
} }
export const addResourcePurchaseToUser = async (userId, purchaseData) => { export const addResourcePurchaseToUser = async (userId, purchaseData) => {
return await prisma.user.update({ return await prisma.user.update({
where: { id: userId }, where: { id: userId },
data: { data: {
purchased: { purchased: {
create: { create: {
resourceId: purchaseData.resourceId, resourceId: purchaseData.resourceId,
amountPaid: purchaseData.amountPaid, amountPaid: purchaseData.amountPaid,
},
}, },
}, },
include: { },
purchased: { include: {
include: { purchased: {
resource: true, include: {
}, resource: true,
}, },
}, },
}); },
}; });
};
export const addCoursePurchaseToUser = async (userId, purchaseData) => { export const addCoursePurchaseToUser = async (userId, purchaseData) => {
return await prisma.user.update({ return await prisma.user.update({
where: { id: userId }, where: { id: userId },
data: { data: {
purchased: { purchased: {
create: { create: {
courseId: purchaseData.courseId, courseId: purchaseData.courseId,
amountPaid: purchaseData.amountPaid, amountPaid: purchaseData.amountPaid,
},
}, },
}, },
}); },
}; });
};
export const createUser = async (data) => { export const createUser = async (data) => {
return await prisma.user.create({ return await prisma.user.create({
data, data,
}); });
}; };
export const updateUser = async (id, data) => { export const updateUser = async (id, data) => {
console.log("user modelllll", id, data) console.log("user modelllll", id, data)
return await prisma.user.update({ return await prisma.user.update({
where: { id }, where: { id },
data, data,
}); });
}; };
export const deleteUser = async (id) => { export const deleteUser = async (id) => {
return await prisma.user.delete({ return await prisma.user.delete({
where: { id }, where: { id },
}); });
}; };
export const updateUserSubscription = async (userId, isSubscribed, nwc) => { export const updateUserSubscription = async (userId, isSubscribed, nwc) => {
@ -129,24 +133,27 @@ export const updateUserSubscription = async (userId, isSubscribed, nwc) => {
}); });
}; };
export const checkAndUpdateExpiredSubscriptions = async () => { export const findExpiredSubscriptions = async () => {
const now = new Date(); const now = new Date();
const oneHourAgo = new Date(now.getTime() - 1 * 60 * 60 * 1000); const thirtyOneDaysAgo = new Date(now.getTime() - 31 * 24 * 60 * 60 * 1000);
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000);
const expiredSubscriptions = await prisma.role.findMany({ return await prisma.role.findMany({
where: { where: {
subscribed: true, subscribed: true,
lastPaymentAt: { lastPaymentAt: {
lt: fiveMinutesAgo lt: thirtyOneDaysAgo
} }
}, },
select: { select: {
userId: true userId: true,
nwc: true
} }
}); });
};
const updatePromises = expiredSubscriptions.map(({ userId }) => export const expireUserSubscriptions = async (userIds) => {
const now = new Date();
const updatePromises = userIds.map((userId) =>
prisma.role.update({ prisma.role.update({
where: { userId }, where: { userId },
data: { data: {
@ -160,6 +167,5 @@ export const checkAndUpdateExpiredSubscriptions = async () => {
); );
await prisma.$transaction(updatePromises); await prisma.$transaction(updatePromises);
return userIds.length;
return expiredSubscriptions.length;
}; };

View File

@ -1,4 +1,9 @@
import { checkAndUpdateExpiredSubscriptions } from "@/db/models/userModels"; import { findExpiredSubscriptions, updateUserSubscription, expireUserSubscriptions } from "@/db/models/userModels";
import { webln } from "@getalby/sdk";
import { LightningAddress } from '@getalby/lightning-tools';
const lnAddress = process.env.LIGHTNING_ADDRESS;
const amount = 25; // Set the subscription amount in satoshis
export default async function handler(req, res) { export default async function handler(req, res) {
if (req.headers.authorization !== `Bearer ${process.env.CRON_SECRET}`) { if (req.headers.authorization !== `Bearer ${process.env.CRON_SECRET}`) {
@ -7,8 +12,41 @@ export default async function handler(req, res) {
if (req.method === 'POST') { if (req.method === 'POST') {
try { try {
const updatedCount = await checkAndUpdateExpiredSubscriptions(); const expiredSubscriptions = await findExpiredSubscriptions();
res.status(200).json({ message: `Cron job completed successfully. Updated ${updatedCount} subscriptions.` }); const stillExpired = [];
for (const { userId, nwc } of expiredSubscriptions) {
if (nwc) {
try {
const nwcProvider = new webln.NostrWebLNProvider({
nostrWalletConnectUrl: nwc
});
await nwcProvider.enable();
const ln = new LightningAddress(lnAddress);
await ln.fetch();
const newInvoice = await ln.requestInvoice({ satoshi: amount });
const response = await nwcProvider.sendPayment(newInvoice?.paymentRequest);
if (response && response?.preimage) {
await updateUserSubscription(userId, true, nwc);
continue; // Skip adding to stillExpired list
}
} catch (error) {
console.error(`Payment failed for user ${userId}:`, error);
}
}
stillExpired.push(userId);
}
const expiredCount = await expireUserSubscriptions(stillExpired);
res.status(200).json({
message: `Cron job completed successfully.
Processed ${expiredSubscriptions.length} subscriptions.
Expired ${expiredCount} subscriptions.`
});
} catch (error) { } catch (error) {
console.error('Cron job error:', error); console.error('Cron job error:', error);
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });

View File

@ -22,24 +22,19 @@ const Profile = () => {
const [subscribedUntil, setSubscribedUntil] = useState(null); const [subscribedUntil, setSubscribedUntil] = useState(null);
const [subscriptionExpiredAt, setSubscriptionExpiredAt] = useState(null); const [subscriptionExpiredAt, setSubscriptionExpiredAt] = useState(null);
const { data: session, status, update } = useSession(); const { data: session } = useSession();
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const { ndk } = useNDKContext(); const { ndk } = useNDKContext();
const menu = useRef(null); const menu = useRef(null);
useEffect(() => {
// Refetch the session when the component mounts
update();
}, []);
useEffect(() => { useEffect(() => {
if (session && session.user) { if (session && session.user) {
setUser(session.user); setUser(session.user);
if (session.user.role) { if (session.user.role) {
setSubscribed(session.user.role.subscribed); setSubscribed(session.user.role.subscribed);
const subscribedAt = new Date(session.user.role.lastPaymentAt); const subscribedAt = new Date(session.user.role.lastPaymentAt);
// The user is subscribed until the date in subscribedAt + 30 days // The user is subscribed until the date in subscribedAt + 31 days
const subscribedUntil = new Date(subscribedAt.getTime() + 30 * 24 * 60 * 60 * 1000); const subscribedUntil = new Date(subscribedAt.getTime() + 31 * 24 * 60 * 60 * 1000);
setSubscribedUntil(subscribedUntil); setSubscribedUntil(subscribedUntil);
if (session.user.role.subscriptionExpiredAt) { if (session.user.role.subscriptionExpiredAt) {
const expiredAt = new Date(session.user.role.subscriptionExpiredAt) const expiredAt = new Date(session.user.role.subscriptionExpiredAt)

View File

@ -7,7 +7,7 @@ export const findKind0Fields = async (kind0) => {
const findTruthyPropertyValue = (object, properties) => { const findTruthyPropertyValue = (object, properties) => {
for (const property of properties) { for (const property of properties) {
if (object[property]) { if (object?.[property]) {
return object[property]; return object[property];
} }
} }

View File

@ -3,7 +3,7 @@
"crons": [ "crons": [
{ {
"path": "/api/users/subscription/cron", "path": "/api/users/subscription/cron",
"schedule": "0 * * * *" "schedule": "0 0 * * *"
} }
] ]
} }