feat: bootstrap commercial AI drama platform
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user