Add customer contract clearance console
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { dbAll, dbRun, withTransaction } from "../server/db.mjs";
|
||||
|
||||
const api = process.env.AI_DRAMA_API_BASE || "http://127.0.0.1:8787";
|
||||
const runId = Date.now();
|
||||
const organizationId = "org-studio-lab";
|
||||
const workspaceId = "ws-local-aidrama";
|
||||
const projectId = "thunder-mouth";
|
||||
const northstarOrganizationId = "org-northstar";
|
||||
const customerCode = `SMOKE-CUST-${runId}`;
|
||||
const contractNumber = `SMOKE-CTR-${runId}`;
|
||||
const expiredContractNumber = `SMOKE-EXP-${runId}`;
|
||||
let customerId = "";
|
||||
let contractId = "";
|
||||
let expiredContractId = "";
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const response = await fetch(`${api}${path}`, {
|
||||
...options,
|
||||
headers: { "content-type": "application/json", ...(options.headers || {}) }
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
return { response, payload };
|
||||
}
|
||||
|
||||
function expectOk(result, label) {
|
||||
assert.equal(result.response.ok, true, `${label}: ${result.response.status} ${result.payload.detail || result.payload.error || ""}`);
|
||||
return result.payload;
|
||||
}
|
||||
|
||||
function expectStatus(result, status, label) {
|
||||
assert.equal(result.response.status, status, `${label}: ${result.response.status} ${result.payload.detail || result.payload.error || ""}`);
|
||||
return result.payload;
|
||||
}
|
||||
|
||||
async function login(email) {
|
||||
const result = expectOk(await request("/api/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password: "Demo@123456" })
|
||||
}), `${email} login`);
|
||||
return { authorization: `Bearer ${result.session.token}` };
|
||||
}
|
||||
|
||||
function futureIso(days) {
|
||||
return new Date(Date.now() + days * 86400000).toISOString();
|
||||
}
|
||||
|
||||
function pastIso(days) {
|
||||
return new Date(Date.now() - days * 86400000).toISOString();
|
||||
}
|
||||
|
||||
function contractBody(number, title, overrides = {}) {
|
||||
return {
|
||||
customerId,
|
||||
contractNumber: number,
|
||||
title,
|
||||
contractType: "production_license",
|
||||
status: "active",
|
||||
approvalStatus: "approved",
|
||||
effectiveAt: pastIso(10),
|
||||
expiresAt: futureIso(365),
|
||||
signedAt: pastIso(9),
|
||||
currency: "CNY",
|
||||
amount: 0,
|
||||
licenseScope: {
|
||||
channels: ["local-file", "private-delivery-portal"],
|
||||
territories: ["CN"],
|
||||
deliverables: ["vertical-video", "delivery-manifest"],
|
||||
platforms: ["private-preview", "internal-review"],
|
||||
exclusive: false,
|
||||
commercialUse: true,
|
||||
language: ["zh-CN"]
|
||||
},
|
||||
rights: {
|
||||
originalStory: true,
|
||||
derivativeProduction: true,
|
||||
aiGeneratedAssetsAllowed: true,
|
||||
voiceCloneAllowed: false,
|
||||
fixedVoiceRequired: true,
|
||||
singleFramePolicyRequired: true
|
||||
},
|
||||
deliveryTerms: { customerPortalAllowed: true, requireClearanceCertificate: true, maxAccessDays: 14 },
|
||||
evidence: { contractRef: `smoke://contract/${number}`, rightsEvidenceRef: `smoke://rights/${number}`, signedCopyPath: `storage/contracts/smoke/${number}.json` },
|
||||
risk: { paidCloudAllowed: false, publicModelAllowed: false, rightsReview: "passed" },
|
||||
approvalNote: "smoke test contract",
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
const ownerAuth = await login("producer@local.test");
|
||||
const writerAuth = await login("writer@local.test");
|
||||
const northstarAuth = await login("producer2@local.test");
|
||||
const headers = { ...ownerAuth, "x-organization-id": organizationId, "x-workspace-id": workspaceId, "x-project-id": projectId };
|
||||
const writerHeaders = { ...writerAuth, "x-organization-id": organizationId, "x-workspace-id": workspaceId, "x-project-id": projectId };
|
||||
const northstarHeaders = { ...northstarAuth, "x-organization-id": northstarOrganizationId, "x-workspace-id": "ws-northstar-main", "x-project-id": "northstar-pilot" };
|
||||
|
||||
try {
|
||||
const seeded = expectOk(await request(`/api/organizations/${organizationId}/contracts`, { headers }), "owner lists seeded contracts");
|
||||
assert.ok(seeded.customers.some((customer) => customer.id === "cust-xinghe-release"), "seeded customer must be visible");
|
||||
assert.ok(seeded.contracts.some((contract) => contract.id === "contract-xh-rain-night-2026"), "seeded contract must be visible");
|
||||
assert.ok(seeded.projects.some((project) => project.id === projectId), "current project must be selectable for binding");
|
||||
|
||||
const seededClearance = expectOk(await request(`/api/organizations/${organizationId}/contracts/clearance?projectId=${encodeURIComponent(projectId)}&channelKind=private-delivery-portal`, { headers }), "seeded contract clearance");
|
||||
assert.equal(seededClearance.clearance.status, "pass", "seeded thunder-mouth contract clearance must pass");
|
||||
assert.ok(seededClearance.clearance.bindings.some((binding) => binding.contract.id === "contract-xh-rain-night-2026"), "clearance must include seeded contract binding");
|
||||
|
||||
expectStatus(await request(`/api/organizations/${organizationId}/contracts`, { headers: writerHeaders }), 403, "writer cannot list contracts");
|
||||
expectStatus(await request(`/api/organizations/${organizationId}/contracts/contract-ns-pilot-2026/bind-project`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ projectId, usageMode: "supplemental-rights" })
|
||||
}), 404, "studio owner cannot bind northstar contract");
|
||||
|
||||
const northstar = expectOk(await request(`/api/organizations/${northstarOrganizationId}/contracts`, { headers: northstarHeaders }), "northstar lists own contracts");
|
||||
assert.ok(northstar.contracts.some((contract) => contract.id === "contract-ns-pilot-2026"), "northstar seeded contract must be visible inside its organization");
|
||||
assert.ok(!northstar.contracts.some((contract) => contract.id === "contract-xh-rain-night-2026"), "northstar must not see studio contracts");
|
||||
|
||||
const customer = expectOk(await request(`/api/organizations/${organizationId}/customers`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
name: `烟测发行客户 ${runId}`,
|
||||
legalName: `烟测发行客户 ${runId} 有限公司`,
|
||||
code: customerCode,
|
||||
customerType: "distributor",
|
||||
status: "active",
|
||||
industry: "AI 短剧发行",
|
||||
region: "CN",
|
||||
billingEmail: "smoke@example.local",
|
||||
tags: ["smoke", "contracts"],
|
||||
notes: "合同工作台 smoke 自动创建"
|
||||
})
|
||||
}), "create customer");
|
||||
customerId = customer.customer.id;
|
||||
assert.equal(customer.customer.code, customerCode, "customer code must be persisted");
|
||||
|
||||
const contact = expectOk(await request(`/api/organizations/${organizationId}/customers/${encodeURIComponent(customerId)}/contacts`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ name: "烟测制片窗口", role: "交付验收", email: "acceptance@example.local", isPrimary: true })
|
||||
}), "create customer contact");
|
||||
assert.equal(contact.customer.contacts[0].isPrimary, true, "primary contact must be persisted");
|
||||
|
||||
const contract = expectOk(await request(`/api/organizations/${organizationId}/contracts`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(contractBody(contractNumber, `烟测商用授权 ${runId}`))
|
||||
}), "create active approved contract");
|
||||
contractId = contract.contract.id;
|
||||
assert.equal(contract.contract.contractNumber, contractNumber, "contract number must be persisted");
|
||||
|
||||
const bound = expectOk(await request(`/api/organizations/${organizationId}/contracts/${encodeURIComponent(contractId)}/bind-project`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ projectId, usageMode: "supplemental-rights", status: "active", notes: "smoke supplemental rights" })
|
||||
}), "bind active contract to project");
|
||||
assert.equal(bound.binding.projectId, projectId, "binding project must be persisted");
|
||||
assert.equal(bound.clearance.status, "pass", "valid supplemental contract must not block clearance");
|
||||
|
||||
const expired = expectOk(await request(`/api/organizations/${organizationId}/contracts`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(contractBody(expiredContractNumber, `烟测过期授权 ${runId}`, { expiresAt: pastIso(1) }))
|
||||
}), "create expired contract");
|
||||
expiredContractId = expired.contract.id;
|
||||
expectOk(await request(`/api/organizations/${organizationId}/contracts/${encodeURIComponent(expiredContractId)}/bind-project`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ projectId, usageMode: "supplemental-rights", status: "active", notes: "smoke expired rights" })
|
||||
}), "bind expired contract");
|
||||
const blocked = expectOk(await request(`/api/organizations/${organizationId}/contracts/clearance?projectId=${encodeURIComponent(projectId)}&channelKind=private-delivery-portal`, { headers }), "expired contract blocks clearance");
|
||||
assert.equal(blocked.clearance.status, "blocked", "expired active binding must block project contract clearance");
|
||||
assert.ok(blocked.clearance.blockers.some((item) => item.code === "contract_expired"), "clearance blockers must include contract_expired");
|
||||
|
||||
const events = dbAll("SELECT event_type FROM contract_license_events WHERE organization_id = ? AND (contract_id = ? OR contract_id = ?)", [organizationId, contractId, expiredContractId]);
|
||||
assert.ok(events.some((event) => event.event_type === "contract.created"), "contract creation must write license event");
|
||||
assert.ok(events.some((event) => event.event_type === "contract.project_binding.created"), "project binding must write license event");
|
||||
|
||||
console.log(`contracts smoke passed: ${contractId} / ${expiredContractId}`);
|
||||
} finally {
|
||||
withTransaction(() => {
|
||||
for (const id of [contractId, expiredContractId].filter(Boolean)) {
|
||||
dbRun("DELETE FROM contract_license_events WHERE contract_id = ?", [id]);
|
||||
dbRun("DELETE FROM contract_project_bindings WHERE contract_id = ?", [id]);
|
||||
dbRun("DELETE FROM audit_logs WHERE target_id = ? OR metadata_json LIKE ?", [id, `%${id}%`]);
|
||||
dbRun("DELETE FROM production_contracts WHERE id = ?", [id]);
|
||||
}
|
||||
if (customerId) {
|
||||
dbRun("DELETE FROM customer_contacts WHERE customer_id = ?", [customerId]);
|
||||
dbRun("DELETE FROM contract_license_events WHERE customer_id = ?", [customerId]);
|
||||
dbRun("DELETE FROM audit_logs WHERE target_id = ? OR metadata_json LIKE ?", [customerId, `%${customerId}%`]);
|
||||
dbRun("DELETE FROM customers WHERE id = ?", [customerId]);
|
||||
}
|
||||
dbRun("DELETE FROM customers WHERE organization_id = ? AND code = ?", [organizationId, customerCode]);
|
||||
dbRun("DELETE FROM production_contracts WHERE organization_id = ? AND contract_number IN (?, ?)", [organizationId, contractNumber, expiredContractNumber]);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user