Files
admin b3239487b7
CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (push) Successful in 10s
CI (SIAX Cloud) / sonar (pull_request) Skipped
Security Scan (SIAX Cloud) / security (push) Successful in 13s
masterplan-lock / check (pull_request) Successful in 7s
CI (SIAX Cloud) / security (push) Successful in 26s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 17s
CI (SIAX Cloud) / security (pull_request) Successful in 19s
CI (SIAX Cloud) / quality (push) Successful in 1m9s
CI (SIAX Cloud) / quality (pull_request) Successful in 2m26s
CI (SIAX Cloud) / integration (pull_request) Successful in 2m33s
feat(document-workbench): wire customer upload, structuring, credits and readiness
- POST /api/0pt/documents/upload: real multipart bytes into DOC0 through the
  durable attempt journal and the existing coordinator; verified tenant/actor
  only, project authorization fail-closed.
- POST /api/0pt/documents/structure: DOC0 segments -> INF0 -> evidence-bound
  validation -> optional JSON/CSV export, with credit reserve/settle.
- Credits: balance, server-owned price book, Stripe Checkout and idempotent
  webhook reconciliation via a Stripe re-read of session + payment intent.
- /portal/dokument and /portal/krediter: SIAX-styled workspace.
- @siax/access: universal, framework-neutral OpenFGA project relationship
  primitive reusable by DOC0/N0D/other products.
- Readiness: real Redis PING probe instead of presence-only; queue is now a
  live dependency when configured.
- Dockerfile: copy every workspace manifest so --frozen-lockfile works.
- Format/build hygiene: .prettierignore, .worktrees ignored, valid manifest YAML.
2026-11-02 14:46:10 +01:00

50 lines
1.9 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
creditsForUsage,
findPack,
parseCreditPriceBook,
readCreditPriceBook,
} from '../../src/document-workbench/price-book';
const valid = {
version: '2026-11-01',
currency: 'SEK',
packs: [
{ id: 'starter', credits: 100, amountMinor: 9900 },
{ id: 'pro', credits: 2000, amountMinor: 129900 },
],
metering: { pagesPerCredit: 5, inputTokensPerCredit: 20000, outputTokensPerCredit: 4000 },
};
describe('credit price book', () => {
it('parses a valid server-owned price book', () => {
const book = parseCreditPriceBook(valid);
expect(book.packs).toHaveLength(2);
expect(findPack(book, 'pro')?.credits).toBe(2000);
});
it('fails closed when the env price book is absent or invalid', () => {
expect(readCreditPriceBook({} as unknown as NodeJS.ProcessEnv)).toBeUndefined();
expect(readCreditPriceBook({ OPT_CREDIT_PRICE_BOOK: '{not json' } as unknown as NodeJS.ProcessEnv)).toBeUndefined();
expect(
readCreditPriceBook({
OPT_CREDIT_PRICE_BOOK: JSON.stringify({ ...valid, currency: 'sek' }),
} as unknown as NodeJS.ProcessEnv),
).toBeUndefined();
});
it('rejects duplicate pack ids, unknown currencies and non-integer money', () => {
expect(() => parseCreditPriceBook({ ...valid, packs: [valid.packs[0], valid.packs[0]] })).toThrow();
expect(() =>
parseCreditPriceBook({ ...valid, packs: [{ id: 'x', credits: 1, amountMinor: 1.5 }] }),
).toThrow();
});
it('rounds every metered component up to its billing block', () => {
const book = parseCreditPriceBook(valid);
expect(creditsForUsage(book, { pages: 0, inputTokens: 0, outputTokens: 0 })).toBe(0);
expect(creditsForUsage(book, { pages: 1, inputTokens: 1, outputTokens: 1 })).toBe(3);
expect(creditsForUsage(book, { pages: 6, inputTokens: 40001, outputTokens: 8001 })).toBe(8);
});
});