Files
memind/server/portal-billing-routes.mjs

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,
});
},
);
}