earnrate 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -11
- package/data/CHANGELOG.md +40 -0
- package/data/VERSION +1 -1
- package/data/cards/amex-blue-business-plus.yaml +14 -6
- package/data/cards/amex-blue-cash-everyday.yaml +12 -3
- package/data/cards/amex-blue-cash-preferred.yaml +17 -3
- package/data/cards/amex-gold.yaml +19 -3
- package/data/cards/amex-platinum.yaml +19 -3
- package/data/cards/bank-of-america-atmos-rewards-ascent.yaml +10 -2
- package/data/cards/bank-of-america-atmos-rewards-business.yaml +14 -3
- package/data/cards/bank-of-america-atmos-rewards-summit.yaml +4 -1
- package/data/cards/bank-of-america-customized-cash-rewards.yaml +10 -4
- package/data/cards/bilt-obsidian.yaml +4 -3
- package/data/cards/capital-one-kohls-card.yaml +11 -2
- package/data/cards/capital-one-kohls-rewards-visa.yaml +8 -2
- package/data/cards/capital-one-savor.yaml +32 -10
- package/data/cards/capital-one-venture-x.yaml +19 -4
- package/data/cards/capital-one-venture.yaml +19 -5
- package/data/cards/chase-aeroplan.yaml +15 -3
- package/data/cards/chase-freedom-flex.yaml +50 -10
- package/data/cards/chase-freedom-unlimited.yaml +6 -2
- package/data/cards/chase-ihg-one-rewards-business.yaml +18 -2
- package/data/cards/chase-ihg-one-rewards-premier-select.yaml +14 -2
- package/data/cards/chase-ihg-one-rewards-premier.yaml +15 -2
- package/data/cards/chase-ihg-one-rewards.yaml +8 -2
- package/data/cards/chase-ink-business-cash.yaml +7 -3
- package/data/cards/chase-ink-business-preferred.yaml +9 -2
- package/data/cards/chase-prime-visa.yaml +24 -5
- package/data/cards/chase-sapphire-preferred.yaml +59 -6
- package/data/cards/chase-sapphire-reserve.yaml +12 -2
- package/data/cards/chase-southwest-performance-business.yaml +22 -4
- package/data/cards/chase-southwest-plus.yaml +13 -3
- package/data/cards/chase-southwest-premier-business.yaml +22 -5
- package/data/cards/chase-southwest-premier.yaml +13 -3
- package/data/cards/chase-southwest-priority.yaml +18 -2
- package/data/cards/chase-united-business.yaml +5 -2
- package/data/cards/chase-united-club-business.yaml +2 -2
- package/data/cards/chase-united-club.yaml +16 -3
- package/data/cards/chase-united-explorer.yaml +5 -2
- package/data/cards/chase-united-gateway.yaml +2 -2
- package/data/cards/chase-united-quest.yaml +12 -3
- package/data/cards/citi-aadvantage-business.yaml +10 -4
- package/data/cards/citi-aadvantage-executive.yaml +8 -8
- package/data/cards/citi-aadvantage-mileup.yaml +7 -4
- package/data/cards/citi-aadvantage-platinum-select.yaml +1 -1
- package/data/cards/citi-costco-anywhere-visa-business.yaml +5 -2
- package/data/cards/citi-costco-anywhere-visa.yaml +6 -3
- package/data/cards/citi-custom-cash.yaml +43 -12
- package/data/cards/citi-double-cash.yaml +9 -3
- package/data/cards/citi-my-best-buy-visa.yaml +6 -3
- package/data/cards/citi-strata-elite.yaml +12 -2
- package/data/cards/citi-strata-premier.yaml +24 -2
- package/data/cards/sofi-unlimited-2.yaml +17 -3
- package/data/cards/td-bank-nordstrom-credit-card.yaml +12 -3
- package/data/cards/td-bank-nordstrom-visa.yaml +19 -3
- package/data/cards/td-bank-target-circle-card.yaml +13 -4
- package/data/cards/usbank-cash-plus.yaml +29 -7
- package/data/cards/wells-fargo-active-cash.yaml +9 -2
- package/data/cards/wells-fargo-autograph.yaml +31 -7
- package/data/issuers.yaml +32 -0
- package/data/merchants/seed.yaml +7 -0
- package/dist/cli/bin.js +10 -1
- package/dist/cli/cheatsheet.js +10 -5
- package/dist/cli/explore.js +6 -13
- package/dist/cli/import.js +2 -2
- package/dist/cli/offers.js +17 -7
- package/dist/cli/run.js +14 -17
- package/dist/cli/wallet.js +8 -5
- package/dist/data/file-store.js +6 -7
- package/dist/data/integrity.js +4 -0
- package/dist/data/load.js +4 -1
- package/dist/data/store.js +6 -22
- package/dist/data/wallet-edit.js +38 -13
- package/dist/engine/cheat-sheet.js +4 -5
- package/dist/engine/compare.js +2 -0
- package/dist/engine/evaluate.js +2 -4
- package/dist/engine/explore.js +4 -36
- package/dist/engine/memberships.js +1 -1
- package/dist/engine/offers.js +2 -1
- package/dist/engine/periods.js +18 -0
- package/dist/engine/recommend.js +14 -15
- package/dist/engine/setup.js +14 -24
- package/dist/engine/simulate.js +7 -6
- package/dist/engine/text.js +13 -1
- package/dist/engine/wallet-change.js +10 -12
- package/dist/engine/wallet-summary.js +64 -28
- package/dist/format/card-text.js +7 -20
- package/dist/format/explore.js +2 -2
- package/dist/format/offers.js +2 -2
- package/dist/format/recommendation.js +2 -2
- package/dist/format/wallet.js +1 -1
- package/dist/import/analyze.js +2 -1
- package/dist/mcp/context.js +30 -9
- package/dist/mcp/inputs.js +1 -1
- package/dist/mcp/schemas.js +1 -0
- package/dist/mcp/server.js +7 -1
- package/dist/mcp/tools/account.js +44 -16
- package/dist/mcp/tools/answers.js +58 -19
- package/dist/mcp/tools/wallet.js +58 -24
- package/dist/merchants/cache.js +0 -10
- package/dist/merchants/resolve.js +81 -4
- package/dist/schema/card.js +4 -1
- package/dist/schema/issuer.js +8 -0
- package/dist/schema/wallet.js +1 -1
- package/package.json +1 -1
|
@@ -3,8 +3,8 @@ name: U.S. Bank Cash+ Visa Signature Card
|
|
|
3
3
|
issuer: usbank
|
|
4
4
|
network: visa
|
|
5
5
|
currency: cash
|
|
6
|
-
last_verified: 2026-
|
|
7
|
-
verified_by:
|
|
6
|
+
last_verified: 2026-11-01
|
|
7
|
+
verified_by: claude
|
|
8
8
|
source_urls:
|
|
9
9
|
- https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
|
|
10
10
|
- https://cashplus.usbank.com/cash-plus/samplemerchants
|
|
@@ -64,14 +64,16 @@ choices:
|
|
|
64
64
|
rules:
|
|
65
65
|
- id: five-percent
|
|
66
66
|
match: { choice: five-percent }
|
|
67
|
+
# Ground transportation leaves out parking and tolls (also cruises and
|
|
68
|
+
# tickets bought through travel agencies); no other 5% option covers them.
|
|
69
|
+
exclude: { mccs: [7523, 4784] }
|
|
67
70
|
rate: 5
|
|
68
71
|
cap: five-percent-combined
|
|
69
72
|
- id: two-percent
|
|
70
73
|
match: { choice: two-percent }
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
exclude: { categories: [gas.superstore] }
|
|
74
|
+
# Fast food is its own 5% option, not part of Restaurants. Gas at grocery
|
|
75
|
+
# stores is left in until the todo below is settled.
|
|
76
|
+
exclude: { categories: [gas.superstore, dining.fast_food] }
|
|
75
77
|
rate: 2
|
|
76
78
|
- id: travel-center
|
|
77
79
|
match: { merchants: [usbank-travel-center] }
|
|
@@ -94,9 +96,29 @@ evidence:
|
|
|
94
96
|
- rule: five-percent
|
|
95
97
|
url: https://cashplus.usbank.com/cash-plus/samplemerchants
|
|
96
98
|
quote: 'This category includes only the merchants listed below'
|
|
99
|
+
- rule: five-percent
|
|
100
|
+
url: https://cashplus.usbank.com/cash-plus/samplemerchants
|
|
101
|
+
quote: 'steam ships, cruise lines, freight trains, parking meters, parking garages and toll stations'
|
|
102
|
+
- rule: five-percent
|
|
103
|
+
url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
|
|
104
|
+
quote: 'Purchases over $2,000 in the 5% categories will earn 1% cash back.'
|
|
105
|
+
- rule: two-percent
|
|
106
|
+
url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
|
|
107
|
+
quote: 'Gas station and electric vehicle charging station purchases at discount stores/supercenters, wholesale clubs and grocery stores/supermarkets'
|
|
108
|
+
- rule: two-percent
|
|
109
|
+
url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
|
|
110
|
+
quote: 'Excludes discount stores/supercenters and wholesale clubs'
|
|
111
|
+
- rule: two-percent
|
|
112
|
+
url: https://cashplus.usbank.com/cash-plus/samplemerchants
|
|
113
|
+
quote: "Excludes most fast food restaurants like McDonald's"
|
|
114
|
+
- rule: travel-center
|
|
115
|
+
url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
|
|
116
|
+
quote: 'additional 4% cash back for every $1 in eligible Net Purchases spent on prepaid air, car and hotel reservations'
|
|
97
117
|
- rule: annual_fee
|
|
98
118
|
url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
|
|
99
|
-
quote: '
|
|
119
|
+
quote: '$0 Annual fee'
|
|
100
120
|
- rule: welcome_offer
|
|
101
121
|
url: https://www.usbank.com/credit-cards/cash-plus-visa-signature-credit-card.html
|
|
102
122
|
quote: 'Earn a $200 rewards bonus after you spend $1,000 in eligible purchases within the first 90 days of account opening'
|
|
123
|
+
todo:
|
|
124
|
+
- "Gas at grocery stores (Kroger, Hy-Vee, Meijer) is modeled at 2%, as U.S. Bank's sample merchants page says it may qualify, varying by location; the card page's footnote says 1%, so it may earn 1% at some stations."
|
|
@@ -3,10 +3,11 @@ name: Wells Fargo Active Cash Card
|
|
|
3
3
|
issuer: wells-fargo
|
|
4
4
|
network: visa
|
|
5
5
|
currency: cash
|
|
6
|
-
last_verified: 2026-
|
|
7
|
-
verified_by:
|
|
6
|
+
last_verified: 2026-11-01
|
|
7
|
+
verified_by: claude
|
|
8
8
|
source_urls:
|
|
9
9
|
- https://creditcards.wellsfargo.com/active-cash-credit-card/
|
|
10
|
+
- https://www.wellsfargo.com/credit-cards/active-cash/terms/
|
|
10
11
|
base_rate: 2
|
|
11
12
|
annual_fee: 0
|
|
12
13
|
welcome_offer:
|
|
@@ -23,6 +24,9 @@ evidence:
|
|
|
23
24
|
- rule: base_rate
|
|
24
25
|
url: https://creditcards.wellsfargo.com/active-cash-credit-card/
|
|
25
26
|
quote: '2% cash rewards are earned for every $1 spent in net purchases'
|
|
27
|
+
- rule: base_rate
|
|
28
|
+
url: https://www.wellsfargo.com/credit-cards/active-cash/terms/
|
|
29
|
+
quote: 'For every $1.00 you spend with this Credit Card for net purchases, you earn two cents ($0.02) in Cash Rewards.'
|
|
26
30
|
- rule: annual_fee
|
|
27
31
|
url: https://creditcards.wellsfargo.com/active-cash-credit-card/
|
|
28
32
|
quote: '$0 annual fee'
|
|
@@ -32,3 +36,6 @@ evidence:
|
|
|
32
36
|
- rule: welcome_offer
|
|
33
37
|
url: https://creditcards.wellsfargo.com/active-cash-credit-card/
|
|
34
38
|
quote: 'To qualify for the $100 cash rewards bonus, a total of at least $500 in net purchases'
|
|
39
|
+
- rule: welcome_offer
|
|
40
|
+
url: https://www.wellsfargo.com/credit-cards/active-cash/terms/
|
|
41
|
+
quote: 'opened one within the last 48 months from the date of this application'
|
|
@@ -3,11 +3,12 @@ name: Wells Fargo Autograph Card
|
|
|
3
3
|
issuer: wells-fargo
|
|
4
4
|
network: visa
|
|
5
5
|
currency: WFR
|
|
6
|
-
last_verified: 2026-
|
|
7
|
-
verified_by:
|
|
6
|
+
last_verified: 2026-11-01
|
|
7
|
+
verified_by: claude
|
|
8
8
|
source_urls:
|
|
9
9
|
- https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
10
10
|
- https://www.wellsfargo.com/credit-cards/autograph-visa/streaming/
|
|
11
|
+
- https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
|
|
11
12
|
base_rate: 1
|
|
12
13
|
annual_fee: 0
|
|
13
14
|
welcome_offer:
|
|
@@ -18,8 +19,9 @@ welcome_offer:
|
|
|
18
19
|
rules:
|
|
19
20
|
- id: dining
|
|
20
21
|
match: { categories: [dining] }
|
|
22
|
+
exclude: { merchants: [doordash, uber-eats] }
|
|
21
23
|
rate: 3
|
|
22
|
-
note: Restaurants, bars (drinking places), fast food and caterers, by Visa merchant code.
|
|
24
|
+
note: Restaurants, bars (drinking places), fast food and caterers, by Visa merchant code. Wells Fargo says bakeries, food at grocery stores and third-party delivery services may not earn 3X.
|
|
23
25
|
- id: travel
|
|
24
26
|
match: { categories: [travel] }
|
|
25
27
|
rate: 3
|
|
@@ -31,43 +33,65 @@ rules:
|
|
|
31
33
|
note: Passenger rail, taxis, limousines, ferries, tolls and parking. Intercity bus lines aren't on Wells Fargo's list.
|
|
32
34
|
- id: gas
|
|
33
35
|
match: { categories: [gas, ev_charging] }
|
|
36
|
+
exclude: { categories: [gas.superstore, gas.grocery_store] }
|
|
34
37
|
rate: 3
|
|
35
|
-
note: Gas stations, automated fuel dispensers and EV charging stations, by Visa merchant code.
|
|
38
|
+
note: Gas stations, automated fuel dispensers and EV charging stations, by Visa merchant code. Fuel or charging at superstores, warehouse clubs, grocery stores, car washes and auto repair shops may not earn 3X.
|
|
36
39
|
- id: streaming
|
|
37
40
|
match: { categories: [streaming, tv_internet] }
|
|
38
41
|
rate: 3
|
|
39
|
-
note: Cable and pay TV, digital goods (books, movies, music) and subscription services. Subscriptions bundled with another product or billed by a third party (an app store, a car) may not count. An internet-only bill may not code as pay TV.
|
|
42
|
+
note: Cable and pay TV, digital goods (books, movies, music) and subscription services. Subscriptions bundled with another product or billed by a third party (an app store, a cable, phone or internet provider, an online retailer, a car) may not count. An internet-only bill may not code as pay TV.
|
|
40
43
|
- id: phone
|
|
41
44
|
match: { categories: [phone] }
|
|
42
45
|
rate: 3
|
|
43
|
-
note: Landline and cell phone providers.
|
|
46
|
+
note: Landline and cell phone providers. Phones, accessories, add-ons like insurance or extra data, and plans bundled or billed by a third party (an internet provider, a car) may not count.
|
|
44
47
|
# Not modeled: cell phone protection.
|
|
45
48
|
# Issuer text backing each rule; `yarn check-sources` checks it weekly.
|
|
46
49
|
evidence:
|
|
47
50
|
- rule: base_rate
|
|
48
51
|
url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
49
52
|
quote: '1 rewards point will be earned per $1 spent on other net purchases'
|
|
53
|
+
- rule: travel
|
|
54
|
+
url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
|
|
55
|
+
quote: 'Airlines Hotel/motels Campgrounds Timeshares Cruise lines Travel agencies Discount travel sites Vehicle/auto rentals'
|
|
50
56
|
- rule: dining
|
|
51
57
|
url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
52
58
|
quote: 'Dining: eating places and restaurants, drinking places, fast food restaurants'
|
|
59
|
+
- rule: dining
|
|
60
|
+
url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
|
|
61
|
+
quote: 'Grocery stores and other miscellaneous places that serve food or operate restaurants on site Third-party delivery services'
|
|
53
62
|
- rule: travel
|
|
54
63
|
url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
55
64
|
quote: 'Travel: airline, hotel/motel, timeshare, or vehicle/auto rental, cruise lines'
|
|
56
65
|
- rule: transit
|
|
57
66
|
url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
58
|
-
quote: 'Transit: passenger railway, taxis, limousines, ferries, toll bridges and highways'
|
|
67
|
+
quote: 'Transit: passenger railway, taxis, limousines, ferries, toll bridges and highways, parking lots and garages'
|
|
68
|
+
- rule: transit
|
|
69
|
+
url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
|
|
70
|
+
quote: 'Ferries Parking lots and garages Limousines Taxis Passenger railway Toll bridges and highways'
|
|
59
71
|
- rule: gas
|
|
60
72
|
url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
61
73
|
quote: 'Gas: gas stations, automated fuel dispensers, and electric vehicle charging stations'
|
|
74
|
+
- rule: gas
|
|
75
|
+
url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
|
|
76
|
+
quote: 'Gas or electric vehicle charging purchases made at the following locations may not earn bonus Points'
|
|
77
|
+
- rule: gas
|
|
78
|
+
url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
|
|
79
|
+
quote: 'Auto repair stores Superstores Car washes Warehouse/membership clubs Grocery stores'
|
|
62
80
|
- rule: streaming
|
|
63
81
|
url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
64
82
|
quote: 'Popular streaming services: cable and other pay television, digital goods, books'
|
|
65
83
|
- rule: streaming
|
|
66
84
|
url: https://www.wellsfargo.com/credit-cards/autograph-visa/streaming/
|
|
67
85
|
quote: 'You may earn 3X on these streaming services'
|
|
86
|
+
- rule: streaming
|
|
87
|
+
url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
|
|
88
|
+
quote: 'Streaming services that a third party bills, such as a digital platform; a cable, telecommunications, or internet provider'
|
|
68
89
|
- rule: phone
|
|
69
90
|
url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
70
91
|
quote: 'Phone plans: landline and cell phone providers considered telecommunication services'
|
|
92
|
+
- rule: phone
|
|
93
|
+
url: https://www.wellsfargo.com/credit-cards/autograph-visa/terms/
|
|
94
|
+
quote: 'Phone-plan services that a third party bundles or bills, such as an internet provider or in-vehicle service'
|
|
71
95
|
- rule: annual_fee
|
|
72
96
|
url: https://creditcards.wellsfargo.com/autograph-visa-credit-card/
|
|
73
97
|
quote: '$0 Annual Fee'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# The banks behind the cards: the name shown for each, and other words people
|
|
2
|
+
# search with ("bofa", "amex"). Every card's `issuer` must be one of these.
|
|
3
|
+
|
|
4
|
+
- id: amex
|
|
5
|
+
name: American Express
|
|
6
|
+
search: [amex, american express]
|
|
7
|
+
- id: bank-of-america
|
|
8
|
+
name: Bank of America
|
|
9
|
+
search: [bofa, boa, bankofamerica]
|
|
10
|
+
- id: capital-one
|
|
11
|
+
name: Capital One
|
|
12
|
+
search: [capitalone]
|
|
13
|
+
- id: chase
|
|
14
|
+
name: Chase
|
|
15
|
+
- id: citi
|
|
16
|
+
name: Citi
|
|
17
|
+
search: [citibank]
|
|
18
|
+
- id: column
|
|
19
|
+
# Column N.A. issues the Bilt card; people know it as Bilt.
|
|
20
|
+
name: Bilt
|
|
21
|
+
search: [bilt]
|
|
22
|
+
- id: sofi
|
|
23
|
+
name: SoFi
|
|
24
|
+
- id: td-bank
|
|
25
|
+
name: TD Bank
|
|
26
|
+
search: [td]
|
|
27
|
+
- id: usbank
|
|
28
|
+
name: U.S. Bank
|
|
29
|
+
search: [us bank]
|
|
30
|
+
- id: wells-fargo
|
|
31
|
+
name: Wells Fargo
|
|
32
|
+
search: [wf, wellsfargo]
|
package/data/merchants/seed.yaml
CHANGED
|
@@ -104,6 +104,13 @@
|
|
|
104
104
|
category: streaming,
|
|
105
105
|
}
|
|
106
106
|
- { id: max, name: Max, aliases: [hbo max], category: streaming }
|
|
107
|
+
- { id: peacock, name: Peacock, aliases: [peacock tv], category: streaming }
|
|
108
|
+
- {
|
|
109
|
+
id: showtime,
|
|
110
|
+
name: Showtime,
|
|
111
|
+
aliases: [paramount plus with showtime],
|
|
112
|
+
category: streaming,
|
|
113
|
+
}
|
|
107
114
|
- { id: spotify, name: Spotify, category: streaming }
|
|
108
115
|
- {
|
|
109
116
|
id: apple-tv,
|
package/dist/cli/bin.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { styleText } from 'node:util';
|
|
3
|
+
import { CLI_NAME, WALLET_ENV_VAR } from "../brand.js";
|
|
3
4
|
import { localToday } from "../engine/periods.js";
|
|
4
5
|
import { run } from "./run.js";
|
|
5
|
-
if (process.argv[2] === 'mcp') {
|
|
6
|
+
if (process.argv[2] === 'mcp' && /^(-h|--help)$/.test(process.argv[3] ?? '')) {
|
|
7
|
+
process.stdout.write(`Usage: ${CLI_NAME} mcp
|
|
8
|
+
|
|
9
|
+
Runs earnrate as an MCP server over stdio, for an AI assistant to start. It
|
|
10
|
+
uses the same wallet file as the command line (${WALLET_ENV_VAR}, or
|
|
11
|
+
~/.earnrate/wallet.yaml).
|
|
12
|
+
`);
|
|
13
|
+
}
|
|
14
|
+
else if (process.argv[2] === 'mcp') {
|
|
6
15
|
const { serveMcp } = await import("../mcp/serve.js");
|
|
7
16
|
await serveMcp(process.env);
|
|
8
17
|
}
|
package/dist/cli/cheatsheet.js
CHANGED
|
@@ -15,6 +15,7 @@ name with --cards, or else from every card earnrate knows.
|
|
|
15
15
|
|
|
16
16
|
Options:
|
|
17
17
|
--cards <ids> Use these cards instead of your wallet, comma separated.
|
|
18
|
+
--every-card Pick from every card earnrate knows, even with a wallet.
|
|
18
19
|
--member <name> A membership you have, like amazon-prime or costco. Repeatable.
|
|
19
20
|
--valuation <profile> flat or points-heavy, instead of your wallet's values.
|
|
20
21
|
--date <YYYY-MM-DD> The day to check. Default: today.
|
|
@@ -30,6 +31,7 @@ export async function runCheatSheet(argv, io) {
|
|
|
30
31
|
valuation: { type: 'string' },
|
|
31
32
|
date: { type: 'string' },
|
|
32
33
|
cards: { type: 'string' },
|
|
34
|
+
'every-card': { type: 'boolean' },
|
|
33
35
|
member: { type: 'string', multiple: true },
|
|
34
36
|
json: { type: 'boolean' },
|
|
35
37
|
help: { type: 'boolean', short: 'h' },
|
|
@@ -45,7 +47,9 @@ export async function runCheatSheet(argv, io) {
|
|
|
45
47
|
io.stdout(CHEATSHEET_HELP);
|
|
46
48
|
return 0;
|
|
47
49
|
}
|
|
48
|
-
const problem =
|
|
50
|
+
const problem = values['every-card'] && values.cards !== undefined
|
|
51
|
+
? 'Pick --cards or --every-card, not both.'
|
|
52
|
+
: (valuationProblem(values.valuation) ?? dateProblem(values.date));
|
|
49
53
|
if (problem !== null) {
|
|
50
54
|
io.stderr(`${problem}\n`);
|
|
51
55
|
return USAGE_ERROR;
|
|
@@ -58,17 +62,18 @@ export async function runCheatSheet(argv, io) {
|
|
|
58
62
|
if (loaded === null)
|
|
59
63
|
return RUNTIME_ERROR;
|
|
60
64
|
const date = values.date ?? io.today;
|
|
61
|
-
if (loaded === 'none') {
|
|
65
|
+
if (loaded === 'none' || values['every-card']) {
|
|
66
|
+
const saved = await savedSettings(io);
|
|
62
67
|
const sheet = catalogCheatSheet(catalog, date, {
|
|
63
68
|
filters: ALL_PERSONAL_CARDS,
|
|
64
|
-
memberships: values.member ??
|
|
69
|
+
memberships: values.member ?? saved.memberships,
|
|
65
70
|
valuations: values.valuation !== undefined && isValuationProfile(values.valuation)
|
|
66
71
|
? VALUATION_PROFILES[values.valuation]
|
|
67
|
-
:
|
|
72
|
+
: saved.valuations,
|
|
68
73
|
});
|
|
69
74
|
io.stdout(values.json
|
|
70
75
|
? `${JSON.stringify(sheet, null, 2)}\n`
|
|
71
|
-
: `${formatCheatSheet(sheet)}
|
|
76
|
+
: `${formatCheatSheet(sheet)}${loaded === 'none' ? `\n${NO_WALLET_HINT}\n` : ''}`);
|
|
72
77
|
return 0;
|
|
73
78
|
}
|
|
74
79
|
const wallet = values.valuation !== undefined && isValuationProfile(values.valuation)
|
package/dist/cli/explore.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
2
|
import { CLI_NAME } from "../brand.js";
|
|
3
3
|
import { loadCatalog, walletPath } from "../data/load.js";
|
|
4
|
-
import { ALL_PERSONAL_CARDS, exploreCards,
|
|
4
|
+
import { ALL_PERSONAL_CARDS, exploreCards, } from "../engine/explore.js";
|
|
5
5
|
import { isValuationProfile, profileText, VALUATION_PROFILES, withValuationProfile, } from "../engine/profiles.js";
|
|
6
|
-
import { resolveSpendLines, simulateAdditions, simulateCatalog, spendLineFor,
|
|
6
|
+
import { resolveSpendLines, simulateAdditions, simulateCatalog, spendLineFor, typicalSpend, TYPICAL_SPEND_NOTE, } from "../engine/simulate.js";
|
|
7
7
|
import { formatAdditions, formatCatalogSimulation, formatExploration, } from "../format/explore.js";
|
|
8
8
|
import { BOTH_CHANNELS, cardList, channelOf, dateProblem, membershipProblem, splitAmount, valuationProblem, } from "./args.js";
|
|
9
9
|
import { cardsToUse, RUNTIME_ERROR, savedSettings, USAGE_ERROR, } from "./run.js";
|
|
@@ -141,10 +141,9 @@ export async function runBest(argv, io) {
|
|
|
141
141
|
io.stderr(`${settings}\n`);
|
|
142
142
|
return USAGE_ERROR;
|
|
143
143
|
}
|
|
144
|
-
const category = findCategory(catalog, text);
|
|
145
144
|
const result = exploreCards(catalog, {
|
|
146
|
-
merchant:
|
|
147
|
-
category,
|
|
145
|
+
merchant: text,
|
|
146
|
+
category: null,
|
|
148
147
|
amount,
|
|
149
148
|
channel: channelOf(values),
|
|
150
149
|
date: values.date ?? io.today,
|
|
@@ -160,13 +159,7 @@ function parseSpend(catalog, args) {
|
|
|
160
159
|
const match = /^(.+)=\$?(\d+(?:\.\d+)?)$/.exec(arg);
|
|
161
160
|
if (!match?.[1] || match[2] === undefined)
|
|
162
161
|
return `"${arg}" isn't <what>=<dollars a month>, e.g. groceries=600.`;
|
|
163
|
-
|
|
164
|
-
const category = findCategory(catalog, what);
|
|
165
|
-
lines.push({
|
|
166
|
-
merchant: category === null ? what : null,
|
|
167
|
-
category,
|
|
168
|
-
monthly: Number(match[2]),
|
|
169
|
-
});
|
|
162
|
+
lines.push(spendLineFor(catalog, match[1].replace(/[-_]/g, ' '), Number(match[2])));
|
|
170
163
|
}
|
|
171
164
|
if (lines.length === 0)
|
|
172
165
|
return `Give your monthly spending, e.g. ${CLI_NAME} simulate groceries=600 dining=300 other=1000.`;
|
|
@@ -203,7 +196,7 @@ export async function runSimulate(argv, io) {
|
|
|
203
196
|
}
|
|
204
197
|
const typical = positionals.length === 0;
|
|
205
198
|
const spend = typical
|
|
206
|
-
?
|
|
199
|
+
? typicalSpend(catalog)
|
|
207
200
|
: parseSpend(catalog, positionals);
|
|
208
201
|
if (typeof spend === 'string') {
|
|
209
202
|
io.stderr(`${spend}\n`);
|
package/dist/cli/import.js
CHANGED
|
@@ -12,7 +12,7 @@ import { cardValuation } from "../engine/valuation.js";
|
|
|
12
12
|
import { analyzeStatement, } from "../import/analyze.js";
|
|
13
13
|
import { parseStatement } from "../import/statement.js";
|
|
14
14
|
import { displayPath, readWallet, RUNTIME_ERROR, USAGE_ERROR, } from "./run.js";
|
|
15
|
-
import { dollarsAndCents } from "../engine/text.js";
|
|
15
|
+
import { currencyName, dollarsAndCents } from "../engine/text.js";
|
|
16
16
|
const IMPORT_HELP = `Usage: ${CLI_NAME} import <statement.csv> --card <card-id> [--apply]
|
|
17
17
|
${CLI_NAME} import <statement.csv>:<card-id> [<statement.csv>:<card-id> ...]
|
|
18
18
|
|
|
@@ -64,7 +64,7 @@ function valuationNote(catalog, wallet, cardIds, profile) {
|
|
|
64
64
|
}
|
|
65
65
|
if (values.size === 0)
|
|
66
66
|
return 'Cash back counted as cash.';
|
|
67
|
-
const parts = [...values].map(([id, cents]) => `${String(cents)}¢ for ${catalog
|
|
67
|
+
const parts = [...values].map(([id, cents]) => `${String(cents)}¢ for ${currencyName(catalog, id)}`);
|
|
68
68
|
const list = parts.length > 1
|
|
69
69
|
? `${parts.slice(0, -1).join(', ')} and ${parts.at(-1) ?? ''}`
|
|
70
70
|
: (parts[0] ?? '');
|
package/dist/cli/offers.js
CHANGED
|
@@ -3,9 +3,10 @@ import { CLI_NAME } from "../brand.js";
|
|
|
3
3
|
import { loadCatalog, walletPath } from "../data/load.js";
|
|
4
4
|
import { compareOffers } from "../engine/offers.js";
|
|
5
5
|
import { isValuationProfile, withValuationProfile } from "../engine/profiles.js";
|
|
6
|
+
import { currencyName } from "../engine/text.js";
|
|
6
7
|
import { formatOffers } from "../format/offers.js";
|
|
7
|
-
import { valuationProblem } from "./args.js";
|
|
8
|
-
import { cardsToUse, RUNTIME_ERROR, USAGE_ERROR } from "./run.js";
|
|
8
|
+
import { cardList, valuationProblem } from "./args.js";
|
|
9
|
+
import { cardsToUse, RUNTIME_ERROR, savedSettings, USAGE_ERROR, } from "./run.js";
|
|
9
10
|
const OFFERS_HELP = `Usage: ${CLI_NAME} offers [--cash | --points] [--no-fee] [--business] [options]
|
|
10
11
|
|
|
11
12
|
Ranks the welcome offers on cards you don't have by first-year value: the
|
|
@@ -18,6 +19,8 @@ Options:
|
|
|
18
19
|
--no-fee Cards with no annual fee.
|
|
19
20
|
--business Business cards instead of personal ones.
|
|
20
21
|
--held Include cards already in your wallet.
|
|
22
|
+
--cards <ids> Cards you have, instead of your wallet, comma separated.
|
|
23
|
+
--member <name> A membership you have, in place of the saved ones.
|
|
21
24
|
--valuation <profile> flat or points-heavy, instead of your wallet's values.
|
|
22
25
|
--all List every offer, not just the top 10.
|
|
23
26
|
--json Print the full comparison as JSON.
|
|
@@ -34,6 +37,8 @@ export async function runOffers(argv, io) {
|
|
|
34
37
|
'no-fee': { type: 'boolean' },
|
|
35
38
|
business: { type: 'boolean' },
|
|
36
39
|
held: { type: 'boolean' },
|
|
40
|
+
cards: { type: 'string' },
|
|
41
|
+
member: { type: 'string', multiple: true },
|
|
37
42
|
valuation: { type: 'string' },
|
|
38
43
|
all: { type: 'boolean' },
|
|
39
44
|
json: { type: 'boolean' },
|
|
@@ -61,12 +66,18 @@ export async function runOffers(argv, io) {
|
|
|
61
66
|
}
|
|
62
67
|
const catalog = await loadCatalog();
|
|
63
68
|
const found = await cardsToUse(catalog, walletPath(io.env), io, {
|
|
64
|
-
cards:
|
|
65
|
-
members: [],
|
|
69
|
+
cards: cardList(values.cards),
|
|
70
|
+
members: values.member ?? [],
|
|
66
71
|
});
|
|
67
72
|
if (found === null)
|
|
68
73
|
return RUNTIME_ERROR;
|
|
69
|
-
const loaded = found === 'none'
|
|
74
|
+
const loaded = found === 'none'
|
|
75
|
+
? await savedSettings(io).then((saved) => ({
|
|
76
|
+
valuations: saved.valuations,
|
|
77
|
+
memberships: values.member ?? saved.memberships,
|
|
78
|
+
cards: [],
|
|
79
|
+
}))
|
|
80
|
+
: found;
|
|
70
81
|
const wallet = values.valuation && isValuationProfile(values.valuation)
|
|
71
82
|
? withValuationProfile(loaded, values.valuation)
|
|
72
83
|
: loaded;
|
|
@@ -80,7 +91,6 @@ export async function runOffers(argv, io) {
|
|
|
80
91
|
io.stdout(`${JSON.stringify(comparison, null, 2)}\n`);
|
|
81
92
|
return 0;
|
|
82
93
|
}
|
|
83
|
-
|
|
84
|
-
io.stdout(formatOffers(comparison, currencyName, values.all ?? false));
|
|
94
|
+
io.stdout(formatOffers(comparison, (id) => currencyName(catalog, id), values.all ?? false));
|
|
85
95
|
return 0;
|
|
86
96
|
}
|
package/dist/cli/run.js
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { CLI_NAME, CONFIG_DIR, LLM_ENV_VAR, LLM_MAX_CALLS_ENV_VAR, LLM_MODEL_ENV_VAR, MERCHANT_CACHE_ENV_VAR, WALLET_ENV_VAR, } from "../brand.js";
|
|
6
6
|
import { walletProblems } from "../data/integrity.js";
|
|
7
7
|
import { loadCatalog, loadWallet, walletPath } from "../data/load.js";
|
|
8
|
-
import { ALL_PERSONAL_CARDS,
|
|
8
|
+
import { ALL_PERSONAL_CARDS, exploreCards } from "../engine/explore.js";
|
|
9
9
|
import { profileText, VALUATION_PROFILES, withValuationProfile, } from "../engine/profiles.js";
|
|
10
10
|
import { recommend } from "../engine/recommend.js";
|
|
11
11
|
import { walletFromCards } from "../engine/transient.js";
|
|
@@ -14,7 +14,7 @@ import { formatRecommendation, plain, } from "../format/recommendation.js";
|
|
|
14
14
|
import { DEFAULT_LLM_MODEL } from "../merchants/anthropic.js";
|
|
15
15
|
import { DEFAULT_MAX_CALLS } from "../merchants/fallback.js";
|
|
16
16
|
import { llmEnabled, openLlmSession } from "../merchants/from-env.js";
|
|
17
|
-
import {
|
|
17
|
+
import { resolvePlace, withCallerCategory } from "../merchants/resolve.js";
|
|
18
18
|
import { packageVersion } from "../version.js";
|
|
19
19
|
import { membershipProblem, parseCliArgs } from "./args.js";
|
|
20
20
|
import { runCheatSheet } from "./cheatsheet.js";
|
|
@@ -63,7 +63,7 @@ Options:
|
|
|
63
63
|
--country <code> Where you're paying. Default: US.
|
|
64
64
|
--cards <ids> Answer for these cards instead of your wallet, comma
|
|
65
65
|
separated: --cards amex-gold,chase-prime-visa.
|
|
66
|
-
--member <name> A membership you have,
|
|
66
|
+
--member <name> A membership you have, in place of the saved ones.
|
|
67
67
|
--category <id> For a merchant earnrate doesn't know: its category,
|
|
68
68
|
e.g. grocery.supermarket. Ignored for known merchants.
|
|
69
69
|
--llm, --no-llm Ask Claude to classify merchants earnrate doesn't know.
|
|
@@ -87,7 +87,9 @@ export function displayPath(file) {
|
|
|
87
87
|
return file.startsWith(`${home}/`) ? `~${file.slice(home.length)}` : file;
|
|
88
88
|
}
|
|
89
89
|
// The cards to answer for: those named with --cards, else the wallet file.
|
|
90
|
-
// 'none' means no
|
|
90
|
+
// 'none' means no cards saved or named, which answers from every card.
|
|
91
|
+
// Named cards use the saved memberships (unless --member names some) and the
|
|
92
|
+
// saved point values, as the MCP tools do.
|
|
91
93
|
export async function cardsToUse(catalog, file, io, named) {
|
|
92
94
|
const problem = membershipProblem(catalog, named.members);
|
|
93
95
|
if (problem !== null) {
|
|
@@ -95,15 +97,16 @@ export async function cardsToUse(catalog, file, io, named) {
|
|
|
95
97
|
return null;
|
|
96
98
|
}
|
|
97
99
|
if (named.cards !== null) {
|
|
100
|
+
const saved = await savedSettings(io);
|
|
98
101
|
const wallet = walletFromCards(catalog, {
|
|
99
102
|
cards: named.cards.map((id) => ({ id })),
|
|
100
|
-
memberships: named.members,
|
|
103
|
+
memberships: named.members.length > 0 ? named.members : saved.memberships,
|
|
101
104
|
}, io.today);
|
|
102
105
|
if (typeof wallet === 'string') {
|
|
103
106
|
io.stderr(`${wallet}\n`);
|
|
104
107
|
return null;
|
|
105
108
|
}
|
|
106
|
-
return wallet;
|
|
109
|
+
return { ...wallet, valuations: saved.valuations };
|
|
107
110
|
}
|
|
108
111
|
try {
|
|
109
112
|
await access(file);
|
|
@@ -119,7 +122,7 @@ export async function cardsToUse(catalog, file, io, named) {
|
|
|
119
122
|
io.stderr(`Your wallet at ${displayPath(file)} has problems:\n${problems.map((p) => ` - ${p}\n`).join('')}`);
|
|
120
123
|
return null;
|
|
121
124
|
}
|
|
122
|
-
return loaded;
|
|
125
|
+
return loaded.cards.length === 0 ? 'none' : loaded;
|
|
123
126
|
}
|
|
124
127
|
export const NO_WALLET_HINT = `These are every card earnrate knows. For the best of your own cards, name them with --cards amex-gold,chase-prime-visa, or save them: ${CLI_NAME} add <card-id> (${CLI_NAME} cards finds ids).`;
|
|
125
128
|
// The point values and memberships saved in the wallet, which every answer
|
|
@@ -194,14 +197,7 @@ export async function run(argv, io) {
|
|
|
194
197
|
io.stderr(`Unknown category "${options.category}". Use one of: ${catalog.categories.map((c) => c.id).join(', ')}.\n`);
|
|
195
198
|
return USAGE_ERROR;
|
|
196
199
|
}
|
|
197
|
-
|
|
198
|
-
const resolvedName = resolveMerchant(catalog, purchase.merchant, purchase.channel);
|
|
199
|
-
const named = resolvedName.source === 'none' && options.category === null
|
|
200
|
-
? findCategory(catalog, purchase.merchant)
|
|
201
|
-
: null;
|
|
202
|
-
let merchant = named === null
|
|
203
|
-
? withCallerCategory(resolvedName, options.category)
|
|
204
|
-
: categoryResolution(catalog, named, purchase.channel);
|
|
200
|
+
let merchant = withCallerCategory(resolvePlace(catalog, purchase.merchant, purchase.channel), options.category);
|
|
205
201
|
const notices = [];
|
|
206
202
|
if (llmOn && merchant.source === 'none') {
|
|
207
203
|
const opened = await openLlmSession({
|
|
@@ -219,6 +215,7 @@ export async function run(argv, io) {
|
|
|
219
215
|
notices.push(...resolved.notices);
|
|
220
216
|
}
|
|
221
217
|
if (loaded === 'none') {
|
|
218
|
+
const saved = await savedSettings(io);
|
|
222
219
|
const explored = exploreCards(catalog, {
|
|
223
220
|
merchant: purchase.merchant,
|
|
224
221
|
category: null,
|
|
@@ -227,10 +224,10 @@ export async function run(argv, io) {
|
|
|
227
224
|
date: purchase.date,
|
|
228
225
|
}, {
|
|
229
226
|
filters: ALL_PERSONAL_CARDS,
|
|
230
|
-
memberships: options.members,
|
|
227
|
+
memberships: options.members.length > 0 ? options.members : saved.memberships,
|
|
231
228
|
valuations: options.valuation
|
|
232
229
|
? VALUATION_PROFILES[options.valuation]
|
|
233
|
-
:
|
|
230
|
+
: saved.valuations,
|
|
234
231
|
}, merchant);
|
|
235
232
|
if (options.json) {
|
|
236
233
|
io.stdout(`${JSON.stringify(explored, null, 2)}\n`);
|
package/dist/cli/wallet.js
CHANGED
|
@@ -3,6 +3,7 @@ import { CLI_NAME } from "../brand.js";
|
|
|
3
3
|
import { FileWalletStore } from "../data/file-store.js";
|
|
4
4
|
import { loadCatalog, loadWallet, walletPath } from "../data/load.js";
|
|
5
5
|
import { isValuationProfile, profileText, VALUATION_PROFILES, } from "../engine/profiles.js";
|
|
6
|
+
import { membershipName } from "../engine/memberships.js";
|
|
6
7
|
import { knownMemberships, searchCards } from "../engine/setup.js";
|
|
7
8
|
import { formatCardInfo, formatNotes } from "../format/cards.js";
|
|
8
9
|
import { displayPath, RUNTIME_ERROR, USAGE_ERROR } from "./run.js";
|
|
@@ -41,19 +42,21 @@ ${CARD_OPTIONS}
|
|
|
41
42
|
`,
|
|
42
43
|
update: `Usage: ${CLI_NAME} update <card-id> [options]
|
|
43
44
|
|
|
44
|
-
Changes a card in your wallet: picks for this period, label, anniversary
|
|
45
|
-
activations. Picks for the current period are merged with
|
|
45
|
+
Changes a card in your wallet: picks for this period, label, anniversary,
|
|
46
|
+
activations or welcome bonus. Picks for the current period are merged with
|
|
47
|
+
ones already set.
|
|
46
48
|
|
|
47
49
|
${CARD_OPTIONS}
|
|
48
50
|
`,
|
|
49
51
|
remove: `Usage: ${CLI_NAME} remove <card-id>
|
|
50
52
|
|
|
51
|
-
Removes a card from your wallet, with its tracked cap usage
|
|
53
|
+
Removes a card from your wallet, with its tracked cap usage and bonus
|
|
54
|
+
progress. Your memberships and point values stay.
|
|
52
55
|
`,
|
|
53
56
|
memberships: `Usage: ${CLI_NAME} memberships [<membership>...] [--none]
|
|
54
57
|
|
|
55
58
|
With no arguments, shows your memberships and the ones earnrate knows. With
|
|
56
|
-
names, replaces the list. --none clears it.
|
|
59
|
+
names, replaces the list. --none clears it. No cards need to be saved first.
|
|
57
60
|
`,
|
|
58
61
|
values: `Usage: ${CLI_NAME} values [<currency>=<cents>...] [--profile <name>]
|
|
59
62
|
|
|
@@ -213,7 +216,7 @@ export async function runWalletCommand(command, argv, io) {
|
|
|
213
216
|
}
|
|
214
217
|
const list = values.none ? [] : positionals;
|
|
215
218
|
return report(await store.setMemberships(list), list.length > 0
|
|
216
|
-
? `Memberships set to ${list.join(', ')} in ${where}.`
|
|
219
|
+
? `Memberships set to ${list.map((m) => `${membershipName(catalog, m)} (${m})`).join(', ')} in ${where}.`
|
|
217
220
|
: `Memberships cleared in ${where}.`, null, io);
|
|
218
221
|
}
|
|
219
222
|
if (positionals.length !== 1) {
|
package/dist/data/file-store.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { bookPurchase } from "../engine/record.js";
|
|
2
2
|
import { addToWallet, removeFromWallet, updateInWallet, withMemberships, withValuations, } from "../engine/wallet-change.js";
|
|
3
3
|
import { loadWallet } from "./load.js";
|
|
4
|
-
import {
|
|
4
|
+
import { emptyWallet, } from "./store.js";
|
|
5
|
+
import { addCardText, commitWalletText, EMPTY_WALLET_TEXT, readWalletText, removeCardText, setMembershipsText, setValuationsText, updateCardText, writeProgress, } from "./wallet-edit.js";
|
|
5
6
|
const failed = (error) => ({ ok: false, error });
|
|
6
7
|
const errorText = (e) => (e instanceof Error ? e.message : String(e));
|
|
7
8
|
const queues = new Map();
|
|
@@ -56,7 +57,7 @@ export class FileWalletStore {
|
|
|
56
57
|
}
|
|
57
58
|
});
|
|
58
59
|
}
|
|
59
|
-
//
|
|
60
|
+
// Changing or removing a card needs a wallet file to change.
|
|
60
61
|
existing(apply) {
|
|
61
62
|
return (wallet) => wallet === null
|
|
62
63
|
? `No wallet at ${this.where} yet. Add a card first to create it.`
|
|
@@ -69,15 +70,13 @@ export class FileWalletStore {
|
|
|
69
70
|
return this.change(this.existing((w) => updateInWallet(this.catalog, w, input, today)), (text, change) => updateCardText(text ?? '', input.id, change.fields));
|
|
70
71
|
}
|
|
71
72
|
removeCard(cardId) {
|
|
72
|
-
return this.change(this.existing((w) => w
|
|
73
|
-
? failed(`${cardId} is the last card in ${this.where}. To start over, delete that file; to swap cards, add the new one first.`)
|
|
74
|
-
: removeFromWallet(w, cardId)), (text) => removeCardText(text ?? '', cardId));
|
|
73
|
+
return this.change(this.existing((w) => removeFromWallet(w, cardId)), (text) => removeCardText(text ?? '', cardId));
|
|
75
74
|
}
|
|
76
75
|
setMemberships(memberships) {
|
|
77
|
-
return this.change(
|
|
76
|
+
return this.change((w) => withMemberships(this.catalog, w ?? emptyWallet(), memberships), (text, change) => setMembershipsText(text ?? EMPTY_WALLET_TEXT, change.wallet.memberships));
|
|
78
77
|
}
|
|
79
78
|
setValuations(changes) {
|
|
80
|
-
return this.change(
|
|
79
|
+
return this.change((w) => withValuations(this.catalog, w ?? emptyWallet(), changes), (text) => setValuationsText(text ?? EMPTY_WALLET_TEXT, changes));
|
|
81
80
|
}
|
|
82
81
|
record(purchase, merchant) {
|
|
83
82
|
return withWalletLock(this.where, async () => {
|
package/dist/data/integrity.js
CHANGED
|
@@ -179,6 +179,7 @@ export function catalogProblems(catalog) {
|
|
|
179
179
|
['currency', catalog.currencies.map((c) => c.id)],
|
|
180
180
|
['card', catalog.cards.map((c) => c.id)],
|
|
181
181
|
['membership', catalog.memberships.map((m) => m.id)],
|
|
182
|
+
['issuer', catalog.issuers.map((i) => i.id)],
|
|
182
183
|
]) {
|
|
183
184
|
for (const id of duplicates(ids))
|
|
184
185
|
problems.push(`duplicate ${kind} id '${id}'`);
|
|
@@ -203,8 +204,11 @@ export function catalogProblems(catalog) {
|
|
|
203
204
|
for (const id of m.members_shop_at)
|
|
204
205
|
if (!merchantIds.has(id))
|
|
205
206
|
problems.push(`membership ${m.id}: members_shop_at '${id}' is not a merchant`);
|
|
207
|
+
const issuers = new Set(catalog.issuers.map((i) => i.id));
|
|
206
208
|
for (const card of catalog.cards) {
|
|
207
209
|
problems.push(...cardProblems(card, catalog, merchantIds));
|
|
210
|
+
if (!issuers.has(card.issuer))
|
|
211
|
+
problems.push(`${card.id}: unknown issuer '${card.issuer}' (data/issuers.yaml)`);
|
|
208
212
|
}
|
|
209
213
|
return problems;
|
|
210
214
|
}
|