/** * Shared payment store — the single source of truth that lets the OWNER/ADMIN * side and the CUSTOMER side talk to each other in this demo (no backend). * * Both invoices and transactions live in localStorage so a payment request the * owner creates appears in the customer's Outstanding list, and a payment the * customer makes (or the owner marks manually) shows back on the owner's * Payment Management page. In production these reads/writes would hit the CRM * billing API instead — the component surface would stay the same. */ import { INVOICES } from './invoices'; const INVOICES_KEY = 'lynkeduppro.payments.invoices'; const TXNS_KEY = 'lynkeduppro.payments.transactions'; const UPDATE_EVENT = 'payments:updated'; /** * Default customer mobile number for the demo. The owner/admin "New Payment * Request" form prefills this so a request lands with the current demo * customer. In production this comes from a customer lookup instead. */ export const DEMO_CUSTOMER_PHONE = '214-555-2600'; /** Compare two phone numbers by digits only (ignores spaces, dashes, etc.). */ export const normalizePhone = (phone) => String(phone || '').replace(/\D/g, ''); /** * Does this invoice belong to the given customer phone? Invoices without a * `customerPhone` are treated as unassigned and visible to any customer (keeps * the original seed invoices working). */ export const invoiceMatchesCustomer = (invoice, phone) => !invoice.customerPhone || normalizePhone(invoice.customerPhone) === normalizePhone(phone); const read = (key, fallback) => { try { const raw = localStorage.getItem(key); return raw ? JSON.parse(raw) : fallback; } catch { return fallback; } }; const write = (key, value) => { try { localStorage.setItem(key, JSON.stringify(value)); } catch { /* localStorage unavailable (private mode) — best effort only. */ } }; /** Notify same-tab listeners (the native `storage` event only fires cross-tab). */ const emit = () => { try { window.dispatchEvent(new Event(UPDATE_EVENT)); } catch { /* non-browser / SSR — nothing to notify. */ } }; /** Subscribe to any invoice/transaction change. Returns an unsubscribe fn. */ export const subscribe = (callback) => { window.addEventListener(UPDATE_EVENT, callback); window.addEventListener('storage', callback); return () => { window.removeEventListener(UPDATE_EVENT, callback); window.removeEventListener('storage', callback); }; }; /* ------------------------------- Invoices -------------------------------- */ /** Load invoices, seeding from the static sample list on first run. */ export const loadInvoices = () => { const stored = read(INVOICES_KEY, null); if (Array.isArray(stored)) return stored; write(INVOICES_KEY, INVOICES); return INVOICES; }; export const saveInvoices = (list) => { write(INVOICES_KEY, list); emit(); }; export const getInvoice = (id) => loadInvoices().find((i) => i.id === id) || null; /** Append a new invoice (payment request) and return it. */ export const addInvoice = (invoice) => { const next = [...loadInvoices(), invoice]; saveInvoices(next); return invoice; }; /** Next sequential invoice id, e.g. "INV-2068". */ export const nextInvoiceId = () => { const max = loadInvoices().reduce((m, inv) => { const n = parseInt(String(inv.id).replace(/\D/g, ''), 10); return Number.isFinite(n) && n > m ? n : m; }, 2000); return `INV-${max + 1}`; }; /* ----------------------------- Transactions ------------------------------ */ export const loadTransactions = () => read(TXNS_KEY, []); export const saveTransactions = (list) => { write(TXNS_KEY, list); emit(); }; /** Has this invoice been paid (by the customer or marked by the owner)? */ export const isInvoicePaid = (invoiceId) => loadTransactions().some((t) => t.invoiceId === invoiceId && t.status === 'paid'); /** * Mark an invoice paid by writing a `paid` transaction — used both by the * customer success flow and by the owner's manual "Mark as Paid" action. * No-ops if a paid transaction for this invoice already exists. */ export const markInvoicePaid = (invoiceId, { method = 'manual' } = {}) => { const inv = getInvoice(invoiceId); if (!inv || isInvoicePaid(invoiceId)) return null; const txn = { id: `${method}-${invoiceId}-${Date.now()}`, invoiceId, amount: inv.amount, currency: inv.currency, customerEmail: null, status: 'paid', createdAt: new Date().toISOString(), method, }; saveTransactions([txn, ...loadTransactions()]); return txn; };