1#[derive(Debug)]
13pub struct MerchantEntry {
14 pub pattern: &'static str,
16 pub account: &'static str,
18 pub category: &'static str,
20}
21
22pub static MERCHANT_PATTERNS: &[MerchantEntry] = &[
26 MerchantEntry {
28 pattern: r"WHOLE\s*FOODS",
29 account: "Expenses:Groceries",
30 category: "Groceries",
31 },
32 MerchantEntry {
33 pattern: r"TRADER\s*JOE",
34 account: "Expenses:Groceries",
35 category: "Groceries",
36 },
37 MerchantEntry {
38 pattern: "KROGER",
39 account: "Expenses:Groceries",
40 category: "Groceries",
41 },
42 MerchantEntry {
43 pattern: "SAFEWAY",
44 account: "Expenses:Groceries",
45 category: "Groceries",
46 },
47 MerchantEntry {
48 pattern: "PUBLIX",
49 account: "Expenses:Groceries",
50 category: "Groceries",
51 },
52 MerchantEntry {
53 pattern: "ALDI",
54 account: "Expenses:Groceries",
55 category: "Groceries",
56 },
57 MerchantEntry {
58 pattern: "LIDL",
59 account: "Expenses:Groceries",
60 category: "Groceries",
61 },
62 MerchantEntry {
63 pattern: "COSTCO",
64 account: "Expenses:Groceries",
65 category: "Groceries",
66 },
67 MerchantEntry {
68 pattern: r"SAM'?S\s*CLUB",
69 account: "Expenses:Groceries",
70 category: "Groceries",
71 },
72 MerchantEntry {
73 pattern: "WEGMANS",
74 account: "Expenses:Groceries",
75 category: "Groceries",
76 },
77 MerchantEntry {
78 pattern: r"H[\s-]?E[\s-]?B\b",
79 account: "Expenses:Groceries",
80 category: "Groceries",
81 },
82 MerchantEntry {
83 pattern: "MEIJER",
84 account: "Expenses:Groceries",
85 category: "Groceries",
86 },
87 MerchantEntry {
88 pattern: r"FOOD\s*LION",
89 account: "Expenses:Groceries",
90 category: "Groceries",
91 },
92 MerchantEntry {
93 pattern: "SPROUTS",
94 account: "Expenses:Groceries",
95 category: "Groceries",
96 },
97 MerchantEntry {
98 pattern: "INSTACART",
99 account: "Expenses:Groceries",
100 category: "Groceries",
101 },
102 MerchantEntry {
104 pattern: "STARBUCKS",
105 account: "Expenses:Dining:Coffee",
106 category: "Dining",
107 },
108 MerchantEntry {
109 pattern: "DUNKIN",
110 account: "Expenses:Dining:Coffee",
111 category: "Dining",
112 },
113 MerchantEntry {
114 pattern: r"PEET'?S\s*COFFEE",
115 account: "Expenses:Dining:Coffee",
116 category: "Dining",
117 },
118 MerchantEntry {
119 pattern: "MCDONALD",
120 account: "Expenses:Dining:FastFood",
121 category: "Dining",
122 },
123 MerchantEntry {
124 pattern: r"BURGER\s*KING",
125 account: "Expenses:Dining:FastFood",
126 category: "Dining",
127 },
128 MerchantEntry {
129 pattern: r"TACO\s*BELL",
130 account: "Expenses:Dining:FastFood",
131 category: "Dining",
132 },
133 MerchantEntry {
134 pattern: r"CHICK[\s-]?FIL[\s-]?A",
135 account: "Expenses:Dining:FastFood",
136 category: "Dining",
137 },
138 MerchantEntry {
139 pattern: "SUBWAY",
140 account: "Expenses:Dining:FastFood",
141 category: "Dining",
142 },
143 MerchantEntry {
144 pattern: "CHIPOTLE",
145 account: "Expenses:Dining:FastFood",
146 category: "Dining",
147 },
148 MerchantEntry {
149 pattern: "PANERA",
150 account: "Expenses:Dining:FastFood",
151 category: "Dining",
152 },
153 MerchantEntry {
154 pattern: "DOORDASH",
155 account: "Expenses:Dining:Delivery",
156 category: "Dining",
157 },
158 MerchantEntry {
159 pattern: "GRUBHUB",
160 account: "Expenses:Dining:Delivery",
161 category: "Dining",
162 },
163 MerchantEntry {
164 pattern: r"UBER\s*EATS",
165 account: "Expenses:Dining:Delivery",
166 category: "Dining",
167 },
168 MerchantEntry {
170 pattern: r"UBER\s*(TRIP|RIDE|BV)",
171 account: "Expenses:Transport:Rideshare",
172 category: "Transport",
173 },
174 MerchantEntry {
175 pattern: "LYFT",
176 account: "Expenses:Transport:Rideshare",
177 category: "Transport",
178 },
179 MerchantEntry {
180 pattern: r"SHELL\b",
181 account: "Expenses:Transport:Fuel",
182 category: "Transport",
183 },
184 MerchantEntry {
185 pattern: "CHEVRON",
186 account: "Expenses:Transport:Fuel",
187 category: "Transport",
188 },
189 MerchantEntry {
190 pattern: "EXXON",
191 account: "Expenses:Transport:Fuel",
192 category: "Transport",
193 },
194 MerchantEntry {
195 pattern: r"BP\b",
196 account: "Expenses:Transport:Fuel",
197 category: "Transport",
198 },
199 MerchantEntry {
200 pattern: "SPEEDWAY",
201 account: "Expenses:Transport:Fuel",
202 category: "Transport",
203 },
204 MerchantEntry {
205 pattern: "PARKING",
206 account: "Expenses:Transport:Parking",
207 category: "Transport",
208 },
209 MerchantEntry {
210 pattern: r"E[\s-]?Z\s*PASS",
211 account: "Expenses:Transport:Tolls",
212 category: "Transport",
213 },
214 MerchantEntry {
216 pattern: "AMAZON|AMZN",
217 account: "Expenses:Shopping:Amazon",
218 category: "Shopping",
219 },
220 MerchantEntry {
221 pattern: r"WALMART|WM\s*SUPERCENTER",
222 account: "Expenses:Shopping",
223 category: "Shopping",
224 },
225 MerchantEntry {
226 pattern: r"TARGET\b",
227 account: "Expenses:Shopping",
228 category: "Shopping",
229 },
230 MerchantEntry {
231 pattern: r"BEST\s*BUY",
232 account: "Expenses:Shopping:Electronics",
233 category: "Shopping",
234 },
235 MerchantEntry {
236 pattern: r"APPLE\.COM|APPLE\s*STORE",
237 account: "Expenses:Shopping:Electronics",
238 category: "Shopping",
239 },
240 MerchantEntry {
241 pattern: r"HOME\s*DEPOT",
242 account: "Expenses:Shopping:Home",
243 category: "Shopping",
244 },
245 MerchantEntry {
246 pattern: r"LOWE'?S",
247 account: "Expenses:Shopping:Home",
248 category: "Shopping",
249 },
250 MerchantEntry {
251 pattern: "IKEA",
252 account: "Expenses:Shopping:Home",
253 category: "Shopping",
254 },
255 MerchantEntry {
257 pattern: "NETFLIX",
258 account: "Expenses:Subscriptions:Streaming",
259 category: "Subscriptions",
260 },
261 MerchantEntry {
262 pattern: "SPOTIFY",
263 account: "Expenses:Subscriptions:Streaming",
264 category: "Subscriptions",
265 },
266 MerchantEntry {
267 pattern: "HULU",
268 account: "Expenses:Subscriptions:Streaming",
269 category: "Subscriptions",
270 },
271 MerchantEntry {
272 pattern: r"DISNEY\s*\+|DISNEYPLUS",
273 account: "Expenses:Subscriptions:Streaming",
274 category: "Subscriptions",
275 },
276 MerchantEntry {
277 pattern: r"HBO\s*MAX|MAX\.COM",
278 account: "Expenses:Subscriptions:Streaming",
279 category: "Subscriptions",
280 },
281 MerchantEntry {
282 pattern: r"APPLE\s*(TV|MUSIC|ONE|ICLOUD)",
283 account: "Expenses:Subscriptions:Apple",
284 category: "Subscriptions",
285 },
286 MerchantEntry {
287 pattern: r"AMAZON\s*PRIME",
288 account: "Expenses:Subscriptions:Amazon",
289 category: "Subscriptions",
290 },
291 MerchantEntry {
292 pattern: "ADOBE",
293 account: "Expenses:Subscriptions:Software",
294 category: "Subscriptions",
295 },
296 MerchantEntry {
297 pattern: r"MICROSOFT\s*(365|OFFICE)",
298 account: "Expenses:Subscriptions:Software",
299 category: "Subscriptions",
300 },
301 MerchantEntry {
302 pattern: "GITHUB",
303 account: "Expenses:Subscriptions:Software",
304 category: "Subscriptions",
305 },
306 MerchantEntry {
307 pattern: "OPENAI|CHATGPT",
308 account: "Expenses:Subscriptions:Software",
309 category: "Subscriptions",
310 },
311 MerchantEntry {
313 pattern: r"AT\s*&?\s*T\b",
314 account: "Expenses:Utilities:Phone",
315 category: "Utilities",
316 },
317 MerchantEntry {
318 pattern: "VERIZON",
319 account: "Expenses:Utilities:Phone",
320 category: "Utilities",
321 },
322 MerchantEntry {
323 pattern: r"T[\s-]?MOBILE",
324 account: "Expenses:Utilities:Phone",
325 category: "Utilities",
326 },
327 MerchantEntry {
328 pattern: "COMCAST|XFINITY",
329 account: "Expenses:Utilities:Internet",
330 category: "Utilities",
331 },
332 MerchantEntry {
333 pattern: "SPECTRUM",
334 account: "Expenses:Utilities:Internet",
335 category: "Utilities",
336 },
337 MerchantEntry {
339 pattern: "CVS",
340 account: "Expenses:Health:Pharmacy",
341 category: "Health",
342 },
343 MerchantEntry {
344 pattern: "WALGREENS",
345 account: "Expenses:Health:Pharmacy",
346 category: "Health",
347 },
348 MerchantEntry {
349 pattern: r"PLANET\s*FITNESS",
350 account: "Expenses:Health:Fitness",
351 category: "Health",
352 },
353 MerchantEntry {
354 pattern: "PELOTON",
355 account: "Expenses:Health:Fitness",
356 category: "Health",
357 },
358 MerchantEntry {
360 pattern: "AIRBNB",
361 account: "Expenses:Travel:Lodging",
362 category: "Travel",
363 },
364 MerchantEntry {
365 pattern: r"BOOKING\.COM",
366 account: "Expenses:Travel:Lodging",
367 category: "Travel",
368 },
369 MerchantEntry {
370 pattern: "MARRIOTT",
371 account: "Expenses:Travel:Lodging",
372 category: "Travel",
373 },
374 MerchantEntry {
375 pattern: "HILTON",
376 account: "Expenses:Travel:Lodging",
377 category: "Travel",
378 },
379 MerchantEntry {
380 pattern: "EXPEDIA",
381 account: "Expenses:Travel",
382 category: "Travel",
383 },
384 MerchantEntry {
386 pattern: "VENMO",
387 account: "Expenses:Transfers:Venmo",
388 category: "Financial",
389 },
390 MerchantEntry {
391 pattern: "PAYPAL",
392 account: "Expenses:Transfers:PayPal",
393 category: "Financial",
394 },
395 MerchantEntry {
396 pattern: "ZELLE",
397 account: "Expenses:Transfers:Zelle",
398 category: "Financial",
399 },
400 MerchantEntry {
402 pattern: "TICKETMASTER",
403 account: "Expenses:Entertainment",
404 category: "Entertainment",
405 },
406 MerchantEntry {
407 pattern: r"STEAM\s*(GAMES|PURCHASE)",
408 account: "Expenses:Entertainment:Games",
409 category: "Entertainment",
410 },
411];
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416
417 #[test]
418 fn merchant_patterns_not_empty() {
419 assert!(!MERCHANT_PATTERNS.is_empty());
420 assert!(
421 MERCHANT_PATTERNS.len() > 50,
422 "Expected at least 50 merchant patterns"
423 );
424 }
425
426 #[test]
427 fn all_patterns_compile() {
428 for entry in MERCHANT_PATTERNS {
429 let result = regex::RegexBuilder::new(entry.pattern)
430 .case_insensitive(true)
431 .build();
432 assert!(
433 result.is_ok(),
434 "Pattern '{}' for {} failed to compile: {:?}",
435 entry.pattern,
436 entry.account,
437 result.err()
438 );
439 }
440 }
441
442 #[test]
443 fn patterns_have_valid_accounts() {
444 for entry in MERCHANT_PATTERNS {
445 assert!(
446 entry.account.starts_with("Expenses:") || entry.account.starts_with("Income:"),
447 "Pattern '{}' has invalid account '{}' — must start with Expenses: or Income:",
448 entry.pattern,
449 entry.account,
450 );
451 }
452 }
453
454 #[test]
455 fn patterns_have_categories() {
456 for entry in MERCHANT_PATTERNS {
457 assert!(
458 !entry.category.is_empty(),
459 "Pattern '{}' is missing a category",
460 entry.pattern,
461 );
462 }
463 }
464}