ec27228a8e
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>
125 lines
3.6 KiB
TypeScript
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`);
|
|
}
|