feat(billing): add formula A/B admin UI and subscription renew status
Expose dual metering formula editing with batch user assignment, show auto-renew and pending-recharge states in subscriptions, and document billing acceptance checks for production rollout. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+21
-5
@@ -660,11 +660,12 @@ export function createAdminApp(services) {
|
||||
res.json(await skillRuntimeConfigService.getPublicRuntimeConfig());
|
||||
});
|
||||
|
||||
adminApi.get('/billing/config', requireAdmin, async (_req, res) => {
|
||||
adminApi.get('/billing/config', requireAdmin, async (req, res) => {
|
||||
if (!billingConfigService?.getAdminConfig) {
|
||||
return res.status(503).json({ message: '计费公式配置服务未启用' });
|
||||
}
|
||||
return res.json(await billingConfigService.getAdminConfig());
|
||||
const formula = req.query.formula ? String(req.query.formula) : undefined;
|
||||
return res.json(await billingConfigService.getAdminConfig({ formula }));
|
||||
});
|
||||
|
||||
const updateBillingConfig = async (req, res) => {
|
||||
@@ -672,9 +673,10 @@ export function createAdminApp(services) {
|
||||
return res.status(503).json({ message: '计费公式配置服务未启用' });
|
||||
}
|
||||
try {
|
||||
const formula = req.body?.formula ? String(req.body.formula) : undefined;
|
||||
return res.json(await billingConfigService.updateAdminConfig(
|
||||
req.body?.config ?? req.body ?? {},
|
||||
{ updatedBy: req.currentUser.id },
|
||||
{ updatedBy: req.currentUser.id, formula },
|
||||
));
|
||||
} catch (error) {
|
||||
if (error?.code === 'BILLING_CONFIG_ENV_LOCKED') {
|
||||
@@ -687,11 +689,25 @@ export function createAdminApp(services) {
|
||||
adminApi.put('/billing/config', requireAdmin, updateBillingConfig);
|
||||
adminApi.patch('/billing/config', requireAdmin, updateBillingConfig);
|
||||
|
||||
adminApi.get('/billing/runtime', requireAdmin, async (_req, res) => {
|
||||
adminApi.post('/billing/formula-assignments', requireAdmin, async (req, res) => {
|
||||
if (!billingConfigService?.assignBillingFormula) {
|
||||
return res.status(503).json({ message: '计费公式配置服务未启用' });
|
||||
}
|
||||
const { userIds, formula } = req.body ?? {};
|
||||
const result = await billingConfigService.assignBillingFormula(userIds, formula);
|
||||
if (!result.ok) {
|
||||
return res.status(400).json(result);
|
||||
}
|
||||
return res.json(result);
|
||||
});
|
||||
|
||||
adminApi.get('/billing/runtime', requireAdmin, async (req, res) => {
|
||||
if (!billingConfigService?.getRuntimeState) {
|
||||
return res.status(503).json({ message: '计费公式配置服务未启用' });
|
||||
}
|
||||
return res.json(await billingConfigService.getRuntimeState());
|
||||
const formula = req.query.formula ? String(req.query.formula) : undefined;
|
||||
const userId = req.query.userId ? String(req.query.userId) : undefined;
|
||||
return res.json(await billingConfigService.getRuntimeState({ formula, userId }));
|
||||
});
|
||||
|
||||
adminApi.get('/system-tests/accounts', requireAdmin, async (_req, res) => {
|
||||
|
||||
@@ -28,8 +28,15 @@ function createServices({ role = 'admin' } = {}) {
|
||||
getMe: async () => ({ id: 'admin-id', username: 'admin', role }),
|
||||
},
|
||||
billingConfigService: {
|
||||
getAdminConfig: async () => ({
|
||||
getAdminConfig: async ({ formula = 'A' } = {}) => ({
|
||||
config: stored,
|
||||
formulas: { A: stored, B: stored },
|
||||
formulaMeta: {
|
||||
A: { source: 'env', updatedAt: null, updatedBy: null },
|
||||
B: { source: 'env', updatedAt: null, updatedBy: null },
|
||||
},
|
||||
activeFormula: formula,
|
||||
defaultFormula: 'A',
|
||||
source: 'env',
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
@@ -39,12 +46,24 @@ function createServices({ role = 'admin' } = {}) {
|
||||
stored = { ...stored, ...(patch.config ?? patch) };
|
||||
return {
|
||||
config: stored,
|
||||
formulas: { A: stored, B: stored },
|
||||
formulaMeta: {
|
||||
A: { source: 'admin-db', updatedAt: Date.now(), updatedBy: context?.updatedBy ?? null },
|
||||
B: { source: 'admin-db', updatedAt: null, updatedBy: null },
|
||||
},
|
||||
activeFormula: context?.formula ?? 'A',
|
||||
defaultFormula: 'A',
|
||||
source: 'admin-db',
|
||||
updatedAt: Date.now(),
|
||||
updatedBy: context?.updatedBy ?? null,
|
||||
formula: '最终扣费 = 上游成本(USD) × 汇率 × 毛利倍数',
|
||||
};
|
||||
},
|
||||
assignBillingFormula: async (userIds, formula) => ({
|
||||
ok: true,
|
||||
updated: userIds.length,
|
||||
formula,
|
||||
}),
|
||||
getRuntimeState: async () => ({
|
||||
source: 'admin-db',
|
||||
config: stored,
|
||||
|
||||
@@ -165,6 +165,7 @@ export async function bootstrapAdminServices() {
|
||||
getPlanAsync: (planType) => planCatalogService.getPlan(planType),
|
||||
});
|
||||
subscriptionService._planCatalogService = planCatalogService;
|
||||
userAuth.setSubscriptionService(subscriptionService);
|
||||
const { createPageTemplateCatalogService } = await importMemind('page-template-catalog.mjs');
|
||||
const templateCatalogService = createPageTemplateCatalogService(pool, {
|
||||
userAuth,
|
||||
|
||||
Reference in New Issue
Block a user