@@ -0,0 +1,109 @@
|
||||
// Synthetic injected port; canonical caller/tenancy checks, not live gateway or persistence.
|
||||
import {
|
||||
type Actor,
|
||||
buildRegistry,
|
||||
Container,
|
||||
createCaller,
|
||||
type ServiceReadiness,
|
||||
type TransportCaller,
|
||||
} from "@conusai-kit/core"
|
||||
import { createMemoryDataSource, DATA_SOURCE, entitiesOf, SYSTEM_DATA } from "@conusai-kit/data"
|
||||
import { createTenancyHooks, TENANCY, tenancyModule, users } from "@conusai-kit/tenancy"
|
||||
import { simulatedBusinessAdmission } from "../../../test/helpers/business-admission.js"
|
||||
import { LEDGER_SERVICE, type LedgerService } from "../core/ports.js"
|
||||
import { ledgerModule } from "../module.js"
|
||||
|
||||
/** The platform project of the fake deployment. */
|
||||
export const BINDING_ID = "01900000-0000-7000-8000-000000000001"
|
||||
|
||||
export interface Tenant {
|
||||
readonly owner: Actor
|
||||
readonly call: TransportCaller
|
||||
readonly orgSlug: string
|
||||
readonly orgId: string
|
||||
readonly workspaceSlug: string
|
||||
readonly workspaceId: string
|
||||
readonly projectSlug: string
|
||||
readonly projectId: string
|
||||
readonly slugs: { org: string; workspace: string; project: string }
|
||||
}
|
||||
|
||||
export function ledgerApp(service: LedgerService, enabled = true, ready: boolean | ServiceReadiness = true) {
|
||||
const registry = buildRegistry([tenancyModule, ...(enabled ? [ledgerModule] : [])])
|
||||
const entities = entitiesOf(registry, "postgres")
|
||||
const data = createMemoryDataSource({ entities })
|
||||
const root = new Container()
|
||||
root.value(DATA_SOURCE, data)
|
||||
root.value(SYSTEM_DATA, data)
|
||||
root.value(LEDGER_SERVICE, service)
|
||||
|
||||
const caller = (actor: Actor): TransportCaller => {
|
||||
const tenancy = createTenancyHooks({ data, registry })
|
||||
const container = root.child()
|
||||
container.value(TENANCY, tenancy)
|
||||
return createCaller(
|
||||
{
|
||||
registry,
|
||||
container,
|
||||
tenancy,
|
||||
...(ready ? { serviceReadiness: ready === true ? simulatedBusinessAdmission : ready } : {}),
|
||||
},
|
||||
{ actor },
|
||||
)
|
||||
}
|
||||
|
||||
const signUp = async (email: string): Promise<Actor> => {
|
||||
const user = await data.global(users).insert({
|
||||
issuer: "https://id.example.test",
|
||||
subject: email,
|
||||
email,
|
||||
displayName: email.split("@")[0] ?? email,
|
||||
locale: "en",
|
||||
})
|
||||
return { userId: user.id }
|
||||
}
|
||||
|
||||
/** An organization with one workspace and one project, owned by a new user. */
|
||||
const tenant = async (slug: string): Promise<Tenant> => {
|
||||
const owner = await signUp(`owner@${slug}.test`)
|
||||
const call = caller(owner)
|
||||
const org = (await call.callByName("tenancy.org.create", { slug, name: slug })) as { orgId: string; slug: string }
|
||||
const workspace = (await call.callByName("tenancy.workspace.create", {
|
||||
org: slug,
|
||||
slug: `ws-${slug}`,
|
||||
name: `Workspace ${slug}`,
|
||||
visibility: "org",
|
||||
})) as { workspaceId: string; slug: string }
|
||||
const project = (await call.callByName("tenancy.project.create", {
|
||||
org: slug,
|
||||
workspace: workspace.slug,
|
||||
slug: `p-${slug}`,
|
||||
name: `Project ${slug}`,
|
||||
moduleKey: "ledger",
|
||||
})) as { projectId: string; slug: string }
|
||||
return {
|
||||
owner,
|
||||
call,
|
||||
orgSlug: org.slug,
|
||||
orgId: org.orgId,
|
||||
workspaceSlug: workspace.slug,
|
||||
workspaceId: workspace.workspaceId,
|
||||
projectSlug: project.slug,
|
||||
projectId: project.projectId,
|
||||
slugs: { org: org.slug, workspace: workspace.slug, project: project.slug },
|
||||
}
|
||||
}
|
||||
|
||||
/** A second user who joins `t`'s organization with the org role `role`. */
|
||||
const member = async (t: Tenant, email: string, role: "viewer" | "member"): Promise<TransportCaller> => {
|
||||
const invite = (await t.call.callByName("tenancy.member.invite", { org: t.orgSlug, email, role })) as {
|
||||
token: string
|
||||
}
|
||||
const actor = await signUp(email)
|
||||
const call = caller(actor)
|
||||
await call.callByName("tenancy.invitation.accept", { org: t.orgSlug, token: invite.token })
|
||||
return call
|
||||
}
|
||||
|
||||
return { registry, data, caller, signUp, tenant, member }
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { positiveMinor, type RecordTerms, recordTerms, revision } from "../contract/index.js"
|
||||
import { recordBalanced, transition, validateBalanced } from "../core/index.js"
|
||||
import type { LedgerService } from "../core/ports.js"
|
||||
import { retryIdentity } from "../ui/retry.js"
|
||||
|
||||
export const A = "acc_00000000-0000-4000-8000-000000000001"
|
||||
export const B = "acc_00000000-0000-4000-8000-000000000002"
|
||||
export const T = "txn_00000000-0000-4000-8000-000000000001"
|
||||
export const terms: RecordTerms = {
|
||||
effectiveAt: "2026-11-03T00:00:00Z",
|
||||
description: "Synthetic transfer",
|
||||
status: "pending",
|
||||
entries: [
|
||||
{ accountId: A, direction: "debit", amountMinor: "9007199254740993" },
|
||||
{ accountId: B, direction: "credit", amountMinor: "9007199254740993" },
|
||||
],
|
||||
}
|
||||
export const service = () =>
|
||||
({
|
||||
verify: vi.fn(async () => ({ checkedAccounts: 2, drift: [] as string[] })),
|
||||
account: vi.fn(async (id: string) => ({ accountId: id, currency: "EUR", status: "active" })),
|
||||
record: vi.fn(async () => ({ transactionId: T })),
|
||||
transition: vi.fn(async () => ({ transactionId: T })),
|
||||
}) as unknown as LedgerService
|
||||
|
||||
describe("exact ledger core and retry policy (port tier)", () => {
|
||||
it("accepts i64 maximum and refuses overflow, zero, floats and unsafe numeric inputs", () => {
|
||||
for (const schema of [positiveMinor, revision]) {
|
||||
expect(schema.parse("9223372036854775807")).toBe("9223372036854775807")
|
||||
for (const invalid of ["9223372036854775808", "0", "01", "1.5", 9007199254740992])
|
||||
expect(schema.safeParse(invalid).success).toBe(false)
|
||||
}
|
||||
expect(recordTerms.parse(terms).entries[0]?.amountMinor).toBe("9007199254740993")
|
||||
})
|
||||
it("balances exact BigInt totals by currency and detects one-unit and cross-currency negatives", async () => {
|
||||
const s = service()
|
||||
await validateBalanced(s, terms)
|
||||
const [debit, credit] = terms.entries
|
||||
if (!debit || !credit) throw new Error("missing synthetic entries")
|
||||
await expect(
|
||||
validateBalanced(s, {
|
||||
...terms,
|
||||
entries: [debit, { ...credit, amountMinor: "9007199254740992" }],
|
||||
}),
|
||||
).rejects.toThrow()
|
||||
vi.mocked(s.account).mockImplementation(
|
||||
async (id) => ({ accountId: id, currency: id === A ? "EUR" : "USD", status: "active" }) as never,
|
||||
)
|
||||
await expect(validateBalanced(s, terms)).rejects.toThrow()
|
||||
})
|
||||
it("does not pre-read mutable account or transaction state before service replay", async () => {
|
||||
const s = service()
|
||||
vi.mocked(s.account).mockRejectedValue(new Error("archived after first commit"))
|
||||
await recordBalanced(s, terms, "retry")
|
||||
await recordBalanced(s, terms, "retry")
|
||||
expect(s.account).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(s.record).mock.calls.map((call) => call[1])).toEqual([
|
||||
"app-ledger:record:retry",
|
||||
"app-ledger:record:retry",
|
||||
])
|
||||
await transition(s, "post", T, "9007199254740993", "retry")
|
||||
expect(s.transition).toHaveBeenCalledWith("post", T, "9007199254740993", "app-ledger:post:retry")
|
||||
})
|
||||
it("fences new holds, posts and reversals on drift, fails closed on verifier outage, permits void recovery", async () => {
|
||||
const s = service()
|
||||
vi.mocked(s.verify).mockResolvedValue({ checkedAccounts: 2, drift: [A] })
|
||||
await expect(recordBalanced(s, terms, "r")).rejects.toThrow()
|
||||
for (const action of ["post", "reverse"] as const)
|
||||
await expect(transition(s, action, T, "1", "r")).rejects.toThrow()
|
||||
expect(s.record).not.toHaveBeenCalled()
|
||||
expect(s.transition).not.toHaveBeenCalled()
|
||||
await transition(s, "void", T, "1", "r")
|
||||
vi.mocked(s.verify).mockRejectedValue(new Error("offline"))
|
||||
await expect(recordBalanced(s, terms, "r")).rejects.toThrow("offline")
|
||||
})
|
||||
it("preserves service state/funds/overflow refusals without replacing them with local storage", async () => {
|
||||
const s = service()
|
||||
for (const reason of ["transaction-state", "insufficient-funds", "overflow", "revision-conflict"]) {
|
||||
const error = new Error(reason)
|
||||
vi.mocked(s.transition).mockRejectedValueOnce(error)
|
||||
await expect(transition(s, "post", T, "1", "r")).rejects.toBe(error)
|
||||
}
|
||||
})
|
||||
it("reuses identical payload keys, rotates for changed input and for returning to an earlier payload", () => {
|
||||
const key = retryIdentity()
|
||||
const first = key(terms)
|
||||
expect(key(structuredClone(terms))).toBe(first)
|
||||
expect(key({ ...terms, description: "Changed operational label" })).not.toBe(first)
|
||||
expect(key(terms)).not.toBe(first)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { IsolationFixture } from "@conusai-kit/testing"
|
||||
import { businessModulesOn } from "../../index.js"
|
||||
|
||||
const id = "00000000-0000-4000-8000-000000000001"
|
||||
const acc = `acc_${id}`
|
||||
const txn = `txn_${id}`
|
||||
const fixtures: Record<string, IsolationFixture> = {}
|
||||
for (const name of ["accounts.list", "balances.list", "transactions.list", "verify"])
|
||||
fixtures[`ledger.${name}`] = { input: (t) => ({ org: t.orgSlug }) }
|
||||
for (const name of ["accounts.get", "entries.list"])
|
||||
fixtures[`ledger.${name}`] = { input: (t) => ({ org: t.orgSlug, accountId: acc }) }
|
||||
fixtures["ledger.transactions.get"] = { input: (t) => ({ org: t.orgSlug, transactionId: txn }) }
|
||||
fixtures["ledger.accounts.create"] = {
|
||||
input: (t) => ({
|
||||
org: t.orgSlug,
|
||||
requestId: id,
|
||||
code: "SYNTHETIC",
|
||||
name: "Operational",
|
||||
currency: "EUR",
|
||||
type: "asset",
|
||||
normalBalance: "debit",
|
||||
allowNegative: false,
|
||||
}),
|
||||
}
|
||||
fixtures["ledger.accounts.update"] = {
|
||||
input: (t) => ({
|
||||
org: t.orgSlug,
|
||||
accountId: acc,
|
||||
requestId: id,
|
||||
expectedVersion: "1",
|
||||
name: "Operational",
|
||||
status: "archived",
|
||||
allowNegative: false,
|
||||
}),
|
||||
}
|
||||
fixtures["ledger.transactions.record"] = {
|
||||
input: (t) => ({
|
||||
org: t.orgSlug,
|
||||
requestId: id,
|
||||
confirmed: true,
|
||||
terms: {
|
||||
effectiveAt: "2026-11-03T00:00:00Z",
|
||||
description: "Synthetic",
|
||||
status: "pending",
|
||||
entries: [
|
||||
{ accountId: acc, direction: "debit", amountMinor: "1" },
|
||||
{ accountId: acc, direction: "credit", amountMinor: "1" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
}
|
||||
for (const name of ["post", "void", "reverse"])
|
||||
fixtures[`ledger.transactions.${name}`] = {
|
||||
input: (t) => ({ org: t.orgSlug, transactionId: txn, expectedVersion: "1", requestId: id, confirmed: true }),
|
||||
}
|
||||
export const ledgerIsolationFixtures = fixtures
|
||||
export const isolation = { seed: async () => ({}), fixtures: businessModulesOn.includes("ledger") ? fixtures : {} }
|
||||
@@ -0,0 +1,18 @@
|
||||
import { buildRegistry } from "@conusai-kit/core"
|
||||
import { tenancyModule } from "@conusai-kit/tenancy"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { ledgerModule } from "../index.js"
|
||||
|
||||
const registry = buildRegistry([tenancyModule, ledgerModule])
|
||||
|
||||
describe("the ledger module", () => {
|
||||
it("registers beside the kernel", () => {
|
||||
expect(registry.modules.map((module) => module.id)).toContain("ledger")
|
||||
})
|
||||
|
||||
it("has the same messages in English and in Lithuanian", () => {
|
||||
const { en, lt } = registry.catalogues
|
||||
expect(Object.keys(lt?.ledger ?? {}).sort()).toEqual(Object.keys(en?.ledger ?? {}).sort())
|
||||
expect(Object.keys(en?.ledger ?? {}).length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,151 @@
|
||||
import type { IsolationTenant } from "@conusai-kit/testing"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { simulatedBusinessAdmission } from "../../../test/helpers/business-admission.js"
|
||||
import type { LedgerService } from "../core/ports.js"
|
||||
import { ledgerApp } from "./app.js"
|
||||
import { ledgerIsolationFixtures } from "./isolation.js"
|
||||
|
||||
const fixtureInput = (name: string, tenant: IsolationTenant) => {
|
||||
const fixture = ledgerIsolationFixtures[name]
|
||||
if (!fixture) throw new Error("missing isolation fixture")
|
||||
return fixture.input(tenant)
|
||||
}
|
||||
const resultAccount = {
|
||||
accountId: "acc_00000000-0000-4000-8000-000000000001",
|
||||
code: "SYNTHETIC",
|
||||
name: "Operational",
|
||||
currency: "EUR",
|
||||
type: "asset",
|
||||
normalBalance: "debit",
|
||||
allowNegative: false,
|
||||
status: "active",
|
||||
revision: "1",
|
||||
} as const
|
||||
const resultTransaction = {
|
||||
transactionId: "txn_00000000-0000-4000-8000-000000000001",
|
||||
revision: "1",
|
||||
effectiveAt: "2026-11-03T00:00:00Z",
|
||||
description: "Synthetic",
|
||||
reference: null,
|
||||
reverses: null,
|
||||
status: "pending",
|
||||
entries: [],
|
||||
} as const
|
||||
const fake = () =>
|
||||
({
|
||||
accounts: vi.fn(async () => ({ rows: [] })),
|
||||
verify: vi.fn(async () => ({ checkedAccounts: 0, drift: [] })),
|
||||
createAccount: vi.fn(async () => resultAccount),
|
||||
updateAccount: vi.fn(async () => resultAccount),
|
||||
record: vi.fn(async () => ({ ...resultTransaction, entries: [] })),
|
||||
transition: vi.fn(async () => ({ ...resultTransaction, entries: [] })),
|
||||
}) as unknown as LedgerService
|
||||
|
||||
describe("Ledger actual caller with synthetic admission/port", () => {
|
||||
it("refuses all 13 procedures with wrong-tenant slugs before service access", async () => {
|
||||
const s = fake()
|
||||
const app = ledgerApp(s)
|
||||
const a = await app.tenant("alpha")
|
||||
const b = await app.tenant("beta")
|
||||
const tenant = { orgSlug: b.orgSlug } as IsolationTenant
|
||||
expect(Object.keys(ledgerIsolationFixtures)).toHaveLength(13)
|
||||
for (const [name, fixture] of Object.entries(ledgerIsolationFixtures)) {
|
||||
await expect(a.call.callByName(name, fixture.input(tenant))).rejects.toHaveProperty("problem.status", 404)
|
||||
}
|
||||
expect(s.accounts).not.toHaveBeenCalled()
|
||||
})
|
||||
it("allows viewer reads, refuses viewer writes and separates member writer from account/reversal admin", async () => {
|
||||
const app = ledgerApp(fake())
|
||||
const t = await app.tenant("alpha")
|
||||
const viewer = await app.member(t, "viewer@alpha.test", "viewer")
|
||||
const member = await app.member(t, "writer@alpha.test", "member")
|
||||
expect(await viewer.callByName("ledger.accounts.list", { org: t.orgSlug })).toEqual({ rows: [] })
|
||||
const tenant = { orgSlug: t.orgSlug } as IsolationTenant
|
||||
for (const name of [
|
||||
"ledger.accounts.create",
|
||||
"ledger.accounts.update",
|
||||
"ledger.transactions.record",
|
||||
"ledger.transactions.post",
|
||||
"ledger.transactions.void",
|
||||
"ledger.transactions.reverse",
|
||||
]) {
|
||||
await expect(viewer.callByName(name, fixtureInput(name, tenant))).rejects.toHaveProperty("problem.status", 404)
|
||||
}
|
||||
for (const name of ["ledger.accounts.create", "ledger.accounts.update", "ledger.transactions.reverse"])
|
||||
await expect(member.callByName(name, fixtureInput(name, tenant))).rejects.toHaveProperty("problem.status", 404)
|
||||
})
|
||||
it("refuses unregistered default-off module and missing readiness/binding", async () => {
|
||||
for (const [enabled, ready] of [
|
||||
[false, true],
|
||||
[true, false],
|
||||
]) {
|
||||
const s = fake()
|
||||
const app = ledgerApp(s, enabled, ready)
|
||||
const t = await app.tenant("alpha")
|
||||
await expect(t.call.callByName("ledger.accounts.list", { org: t.orgSlug })).rejects.toThrow()
|
||||
expect(s.accounts).not.toHaveBeenCalled()
|
||||
}
|
||||
})
|
||||
it("refuses missing approved service scopes and Convex before any adapter call", async () => {
|
||||
for (const readiness of [
|
||||
{
|
||||
...simulatedBusinessAdmission,
|
||||
observe: async () => ({
|
||||
...(await simulatedBusinessAdmission.observe({} as never, {} as never)),
|
||||
authorized: false,
|
||||
}),
|
||||
},
|
||||
{ ...simulatedBusinessAdmission, profile: "convex" as const },
|
||||
]) {
|
||||
const s = fake()
|
||||
const app = ledgerApp(s, true, readiness)
|
||||
const t = await app.tenant("alpha")
|
||||
await expect(t.call.callByName("ledger.accounts.list", { org: t.orgSlug })).rejects.toHaveProperty(
|
||||
"problem.code",
|
||||
readiness.profile === "convex" ? "service_profile_unsupported" : "service_permission_missing",
|
||||
)
|
||||
expect(s.accounts).not.toHaveBeenCalled()
|
||||
}
|
||||
})
|
||||
it("allows member financial writes and owner account/reversal administration through the real caller", async () => {
|
||||
const service = fake()
|
||||
const app = ledgerApp(service)
|
||||
const t = await app.tenant("alpha")
|
||||
const member = await app.member(t, "writer@alpha.test", "member")
|
||||
const tenant = { orgSlug: t.orgSlug } as IsolationTenant
|
||||
for (const name of ["ledger.transactions.record", "ledger.transactions.post", "ledger.transactions.void"]) {
|
||||
expect(await member.callByName(name, fixtureInput(name, tenant))).toHaveProperty(
|
||||
"transactionId",
|
||||
resultTransaction.transactionId,
|
||||
)
|
||||
}
|
||||
for (const name of ["ledger.accounts.create", "ledger.accounts.update"]) {
|
||||
expect(await t.call.callByName(name, fixtureInput(name, tenant))).toHaveProperty(
|
||||
"accountId",
|
||||
resultAccount.accountId,
|
||||
)
|
||||
}
|
||||
expect(
|
||||
await t.call.callByName("ledger.transactions.reverse", fixtureInput("ledger.transactions.reverse", tenant)),
|
||||
).toHaveProperty("transactionId", resultTransaction.transactionId)
|
||||
})
|
||||
it("requires explicit financial confirmation and refuses extra body-selected authority", async () => {
|
||||
const service = fake()
|
||||
const app = ledgerApp(service)
|
||||
const t = await app.tenant("alpha")
|
||||
const input = fixtureInput("ledger.transactions.record", { orgSlug: t.orgSlug } as IsolationTenant) as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
for (const invalid of [
|
||||
{ ...input, confirmed: false },
|
||||
{ ...input, appTenantId: "forged", platformOrg: "beta" },
|
||||
]) {
|
||||
await expect(t.call.callByName("ledger.transactions.record", invalid)).rejects.toHaveProperty(
|
||||
"problem.status",
|
||||
422,
|
||||
)
|
||||
}
|
||||
expect(service.record).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
// A refusal of the Ledger service reaches the person as the kit's sentence for it, in their language.
|
||||
// Before the kit helper, the module mapped refusals itself and compared a `BusinessErrorCode` with Ledger's
|
||||
// reasons (`unbalanced`, `insufficient-funds`), which it never is: an unbalanced posting was answered
|
||||
// "reload the ledger record". Generated HTTP client with a synthetic binding; no gateway/live acceptance.
|
||||
import type { RequestBusinessBinding } from "@conusai-kit/business"
|
||||
import { describeBusinessRefusal } from "@conusai-kit/business/refusals"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { sdkLedgerService } from "../adapters/service.js"
|
||||
import { LEDGER_ERRORS } from "../contract/index.js"
|
||||
import type { LedgerService } from "../core/ports.js"
|
||||
import { en } from "../i18n/en.js"
|
||||
import { lt } from "../i18n/lt.js"
|
||||
import { ledgerFailure } from "../ui/errors.js"
|
||||
import { ledgerLabels } from "../ui/labels.js"
|
||||
|
||||
const id = "acc_00000000-0000-4000-8000-000000000001"
|
||||
function refusing(reason: string, status: number) {
|
||||
const binding = {
|
||||
orgSlug: "verified-org",
|
||||
client: (factory: Parameters<RequestBusinessBinding["client"]>[0]) => {
|
||||
const client = factory({ baseUrl: "https://ledger.example.test", auth: "synthetic", throwOnError: false })
|
||||
client.setConfig({
|
||||
fetch: async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
type: `urn:conusai:ledger:${reason}`,
|
||||
status,
|
||||
code: `LEDGER_${reason.replaceAll("-", "_").toUpperCase()}`,
|
||||
title: "Ledger request refused",
|
||||
}),
|
||||
{ status, headers: { "content-type": "application/json" } },
|
||||
),
|
||||
})
|
||||
return client
|
||||
},
|
||||
} as RequestBusinessBinding
|
||||
return sdkLedgerService(binding)
|
||||
}
|
||||
const terms: Parameters<LedgerService["record"]>[0] = {
|
||||
effectiveAt: "2026-11-03T00:00:00Z",
|
||||
description: "Synthetic",
|
||||
status: "posted",
|
||||
entries: [
|
||||
{ accountId: id, direction: "debit", amountMinor: "100" },
|
||||
{ accountId: id, direction: "credit", amountMinor: "99" },
|
||||
],
|
||||
}
|
||||
|
||||
async function refusalOf(reason: string, status: number): Promise<unknown> {
|
||||
try {
|
||||
await refusing(reason, status).record(terms, "stable-key")
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
throw new Error("expected a refusal")
|
||||
}
|
||||
|
||||
describe("Ledger refusals through the kit", () => {
|
||||
it("tells the person an unbalanced posting does not balance, in English and Lithuanian", async () => {
|
||||
const error = await refusalOf("unbalanced", 409)
|
||||
expect(error).toHaveProperty("problem.code", "business_unbalanced")
|
||||
expect(error).toHaveProperty("problem.status", 409)
|
||||
expect(describeBusinessRefusal(error, "en")).toBe("Debits and credits do not balance.")
|
||||
expect(describeBusinessRefusal(error, "lt")).toBe("Debetas ir kreditas nesutampa.")
|
||||
})
|
||||
|
||||
it("names insufficient funds and a transaction's state as such", async () => {
|
||||
expect(await refusalOf("insufficient-funds", 409)).toHaveProperty("problem.code", "business_insufficient_funds")
|
||||
expect(await refusalOf("transaction-state", 409)).toHaveProperty("problem.code", "business_transaction_state")
|
||||
})
|
||||
|
||||
it("shows a form the kit's sentence, a conflict in this module's words, and the module's own drift fence", async () => {
|
||||
const labelsOf = (messages: Record<string, string>, locale: "en" | "lt") =>
|
||||
ledgerLabels((key) => messages[key] ?? key, locale)
|
||||
const english = labelsOf(en, "en")
|
||||
const lithuanian = labelsOf(lt, "lt")
|
||||
expect(ledgerFailure(await refusalOf("unbalanced", 409), english)).toBe("Debits and credits do not balance.")
|
||||
expect(ledgerFailure(await refusalOf("unbalanced", 409), lithuanian)).toBe("Debetas ir kreditas nesutampa.")
|
||||
expect(ledgerFailure(await refusalOf("revision-conflict", 409), english)).toBe(en.revisionConflict)
|
||||
expect(ledgerFailure(await refusalOf("idempotency-conflict", 409), lithuanian)).toBe(lt.idempotencyConflict)
|
||||
expect(ledgerFailure({ problem: { code: LEDGER_ERRORS.drift } }, english)).toBe(en.drift)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,106 @@
|
||||
// Generated HTTP client with a synthetic RequestBusinessBinding. No gateway/live acceptance.
|
||||
import type { RequestBusinessBinding } from "@conusai-kit/business"
|
||||
import { Container } from "@conusai-kit/core"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { ledgerServiceOf, sdkLedgerService } from "../adapters/service.js"
|
||||
|
||||
const id = "acc_00000000-0000-4000-8000-000000000001"
|
||||
const txn = "txn_00000000-0000-4000-8000-000000000001"
|
||||
function adapter(response: (request: Request) => Promise<Response>) {
|
||||
const calls: Request[] = []
|
||||
const binding = {
|
||||
orgSlug: "verified-org",
|
||||
client: (factory: Parameters<RequestBusinessBinding["client"]>[0]) => {
|
||||
const client = factory({ baseUrl: "https://ledger.example.test", auth: "synthetic", throwOnError: false })
|
||||
client.setConfig({
|
||||
fetch: async (input) => {
|
||||
const request = new Request(input)
|
||||
calls.push(request.clone())
|
||||
return response(request)
|
||||
},
|
||||
})
|
||||
return client
|
||||
},
|
||||
} as RequestBusinessBinding
|
||||
return { service: sdkLedgerService(binding), calls }
|
||||
}
|
||||
const json = (text: string, status = 200) =>
|
||||
new Response(text, { status, headers: { "content-type": "application/json" } })
|
||||
describe("generated Ledger HTTP adapter (synthetic fetch tier)", () => {
|
||||
it("retains raw JSON integers above 2^53 in balances and revisions", async () => {
|
||||
const a = adapter(async (request) =>
|
||||
request.url.includes("balances")
|
||||
? json(
|
||||
`{"data":[{"account_id":"${id}","currency":"EUR","posted":9007199254740993,"pending":-9007199254740993,"available":9223372036854775807}]}`,
|
||||
)
|
||||
: json(
|
||||
`{"ext":{},"id":"${id}","code":"TEST","name":"Synthetic","currency":"EUR","type":"asset","normal_balance":"debit","allow_negative":false,"status":"active","revision":9007199254740993}`,
|
||||
),
|
||||
)
|
||||
expect((await a.service.balances()).rows[0]).toMatchObject({
|
||||
posted: "9007199254740993",
|
||||
pending: "-9007199254740993",
|
||||
available: "9223372036854775807",
|
||||
})
|
||||
expect((await a.service.account(id)).revision).toBe("9007199254740993")
|
||||
expect(new URL(a.calls[0]?.url ?? "").pathname).toContain("/orgs/verified-org/ledger/balances")
|
||||
})
|
||||
it("sends exact unquoted i64 amount and revision tokens through generated SDK serialization", async () => {
|
||||
const bodies: string[] = []
|
||||
const a = adapter(async (request) => {
|
||||
bodies.push(await request.text())
|
||||
return json(
|
||||
`{"ext":{},"id":"${txn}","revision":9007199254740993,"effective_at":"2026-11-03T00:00:00Z","description":"Synthetic","status":"pending","entries":[]}`,
|
||||
)
|
||||
})
|
||||
await a.service.record(
|
||||
{
|
||||
effectiveAt: "2026-11-03T00:00:00Z",
|
||||
description: "Synthetic",
|
||||
status: "pending",
|
||||
entries: [
|
||||
{ accountId: id, direction: "debit", amountMinor: "9007199254740993" },
|
||||
{ accountId: id, direction: "credit", amountMinor: "9007199254740993" },
|
||||
],
|
||||
},
|
||||
"stable-key",
|
||||
)
|
||||
await a.service.transition("post", txn, "9007199254740993", "stable-post")
|
||||
expect(bodies[0]).toContain('"amount_minor":9007199254740993')
|
||||
expect(bodies[0]).toContain('"idempotency_key":"stable-key"')
|
||||
expect(bodies[1]).toContain('"revision":9007199254740993')
|
||||
})
|
||||
it("redacts domain conflicts, invalid inputs and upstream failures", async () => {
|
||||
for (const [status, reason, code] of [
|
||||
[409, "revision-conflict", "business_revision_conflict"],
|
||||
[409, "idempotency-conflict", "business_idempotency_conflict"],
|
||||
[422, "unbalanced", "business_unbalanced"],
|
||||
[422, "invalid-request", "business_invalid"],
|
||||
[503, "unavailable", "business_unavailable"],
|
||||
] as const) {
|
||||
const a = adapter(async () =>
|
||||
json(
|
||||
JSON.stringify({
|
||||
type: `urn:conusai:ledger:${reason}`,
|
||||
status,
|
||||
code: `LEDGER_${reason.replaceAll("-", "_").toUpperCase()}`,
|
||||
title: "SENSITIVE",
|
||||
detail: "SECRET_PERSON_TOKEN",
|
||||
}),
|
||||
status,
|
||||
),
|
||||
)
|
||||
try {
|
||||
await a.service.account(id)
|
||||
throw new Error("expected refusal")
|
||||
} catch (error) {
|
||||
expect(JSON.stringify(error)).not.toMatch(/SENSITIVE|SECRET_PERSON_TOKEN/)
|
||||
expect(error).toHaveProperty("problem.status", status)
|
||||
expect(error).toHaveProperty("problem.code", code)
|
||||
}
|
||||
}
|
||||
})
|
||||
it("refuses missing or unverified request binding without deployment credential fallback", () => {
|
||||
expect(() => ledgerServiceOf(new Container(), "effect")).toThrow()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user