Classify prolist as ads; stop reporting intentionally-skipped ledger types

- 'prolist' (Etsy Promoted Listings / onsite ads) now maps to
  Marketing & Advertising instead of being skipped
- classifyLedgerEntry distinguishes 'skip' (credits, disbursements,
  refunds — deliberately not expenses) from null (unrecognised debit).
  Only null entries are reported, so disbursements/refunds no longer
  appear in the 'unknown charge types' list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
dlawler489 2026-06-13 15:44:01 +10:00
parent 7a626257cd
commit d5742940ec

View file

@ -337,22 +337,25 @@ export interface LedgerSyncResult {
// there is no per-entry divisor, so /100 (all Etsy currencies are 2-decimal)
const ledgerAmount = (entry: any): number => (entry.amount || 0) / 100;
// Map a debit ledger entry to an expense category, or null to skip it.
// Conservative: only genuine business costs become expenses. Credits (sales),
// disbursements to bank, and refunds (revenue reversals) are never expenses.
export const classifyLedgerEntry = (entry: any): string | null => {
if ((entry.amount || 0) >= 0) return null; // credits: sale payments, misc credits
// Classify a ledger entry:
// - a category string → import as an expense
// - 'skip' → deliberately not an expense (credit, disbursement, refund)
// - null → an unrecognised debit, reported so rules can be refined
// Conservative: only genuine business costs become expenses.
export const classifyLedgerEntry = (entry: any): string | 'skip' | null => {
if ((entry.amount || 0) >= 0) return 'skip'; // credits: sale payments, misc credits
const text = `${entry.description || ''} ${entry.ledger_type || ''} ${entry.reference_type || ''}`.toLowerCase();
// Transfers and revenue reversals are not expenses
if (/disburse|deposit|withdraw|payout/.test(text)) return null;
if (/refund|reversal|return/.test(text)) return null;
// Transfers to bank and revenue reversals are intentionally not expenses
if (/disburse|deposit|withdraw|payout/.test(text)) return 'skip';
if (/refund|reversal|return/.test(text)) return 'skip';
// Etsy's onsite ads are logged as "prolist" (Promoted Listings)
if (/prolist|promoted|ads|advertis|marketing|offsite/.test(text)) return 'Marketing & Advertising';
if (/listing/.test(text)) return 'Listing Fees';
if (/transaction/.test(text)) return 'Transaction Fees';
if (/processing|payment fee/.test(text)) return 'Payment Processing Fees';
if (/ads|advertis|marketing|offsite/.test(text)) return 'Marketing & Advertising';
if (/shipping|postage|label/.test(text)) return 'Shipping & Postage';
if (/vat|gst|\btax\b/.test(text)) return 'Taxes & GST';
if (/subscription|etsy plus/.test(text)) return 'Subscriptions';
@ -400,8 +403,9 @@ export const syncLedgerEntries = async (
for (const entry of entries) {
result.entriesSeen++;
const category = classifyLedgerEntry(entry);
if (!category) {
// Collect unclassified debits for reporting (capped)
if (category === 'skip') continue; // deliberately not an expense
if (category === null) {
// Genuinely unrecognised debit — report (capped) so rules can be refined
if ((entry.amount || 0) < 0 && result.unknownDebits.length < 25) {
const desc = String(entry.description || entry.ledger_type || 'unknown').trim();
if (!result.unknownDebits.includes(desc)) result.unknownDebits.push(desc);