Files
accounted/lib/providers/bokio/attachments.ts
T
Jakob Wennberg ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00

125 lines
3.6 KiB
TypeScript

import type { BokioClient } from './client';
/**
* Bokio document (upload) resource config + fetchers.
*
* Bokio exposes receipts/underlag via two endpoints:
* - GET /companies/{cid}/uploads : list, carries journalEntryId
* - GET /companies/{cid}/uploads/{id}/download : raw bytes (octet-stream)
*
* The list does NOT include a filename, and the download is served as
* application/octet-stream, so the real file type comes from the list item's
* `contentType`, and a filename has to be synthesised by the caller.
*
* The link between an upload and a gnubok verifikat is recovered from the
* Bokio journal entry's human voucher number (e.g. "V342"): the SIE import
* preserves it on journal_entries.source_voucher_series / source_voucher_number.
* Bokio restarts numbering at V1 every fiscal year, so the number alone is not
* unique: callers must scope the match by fiscal year (the entry's date).
*/
const UPLOADS_PATH = '/uploads';
const JOURNAL_ENTRIES_PATH = '/journal-entries';
/** Bokio's pageSize caps at 100. */
const PAGE_SIZE = 100;
/** A `V342`-style voucher number: one or more letters (series) + digits. */
const VOUCHER_NUMBER_RE = /^([A-Za-z]+)(\d+)$/;
export interface BokioUpload {
id: string;
description: string | null;
contentType: string | null;
journalEntryId: string | null;
}
interface BokioJournalEntry {
id: string;
journalEntryNumber: string | null;
date: string;
}
/** A Bokio voucher reference parsed from its journalEntryNumber. */
export interface BokioVoucherRef {
/** Voucher series letter(s), e.g. "V". */
series: string;
/** Numeric part of the voucher number, e.g. 342. */
number: number;
/** Entry date (YYYY-MM-DD), used to scope the match by fiscal year. */
date: string;
}
async function paginate<T>(
client: BokioClient,
accessToken: string,
companyId: string,
path: string,
): Promise<T[]> {
const all: T[] = [];
let page = 1;
let totalPages = 1;
do {
const result = await client.getPage<T>(accessToken, companyId, path, {
page,
pageSize: PAGE_SIZE,
});
all.push(...result.items);
totalPages = result.totalPages;
page++;
} while (page <= totalPages);
return all;
}
/** Page every upload (receipt) for a company. */
export function fetchBokioUploads(
client: BokioClient,
accessToken: string,
companyId: string,
): Promise<BokioUpload[]> {
return paginate<BokioUpload>(client, accessToken, companyId, UPLOADS_PATH);
}
/**
* Build a GUID → voucher-reference index from Bokio's journal entries.
* An upload only carries the entry's GUID; this resolves it to the human
* voucher number (and date) that gnubok preserved from the SIE import.
* Entries with an unparseable number are skipped.
*/
export async function fetchBokioVoucherIndex(
client: BokioClient,
accessToken: string,
companyId: string,
): Promise<Map<string, BokioVoucherRef>> {
const entries = await paginate<BokioJournalEntry>(
client,
accessToken,
companyId,
JOURNAL_ENTRIES_PATH,
);
const index = new Map<string, BokioVoucherRef>();
for (const entry of entries) {
const match = VOUCHER_NUMBER_RE.exec(entry.journalEntryNumber ?? '');
if (!match) continue;
index.set(entry.id, {
series: match[1],
number: Number(match[2]),
date: entry.date,
});
}
return index;
}
/** Download a single upload's bytes. */
export function downloadBokioUpload(
client: BokioClient,
accessToken: string,
companyId: string,
uploadId: string,
): Promise<{ bytes: ArrayBuffer; contentType: string | null }> {
return client.getBytes(accessToken, companyId, `${UPLOADS_PATH}/${uploadId}/download`);
}