diff --git a/apps/web/.gitignore b/apps/web/.gitignore
index e77579ff4..e18d7cc15 100644
--- a/apps/web/.gitignore
+++ b/apps/web/.gitignore
@@ -26,6 +26,7 @@ yarn-error.log*
.pnpm-debug.log*
# local env files
+.env
.env.local
.env.development.local
.env.test.local
@@ -39,3 +40,4 @@ yarn-error.log*
# analyze
/analyze
+.yarn/*
\ No newline at end of file
diff --git a/apps/web/app/(with-contexts)/(with-layout)/checkout/verify/page.tsx b/apps/web/app/(with-contexts)/(with-layout)/checkout/verify/page.tsx
index 506036d38..41c6c5583 100644
--- a/apps/web/app/(with-contexts)/(with-layout)/checkout/verify/page.tsx
+++ b/apps/web/app/(with-contexts)/(with-layout)/checkout/verify/page.tsx
@@ -21,20 +21,21 @@ export default function Page() {
const verifyPayment = async () => {
setPaymentStatus("pending"); // Hide check status again
const fetch = new FetchBuilder()
- .setUrl(`${address.backend}/api/payment/verify-new`)
+ .setUrl(`${address.backend}/api/payment/verify`)
.setHeaders({
"Content-Type": "application/json",
})
- .setPayload(JSON.stringify({ id }))
+ .setPayload(JSON.stringify({ purchaseId: id }))
.build();
try {
setLoading(true);
const response = await fetch.exec();
if (response.status) {
- setPaymentStatus(response.status);
+ setPaymentStatus(response.status === "success" ? "paid" : response.status);
}
} catch (error) {
+ console.error("Error verifying payment:", error);
} finally {
setLoading(false);
}
diff --git a/apps/web/app/api/payment/verify-new/route.ts b/apps/web/app/api/payment/verify-new/route.ts
index 5c469042a..4425f8305 100644
--- a/apps/web/app/api/payment/verify-new/route.ts
+++ b/apps/web/app/api/payment/verify-new/route.ts
@@ -12,6 +12,8 @@ interface RequestPayload {
export async function POST(req: NextRequest) {
const body: RequestPayload = await req.json();
const domainName = req.headers.get("domain");
+ console.log("Domain:", domainName);
+ console.log("Body:", body);
try {
const domain = await getDomain(domainName);
@@ -34,8 +36,10 @@ export async function POST(req: NextRequest) {
if (!id) {
return Response.json({ message: "Bad request" }, { status: 400 });
}
-
+ console.log("Invoice ID:", id);
+ console.log("Invoices", await InvoiceModel.find());
const invoice = await InvoiceModel.findOne({ invoiceId: id });
+ console.log("Invoice:", invoice);
if (!invoice) {
return Response.json(
diff --git a/apps/web/components/admin/dashboard/metric.tsx b/apps/web/components/admin/dashboard/metric.tsx
index 3580525cf..885206ed9 100644
--- a/apps/web/components/admin/dashboard/metric.tsx
+++ b/apps/web/components/admin/dashboard/metric.tsx
@@ -56,7 +56,8 @@ export const Metric = ({
query {
activities: getActivities(
type: ${type.toUpperCase()},
- duration: _${internalDuration.toUpperCase()}
+ duration: _${internalDuration.toUpperCase()},
+ points: true
) {
count,
points {
@@ -77,24 +78,46 @@ export const Metric = ({
setLoading(true);
const response = await fetch.exec();
if (response.activities) {
- const pointsWithDate = response.activities.points.map(
- (point: { date: string; count: number }) => {
- return {
- date: new Date(
- +point.date,
- ).toLocaleDateString(),
- count: point.count,
- };
- },
- );
+ const pointsWithDate =
+ response.activities.points?.map(
+ (point: {
+ date: string | number | Date;
+ count: number;
+ }) => {
+ console.log("Processing point:", point);
+ // Verificar el tipo de date y convertirlo apropiadamente
+ let dateObj: Date;
+
+ if (typeof point.date === "object") {
+ // Si es un objeto Date
+ dateObj = new Date(point.date.toString());
+ } else if (typeof point.date === "string") {
+ // Si es un string ISO
+ dateObj = new Date(point.date);
+ } else {
+ // Si es un timestamp numérico
+ dateObj = new Date(point.date);
+ }
+
+ return {
+ date: dateObj.toLocaleDateString(),
+ count: point.count,
+ };
+ },
+ ) || [];
+
+ console.log(`Processed points:`, pointsWithDate);
+ console.log(`Total count:`, response.activities.count);
setData({
count: response.activities.count,
points: pointsWithDate,
});
+ } else {
+ console.log(`No activities data returned from API`);
}
} catch (err: any) {
- console.log("Error in fetching activities"); // eslint-disable-line
+ console.error("Error in fetching activities:", err); // eslint-disable-line
} finally {
setLoading(false);
}
diff --git a/apps/web/components/admin/settings/index.tsx b/apps/web/components/admin/settings/index.tsx
index d899bf5a2..db80880c6 100644
--- a/apps/web/components/admin/settings/index.tsx
+++ b/apps/web/components/admin/settings/index.tsx
@@ -85,6 +85,7 @@ import { Copy, Info } from "lucide-react";
import { Label } from "@components/ui/label";
import { Input } from "@components/ui/input";
import Resources from "@components/resources";
+import { PAYMENT_METHOD_MERCADOPAGO } from "@courselit/common-models/dist/ui-constants";
const {
PAYMENT_METHOD_PAYPAL,
@@ -163,6 +164,7 @@ const Settings = (props: SettingsProps) => {
settings.lemonsqueezySubscriptionMonthlyVariantId,
lemonsqueezySubscriptionYearlyVariantId:
settings.lemonsqueezySubscriptionYearlyVariantId,
+ mercadopagoAccessToken: settings.mercadopagoAccessToken,
}),
);
}, [settings]);
@@ -195,7 +197,8 @@ const Settings = (props: SettingsProps) => {
codeInjectionHead,
codeInjectionBody,
mailingAddress,
- hideCourseLitBranding
+ hideCourseLitBranding,
+ mercadopagoAccessToken
}
},
apikeys: getApikeys {
@@ -247,6 +250,7 @@ const Settings = (props: SettingsProps) => {
settingsResponse.lemonsqueezySubscriptionMonthlyVariantId || "",
lemonsqueezySubscriptionYearlyVariantId:
settingsResponse.lemonsqueezySubscriptionYearlyVariantId || "",
+ mercadopagoAccessToken: settingsResponse.mercadopagoAccessToken,
};
setSettings(
Object.assign({}, settings, settingsResponseWithNullsRemoved),
@@ -291,7 +295,8 @@ const Settings = (props: SettingsProps) => {
codeInjectionHead,
codeInjectionBody,
mailingAddress,
- hideCourseLitBranding
+ hideCourseLitBranding,
+ mercadopagoAccessToken
}
}
}`;
@@ -358,7 +363,8 @@ const Settings = (props: SettingsProps) => {
codeInjectionHead,
codeInjectionBody,
mailingAddress,
- hideCourseLitBranding
+ hideCourseLitBranding,
+ mercadopagoAccessToken
}
}
}`;
@@ -431,7 +437,8 @@ const Settings = (props: SettingsProps) => {
codeInjectionHead,
codeInjectionBody,
mailingAddress,
- hideCourseLitBranding
+ hideCourseLitBranding,
+ mercadopagoAccessToken
}
}
}`;
@@ -496,7 +503,8 @@ const Settings = (props: SettingsProps) => {
codeInjectionHead,
codeInjectionBody,
mailingAddress,
- hideCourseLitBranding
+ hideCourseLitBranding,
+ mercadopagoAccessToken
}
}
}`;
@@ -554,7 +562,8 @@ const Settings = (props: SettingsProps) => {
$lemonsqueezyWebhookSecret: String
$lemonsqueezyOneTimeVariantId: String,
$lemonsqueezySubscriptionMonthlyVariantId: String,
- $lemonsqueezySubscriptionYearlyVariantId: String
+ $lemonsqueezySubscriptionYearlyVariantId: String,
+ $mercadopagoAccessToken: String
) {
settings: updatePaymentInfo(siteData: {
currencyISOCode: $currencyISOCode,
@@ -569,7 +578,8 @@ const Settings = (props: SettingsProps) => {
lemonsqueezyWebhookSecret: $lemonsqueezyWebhookSecret,
lemonsqueezyOneTimeVariantId: $lemonsqueezyOneTimeVariantId,
lemonsqueezySubscriptionMonthlyVariantId: $lemonsqueezySubscriptionMonthlyVariantId,
- lemonsqueezySubscriptionYearlyVariantId: $lemonsqueezySubscriptionYearlyVariantId
+ lemonsqueezySubscriptionYearlyVariantId: $lemonsqueezySubscriptionYearlyVariantId,
+ mercadopagoAccessToken: $mercadopagoAccessToken
}) {
settings {
title,
@@ -595,11 +605,18 @@ const Settings = (props: SettingsProps) => {
codeInjectionHead,
codeInjectionBody,
mailingAddress,
- hideCourseLitBranding
+ hideCourseLitBranding,
+ mercadopagoAccessToken
}
}
}`;
+ console.log("Mutation para guardar configuración:", query);
+ console.log(
+ "Token de Mercado Pago:",
+ newSettings.mercadopagoAccessToken,
+ );
+
try {
const fetchRequest = fetch
.setPayload({
@@ -623,6 +640,8 @@ const Settings = (props: SettingsProps) => {
newSettings.lemonsqueezySubscriptionMonthlyVariantId,
lemonsqueezySubscriptionYearlyVariantId:
newSettings.lemonsqueezySubscriptionYearlyVariantId,
+ mercadopagoAccessToken:
+ newSettings.mercadopagoAccessToken,
},
})
.build();
@@ -690,6 +709,9 @@ const Settings = (props: SettingsProps) => {
lemonsqueezySubscriptionYearlyVariantId: getNewSettings
? newSettings.lemonsqueezySubscriptionYearlyVariantId
: settings.lemonsqueezySubscriptionYearlyVariantId,
+ mercadopagoAccessToken: getNewSettings
+ ? newSettings.mercadopagoAccessToken
+ : settings.mercadopagoAccessToken,
});
const removeApikey = async (keyId: string) => {
@@ -920,6 +942,12 @@ const Settings = (props: SettingsProps) => {
!x.lemonsqueezy,
),
},
+ {
+ label: capitalize(
+ PAYMENT_METHOD_MERCADOPAGO.toLowerCase(),
+ ),
+ value: PAYMENT_METHOD_MERCADOPAGO,
+ },
]}
onChange={(value) =>
setNewSettings(
@@ -1042,6 +1070,22 @@ const Settings = (props: SettingsProps) => {
/>
>
)}
+ {newSettings.paymentMethod ===
+ PAYMENT_METHOD_MERCADOPAGO && (
+ <>
+
+ >
+ )}
{newSettings.paymentMethod ===
PAYMENT_METHOD_PAYPAL && (
import("./stripe"));
const Razorpay = dynamic(() => import("./razorpay"));
const Free = dynamic(() => import("./free"));
const Lemonsqueezy = dynamic(() => import("./lemonsqueezy"));
+const MercadoPago = dynamic(() => import("./mercadopago"));
interface CheckoutExternalProps {
course: Course;
@@ -39,6 +40,9 @@ const CheckoutExternal = (props: CheckoutExternalProps) => {
{paymentMethod === UIConstants.PAYMENT_METHOD_PAYPAL && (
<>>
)}
+ {paymentMethod === UIConstants.PAYMENT_METHOD_MERCADOPAGO && (
+
+ )}
>
)}
diff --git a/apps/web/components/public/checkout/mercadopago.tsx b/apps/web/components/public/checkout/mercadopago.tsx
new file mode 100644
index 000000000..cbc19ae44
--- /dev/null
+++ b/apps/web/components/public/checkout/mercadopago.tsx
@@ -0,0 +1,84 @@
+import React from "react";
+import { Button, useToast } from "@courselit/components-library";
+import {
+ ENROLL_BUTTON_TEXT,
+ TOAST_TITLE_ERROR,
+} from "../../../ui-config/strings";
+import { connect } from "react-redux";
+import { useRouter } from "next/router";
+import type { AppState, AppDispatch } from "@courselit/state-management";
+import { Address, Course, SiteInfo } from "@courselit/common-models";
+import { FetchBuilder } from "@courselit/utils";
+import { actionCreators } from "@courselit/state-management";
+
+const { networkAction } = actionCreators;
+
+interface MercadoPagoProps {
+ course: Course;
+ siteInfo: SiteInfo;
+ address: Address;
+ dispatch: AppDispatch;
+}
+
+const MercadoPago = (props: MercadoPagoProps) => {
+ const { course, siteInfo, address, dispatch } = props;
+ const router = useRouter();
+ const { toast } = useToast();
+
+ const handleClick = async () => {
+ const payload = {
+ courseid: course.courseId,
+ metadata: JSON.stringify({
+ cancelUrl: `${address.frontend}${router.asPath}`,
+ successUrl: `${address.frontend}/checkout/${course.courseId}`,
+ sourceUrl: `/course/${course.slug}/${course.courseId}`,
+ }),
+ };
+ const fetch = new FetchBuilder()
+ .setUrl(`${address.backend}/api/payment/initiate`)
+ .setHeaders({
+ "Content-Type": "application/json",
+ })
+ .setPayload(JSON.stringify(payload))
+ .build();
+
+ try {
+ dispatch(networkAction(true));
+ const response = await fetch.exec({
+ redirectToOnUnAuth: router.asPath,
+ });
+ dispatch(networkAction(false));
+
+ if (response.status === "initiated") {
+ // If the API returns a payment URL, use it for redirection
+ if (response.paymentUrl) {
+ window.location.href = response.paymentUrl;
+ } else {
+ // Fallback to the old behavior
+ router.push(`/checkout/${course.courseId}?id=${response.paymentTracker}`);
+ }
+ } else if (response.status === "success") {
+ router.replace(`/course/${course.slug}/${course.courseId}`);
+ }
+ } catch (err: any) {
+ toast({
+ title: TOAST_TITLE_ERROR,
+ description: err.message,
+ variant: "destructive",
+ });
+ } finally {
+ dispatch(networkAction(false));
+ }
+ };
+
+ return ;
+};
+
+const mapStateToProps = (state: AppState) => ({
+ siteInfo: state.siteinfo,
+ address: state.address,
+});
+
+const mapDispatchToProps = (dispatch: AppDispatch) => ({ dispatch });
+
+export default connect(mapStateToProps, mapDispatchToProps)(MercadoPago);
diff --git a/apps/web/graphql/settings/helpers.ts b/apps/web/graphql/settings/helpers.ts
index d3ff38b22..cf971cf64 100644
--- a/apps/web/graphql/settings/helpers.ts
+++ b/apps/web/graphql/settings/helpers.ts
@@ -96,6 +96,13 @@ export const checkForInvalidPaymentMethodSettings = (
failedPaymentMethod = UIConstants.PAYMENT_METHOD_LEMONSQUEEZY;
}
+ if (
+ siteInfo.paymentMethod === UIConstants.PAYMENT_METHOD_MERCADOPAGO &&
+ !siteInfo.mercadopagoAccessToken
+ ) {
+ failedPaymentMethod = UIConstants.PAYMENT_METHOD_MERCADOPAGO;
+ }
+
return failedPaymentMethod;
};
diff --git a/apps/web/graphql/settings/types.ts b/apps/web/graphql/settings/types.ts
index 1b32c2500..47d416b40 100644
--- a/apps/web/graphql/settings/types.ts
+++ b/apps/web/graphql/settings/types.ts
@@ -60,6 +60,7 @@ const siteType = new GraphQLObjectType({
codeInjectionBody: { type: GraphQLString },
mailingAddress: { type: GraphQLString },
hideCourseLitBranding: { type: GraphQLBoolean },
+ mercadopagoAccessToken: { type: GraphQLString },
},
});
@@ -95,6 +96,7 @@ const sitePaymentUpdateType = new GraphQLInputObjectType({
lemonsqueezyOneTimeVariantId: { type: GraphQLString },
lemonsqueezySubscriptionMonthlyVariantId: { type: GraphQLString },
lemonsqueezySubscriptionYearlyVariantId: { type: GraphQLString },
+ mercadopagoAccessToken: { type: GraphQLString },
},
});
diff --git a/apps/web/models/SiteInfo.ts b/apps/web/models/SiteInfo.ts
index d716b3ad6..514da5c0e 100644
--- a/apps/web/models/SiteInfo.ts
+++ b/apps/web/models/SiteInfo.ts
@@ -25,6 +25,7 @@ const SettingsSchema = new mongoose.Schema({
lemonsqueezyOneTimeVariantId: { type: String },
lemonsqueezySubscriptionMonthlyVariantId: { type: String },
lemonsqueezySubscriptionYearlyVariantId: { type: String },
+ mercadopagoAccessToken: { type: String },
});
export default SettingsSchema;
diff --git a/apps/web/pages/api/graph.ts b/apps/web/pages/api/graph.ts
index 54a2bdb62..cd3a4415f 100644
--- a/apps/web/pages/api/graph.ts
+++ b/apps/web/pages/api/graph.ts
@@ -1,4 +1,4 @@
-import type { NextApiRequest } from "next";
+import type { NextApiRequest, NextApiResponse } from "next";
import schema from "../../graphql";
import { graphql } from "graphql";
import { getAddress } from "../../lib/utils";
@@ -68,12 +68,12 @@ export default async function handler(
subdomain: domain,
address: getAddress(hostname, protocol),
};
- const response = await graphql({
- schema,
- source: query || req.body.query,
- rootValue: null,
- contextValue,
- variableValues: variables,
- });
- return res.status(200).json(response);
+ const response = await graphql({
+ schema,
+ source: query || req.body.query,
+ rootValue: null,
+ contextValue,
+ variableValues: variables,
+ });
+ return res.status(200).json(response);
}
diff --git a/apps/web/pages/api/payment/initiate.ts b/apps/web/pages/api/payment/initiate.ts
index 99ea26eff..0916590b3 100644
--- a/apps/web/pages/api/payment/initiate.ts
+++ b/apps/web/pages/api/payment/initiate.ts
@@ -93,19 +93,61 @@ export default async function handler(
currencyISOCode: siteinfo.currencyISOCode,
});
- const paymentTracker = await paymentMethod.initiate({
- course,
- metadata: JSON.parse(metadata),
- purchaseId: purchase.orderId,
- });
+ // Prepare the metadata with the required information
+ const metadataObj = JSON.parse(metadata);
+ const invoiceId = purchase.orderId;
+
+ console.log("Payment method:", paymentMethod.getName());
+
+ let paymentTracker;
+
+ if (paymentMethod.getName() === 'mercadopago') {
+ // For Mercado Pago, we need to pass the parameters in the format expected by MercadoPagoPayment
+ paymentTracker = await paymentMethod.initiate({
+ metadata: {
+ invoiceId,
+ ...metadataObj
+ },
+ paymentPlan: {
+ type: "onetime",
+ oneTimeAmount: course.cost
+ },
+ // Use type assertion for the entire product object to handle type compatibility
+ product: {
+ id: course.courseId,
+ title: course.title,
+ type: (course.type || "course") as any,
+ cost: course.cost
+ } as any,
+ origin: metadataObj.cancelUrl?.split('/checkout')[0] || ''
+ });
+ } else {
+ // For other payment methods, use the existing format
+ paymentTracker = await paymentMethod.initiate({
+ course,
+ metadata: metadataObj,
+ purchaseId: invoiceId,
+ });
+ }
- purchase.paymentId = paymentTracker;
+ purchase.paymentId = typeof paymentTracker === 'object' && paymentTracker.id
+ ? paymentTracker.id
+ : paymentTracker;
await purchase.save();
- res.status(200).json({
- status: transactionInitiated,
- paymentTracker,
- });
+ // If Mercado Pago returns a URL, use it for redirection
+ if (typeof paymentTracker === 'object' && paymentTracker.url) {
+ res.status(200).json({
+ status: transactionInitiated,
+ paymentTracker: purchase.paymentId,
+ paymentUrl: paymentTracker.url
+ });
+ } else {
+ res.status(200).json({
+ status: transactionInitiated,
+ paymentTracker: purchase.paymentId
+ });
+ }
} catch (err: any) {
error(err.message, { stack: err.stack });
res.status(500).json({
diff --git a/apps/web/pages/api/payment/verify.ts b/apps/web/pages/api/payment/verify.ts
index 3e57138b5..8ca69ca85 100644
--- a/apps/web/pages/api/payment/verify.ts
+++ b/apps/web/pages/api/payment/verify.ts
@@ -41,22 +41,15 @@ export default async function handler(
return res.status(400).json({ message: responses.invalid_input });
}
- try {
- const purchaseRecord: Purchase | null = await PurchaseModel.findOne({
- orderId: purchaseId,
- });
+ const purchaseRecord: Purchase | null = await PurchaseModel.findOne({
+ orderId: purchaseId,
+ });
- if (!purchaseRecord || user!.userId !== purchaseRecord.purchasedBy) {
+ if (!purchaseRecord || user!.userId !== purchaseRecord.purchasedBy) {
return res.status(404).json({ message: responses.item_not_found });
- }
-
- res.status(200).json({
- status: purchaseRecord.status,
- });
- } catch (err: any) {
- error(err.message, { stack: err.stack });
- res.status(500).json({
- message: err.message,
- });
}
+
+ res.status(200).json({
+ status: purchaseRecord.status,
+ });
}
diff --git a/apps/web/pages/api/payment/webhook-old.ts b/apps/web/pages/api/payment/webhook-old.ts
index dc03c245a..8e927624d 100644
--- a/apps/web/pages/api/payment/webhook-old.ts
+++ b/apps/web/pages/api/payment/webhook-old.ts
@@ -5,7 +5,8 @@ import PurchaseModel, { Purchase } from "@/models/Purchase";
import { getPaymentMethod } from "@/payments";
const { transactionSuccess } = constants;
import DomainModel, { Domain } from "@models/Domain";
-import { info } from "@/services/logger";
+import { info, error } from "@/services/logger";
+import ActivityModel from "@/models/Activity";
export default async function handler(
req: NextApiRequest,
@@ -16,24 +17,28 @@ export default async function handler(
}
info(`POST /api/payment/webhook: domain detected: ${req.headers.domain}`);
-
const domain = await DomainModel.findOne({
name: req.headers.domain,
});
if (!domain) {
+ error(`Domain not found: ${req.headers.domain}`);
return res.status(404).json({ message: "Domain not found" });
}
const { body } = req;
const paymentMethod = await getPaymentMethod(domain._id.toString());
- const paymentVerified = paymentMethod.verify(body);
+
+ const paymentVerified = await paymentMethod.verify(body);
+
if (paymentVerified) {
- const purchaseId = paymentMethod.getPaymentIdentifier(body);
+ const purchaseId = await paymentMethod.getPaymentIdentifier(body);
+
const purchaseRecord: Purchase | null = await PurchaseModel.findOne({
orderId: purchaseId,
});
-
+
if (!purchaseRecord) {
+ error(`Purchase record not found for ID: ${purchaseId}`);
return res.status(200).json({
message: "fail",
});
@@ -42,17 +47,34 @@ export default async function handler(
purchaseRecord.status = transactionSuccess;
purchaseRecord.webhookPayload = body;
await (purchaseRecord as any).save();
+ info(`Purchase record updated with success status for ID: ${purchaseId}`);
+
+ // Check for existing activities before finalizing purchase
+ const existingActivities = await ActivityModel.find({
+ domain: domain._id,
+ entityId: purchaseRecord.courseId
+ });
+ info(`Existing activities before finalize: ${existingActivities.length}`);
await finalizePurchase(
purchaseRecord.purchasedBy,
purchaseRecord.courseId,
purchaseRecord.orderId,
);
+ console.log("Purchase finalized");
+ info(`Purchase finalized for user: ${purchaseRecord.purchasedBy}, course: ${purchaseRecord.courseId}`);
+
+ // Check for activities after finalizing purchase
+ const newActivities = await ActivityModel.find({
+ domain: domain._id,
+ entityId: purchaseRecord.courseId
+ });
res.status(200).json({
message: "success",
});
} else {
+ error("Payment verification failed");
res.status(200).json({
message: "fail",
});
diff --git a/apps/web/payments-new/index.ts b/apps/web/payments-new/index.ts
index 576ad92a5..10af763d3 100644
--- a/apps/web/payments-new/index.ts
+++ b/apps/web/payments-new/index.ts
@@ -4,6 +4,7 @@ import DomainModel, { Domain } from "../models/Domain";
import StripePayment from "./stripe-payment";
import RazorpayPayment from "./razorpay-payment";
import LemonSqueezyPayment from "./lemonsqueezy-payment";
+import MercadoPagoPayment from "./mercadopago-payment";
const {
error_unrecognised_payment_method: unrecognisedPaymentMethod,
@@ -36,6 +37,8 @@ export const getPaymentMethodFromSettings = async (
return await new RazorpayPayment(siteInfo).setup();
case UIConstants.PAYMENT_METHOD_LEMONSQUEEZY:
return await new LemonSqueezyPayment(siteInfo).setup();
+ case UIConstants.PAYMENT_METHOD_MERCADOPAGO:
+ return await new MercadoPagoPayment(siteInfo).setup();
case UIConstants.PAYMENT_METHOD_PAYTM:
throw new Error(notYetSupported);
default:
diff --git a/apps/web/payments-new/mercadopago-payment.ts b/apps/web/payments-new/mercadopago-payment.ts
new file mode 100644
index 000000000..1190e50f1
--- /dev/null
+++ b/apps/web/payments-new/mercadopago-payment.ts
@@ -0,0 +1,163 @@
+import { Constants, SiteInfo, UIConstants } from "@courselit/common-models";
+import Payment, { InitiateProps } from "./payment";
+import { responses } from "../config/strings";
+import { getUnitAmount } from "./helpers";
+import { MercadoPagoConfig, Payment as MPPayment, Preference } from 'mercadopago';
+
+const {
+ payment_invalid_settings: paymentInvalidSettings,
+ currency_iso_not_set: currencyISONotSet,
+} = responses;
+
+export default class MercadoPagoPayment implements Payment {
+ public siteinfo: SiteInfo;
+ public name: string;
+ private client: MercadoPagoConfig;
+ private mpPayment: MPPayment;
+ private preference: Preference;
+
+ constructor(siteinfo: SiteInfo) {
+ this.siteinfo = siteinfo;
+ this.name = UIConstants.PAYMENT_METHOD_MERCADOPAGO;
+ }
+
+ async setup() {
+ if (!this.siteinfo.currencyISOCode) {
+ console.error("Error: Currency ISO code not set");
+ throw new Error(currencyISONotSet);
+ }
+
+ console.log("Mercado Pago setup - SiteInfo:", JSON.stringify({
+ currencyISOCode: this.siteinfo.currencyISOCode,
+ paymentMethod: this.siteinfo.paymentMethod,
+ hasAccessToken: !!this.siteinfo.mercadopagoAccessToken
+ }));
+
+ if (!this.siteinfo.mercadopagoAccessToken) {
+ console.error("Error: Mercado Pago access token not set");
+ throw new Error(`${this.name} ${paymentInvalidSettings}`);
+ }
+
+ try {
+ this.client = new MercadoPagoConfig({
+ accessToken: this.siteinfo.mercadopagoAccessToken,
+ options: { timeout: 5000 }
+ });
+
+ this.mpPayment = new MPPayment(this.client);
+ this.preference = new Preference(this.client);
+
+ console.log("Mercado Pago setup successful");
+ return this;
+ } catch (error) {
+ console.error("Error setting up Mercado Pago:", error);
+ throw new Error(`${this.name} ${paymentInvalidSettings}: ${(error as Error).message}`);
+ }
+ }
+
+ async initiate({ metadata, paymentPlan, product, origin }: InitiateProps) {
+ const unit_amount = getUnitAmount(paymentPlan);
+
+ try {
+ console.log("Mercado Pago initiate - paymentPlan:", JSON.stringify(paymentPlan || {}, null, 2));
+ console.log("Mercado Pago initiate - product:", JSON.stringify(product || {}, null, 2));
+
+ const preferenceData = {
+ items: [
+ {
+ id: product.id,
+ title: product.title,
+ quantity: 1,
+ unit_price: unit_amount,
+ }
+ ],
+ back_urls: {
+ success: `${origin}/checkout/verify?id=${metadata.invoiceId}`,
+ failure: `${origin}/checkout?type=${product.type}&id=${product.id}`,
+ pending: `${origin}/checkout?type=${product.type}&id=${product.id}`
+ },
+ auto_return: "approved",
+ external_reference: JSON.stringify(metadata),
+ ...(paymentPlan && paymentPlan.type === Constants.PaymentPlanType.SUBSCRIPTION && {
+ payment_methods: {
+ installments: 1,
+ recurring: true
+ }
+ }),
+ ...(paymentPlan && paymentPlan.type === Constants.PaymentPlanType.EMI && {
+ payment_methods: {
+ installments: paymentPlan.emiTotalInstallments || 1
+ }
+ }),
+ metadata
+ };
+
+ const response = await this.preference.create({ body: preferenceData });
+
+ // Return an object with both the ID and the URL
+ return {
+ id: response.id,
+ url: response.init_point
+ };
+ } catch (error) {
+ console.error("Error creating Mercado Pago preference:", error);
+ throw new Error(`Error creating payment: ${(error as Error).message}`);
+ }
+ }
+
+ async verify(event: any) {
+ if (!event || !event.data || !event.data.id) {
+ return false;
+ }
+
+ try {
+ const paymentId = event.data.id;
+ const response = await this.mpPayment.get({ id: paymentId });
+ return response.status === 'approved';
+ } catch (error) {
+ console.error("Error verifying Mercado Pago payment:", error);
+ return false;
+ }
+ }
+
+ async getPaymentIdentifier(event: any) {
+ const metadata = await this.getMetadata(event);
+ console.log(metadata)
+ return metadata.invoice_id;
+ }
+
+ async getMetadata(event: any) {
+ try {
+ return (await this.mpPayment.get({ id: event.data.id })).metadata as unknown as Record;
+ } catch (e) {
+ console.error("Error parsing Mercado Pago metadata:", e);
+ return {};
+ }
+ }
+
+ getName() {
+ return this.name;
+ }
+
+ async cancel(id: string) {
+ if (!id) return;
+
+ try {
+ await this.mpPayment.cancel({ id });
+ } catch (e) {
+ console.error("Error canceling Mercado Pago payment:", e);
+ }
+ }
+
+ getSubscriptionId(event: any): string {
+ return '';
+ }
+
+ async validateSubscription(subscriptionId: string): Promise {
+ return false;
+ }
+
+ async getCurrencyISOCode() {
+ return this.siteinfo.currencyISOCode!;
+ }
+}
diff --git a/apps/web/payments-new/payment.ts b/apps/web/payments-new/payment.ts
index ba9d60e3d..e260a180d 100644
--- a/apps/web/payments-new/payment.ts
+++ b/apps/web/payments-new/payment.ts
@@ -21,7 +21,7 @@ export default interface Payment {
initiate: (obj: InitiateProps) => void;
verify: (event: any) => Promise;
getPaymentIdentifier: (event: any) => unknown;
- getMetadata: (event: any) => Record;
+ getMetadata: (event: any) => Promise>;
getName: () => string;
cancel: (id: string) => void;
getSubscriptionId: (event: any) => string;
diff --git a/apps/web/payments/index.ts b/apps/web/payments/index.ts
index 5e782e0e3..6203a6ad8 100644
--- a/apps/web/payments/index.ts
+++ b/apps/web/payments/index.ts
@@ -4,6 +4,7 @@ import DomainModel, { Domain } from "../models/Domain";
import StripePayment from "./stripe-payment";
import RazorpayPayment from "./razorpay-payment";
import LemonSqueezyPayment from "./lemonsqueezy-payment";
+import MercadoPagoPayment from "../payments-new/mercadopago-payment";
const {
error_unrecognised_payment_method: unrecognisedPaymentMethod,
@@ -17,6 +18,7 @@ export const getPaymentMethod = async (domainName: string) => {
});
const siteInfo: SiteInfo | null = domain && domain.settings;
+
return await getPaymentMethodFromSettings(siteInfo);
};
@@ -38,6 +40,8 @@ export const getPaymentMethodFromSettings = async (
return await new LemonSqueezyPayment(siteInfo).setup();
case UIConstants.PAYMENT_METHOD_PAYTM:
throw new Error(notYetSupported);
+ case UIConstants.PAYMENT_METHOD_MERCADOPAGO:
+ return await new MercadoPagoPayment(siteInfo).setup();
default:
throw new Error(unrecognisedPaymentMethod);
}
diff --git a/package.json b/package.json
index 90f6ba3f6..2921703c4 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,8 @@
"dev": "yarn workspace @courselit/web dev",
"prepare": "husky",
"release:patch": "./release.sh patch",
- "release:minor": "./release.sh minor"
+ "release:minor": "./release.sh minor",
+ "start": "yarn workspace @courselit/web start"
},
"packageManager": "yarn@3.2.0",
"devDependencies": {
@@ -49,5 +50,9 @@
"resolutions": {
"prosemirror-model": "^1.22.3",
"prosemirror-view": "^1.34.2"
+ },
+ "dependencies": {
+ "mercadopago": "^2.3.0",
+ "react-icons": "^5.5.0"
}
}
diff --git a/packages/common-models/src/constants.ts b/packages/common-models/src/constants.ts
index e5e05d4c7..02b0dfdde 100644
--- a/packages/common-models/src/constants.ts
+++ b/packages/common-models/src/constants.ts
@@ -1,5 +1,6 @@
import {
PAYMENT_METHOD_LEMONSQUEEZY,
+ PAYMENT_METHOD_MERCADOPAGO,
PAYMENT_METHOD_NONE,
PAYMENT_METHOD_PAYPAL,
PAYMENT_METHOD_PAYTM,
@@ -56,6 +57,7 @@ export const paymentMethods = [
PAYMENT_METHOD_RAZORPAY,
PAYMENT_METHOD_STRIPE,
PAYMENT_METHOD_LEMONSQUEEZY,
+ PAYMENT_METHOD_MERCADOPAGO,
] as const;
export const PageType = {
PRODUCT: "product",
diff --git a/packages/common-models/src/site-info.ts b/packages/common-models/src/site-info.ts
index a465030fd..3b9da2ad3 100644
--- a/packages/common-models/src/site-info.ts
+++ b/packages/common-models/src/site-info.ts
@@ -25,4 +25,7 @@ export default interface SiteInfo {
lemonsqueezySubscriptionMonthlyVariantId?: string;
lemonsqueezySubscriptionYearlyVariantId?: string;
lemonsqueezyWebhookSecret?: string;
+ mercadopagoKey?: string;
+ mercadopagoSecret?: string;
+ mercadopagoAccessToken?: string;
}
diff --git a/packages/common-models/src/ui-constants.ts b/packages/common-models/src/ui-constants.ts
index 85c1bce77..b914263f3 100644
--- a/packages/common-models/src/ui-constants.ts
+++ b/packages/common-models/src/ui-constants.ts
@@ -23,6 +23,7 @@ export const PAYMENT_METHOD_LEMONSQUEEZY = "lemonsqueezy";
export const PAYMENT_METHOD_PAYPAL = "paypal";
export const PAYMENT_METHOD_PAYTM = "paytm";
export const PAYMENT_METHOD_RAZORPAY = "razorpay";
+export const PAYMENT_METHOD_MERCADOPAGO = "mercadopago";
export const PAYMENT_METHOD_NONE = "";
// transaction statuses from backend
diff --git a/yarn.lock b/yarn.lock
index 6209d23da..920aed851 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9938,7 +9938,9 @@ __metadata:
eslint-plugin-unused-imports: ^2.0.0
husky: ^9.0.11
lint-staged: ^15.2.2
+ mercadopago: ^2.3.0
prettier: ^3.0.2
+ react-icons: ^5.5.0
languageName: unknown
linkType: soft
@@ -14324,6 +14326,16 @@ __metadata:
languageName: node
linkType: hard
+"mercadopago@npm:^2.3.0":
+ version: 2.3.0
+ resolution: "mercadopago@npm:2.3.0"
+ dependencies:
+ node-fetch: ^2.6.12
+ uuid: ^9.0.0
+ checksum: 5c35f36ed576b396bc05716a80231dc819e43e4f0dfbd49ac67ad62e810e2e43aea89397f0f23a40a7d5ba70b29c2664fe5a6bdc7ee981b2b542bc3a9a83fb35
+ languageName: node
+ linkType: hard
+
"merge-descriptors@npm:1.0.3":
version: 1.0.3
resolution: "merge-descriptors@npm:1.0.3"
@@ -15424,6 +15436,20 @@ __metadata:
languageName: node
linkType: hard
+"node-fetch@npm:^2.6.12":
+ version: 2.7.0
+ resolution: "node-fetch@npm:2.7.0"
+ dependencies:
+ whatwg-url: ^5.0.0
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+ checksum: d76d2f5edb451a3f05b15115ec89fc6be39de37c6089f1b6368df03b91e1633fd379a7e01b7ab05089a25034b2023d959b47e59759cb38d88341b2459e89d6e5
+ languageName: node
+ linkType: hard
+
"node-fetch@npm:^3.2.5":
version: 3.3.2
resolution: "node-fetch@npm:3.3.2"
@@ -16874,6 +16900,15 @@ __metadata:
languageName: node
linkType: hard
+"react-icons@npm:^5.5.0":
+ version: 5.5.0
+ resolution: "react-icons@npm:5.5.0"
+ peerDependencies:
+ react: "*"
+ checksum: cbd74f4b7982e6e18d59798a6b578268c8eb0909d78d87bcf9b25f99b3e544fd189a76551cb5e770d17f154a60b668551aee108aaf8471309b23f7af3b2c5b07
+ languageName: node
+ linkType: hard
+
"react-is@npm:^16.13.1, react-is@npm:^16.7.0":
version: 16.13.1
resolution: "react-is@npm:16.13.1"
@@ -18864,6 +18899,13 @@ __metadata:
languageName: node
linkType: hard
+"tr46@npm:~0.0.3":
+ version: 0.0.3
+ resolution: "tr46@npm:0.0.3"
+ checksum: 726321c5eaf41b5002e17ffbd1fb7245999a073e8979085dacd47c4b4e8068ff5777142fc6726d6ca1fd2ff16921b48788b87225cbc57c72636f6efa8efbffe3
+ languageName: node
+ linkType: hard
+
"tree-kill@npm:^1.2.2":
version: 1.2.2
resolution: "tree-kill@npm:1.2.2"
@@ -19853,6 +19895,13 @@ __metadata:
languageName: node
linkType: hard
+"webidl-conversions@npm:^3.0.0":
+ version: 3.0.1
+ resolution: "webidl-conversions@npm:3.0.1"
+ checksum: c92a0a6ab95314bde9c32e1d0a6dfac83b578f8fa5f21e675bc2706ed6981bc26b7eb7e6a1fab158e5ce4adf9caa4a0aee49a52505d4d13c7be545f15021b17c
+ languageName: node
+ linkType: hard
+
"webidl-conversions@npm:^4.0.2":
version: 4.0.2
resolution: "webidl-conversions@npm:4.0.2"
@@ -19887,6 +19936,16 @@ __metadata:
languageName: node
linkType: hard
+"whatwg-url@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "whatwg-url@npm:5.0.0"
+ dependencies:
+ tr46: ~0.0.3
+ webidl-conversions: ^3.0.0
+ checksum: b8daed4ad3356cc4899048a15b2c143a9aed0dfae1f611ebd55073310c7b910f522ad75d727346ad64203d7e6c79ef25eafd465f4d12775ca44b90fa82ed9e2c
+ languageName: node
+ linkType: hard
+
"whatwg-url@npm:^7.0.0":
version: 7.1.0
resolution: "whatwg-url@npm:7.1.0"