Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import stellarWalletRoutes from "./src/routes/stellar/walletRoutes.js";
import stellarPaymentRoutes from "./src/routes/stellar/paymentRoutes.js";
import stellarDonationRoutes from "./src/routes/stellar/donationRoutes.js";
import payoutRoutes from "./src/routes/payoutRoutes.js";
import webhookRoutes from "./src/routes/webhookRoutes.js";
import { startDeliveryWorker } from "./src/services/webhooks/deliveryWorker.js";

handleUncaughtException();
validateEnv();
Expand Down Expand Up @@ -179,6 +181,16 @@ app.use("/api/stellar/wallet", stellarWalletRoutes);
app.use("/api/stellar/payment", stellarPaymentRoutes);
app.use("/api/stellar/donation", stellarDonationRoutes);
app.use("/api/payouts", payoutRoutes);
app.use("/api/webhooks", webhookRoutes);

// ======================
// WEBHOOK DELIVERY WORKER
// ======================

let webhookWorker = null;
if (process.env.NODE_ENV !== "test") {
webhookWorker = startDeliveryWorker();
}

// ======================
// ERROR HANDLING
Expand Down
9 changes: 9 additions & 0 deletions src/controllers/courses/courseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Course from "../../models/Course.js";
import mongoose from "mongoose";
import logger from "../../config/logger.js";
import { catchAsync, APIError } from "../../middlewares/errorHandler.js";
import { emitEvent } from "../../services/webhooks/webhookService.js";

/**
* Create a new course
Expand Down Expand Up @@ -142,6 +143,14 @@ export const enrollInCourse = async (req, res) => {
}
}

// Emit event after saves — fire-and-forget
emitEvent("course.enrolled", {
courseId: course._id.toString(),
courseTitle: course.title,
userId: req.user._id.toString(),
enrolledAt: new Date().toISOString(),
});

res
.status(200)
.json({
Expand Down
77 changes: 77 additions & 0 deletions src/controllers/stellar/paymentController.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
paymentsConfirmed,
paymentsFailed,
} from "../../config/metrics.js";
import { emitEvent } from "../../services/webhooks/webhookService.js";

/**
* Initialize a payment - creates pending transaction and returns XDR to sign
Expand Down Expand Up @@ -203,6 +204,20 @@ export const initializePayment = async (req, res) => {
await session.commitTransaction();
paymentsInitialized.inc({ type: "purchase" });

// Emit event after commit — fire-and-forget
emitEvent("payment.initialized", {
transactionId: transaction._id.toString(),
buyerId: buyerId.toString(),
buyerWallet: buyer.stellarWallet.publicKey,
creatorId: creator._id.toString(),
creatorWallet: destinationPublicKey,
itemType,
itemId: itemId.toString(),
itemTitle: item.title,
amount: item.price.toString(),
network: NETWORK,
});

logger.info(
`Payment initialized: ${transaction._id} for ${itemType} ${itemId}`
);
Expand Down Expand Up @@ -293,6 +308,21 @@ export const submitPayment = async (req, res) => {
await session.commitTransaction();
paymentsFailed.inc({ type: "purchase", reason: "stellar_error" });

// Emit event after commit
emitEvent("payment.failed", {
transactionId: transaction._id.toString(),
buyerId: buyerId.toString(),
buyerWallet: transaction.buyerWallet,
creatorId: transaction.creator.toString(),
creatorWallet: transaction.creatorWallet,
itemType: transaction.itemType,
itemId: transaction.itemId.toString(),
itemTitle: transaction.itemTitle,
amount: transaction.amount,
network: transaction.network,
failureReason: stellarError.message,
});

logger.error(`Transaction ${transactionId} failed:`, stellarError);

return res.status(400).json({
Expand Down Expand Up @@ -335,6 +365,22 @@ export const submitPayment = async (req, res) => {
await session.commitTransaction();
paymentsFailed.inc({ type: "purchase", reason: "verification_failed" });

// Emit event after commit
emitEvent("payment.failed", {
transactionId: transaction._id.toString(),
buyerId: buyerId.toString(),
buyerWallet: transaction.buyerWallet,
creatorId: transaction.creator.toString(),
creatorWallet: transaction.creatorWallet,
itemType: transaction.itemType,
itemId: transaction.itemId.toString(),
itemTitle: transaction.itemTitle,
amount: transaction.amount,
network: transaction.network,
stellarTxHash: result.hash,
failureReason: `On-chain verification failed: ${verification.reason}`,
});

logger.error(
`Transaction ${transactionId} verification failed: ${verification.reason}`
);
Expand Down Expand Up @@ -387,6 +433,23 @@ export const submitPayment = async (req, res) => {

await buyer.save({ session });
await session.commitTransaction();
paymentsConfirmed.inc({ type: "purchase" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Payment metric is being double-counted here. 📊

paymentsConfirmed.inc({ type: "purchase" }) already runs on Line 401 (right after the confirmed transaction.save), and this newly-added line increments the same counter a second time after commit. Every confirmed purchase will now count as two, which quietly corrupts dashboards and any alerting/SLOs built on paymentsConfirmed. Since the pre-commit increment on Line 401 is already in place, drop this one.

🐛 Suggested fix
     await session.commitTransaction();
-    paymentsConfirmed.inc({ type: "purchase" });
 
     // Emit event after commit — fire-and-forget
     emitEvent("payment.confirmed", {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
paymentsConfirmed.inc({ type: "purchase" });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controllers/stellar/paymentController.js` at line 436, Remove the newly
added paymentsConfirmed.inc({ type: "purchase" }) call from the post-commit
purchase flow in the payment controller. Keep the existing increment after the
confirmed transaction.save unchanged so each confirmed purchase is counted once.


// Emit event after commit — fire-and-forget
emitEvent("payment.confirmed", {
transactionId: transaction._id.toString(),
buyerId: buyerId.toString(),
buyerWallet: transaction.buyerWallet,
creatorId: transaction.creator.toString(),
creatorWallet: transaction.creatorWallet,
itemType: transaction.itemType,
itemId: transaction.itemId.toString(),
itemTitle: transaction.itemTitle,
amount: transaction.amount,
network: transaction.network,
stellarTxHash: result.hash,
ledger: result.ledger,
});

logger.info(
`Payment successful: ${transactionId}, Stellar TX: ${result.hash}`
Expand Down Expand Up @@ -552,6 +615,20 @@ export const cancelTransaction = async (req, res) => {
});
}

// Emit event after update
emitEvent("payment.expired", {
transactionId: transaction._id.toString(),
buyerId: userId.toString(),
buyerWallet: transaction.buyerWallet,
creatorId: transaction.creator.toString(),
creatorWallet: transaction.creatorWallet,
itemType: transaction.itemType,
itemId: transaction.itemId.toString(),
itemTitle: transaction.itemTitle,
amount: transaction.amount,
network: transaction.network,
});

logger.info(`Transaction ${transactionId} cancelled by user ${userId}`);

res.status(200).json({
Expand Down
13 changes: 13 additions & 0 deletions src/controllers/stellar/walletController.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getAccountBalance,
NETWORK,
} from "../../services/stellar/stellarService.js";
import { emitEvent } from "../../services/webhooks/webhookService.js";
import logger from "../../config/logger.js";

/**
Expand Down Expand Up @@ -55,6 +56,13 @@ export const connectWallet = async (req, res) => {

logger.info(`Wallet connected for user ${userId}: ${publicKey}`);

// Emit event — fire-and-forget
emitEvent("wallet.connected", {
userId: userId.toString(),
publicKey,
network: NETWORK,
});

res.status(200).json({
success: true,
message: "Wallet connected successfully",
Expand Down Expand Up @@ -89,6 +97,11 @@ export const disconnectWallet = async (req, res) => {

logger.info(`Wallet disconnected for user ${userId}`);

// Emit event — fire-and-forget
emitEvent("wallet.disconnected", {
userId: userId.toString(),
});

res.status(200).json({
success: true,
message: "Wallet disconnected successfully",
Expand Down
Loading