Spaces:
Sleeping
Sleeping
Z User commited on
Commit ·
351fd2b
1
Parent(s): cb3d98c
fix: remove duplicate searchGmailForPayment function (syntax error)
Browse files
supabase/functions/payment/index.ts
CHANGED
|
@@ -461,125 +461,6 @@ async function markEmailAsRead(msgId: string): Promise<void> {
|
|
| 461 |
await logoutImap(conn, encoder, decoder, tag);
|
| 462 |
}
|
| 463 |
|
| 464 |
-
// No match found within 10 minutes — mark as expired
|
| 465 |
-
await updateOrderStatus(orderId, 'expired', null);
|
| 466 |
-
console.log(`⏰ Order ${orderId} expired — no matching payment found`);
|
| 467 |
-
}
|
| 468 |
-
|
| 469 |
-
// ---------------------------------------------------------------------------
|
| 470 |
-
// Gmail IMAP search — connects to Gmail, searches for PalmPay emails
|
| 471 |
-
// ---------------------------------------------------------------------------
|
| 472 |
-
async function searchGmailForPayment(expectedAmount: number): Promise<{ messageId: string; senderName: string; amount: number } | null> {
|
| 473 |
-
// Connect to Gmail IMAP using Deno's TCP socket + TLS
|
| 474 |
-
// We use a raw IMAP implementation since Deno doesn't have a built-in IMAP library
|
| 475 |
-
|
| 476 |
-
const IMAP_HOST = 'imap.gmail.com';
|
| 477 |
-
const IMAP_PORT = 993;
|
| 478 |
-
|
| 479 |
-
// Connect via TLS
|
| 480 |
-
const conn = await Deno.connectTls({ hostname: IMAP_HOST, port: IMAP_PORT });
|
| 481 |
-
const decoder = new TextDecoder();
|
| 482 |
-
const encoder = new TextEncoder();
|
| 483 |
-
|
| 484 |
-
// Read until we see the server greeting
|
| 485 |
-
await readUntil(conn, decoder, '\r\n');
|
| 486 |
-
|
| 487 |
-
// Login
|
| 488 |
-
const tag = `A${Date.now()}`;
|
| 489 |
-
await writeLine(conn, encoder, `${tag} LOGIN ${GMAIL_EMAIL} ${GMAIL_APP_PASSWORD}\r\n`);
|
| 490 |
-
const loginResp = await readUntil(conn, decoder, `${tag} OK`);
|
| 491 |
-
if (loginResp.includes('BAD') || loginResp.includes('NO')) {
|
| 492 |
-
conn.close();
|
| 493 |
-
throw new Error('Gmail login failed');
|
| 494 |
-
}
|
| 495 |
-
|
| 496 |
-
// Select the "PalmPay" label/folder (or INBOX if no label)
|
| 497 |
-
const folderTag = `${tag}1`;
|
| 498 |
-
await writeLine(conn, encoder, `${folderTag} SELECT INBOX\r\n`);
|
| 499 |
-
await readUntil(conn, decoder, `${folderTag} OK`);
|
| 500 |
-
|
| 501 |
-
// Search for unread emails from PalmPay (last 24 hours)
|
| 502 |
-
const searchTag = `${tag}2`;
|
| 503 |
-
const sinceDate = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0].replace(/-/g, '-');
|
| 504 |
-
await writeLine(conn, encoder, `${searchTag} SEARCH UNSEEN FROM "palmpay" SINCE ${sinceDate}\r\n`);
|
| 505 |
-
const searchResp = await readUntil(conn, decoder, `${searchTag} OK`);
|
| 506 |
-
|
| 507 |
-
// Parse search results — format: * SEARCH 1 2 3 4
|
| 508 |
-
const searchMatch = searchResp.match(/\* SEARCH (.+)/);
|
| 509 |
-
if (!searchMatch) {
|
| 510 |
-
// No unread PalmPay emails
|
| 511 |
-
await logoutImap(conn, encoder, decoder, tag);
|
| 512 |
-
return null;
|
| 513 |
-
}
|
| 514 |
-
|
| 515 |
-
const messageIds = searchMatch[1].trim().split(/\s+/);
|
| 516 |
-
|
| 517 |
-
// Check each email for matching amount
|
| 518 |
-
for (const msgId of messageIds) {
|
| 519 |
-
const fetchTag = `${tag}3`;
|
| 520 |
-
await writeLine(conn, encoder, `${fetchTag} FETCH ${msgId} (BODY.PEEK[HEADER.FIELDS (FROM SUBJECT)] BODY.PEEK[TEXT])\r\n`);
|
| 521 |
-
const fetchResp = await readUntil(conn, decoder, `${fetchTag} OK`);
|
| 522 |
-
|
| 523 |
-
// Parse the email for amount
|
| 524 |
-
const amount = parseAmountFromEmail(fetchResp);
|
| 525 |
-
const senderName = parseSenderFromEmail(fetchResp);
|
| 526 |
-
|
| 527 |
-
if (amount !== null && Math.abs(amount - expectedAmount) < 0.01) {
|
| 528 |
-
// Amount matches! Check that this email hasn't been used for another order
|
| 529 |
-
const usedCheck = await fetch(
|
| 530 |
-
`${SUPABASE_URL}/rest/v1/payment_orders?matched_email_id=not.is.null&select=matched_email_id`,
|
| 531 |
-
{ headers: adminHeaders }
|
| 532 |
-
);
|
| 533 |
-
const usedEmails = await usedCheck.json();
|
| 534 |
-
const usedIds = new Set((usedEmails || []).map((o: Record<string, unknown>) => o.matched_email_id));
|
| 535 |
-
|
| 536 |
-
// Generate a unique email ID (folder + message number)
|
| 537 |
-
const emailUid = `INBOX:${msgId}`;
|
| 538 |
-
|
| 539 |
-
if (!usedIds.has(emailUid)) {
|
| 540 |
-
// Mark the email as read (SEEN)
|
| 541 |
-
const seenTag = `${tag}4`;
|
| 542 |
-
await writeLine(conn, encoder, `${seenTag} STORE ${msgId} +FLAGS (\Seen)\r\n`);
|
| 543 |
-
await readUntil(conn, decoder, `${seenTag} OK`);
|
| 544 |
-
|
| 545 |
-
await logoutImap(conn, encoder, decoder, tag);
|
| 546 |
-
return { messageId: emailUid, senderName, amount };
|
| 547 |
-
}
|
| 548 |
-
}
|
| 549 |
-
}
|
| 550 |
-
|
| 551 |
-
await logoutImap(conn, encoder, decoder, tag);
|
| 552 |
-
return null;
|
| 553 |
-
}
|
| 554 |
-
|
| 555 |
-
// Parse amount from PalmPay email body
|
| 556 |
-
function parseAmountFromEmail(emailText: string): number | null {
|
| 557 |
-
// PalmPay emails typically contain: "Amount: ₦5,000.23" or "You received ₦5,000.23"
|
| 558 |
-
const patterns = [
|
| 559 |
-
/Amount:\s*₦?\s*([\d,]+\.?\d*)/i,
|
| 560 |
-
/received\s*₦?\s*([\d,]+\.?\d*)/i,
|
| 561 |
-
/₦\s*([\d,]+\.?\d*)/i,
|
| 562 |
-
/NGN\s*([\d,]+\.?\d*)/i,
|
| 563 |
-
];
|
| 564 |
-
|
| 565 |
-
for (const pattern of patterns) {
|
| 566 |
-
const match = emailText.match(pattern);
|
| 567 |
-
if (match) {
|
| 568 |
-
const amount = parseFloat(match[1].replace(/,/g, ''));
|
| 569 |
-
if (!isNaN(amount) && amount > 0) return Math.round(amount * 100) / 100;
|
| 570 |
-
}
|
| 571 |
-
}
|
| 572 |
-
return null;
|
| 573 |
-
}
|
| 574 |
-
|
| 575 |
-
// Parse sender name from email
|
| 576 |
-
function parseSenderFromEmail(emailText: string): string {
|
| 577 |
-
const fromMatch = emailText.match(/From:\s*(.+)/i);
|
| 578 |
-
if (fromMatch) return fromMatch[1].trim().split('<')[0].trim().replace(/"/g, '');
|
| 579 |
-
return 'Unknown';
|
| 580 |
-
}
|
| 581 |
-
|
| 582 |
-
// IMAP helper: read until we find a specific string
|
| 583 |
async function readUntil(conn: Deno.TlsConn, decoder: TextDecoder, until: string): Promise<string> {
|
| 584 |
let buf = '';
|
| 585 |
const chunk = new Uint8Array(4096);
|
|
|
|
| 461 |
await logoutImap(conn, encoder, decoder, tag);
|
| 462 |
}
|
| 463 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
async function readUntil(conn: Deno.TlsConn, decoder: TextDecoder, until: string): Promise<string> {
|
| 465 |
let buf = '';
|
| 466 |
const chunk = new Uint8Array(4096);
|