471 lines
11 KiB
JavaScript
471 lines
11 KiB
JavaScript
import { PLAN_CATALOG } from '../billing-subscription.mjs';
|
|
|
|
export function attachPortalBillingRoutes({
|
|
app,
|
|
jsonBody,
|
|
userAuthReady = Promise.resolve(),
|
|
getUserAuth = () => null,
|
|
getRechargeService = () => null,
|
|
getSubscriptionService = () => null,
|
|
getMindSpace = () => null,
|
|
userToken,
|
|
planCatalogFallback = PLAN_CATALOG,
|
|
} = {}) {
|
|
if (
|
|
!app ||
|
|
typeof jsonBody !== 'function' ||
|
|
typeof userToken !== 'function'
|
|
) {
|
|
throw new Error(
|
|
'attachPortalBillingRoutes requires route dependencies',
|
|
);
|
|
}
|
|
|
|
app.get(
|
|
'/auth/billing/ledger',
|
|
async (req, res) => {
|
|
await userAuthReady;
|
|
const userAuth = getUserAuth();
|
|
if (!userAuth) {
|
|
return res.status(503).json({
|
|
message: '未启用用户系统',
|
|
});
|
|
}
|
|
const me = await userAuth.getMe(
|
|
userToken(req),
|
|
);
|
|
if (!me) {
|
|
return res.status(401).json({
|
|
message: '未登录',
|
|
});
|
|
}
|
|
const limit = Math.min(
|
|
Math.max(
|
|
Number(req.query?.limit) || 30,
|
|
1,
|
|
),
|
|
100,
|
|
);
|
|
const entries =
|
|
await userAuth.listBillingLedger({
|
|
userId: me.id,
|
|
limit,
|
|
types: [
|
|
'recharge',
|
|
'adjust',
|
|
'refund',
|
|
],
|
|
});
|
|
res.json({ entries });
|
|
},
|
|
);
|
|
|
|
app.get(
|
|
'/auth/billing/config',
|
|
async (req, res) => {
|
|
await userAuthReady;
|
|
const userAuth = getUserAuth();
|
|
const rechargeService =
|
|
getRechargeService();
|
|
if (!userAuth || !rechargeService) {
|
|
return res.status(503).json({
|
|
message: '未启用计费系统',
|
|
});
|
|
}
|
|
const me = await userAuth.getMe(
|
|
userToken(req),
|
|
);
|
|
if (!me) {
|
|
return res.status(401).json({
|
|
message: '未登录',
|
|
});
|
|
}
|
|
const subscriptionService =
|
|
getSubscriptionService();
|
|
const [config, sub] = await Promise.all([
|
|
rechargeService.getBillingConfig(me.id),
|
|
subscriptionService
|
|
? subscriptionService.getActiveSubscription(
|
|
me.id,
|
|
)
|
|
: null,
|
|
]);
|
|
return res.json({
|
|
...config,
|
|
subscription: sub,
|
|
});
|
|
},
|
|
);
|
|
|
|
app.get(
|
|
'/auth/billing/subscription',
|
|
async (req, res) => {
|
|
await userAuthReady;
|
|
const userAuth = getUserAuth();
|
|
if (!userAuth) {
|
|
return res.status(503).json({
|
|
message: '未启用用户系统',
|
|
});
|
|
}
|
|
const me = await userAuth.getMe(
|
|
userToken(req),
|
|
);
|
|
if (!me) {
|
|
return res.status(401).json({
|
|
message: '未登录',
|
|
});
|
|
}
|
|
const subscriptionService =
|
|
getSubscriptionService();
|
|
const sub = subscriptionService
|
|
? await subscriptionService.getActiveSubscription(
|
|
me.id,
|
|
)
|
|
: null;
|
|
const planCatalog =
|
|
subscriptionService?._planCatalogService;
|
|
const plans =
|
|
!sub && planCatalog
|
|
? await planCatalog.listPlans({
|
|
includeInactive: false,
|
|
})
|
|
: sub
|
|
? undefined
|
|
: planCatalogFallback;
|
|
return res.json({
|
|
subscription: sub,
|
|
plans,
|
|
});
|
|
},
|
|
);
|
|
|
|
app.get(
|
|
'/auth/billing/plans',
|
|
async (req, res) => {
|
|
await userAuthReady;
|
|
const userAuth = getUserAuth();
|
|
if (!userAuth) {
|
|
return res.status(503).json({
|
|
message: '未启用用户系统',
|
|
});
|
|
}
|
|
const me = await userAuth.getMe(
|
|
userToken(req),
|
|
);
|
|
if (!me) {
|
|
return res.status(401).json({
|
|
message: '未登录',
|
|
});
|
|
}
|
|
const subscriptionService =
|
|
getSubscriptionService();
|
|
const planCatalog =
|
|
subscriptionService?._planCatalogService;
|
|
const plans = planCatalog
|
|
? (
|
|
await planCatalog.listPlans({
|
|
includeInactive: false,
|
|
})
|
|
)
|
|
.filter((plan) => plan.priceCents > 0)
|
|
.map((plan) => ({
|
|
key: plan.planType,
|
|
...plan,
|
|
}))
|
|
: Object.entries(planCatalogFallback)
|
|
.filter(
|
|
([, plan]) => plan.priceCents > 0,
|
|
)
|
|
.map(([key, plan]) => ({
|
|
key,
|
|
...plan,
|
|
}));
|
|
const [sub, wallet] = await Promise.all([
|
|
subscriptionService
|
|
? subscriptionService.getActiveSubscription(
|
|
me.id,
|
|
)
|
|
: null,
|
|
userAuth.getUserById(me.id),
|
|
]);
|
|
return res.json({
|
|
plans,
|
|
subscription: sub,
|
|
balanceCents: wallet
|
|
? Number(wallet.balance_cents ?? 0)
|
|
: 0,
|
|
});
|
|
},
|
|
);
|
|
|
|
app.post(
|
|
'/auth/billing/subscribe',
|
|
jsonBody,
|
|
async (req, res) => {
|
|
await userAuthReady;
|
|
const userAuth = getUserAuth();
|
|
const subscriptionService =
|
|
getSubscriptionService();
|
|
if (!userAuth || !subscriptionService) {
|
|
return res.status(503).json({
|
|
message: '未启用订阅系统',
|
|
});
|
|
}
|
|
const me = await userAuth.getMe(
|
|
userToken(req),
|
|
);
|
|
if (!me) {
|
|
return res.status(401).json({
|
|
message: '未登录',
|
|
});
|
|
}
|
|
if (me.status === 'disabled') {
|
|
return res.status(403).json({
|
|
message: '账户已禁用',
|
|
});
|
|
}
|
|
const {
|
|
planType,
|
|
autoRenew = false,
|
|
} = req.body ?? {};
|
|
if (
|
|
!planType ||
|
|
typeof planType !== 'string'
|
|
) {
|
|
return res.status(400).json({
|
|
message: '请选择套餐',
|
|
});
|
|
}
|
|
const result =
|
|
await subscriptionService.purchaseSubscription(
|
|
me.id,
|
|
planType,
|
|
Boolean(autoRenew),
|
|
);
|
|
if (!result.ok) {
|
|
if (
|
|
result.code ===
|
|
'INSUFFICIENT_BALANCE'
|
|
) {
|
|
return res.status(402).json({
|
|
message: result.message,
|
|
code: result.code,
|
|
balanceCents: result.balanceCents,
|
|
requiredCents: result.requiredCents,
|
|
shortfallCents:
|
|
result.shortfallCents,
|
|
});
|
|
}
|
|
if (
|
|
result.code ===
|
|
'DOWNGRADE_NOT_ALLOWED'
|
|
) {
|
|
return res.status(409).json({
|
|
message: result.message,
|
|
code: result.code,
|
|
currentPlanType:
|
|
result.currentPlanType,
|
|
});
|
|
}
|
|
return res.status(400).json({
|
|
message: result.message,
|
|
});
|
|
}
|
|
return res.json({
|
|
subscription: result.subscription,
|
|
balanceCents: result.balanceCents,
|
|
});
|
|
},
|
|
);
|
|
|
|
app.post(
|
|
'/auth/billing/space-purchase',
|
|
jsonBody,
|
|
async (req, res) => {
|
|
await userAuthReady;
|
|
const userAuth = getUserAuth();
|
|
const mindSpace = getMindSpace();
|
|
if (!userAuth || !mindSpace) {
|
|
return res.status(503).json({
|
|
message: '未启用空间系统',
|
|
});
|
|
}
|
|
const me = await userAuth.getMe(
|
|
userToken(req),
|
|
);
|
|
if (!me) {
|
|
return res.status(401).json({
|
|
message: '未登录',
|
|
});
|
|
}
|
|
const sizeMb = Number(req.body?.sizeMb);
|
|
const result =
|
|
await userAuth.purchaseSpaceQuota(
|
|
me.id,
|
|
sizeMb,
|
|
);
|
|
if (!result.ok) {
|
|
if (
|
|
result.code ===
|
|
'INSUFFICIENT_BALANCE'
|
|
) {
|
|
return res.status(402).json({
|
|
message: result.message,
|
|
code: result.code,
|
|
details: {
|
|
code: 'INSUFFICIENT_BALANCE',
|
|
balanceCents:
|
|
result.balanceCents,
|
|
minRechargeCents:
|
|
result.minRechargeCents,
|
|
suggestedTiers:
|
|
result.suggestedTiers,
|
|
},
|
|
});
|
|
}
|
|
return res.status(400).json({
|
|
message: result.message,
|
|
});
|
|
}
|
|
const quota = await mindSpace.getQuota(me.id);
|
|
return res.json({
|
|
quota: quota ?? result.quota,
|
|
balanceCents: result.balanceCents,
|
|
purchasedMb: sizeMb,
|
|
costCents: sizeMb * 200,
|
|
});
|
|
},
|
|
);
|
|
|
|
app.post(
|
|
'/auth/billing/auto-renew',
|
|
jsonBody,
|
|
async (req, res) => {
|
|
await userAuthReady;
|
|
const userAuth = getUserAuth();
|
|
const subscriptionService =
|
|
getSubscriptionService();
|
|
if (!userAuth || !subscriptionService) {
|
|
return res.status(503).json({
|
|
message: '未启用订阅系统',
|
|
});
|
|
}
|
|
const me = await userAuth.getMe(
|
|
userToken(req),
|
|
);
|
|
if (!me) {
|
|
return res.status(401).json({
|
|
message: '未登录',
|
|
});
|
|
}
|
|
const { enabled } = req.body ?? {};
|
|
if (typeof enabled !== 'boolean') {
|
|
return res.status(400).json({
|
|
message: '请传入 enabled: true/false',
|
|
});
|
|
}
|
|
const result =
|
|
await subscriptionService.setAutoRenew(
|
|
me.id,
|
|
enabled,
|
|
);
|
|
return res.json(result);
|
|
},
|
|
);
|
|
|
|
app.post(
|
|
'/auth/billing/recharge-orders',
|
|
jsonBody,
|
|
async (req, res) => {
|
|
await userAuthReady;
|
|
const userAuth = getUserAuth();
|
|
const rechargeService =
|
|
getRechargeService();
|
|
if (!userAuth || !rechargeService) {
|
|
return res.status(503).json({
|
|
message: '未启用计费系统',
|
|
});
|
|
}
|
|
const me = await userAuth.getMe(
|
|
userToken(req),
|
|
);
|
|
if (!me) {
|
|
return res.status(401).json({
|
|
message: '未登录',
|
|
});
|
|
}
|
|
const amountCents = Number(
|
|
req.body?.amountCents,
|
|
);
|
|
const payScene = [
|
|
'native',
|
|
'h5',
|
|
'jsapi',
|
|
].includes(req.body?.payScene)
|
|
? req.body.payScene
|
|
: 'native';
|
|
const result =
|
|
await rechargeService.createOrder({
|
|
userId: me.id,
|
|
amountCents,
|
|
payScene,
|
|
clientIp: req.ip,
|
|
});
|
|
if (!result.ok) {
|
|
return res.status(400).json({
|
|
message: result.message,
|
|
});
|
|
}
|
|
return res.status(201).json({
|
|
order: result.order,
|
|
});
|
|
},
|
|
);
|
|
|
|
app.get(
|
|
'/auth/billing/recharge-orders/:orderId',
|
|
async (req, res) => {
|
|
await userAuthReady;
|
|
const userAuth = getUserAuth();
|
|
const rechargeService =
|
|
getRechargeService();
|
|
if (!userAuth || !rechargeService) {
|
|
return res.status(503).json({
|
|
message: '未启用计费系统',
|
|
});
|
|
}
|
|
const me = await userAuth.getMe(
|
|
userToken(req),
|
|
);
|
|
if (!me) {
|
|
return res.status(401).json({
|
|
message: '未登录',
|
|
});
|
|
}
|
|
const order =
|
|
await rechargeService.getOrderForUser(
|
|
me.id,
|
|
req.params.orderId,
|
|
);
|
|
if (!order) {
|
|
return res.status(404).json({
|
|
message: '订单不存在',
|
|
});
|
|
}
|
|
let balanceCents = null;
|
|
if (order.status === 'paid') {
|
|
const user = await userAuth.getUserById(
|
|
me.id,
|
|
);
|
|
balanceCents = user
|
|
? Number(user.balance_cents ?? 0)
|
|
: null;
|
|
}
|
|
return res.json({
|
|
order,
|
|
balanceCents,
|
|
});
|
|
},
|
|
);
|
|
}
|