earnrate 0.2.6 → 0.2.8

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.
@@ -0,0 +1,31 @@
1
+ // earnrate never stores card numbers, on any surface. A card number is 13 to
2
+ // 19 digits; a phone number in a store's name is 10 or 11, so free text with
3
+ // a longer run is refused or not kept.
4
+ const digitsOnly = (text) => text.replace(/[\s.-]/g, '');
5
+ export const hasCardNumber = (text) => /\d{13,}/.test(digitsOnly(text));
6
+ // A checkout label may carry a last four, but never 8 or more digits.
7
+ export const labelHasCardNumber = (text) => /\d{8,}/.test(digitsOnly(text));
8
+ export const CARD_NUMBER_LABEL = 'The checkout label looks like it has a card number in it. earnrate never stores card numbers; use a name like "Amex Gold".';
9
+ export const CARD_NUMBER_REDEMPTION = 'The redemption note looks like it has a card number in it. earnrate never stores card numbers.';
10
+ // For wallets saved before the schema refused card numbers: drop the field
11
+ // instead of failing to load, since a wallet that can't load can't be fixed.
12
+ export function withoutCardNumbers(raw) {
13
+ if (!isRecord(raw))
14
+ return raw;
15
+ const wallet = { ...raw };
16
+ if (Array.isArray(wallet.cards))
17
+ wallet.cards = wallet.cards.map((card) => isRecord(card) && isCardNumber(card.checkout_label, labelHasCardNumber)
18
+ ? omit(card, 'checkout_label')
19
+ : card);
20
+ if (isRecord(wallet.valuations))
21
+ wallet.valuations = Object.fromEntries(Object.entries(wallet.valuations).map(([currency, value]) => [
22
+ currency,
23
+ isRecord(value) && isCardNumber(value.redemption, hasCardNumber)
24
+ ? omit(value, 'redemption')
25
+ : value,
26
+ ]));
27
+ return wallet;
28
+ }
29
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
30
+ const isCardNumber = (value, test) => typeof value === 'string' && test(value);
31
+ const omit = (record, key) => Object.fromEntries(Object.entries(record).filter(([k]) => k !== key));
@@ -1,11 +1,15 @@
1
1
  import { z } from 'zod';
2
2
  import { CurrencyId, IsoDate, KebabId, PeriodKey, ValueRange, } from "./common.js";
3
+ import { CARD_NUMBER_LABEL, CARD_NUMBER_REDEMPTION, hasCardNumber, labelHasCardNumber, } from "./card-number.js";
3
4
  const Valuation = z.union([
4
5
  z.number().positive(),
5
6
  z.strictObject({
6
7
  value: z.number().positive().optional(),
7
8
  range: ValueRange.optional(),
8
- redemption: z.string().optional(),
9
+ redemption: z
10
+ .string()
11
+ .refine((t) => !hasCardNumber(t), CARD_NUMBER_REDEMPTION)
12
+ .optional(),
9
13
  }),
10
14
  ]);
11
15
  const CapUsage = z.strictObject({
@@ -14,7 +18,11 @@ const CapUsage = z.strictObject({
14
18
  });
15
19
  export const WalletCardSchema = z.strictObject({
16
20
  id: KebabId,
17
- checkout_label: z.string().min(1).optional(),
21
+ checkout_label: z
22
+ .string()
23
+ .min(1)
24
+ .refine((t) => !labelHasCardNumber(t), CARD_NUMBER_LABEL)
25
+ .optional(),
18
26
  // MM-DD, needed only for cardmember_year caps.
19
27
  anniversary: z
20
28
  .string()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "earnrate",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Which credit card earns the most at a merchant, from your cards or every card, and why",
5
5
  "keywords": [
6
6
  "credit cards",