import assert from "node:assert/strict"; import { dbRun } from "../server/db.mjs"; const api = process.env.AI_DRAMA_API_BASE || "http://127.0.0.1:8787"; const studioScope = { "x-organization-id": "org-studio-lab", "x-workspace-id": "ws-local-aidrama", "x-project-id": "thunder-mouth" }; const northstarScope = { "x-organization-id": "org-northstar", "x-workspace-id": "ws-northstar-main", "x-project-id": "northstar-pilot" }; const periodStart = "2025-01-01"; const periodEnd = "2025-01-31"; async function request(path, headers = {}, options = {}) { const response = await fetch(`${api}${path}`, { ...options, headers: { "content-type": "application/json", ...headers, ...(options.headers || {}) } }); const payload = await response.json().catch(() => ({})); return { response, payload }; } async function requestText(path, headers = {}, options = {}) { const response = await fetch(`${api}${path}`, { ...options, headers: { ...headers, ...(options.headers || {}) } }); return { response, body: await response.text() }; } function assertOk(result, label) { assert.equal(result.response.ok, true, `${label}: ${result.response.status} ${result.payload.detail || result.payload.error || ""}`); return result.payload; } async function login(email) { const result = await request("/api/auth/login", {}, { method: "POST", body: JSON.stringify({ email, password: "Demo@123456" }) }); assertOk(result, `${email} 登录`); return { authorization: `Bearer ${result.payload.session.token}` }; } const ownerAuth = await login("producer@local.test"); const writerAuth = await login("writer@local.test"); const northstarAdminAuth = await login("producer2@local.test"); const ownerHeaders = { ...ownerAuth, ...studioScope }; const writerHeaders = { ...writerAuth, ...studioScope }; const northstarHeaders = { ...northstarAdminAuth, ...northstarScope }; let invoiceId = ""; try { const writerList = await request("/api/organizations/org-studio-lab/invoices", writerHeaders); assert.equal(writerList.response.status, 403, "普通成员不能读取账单台账"); const writerGenerate = await request("/api/organizations/org-studio-lab/invoices/generate", writerHeaders, { method: "POST", body: JSON.stringify({ periodStart, periodEnd }) }); assert.equal(writerGenerate.response.status, 403, "普通成员不能生成账单"); const generated = assertOk(await request("/api/organizations/org-studio-lab/invoices/generate", ownerHeaders, { method: "POST", body: JSON.stringify({ periodStart, periodEnd, taxRate: 6, dueDays: 15 }) }), "管理员生成账单"); assert.equal(generated.invoice.status, "draft", "新账单必须从草稿开始"); assert.equal(generated.invoice.taxRate, 6, "税率快照没有保存"); assert.ok(generated.lines.length >= 1, "账单必须包含套餐固定费或用量明细"); invoiceId = generated.invoice.id; const duplicate = assertOk(await request("/api/organizations/org-studio-lab/invoices/generate", ownerHeaders, { method: "POST", body: JSON.stringify({ periodStart, periodEnd, taxRate: 99 }) }), "重复生成账单"); assert.equal(duplicate.idempotent, true, "重复生成必须幂等返回"); assert.equal(duplicate.invoice.id, invoiceId, "重复生成不能创建第二张账单"); assert.equal(duplicate.invoice.taxRate, 6, "幂等返回不能覆盖原账单快照"); const issued = assertOk(await request(`/api/organizations/org-studio-lab/invoices/${encodeURIComponent(invoiceId)}/status`, ownerHeaders, { method: "POST", body: JSON.stringify({ status: "issued" }) }), "账单开票"); assert.equal(issued.invoice.status, "issued", "账单没有进入已开票状态"); assert.ok(issued.invoice.issuedAt, "开票必须记录 issuedAt"); const paid = assertOk(await request(`/api/organizations/org-studio-lab/invoices/${encodeURIComponent(invoiceId)}/status`, ownerHeaders, { method: "POST", body: JSON.stringify({ status: "paid" }) }), "账单标记支付"); assert.equal(paid.invoice.status, "paid", "账单没有进入已支付状态"); assert.ok(paid.invoice.paidAt, "支付必须记录 paidAt"); const invalidTransition = await request(`/api/organizations/org-studio-lab/invoices/${encodeURIComponent(invoiceId)}/status`, ownerHeaders, { method: "POST", body: JSON.stringify({ status: "overdue" }) }); assert.equal(invalidTransition.response.status, 409, "已支付账单不能退回逾期"); assert.equal(invalidTransition.payload.error, "invoice_transition_invalid", "账单状态机错误码不稳定"); const detail = assertOk(await request(`/api/organizations/org-studio-lab/invoices/${encodeURIComponent(invoiceId)}`, ownerHeaders), "管理员读取账单详情"); assert.equal(detail.invoice.status, "paid", "详情状态与状态流转不一致"); assert.ok(Array.isArray(detail.lines), "账单详情必须包含明细"); const writerDetail = await request(`/api/organizations/org-studio-lab/invoices/${encodeURIComponent(invoiceId)}`, writerHeaders); assert.equal(writerDetail.response.status, 403, "普通成员不能读取账单详情"); const jsonExport = assertOk(await request(`/api/organizations/org-studio-lab/invoices/export?format=json&status=paid`, ownerHeaders), "导出账单 JSON"); assert.ok(jsonExport.invoices.some((invoice) => invoice.id === invoiceId), "JSON 导出缺少账单"); const csvExport = await requestText(`/api/organizations/org-studio-lab/invoices/export?format=csv&status=paid`, ownerHeaders); assert.equal(csvExport.response.status, 200, "账单 CSV 导出应成功"); assert.match(csvExport.response.headers.get("content-type") || "", /text\/csv/, "账单 CSV content-type 不正确"); assert.match(csvExport.body, /invoice_number/, "账单 CSV 表头不完整"); assert.match(csvExport.body, new RegExp(invoiceId), "账单 CSV 缺少当前账单"); const crossOrganization = await request(`/api/organizations/org-studio-lab/invoices/${encodeURIComponent(invoiceId)}`, northstarHeaders); assert.equal(crossOrganization.response.status, 403, "跨组织账单详情必须被阻断"); const northstarList = assertOk(await request("/api/organizations/org-northstar/invoices", northstarHeaders), "读取北辰组织账单"); assert.ok(!northstarList.invoices.some((invoice) => invoice.id === invoiceId), "北辰组织不能看到星河账单"); console.log(`billing ledger smoke passed: ${api}`); } finally { if (invoiceId) { dbRun("DELETE FROM billing_account_events WHERE event_type LIKE 'invoice.%' AND next_json LIKE ?", [`%${invoiceId}%`]); dbRun("DELETE FROM audit_logs WHERE target_type = 'organization_invoice' AND target_id = ?", [invoiceId]); dbRun("DELETE FROM organization_invoices WHERE id = ?", [invoiceId]); } }