Files
Jyotisha/frontend/tests/database-billing-adjustments.test.ts
T
Jesse_Chen 9d8c73561f
Staging Backend Quality Gate / validate (push) Failing after 7m30s
Staging Backend Quality Gate / publish (push) Has been skipped
test(deploy): integrate billing admin rollout checks
2026-08-06 20:15:50 +08:00

487 lines
18 KiB
TypeScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import test from "node:test";
import { Client } from "pg";
import { startPostgresFixture } from "./helpers/postgres-fixture.ts";
const runnerPath = fileURLToPath(
new URL("../scripts/db-migrate.mjs", import.meta.url),
);
const ids = {
billing: "71000000-0000-4000-8000-000000000001",
support: "71000000-0000-4000-8000-000000000002",
oneTimeUser: "72000000-0000-4000-8000-000000000001",
retryUser: "72000000-0000-4000-8000-000000000002",
compensateUser: "72000000-0000-4000-8000-000000000003",
oneTimeProduct: "73000000-0000-4000-8000-000000000001",
retryProduct: "73000000-0000-4000-8000-000000000002",
compensateProduct: "73000000-0000-4000-8000-000000000003",
};
const orderNo = (suffix: string) => `JYADJUST${suffix.padEnd(18, "0")}`;
function errorText(error: unknown) {
return error instanceof Error ? error.message : String(error);
}
test("billing order adjustments and redemption reasons are atomic and audited", async () => {
const fixture = startPostgresFixture();
const admin = new Client({
connectionString: fixture.connectionUrl("postgres", "postgres-test-password"),
});
const adminRuntime = new Client({
connectionString: fixture.connectionUrl(
"admin_runtime",
"admin-runtime-test-password",
),
});
const serviceRuntime = new Client({
connectionString: fixture.connectionUrl(
"service_runtime",
"service-runtime-test-password",
),
});
const sql = (statement: string) => fixture.psql(statement);
try {
const migration = spawnSync(process.execPath, [runnerPath], {
encoding: "utf8",
env: {
...process.env,
SCHEMA_DATABASE_URL: fixture.connectionUrl(
"schema_owner",
"schema-owner-test-password",
),
},
});
assert.equal(migration.status, 0, migration.stderr);
await Promise.all([
admin.connect(),
adminRuntime.connect(),
serviceRuntime.connect(),
]);
await serviceRuntime.query("set role service_role");
fixture.psqlAs(
"identity_runtime",
"identity-runtime-test-password",
`insert into identity.users (id,name,email,email_verified,email_verified_at,role) values
('${ids.billing}','Billing Admin','billing-adjust@example.com',true,now(),'admin'),
('${ids.support}','Support Admin','support-adjust@example.com',true,now(),'admin'),
('${ids.oneTimeUser}','One Time User','one-time@example.com',true,now(),'user'),
('${ids.retryUser}','Retry User','retry@example.com',true,now(),'user'),
('${ids.compensateUser}','Compensate User','compensate@example.com',true,now(),'user')`,
);
sql(`
insert into public.admin_users(user_id,created_by) values
('${ids.billing}','${ids.billing}'),('${ids.support}','${ids.billing}');
insert into public.admin_user_roles(admin_user_id,role_id,assigned_by)
select v.user_id,r.id,'${ids.billing}'::uuid
from (values
('${ids.billing}'::uuid,'billing_admin'),
('${ids.support}'::uuid,'support')
) v(user_id,role_code)
join public.admin_roles r on r.code=v.role_code;
insert into public.billing_products(
id,code,version,name,product_type,billing_period,interval_count,
price_cents,enabled,status,effective_from,one_time_per_user
) values
('${ids.oneTimeProduct}','once_credits',1,'一次性积分','credit_pack','none',0,700,true,'published',now(),true),
('${ids.retryProduct}','retry_credits',1,'重试积分','credit_pack','none',0,500,true,'published',now(),false),
('${ids.compensateProduct}','compensate_credits',1,'补偿积分','credit_pack','none',0,400,true,'published',now(),false);
insert into public.product_entitlements(product_id,feature_key,allowance_type,allowance_count,reset_period) values
('${ids.oneTimeProduct}','chat.standard','credits',7,'none'),
('${ids.retryProduct}','chat.standard','credits',5,'none'),
('${ids.compensateProduct}','chat.standard','credits',4,'none');
`);
const insertOrder = async (input: {
suffix: string;
userId: string;
productId: string;
code: string;
price: number;
credits: number;
oneTime: boolean;
entitlements?: unknown[];
}) => {
const entitlements = input.entitlements ?? [
{
featureKey: "chat.standard",
allowanceType: "credits",
allowanceCount: input.credits,
resetPeriod: "none",
modelTier: null,
fairUsePolicyId: null,
metadata: {},
},
];
const result = await admin.query<{ id: string }>(
`insert into public.payment_orders (
order_no,user_id,package_id,product_id,product_code,product_version,
product_snapshot,entitlement_snapshot,money_cents,currency,credits,
grant_type,grant_status
) values (
$1,$2,null,$3,$4,1,
jsonb_build_object(
'id',$3::uuid,'code',$4::text,'version',1,'name',$5::text,'description','',
'productType','credit_pack','billingPeriod','none','intervalCount',0,
'priceCents',$6::integer,'currency','CNY','oneTimePerUser',$7::boolean
),
$8::jsonb,$6,'CNY',$9,'credits','pending'
) returning id`,
[
orderNo(input.suffix),
input.userId,
input.productId,
input.code,
input.code,
input.price,
input.oneTime,
JSON.stringify(entitlements),
input.credits,
],
);
return result.rows[0].id;
};
const onceOrderA = await insertOrder({
suffix: "ONCEA",
userId: ids.oneTimeUser,
productId: ids.oneTimeProduct,
code: "once_credits",
price: 700,
credits: 7,
oneTime: true,
});
await insertOrder({
suffix: "ONCEB",
userId: ids.oneTimeUser,
productId: ids.oneTimeProduct,
code: "once_credits",
price: 700,
credits: 7,
oneTime: true,
});
const raceClients = [0, 1].map(
() =>
new Client({
connectionString: fixture.connectionUrl(
"service_runtime",
"service-runtime-test-password",
),
}),
);
await Promise.all(raceClients.map((client) => client.connect()));
await Promise.all(
raceClients.map((client) => client.query("set role service_role")),
);
try {
const results = await Promise.all([
raceClients[0].query(
"select * from public.settle_order($1,$2,$3,$4)",
[orderNo("ONCEA"), "trade-once-a", 700, "hash-once-a"],
),
raceClients[1].query(
"select * from public.settle_order($1,$2,$3,$4)",
[orderNo("ONCEB"), "trade-once-b", 700, "hash-once-b"],
),
]);
assert.deepEqual(
results.map((result) => result.rows[0].success).sort(),
[false, true],
);
assert.deepEqual(
results.map((result) => result.rows[0].status).sort(),
["one_time_limit", "paid"],
);
} finally {
await Promise.all(raceClients.map((client) => client.end()));
}
assert.equal(
sql(`select credits from public.profiles where id='${ids.oneTimeUser}'`),
"7",
);
assert.equal(
sql(`select count(*) from public.user_product_redemptions where user_id='${ids.oneTimeUser}' and product_code='once_credits'`),
"1",
);
await assert.rejects(
admin.query(
"update public.payment_orders set product_snapshot=jsonb_set(product_snapshot,'{name}','\"tampered\"') where id=$1",
[onceOrderA],
),
(error) => /payment_order_snapshot_immutable/.test(errorText(error)),
);
const retryOrder = await insertOrder({
suffix: "RETRY",
userId: ids.retryUser,
productId: ids.retryProduct,
code: "retry_credits",
price: 500,
credits: 5,
oneTime: false,
});
await admin.query(`
create function public.test_fail_retry_grant() returns trigger
language plpgsql as $$
begin
if new.request_id='${orderNo("RETRY")}' then
raise exception 'forced_retry_grant_failure';
end if;
return new;
end $$;
create trigger test_fail_retry_grant before insert on public.credit_transactions
for each row execute function public.test_fail_retry_grant()
`);
const failedRetry = await serviceRuntime.query(
"select * from public.settle_order($1,$2,$3,$4)",
[orderNo("RETRY"), "trade-retry", 500, "hash-retry"],
);
assert.equal(failedRetry.rows[0].success, false);
assert.equal(failedRetry.rows[0].status, "grant_failed");
assert.equal(
sql(`select credits from public.profiles where id='${ids.retryUser}'`),
"0",
);
await admin.query(
"drop trigger test_fail_retry_grant on public.credit_transactions; drop function public.test_fail_retry_grant()",
);
const retry = await adminRuntime.query(
"select * from public.admin_adjust_order($1,$2,'retry_grant',0,$3,$4)",
[ids.billing, retryOrder, "重试失败权益发放", "retry-adjustment-1"],
);
assert.equal(retry.rows[0].action_success, true);
assert.equal(retry.rows[0].adjustment_version, 1);
assert.equal(
sql(`select credits from public.profiles where id='${ids.retryUser}'`),
"5",
);
const retryAgain = await adminRuntime.query(
"select * from public.admin_adjust_order($1,$2,'retry_grant',0,$3,$4)",
[ids.billing, retryOrder, "同一请求幂等重放", "retry-adjustment-1"],
);
assert.equal(retryAgain.rows[0].adjustment_version, 1);
assert.equal(
sql("select count(*) from audit.admin_audit_logs where request_id='retry-adjustment-1'"),
"1",
);
await assert.rejects(
adminRuntime.query(
"select * from public.admin_adjust_order($1,$2,'record_refund',0,$3,$4)",
[ids.billing, retryOrder, "过期版本不得退款", "refund-stale"],
),
(error) =>
(error as { code?: string }).code === "40001" &&
/admin_order_version_conflict/.test(errorText(error)),
);
const compensateOrder = await insertOrder({
suffix: "COMPENSATE",
userId: ids.compensateUser,
productId: ids.compensateProduct,
code: "compensate_credits",
price: 400,
credits: 4,
oneTime: false,
entitlements: [],
});
const invalidGrant = await serviceRuntime.query(
"select * from public.settle_order($1,$2,$3,$4)",
[orderNo("COMPENSATE"), "trade-compensate", 400, "hash-compensate"],
);
assert.equal(invalidGrant.rows[0].status, "invalid_snapshot");
await assert.rejects(
adminRuntime.query(
"select * from public.admin_adjust_order($1,$2,'compensate',0,'',$3)",
[ids.billing, compensateOrder, "compensate-empty-reason"],
),
(error) => /admin_reason_required/.test(errorText(error)),
);
await assert.rejects(
adminRuntime.query(
"select * from public.admin_adjust_order($1,$2,'compensate',0,$3,$4)",
[ids.support, compensateOrder, "无权人工补偿", "compensate-denied"],
),
(error) => /admin_permission_denied/.test(errorText(error)),
);
const compensated = await adminRuntime.query(
"select * from public.admin_adjust_order($1,$2,'compensate',0,$3,$4)",
[ids.billing, compensateOrder, "确认支付成功,人工补发积分", "compensate-1"],
);
assert.equal(compensated.rows[0].grant_status, "granted");
assert.equal(
sql(`select credits from public.profiles where id='${ids.compensateUser}'`),
"4",
);
assert.equal(
sql(`select count(*) from public.credit_transactions where user_id='${ids.compensateUser}' and transaction_type='compensation'`),
"1",
);
const balanceBeforeRefund = sql(
`select credits from public.profiles where id='${ids.retryUser}'`,
);
const refunded = await adminRuntime.query(
"select * from public.admin_adjust_order($1,$2,'record_refund',1,$3,$4)",
[ids.billing, retryOrder, "线下已退款,仅登记账务状态", "refund-record-1"],
);
assert.equal(refunded.rows[0].status, "refunded");
assert.equal(refunded.rows[0].refund_status, "recorded");
assert.equal(
sql(`select credits from public.profiles where id='${ids.retryUser}'`),
balanceBeforeRefund,
);
assert.equal(
sql(`select after_value->>'externalGatewayRefundAttempted' from audit.admin_audit_logs where request_id='refund-record-1'`),
"f",
);
assert.equal(
sql(`select permission_used||':'||reason from audit.admin_audit_logs where request_id='compensate-1'`),
"billing.adjustments.write:确认支付成功,人工补发积分",
);
assert.equal(
sql("select has_function_privilege('service_role','public.admin_create_redemption_codes(uuid,text,text,text,jsonb)','execute')"),
"f",
);
assert.equal(
sql("select has_function_privilege('service_role','public.admin_update_redemption_code(uuid,text,text,text,uuid,boolean,text,boolean,timestamptz)','execute')"),
"f",
);
assert.equal(
sql("select has_function_privilege('service_role','public.admin_revoke_redemption_code(uuid,text,text,text,uuid)','execute')"),
"f",
);
for (const role of ["service_role", "service_runtime"]) {
assert.equal(
sql(`select has_function_privilege('${role}','public.admin_adjust_order(uuid,uuid,text,integer,text,text)','execute')`),
"f",
);
assert.equal(
sql(`select has_function_privilege('${role}','public.admin_create_redemption_codes(uuid,text,text,text,jsonb,text)','execute')`),
"f",
);
assert.equal(
sql(`select has_function_privilege('${role}','public.admin_update_redemption_code(uuid,text,text,text,uuid,boolean,text,boolean,timestamptz,text)','execute')`),
"f",
);
assert.equal(
sql(`select has_function_privilege('${role}','public.admin_revoke_redemption_code(uuid,text,text,text,uuid,text)','execute')`),
"f",
);
}
assert.equal(
sql("select pg_has_role('service_runtime','service_role','MEMBER')"),
"t",
);
assert.equal(
sql("select has_function_privilege('service_role','public.settle_order(text,text,integer,text)','execute')"),
"t",
);
for (const signature of [
"public.admin_adjust_order(uuid,uuid,text,integer,text,text)",
"public.admin_create_redemption_codes(uuid,text,text,text,jsonb,text)",
"public.admin_update_redemption_code(uuid,text,text,text,uuid,boolean,text,boolean,timestamptz,text)",
"public.admin_revoke_redemption_code(uuid,text,text,text,uuid,text)",
]) {
assert.equal(
sql(`select has_function_privilege('admin_runtime','${signature}','execute')`),
"t",
);
}
await assert.rejects(
serviceRuntime.query(
"select * from public.admin_adjust_order($1,$2,'record_refund',1,$3,$4)",
[ids.billing, retryOrder, "机器身份不得人工退款", "service-refund-denied"],
),
(error) => (error as { code?: string }).code === "42501",
);
await assert.rejects(
serviceRuntime.query(
"select * from public.admin_create_redemption_codes($1,$2,'admin',$3,$4::jsonb,$5)",
[ids.billing, "billing-adjust@example.com", "service-code-denied", "[]", "机器身份不得创建兑换码"],
),
(error) => (error as { code?: string }).code === "42501",
);
await assert.rejects(
adminRuntime.query(
"select * from public.admin_create_redemption_codes($1,$2,'admin',$3,$4::jsonb,'')",
[
ids.billing,
"billing-adjust@example.com",
"code-create-empty",
JSON.stringify([
{
codeHash: "a".repeat(64),
codeMask: "JYOTISH-****-AAAA",
credits: 3,
expiresAt: null,
note: "test",
},
]),
],
),
(error) => /admin_reason_required/.test(errorText(error)),
);
const createdCode = await adminRuntime.query<{ id: string }>(
"select id from public.admin_create_redemption_codes($1,$2,'admin',$3,$4::jsonb,$5)",
[
ids.billing,
"billing-adjust@example.com",
"code-create-1",
JSON.stringify([
{
codeHash: "b".repeat(64),
codeMask: "JYOTISH-****-BBBB",
credits: 3,
expiresAt: null,
note: "created",
},
]),
"创建客服补偿码",
],
);
const codeId = createdCode.rows[0].id;
await adminRuntime.query(
"select * from public.admin_update_redemption_code($1,$2,'admin',$3,$4,true,$5,false,null,$6)",
[
ids.billing,
"billing-adjust@example.com",
"code-update-1",
codeId,
"updated",
"修正兑换码备注",
],
);
await adminRuntime.query(
"select * from public.admin_revoke_redemption_code($1,$2,'admin',$3,$4,$5)",
[
ids.billing,
"billing-adjust@example.com",
"code-revoke-1",
codeId,
"撤销未发放兑换码",
],
);
assert.equal(
sql(`select string_agg(action||':'||permission_used||':'||reason,'|' order by created_at)
from audit.admin_audit_logs where target_id='${codeId}'`),
"redemption_code.create:billing.adjustments.write:创建客服补偿码|redemption_code.update:billing.adjustments.write:修正兑换码备注|redemption_code.revoke:billing.adjustments.write:撤销未发放兑换码",
);
} finally {
await Promise.all([
admin.end().catch(() => {}),
adminRuntime.end().catch(() => {}),
serviceRuntime.end().catch(() => {}),
]);
fixture.stop();
}
});