From d5742940eccba6bd8aaffc6247825fcf0897f995 Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Sat, 13 Jun 2026 15:44:01 +1000 Subject: [PATCH] Classify prolist as ads; stop reporting intentionally-skipped ledger types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - '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 --- server/src/services/etsyApi.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/server/src/services/etsyApi.ts b/server/src/services/etsyApi.ts index 4ddf1fb..73a6018 100644 --- a/server/src/services/etsyApi.ts +++ b/server/src/services/etsyApi.ts @@ -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);