Files
accounted/lib/receipt-hunt/__tests__/portal-directory.test.ts
T
ca6b962662 feat(receipt-hunt): a directory of where suppliers keep their invoices (#1506)
Some vendors never attach anything. They mail "your invoice is ready", or
nothing at all, and the invoice waits behind a login. Seeing "OPENAI
CHATGPT SUBSCR" on a statement is enough to say which page it is on, and
that turns a dead end into a one-click errand.

Deliberately not what Presto does. They log in for you across a thousand
portals, with a desktop app so credentials never leave the machine.
Holding a customer's supplier passwords is the expensive and legally heavy
half of that product, and skipping it costs little of the value: knowing
*where* is most of it.

The lookup runs at read time and nothing is stored against a transaction,
so adding an entry fixes every ledger at once, retroactively, with no
migration and no backfill. A supplier added today starts answering for
purchases made last year.

Eighteen entries, ordered by how many companies actually pay them,
measured across production ledgers and cross-checked against a customer
poll. The two agree closely: Google and OpenAI lead both by a wide
margin. Measured coverage: 769 of 10 182 undocumented purchases, which is
7.6% of rows but 120 of 224 companies, so more than half of all companies
have at least one purchase this can now answer.

Two things it refuses to do. It offers nothing for payments that have no
invoice, reusing the rule that already keeps the hunt from searching mail
for salary and tax: a link there implies somewhere to go. And longer
aliases win, so a Google Cloud bill is not sent to the Workspace console.

Vendors whose billing page could not be pinned down were left out rather
than guessed at, including some with real volume: Fortnox, Apple, Klarna
and Mynt. A wrong URL is worse than a missing one, because it spends the
trust the feature runs on. Every entry here still deserves a human
clicking it before anyone relies on it.

No interface yet: this is the data and the lookup, so the workspace can
use it the day it lands.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 10:39:06 +02:00

78 lines
3.0 KiB
TypeScript

/**
* The directory turns a bank descriptor into a page where the invoice lives.
* What must not happen: sending someone to a portal for a salary run, guessing
* a vendor from a fragment, or pointing a cloud bill at the wrong Google page.
*/
import { describe, it, expect } from 'vitest'
import { PORTAL_DIRECTORY, lookupPortal } from '../portal-directory'
describe('lookupPortal', () => {
it('reads a card descriptor the bank has mangled', () => {
// Real strings from a production ledger.
expect(lookupPortal('ANTHROPIC* CLAUDE SUB SAN FRANCISCO Kortköp/uttag')?.vendor).toBe('Anthropic')
expect(lookupPortal('OPENAI CHATGPT SUBSCR')?.vendor).toBe('OpenAI')
expect(lookupPortal('Kortköp 260228 HETZNER ONLINE GMBH')?.vendor).toBe('Hetzner')
})
it('prefers the longer alias, so a cloud bill is not sent to Workspace', () => {
expect(lookupPortal('GOOGLE CLOUD 6ZZS77')?.vendor).toBe('Google Cloud')
expect(lookupPortal('GOOGLE WORKSPACE REDOV')?.vendor).toBe('Google Workspace')
})
it('never offers a portal for a payment that has no invoice', () => {
// A salary run has nothing to fetch, and a link would be worse than
// silence: it implies somewhere to go.
expect(lookupPortal('Lön Juli Jakob Överföring via internet')).toBeNull()
expect(lookupPortal('Inbetalning skat BG 0000050501055')).toBeNull()
expect(lookupPortal('Skatt lön Juni BG 0000050501055')).toBeNull()
})
it('says nothing about a supplier it does not know', () => {
expect(lookupPortal('ALVIKS KOETT OCH FISK K3667')).toBeNull()
expect(lookupPortal('RESTAURANG RIDD K3667 Kortköp/uttag')).toBeNull()
})
it('handles an empty or missing descriptor', () => {
expect(lookupPortal(null)).toBeNull()
expect(lookupPortal('')).toBeNull()
expect(lookupPortal(' ')).toBeNull()
})
})
describe('the directory itself', () => {
it('sends everyone to a real https page', () => {
// A wrong URL is worse than a missing one: it spends the trust the
// feature runs on.
for (const entry of PORTAL_DIRECTORY) {
expect(() => new URL(entry.url)).not.toThrow()
expect(entry.url.startsWith('https://')).toBe(true)
}
})
it('gives every entry something to match on', () => {
for (const entry of PORTAL_DIRECTORY) {
expect(entry.aliases.length).toBeGreaterThan(0)
for (const alias of entry.aliases) {
expect(alias).toBe(alias.toLowerCase())
// Two characters would match half the mailbox.
expect(alias.length).toBeGreaterThanOrEqual(3)
}
}
})
it('does not name two vendors the same thing', () => {
const names = PORTAL_DIRECTORY.map((e) => e.vendor)
expect(new Set(names).size).toBe(names.length)
})
it('keeps aliases distinct, so a lookup cannot be ambiguous', () => {
const seen = new Map<string, string>()
for (const entry of PORTAL_DIRECTORY) {
for (const alias of entry.aliases) {
expect(seen.has(alias)).toBe(false)
seen.set(alias, entry.vendor)
}
}
})
})