Debuggler LogoDebuggler
All challenges
The Confused Invoice StatusPaymentsStripe Elements
import { useState, useEffect } from "react";

// Mock API that takes longer to process some invoice payments
async function apiPayInvoice(id) {
  const delay = id === 101 ? 600 : 200;
  await new Promise(r => setTimeout(r, delay));
  if (id === 101) {
    throw new Error("Card declined. Insufficient funds.");
  }
  return { success: true };
}

const INVOICES = [
  { id: 101, client: "Acme Corp", amount: "450.00" },
  { id: 102, client: "Stark Industries", amount: "1,250.00" },
  { id: 103, client: "Wayne Enterprises", amount: "800.00" }
];

export default function App() {
  // BUG: Tracking invoice payment statuses using a single shared state object.
  // When multiple invoice payments run concurrently, they read/write to the 
  // same shared activePayment state. If a fast payment (Invoice #102) succeeds 
  // and a slow payment (Invoice #101) subsequently fails, the failure updates 
  // the shared state, wiping out the success of Invoice #102 and showing 
  // the failure status on the wrong invoice!
  //
  // The fix is to track payment statuses per-invoice (e.g. by using a dictionary 
  // of statuses: {[id]: 'processing' | 'success' | 'error'} or mapping state locally).
  const [activePayment, setActivePayment] = useState({ id: null, status: "idle", error: "" });

  useEffect(() => {
    const b = document.body;
    b.style.background = "#fafafa"; b.style.margin = "0";
    return () => { b.style.background = ""; b.style.margin = ""; };
  }, []);

  const handlePay = async (id) => {
    setActivePayment({ id, status: "processing", error: "" });
    
    try {
      await apiPayInvoice(id);
      setActivePayment({ id, status: "success", error: "" });
    } catch (err) {
      setActivePayment({ id, status: "error", error: err.message });
    }
  };

  return (
    <div style={styles.page}>
      <div style={styles.card}>
        <h1 style={styles.h1}>Invoices</h1>
        <p style={styles.sub}>Manage and settle outstanding corporate client invoices.</p>
        
        <div style={styles.list}>
          {INVOICES.map((inv) => {
            const isProcessing = activePayment.id === inv.id && activePayment.status === "processing";
            const hasError = activePayment.id === inv.id && activePayment.status === "error";
            const isSuccess = activePayment.id === inv.id && activePayment.status === "success";

            return (
              <div key={inv.id} style={styles.row}>
                <div style={styles.info}>
                  <div style={styles.client}>
                    {inv.client}
                    <span style={styles.id}>#{inv.id}</span>
                  </div>
                  <div style={styles.amount}>${inv.amount}</div>
                </div>

                <div style={styles.actions}>
                  {isSuccess ? (
                    <span data-testid={`status-paid-${inv.id}`} style={styles.paidStatus}>✓ Paid</span>
                  ) : isProcessing ? (
                    <span data-testid={`status-loading-${inv.id}`} style={styles.loading}>Processing...</span>
                  ) : (
                    <button
                      data-testid={`pay-btn-${inv.id}`}
                      onClick={() => handlePay(inv.id)}
                      style={styles.payBtn}
                    >
                      Pay Now
                    </button>
                  )}
                </div>
                
                {hasError && (
                  <div data-testid={`error-${inv.id}`} style={styles.errorRow}>
                    Error: {activePayment.error}
                  </div>
                )}
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

const styles = {
  page: { display: "flex", justifyContent: "center", padding: "40px 20px", fontFamily: "system-ui, sans-serif" },
  card: { background: "#fff", borderRadius: 12, padding: "24px", width: "100%", maxWidth: 480, boxShadow: "0 4px 6px -1px rgba(0,0,0,0.1)" },
  h1: { margin: "0 0 6px", fontSize: 20, fontWeight: 600, color: "#111827" },
  sub: { margin: "0 0 20px", fontSize: 14, color: "#6B7280" },
  list: { display: "flex", flexDirection: "column", gap: 16 },
  row: { position: "relative", display: "flex", flexWrap: "wrap", justifyContent: "space-between", alignItems: "center", padding: "16px", border: "1px solid #E5E7EB", borderRadius: 8, background: "#FAFBFB" },
  info: { display: "flex", flexDirection: "column", gap: 4 },
  client: { fontSize: 15, fontWeight: 600, color: "#111827" },
  id: { fontSize: 12, color: "#9CA3AF", marginLeft: 6, fontWeight: 400 },
  amount: { fontSize: 14, color: "#374151", fontWeight: 500 },
  actions: { display: "flex", alignItems: "center" },
  payBtn: { background: "#4F46E5", color: "#fff", border: "none", padding: "8px 16px", borderRadius: 6, fontSize: 13, fontWeight: 500, cursor: "pointer", transition: "background-color 0.2s" },
  paidStatus: { color: "#10B981", fontSize: 14, fontWeight: 600 },
  loading: { color: "#6B7280", fontSize: 13, fontStyle: "italic" },
  errorRow: { width: "100%", marginTop: 8, fontSize: 12, color: "#EF4444", fontWeight: 500 }
};

Pay Invoice #101 (which fails) and quickly pay Invoice #102 (which succeeds) right after. Notice that when Invoice #101 fails, it incorrectly wipes out the success checkmark of Invoice #102 and displays the error message on the wrong invoice! Stabilize the parallel invoice payment status.

After you fix it

  • Paying multiple invoices in parallel should update each invoice's status independently.
  • Invoice #102 should stay marked as Paid even if Invoice #101 fails.
  • Errors should be associated with the specific invoice that failed.

Click "Run checks" to verify your solution.