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

This file was deleted.

This file was deleted.

6 changes: 4 additions & 2 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ model Fundraiser {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

venmoUsername String? @map("venmo_username")
venmoEmail String? @map("venmo_email")
venmoUsername String? @map("venmo_username")
venmoEmail String? @map("venmo_email")
venmoLastFourDigits String? @map("venmo_last_four_digits")

organization Organization @relation(fields: [organizationId], references: [id])
organizationId String @map("organization_id") @db.Uuid
Expand Down Expand Up @@ -123,6 +124,7 @@ model Item {
offsale Boolean @default(false)
limit Int?
price Decimal @db.Money
profit Decimal? @db.Money
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

Expand Down
25 changes: 21 additions & 4 deletions backend/src/api/email/email.handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {

export const parseEmailHandler = async (
req: Request<{}, any, MailgunInboundEmailBody, {}>,
res: Response
res: Response,
) => {
try {
const {
Expand Down Expand Up @@ -54,20 +54,37 @@ export const parseEmailHandler = async (
let parsedAmount: Decimal;
let orderId: string;
try {
let transactionNote: string;

if (isVerifiedFormat) {
const result = parseVerifiedVenmoEmail(emailContent);
parsedAmount = result.parsedAmount;
orderId = result.orderId;
transactionNote = result.transactionNote;
} else {
const result = parseUnverifiedVenmoEmail(emailContent);
parsedAmount = result.parsedAmount;
orderId = result.orderId;
transactionNote = result.transactionNote;
}
if (!orderId || parsedAmount == null) {
if (!transactionNote || parsedAmount == null) {
// Parse failed—do not retry
res.status(200).json({ message: "parsed incomplete" });
return;
}

// Extract UUID from the parsed text (order ID may be a substring of the note)
const uuidMatch = transactionNote.match(
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i,
);
if (!uuidMatch) {
console.error("No order ID found in parsed text:", {
transactionNote,
subject,
});
res.status(200).json({ message: "no order id found" });
return;
}

orderId = uuidMatch[0];
} catch (err) {
console.error("Failed to parse Venmo email:", err, { subject });
res.status(200).json({ message: "parse error" });
Expand Down
27 changes: 16 additions & 11 deletions backend/src/api/email/email.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { calculateOrderTotal } from "../order/order.services";

export const parseUnverifiedVenmoEmail = (raw: string) => {
let parsedAmount: Decimal | null = null;
let orderId: string | null = null;

const $ = load(raw);
const amount = $('span[style="color:#148572;float:right;"]').text().trim();
Expand All @@ -17,12 +16,14 @@ export const parseUnverifiedVenmoEmail = (raw: string) => {
parsedAmount = null;
throw new Error("Failed to parse payment amount");
}
orderId = $('table[role="presentation"] tbody tr div p').text().trim();
if (!orderId) {
const transactionNote = $('table[role="presentation"] tbody tr div p')
.text()
.trim();
if (!transactionNote) {
throw new Error("Failed to parse venmo message for retreiving orderId");
}

return { parsedAmount: parsedAmount, orderId: orderId };
return { parsedAmount, transactionNote };
};

export const parseVerifiedVenmoEmail = (raw: string) => {
Expand All @@ -34,28 +35,32 @@ export const parseVerifiedVenmoEmail = (raw: string) => {
$("div.amount-container__amount-text").text().trim() || "0"; // "5"
const centAmount =
$("div.amount-container__text-high").eq(1).text().trim() || "00"; // "01"
const transactionNote = $("p.transaction-note").text().trim() || "NO NOTE"; // "4a1s"
const transactionNote = $("p.transaction-note").text().trim(); // "4a1s"

if (!transactionNote) {
throw new Error("Failed to parse venmo message for retreiving orderId");
}

// Convert to integers
const dollarAmountInt = parseInt(dollarAmount, 10);
const centAmountInt = parseInt(centAmount, 10);

// Convert to Decimal.js value
const parsedAmount = new Decimal(dollarAmountInt).plus(
new Decimal(centAmountInt).dividedBy(100)
new Decimal(centAmountInt).dividedBy(100),
);

// Validate parsed amount
if (isNaN(parsedAmount.toNumber())) {
throw new Error("Failed to parse payment amount");
}

return { parsedAmount, orderId: transactionNote };
return { parsedAmount, transactionNote };
};

export const updateOrderPaymentStatus = async (
orderId: string,
paidAmount: Decimal
paidAmount: Decimal,
) => {
try {
// Calculate expected order total
Expand All @@ -70,8 +75,8 @@ export const updateOrderPaymentStatus = async (

throw new Error(
`Payment amount mismatch: expected $${expectedAmount.toFixed(
2
)}, received $${paidAmount.toFixed(2)}`
2,
)}, received $${paidAmount.toFixed(2)}`,
);
}

Expand All @@ -91,7 +96,7 @@ export const updateOrderPaymentStatus = async (
throw new Error(
`Failed to update order payment status: ${
error instanceof Error ? error.message : "Unknown error"
}`
}`,
);
}
};
Loading