/ src / payment / refund-processor.js
refund-processor.js
  1  #!/usr/bin/env node
  2  
  3  /**
  4   * Automatic Refund Processor
  5   * Detects refund requests in inbound emails and processes them automatically.
  6   *
  7   * Rules:
  8   * - Customer must request within 7 days of purchase
  9   * - Purchase is looked up by sender email
 10   * - Refund is issued automatically if eligible
 11   * - Requests outside the window are silently ignored (no reply)
 12   */
 13  
 14  import { Resend } from 'resend';
 15  import Logger from '../utils/logger.js';
 16  import { run, getOne } from '../utils/db.js';
 17  import { refundPayment } from './paypal.js';
 18  import '../utils/load-env.js';
 19  
 20  const logger = new Logger('RefundProcessor');
 21  
 22  const REFUND_WINDOW_DAYS = 7;
 23  
 24  // Keywords that signal a refund request
 25  const REFUND_KEYWORDS = [
 26    'refund',
 27    'money back',
 28    'give me my money',
 29    'want my money',
 30    'cancel my order',
 31    'cancel my purchase',
 32    'charge back',
 33    'chargeback',
 34  ];
 35  
 36  /**
 37   * Check if email body contains a refund request
 38   * @param {string} body - Email body text
 39   * @returns {boolean}
 40   */
 41  export function isRefundRequest(body) {
 42    if (!body) return false;
 43    const lower = body.toLowerCase();
 44    return REFUND_KEYWORDS.some(kw => lower.includes(kw));
 45  }
 46  
 47  /**
 48   * Find the most recent eligible purchase for a given email address.
 49   * Returns null if not found, already refunded, or outside the 7-day window.
 50   *
 51   * @param {string} email - Customer email address
 52   * @returns {Promise<{ purchase: Object, eligible: boolean, reason: string|null }>}
 53   */
 54  export async function findEligiblePurchase(email) {
 55    const normalized = email.toLowerCase().trim();
 56  
 57    const purchase = await getOne(
 58      `SELECT id, email, paypal_capture_id, amount, currency, status, created_at, refunded_at
 59       FROM purchases
 60       WHERE LOWER(email) = $1
 61       ORDER BY created_at DESC
 62       LIMIT 1`,
 63      [normalized]
 64    );
 65  
 66    if (!purchase) {
 67      return { purchase: null, eligible: false, reason: 'no_purchase' };
 68    }
 69  
 70    if (purchase.status === 'refunded') {
 71      return { purchase, eligible: false, reason: 'already_refunded' };
 72    }
 73  
 74    if (!purchase.paypal_capture_id) {
 75      return { purchase, eligible: false, reason: 'no_capture_id' };
 76    }
 77  
 78    const purchasedAt = new Date(purchase.created_at);
 79    const now = new Date();
 80    const daysSince = (now - purchasedAt) / (1000 * 60 * 60 * 24);
 81  
 82    if (daysSince > REFUND_WINDOW_DAYS) {
 83      return { purchase, eligible: false, reason: 'outside_window' };
 84    }
 85  
 86    return { purchase, eligible: true, reason: null };
 87  }
 88  
 89  /**
 90   * Send a refund confirmation email to the customer
 91   */
 92  async function sendRefundConfirmation(purchase, refundResult) {
 93    const apiKey = process.env.RESEND_API_KEY;
 94    if (!apiKey) {
 95      logger.warn('RESEND_API_KEY not set — skipping refund confirmation email');
 96      return;
 97    }
 98  
 99    const senderEmail = process.env.AUDITANDFIX_SENDER_EMAIL || 'reports@auditandfix.com';
100    const resend = new Resend(apiKey);
101  
102    const amountFormatted = `${purchase.currency} ${(purchase.amount / 100).toFixed(2)}`;
103  
104    try {
105      await resend.emails.send({
106        from: `Audit&Fix <${senderEmail}>`,
107        to: purchase.email,
108        subject: 'Your refund has been processed',
109        html: `
110  <html>
111  <body style="font-family: sans-serif; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
112    <h2>Refund Processed</h2>
113    <p>Hi,</p>
114    <p>We've processed your refund of <strong>${amountFormatted}</strong>. It should appear back in your account within 3–5 business days, depending on your bank.</p>
115    <p>Refund reference: <code>${refundResult.refund_id}</code></p>
116    <p>If you have any questions, just reply to this email.</p>
117    <p>Thanks for giving us a try.</p>
118    <p style="color: #666; font-size: 12px; margin-top: 30px;">Audit&amp;Fix CRO Reports</p>
119  </body>
120  </html>`,
121      });
122  
123      logger.success(`Refund confirmation email sent to ${purchase.email}`);
124    } catch (error) {
125      // Non-fatal — the refund itself succeeded
126      logger.error(`Failed to send refund confirmation email to ${purchase.email}`, error);
127    }
128  }
129  
130  /**
131   * Process a refund request from an inbound email.
132   *
133   * @param {string} fromEmail - Sender's email address
134   * @param {string} body - Email body text
135   * @returns {Promise<{ processed: boolean, reason: string }>}
136   */
137  export async function processRefundRequest(fromEmail, body) {
138    if (!isRefundRequest(body)) {
139      return { processed: false, reason: 'not_a_refund_request' };
140    }
141  
142    logger.info(`Refund request detected from ${fromEmail}`);
143  
144    try {
145      const { purchase, eligible, reason } = await findEligiblePurchase(fromEmail);
146  
147      if (!eligible) {
148        logger.info(`Refund request from ${fromEmail} not eligible: ${reason}`);
149        return { processed: false, reason };
150      }
151  
152      logger.info(
153        `Processing refund for purchase #${purchase.id} (${fromEmail}, capture: ${purchase.paypal_capture_id})`
154      );
155  
156      const refundResult = await refundPayment(
157        purchase.paypal_capture_id,
158        'Customer requested refund'
159      );
160  
161      // Update purchase record
162      await run(
163        `UPDATE purchases
164         SET status = 'refunded', refunded_at = NOW(),
165             refund_reason = 'Customer requested via email', updated_at = NOW()
166         WHERE id = $1`,
167        [purchase.id]
168      );
169  
170      logger.success(
171        `Refund processed for purchase #${purchase.id}: PayPal refund ${refundResult.refund_id}`
172      );
173  
174      await sendRefundConfirmation(purchase, refundResult);
175  
176      return {
177        processed: true,
178        reason: 'refund_issued',
179        purchaseId: purchase.id,
180        refundId: refundResult.refund_id,
181      };
182    } catch (error) {
183      logger.error(`Failed to process refund for ${fromEmail}`, error);
184      return { processed: false, reason: `error: ${error.message}` };
185    }
186  }