Files
ai-drama-platform/scripts/smoke-billing-ledger.mjs
T

127 lines
6.7 KiB
JavaScript

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]);
}
}