feat(billing): enforce 50 GB premium storage cap (was unlimited)
Premium granted unlimited storage because quota.ts treated any paid tier as Infinity. Plan sells 50 GB — now enforced: - quota.ts: PAID_STORAGE_LIMIT_BYTES = 50 GiB; getStorageInfo/checkStorageQuota/ reconcileActualSize use it for paid families instead of Infinity. Meter/ warn/exceeded now computed for both tiers. - storage-usage route: return real limit for paid (was forcing "Unlimited"/null) - StorageMeter: show meter for premium too (had `isPaid -> return null`); exceeded message drops the Upgrade link for premium (delete-only) Free 1 GB unchanged. Premium = 50 GB / 6 members / 3 babies, all enforced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a69546977c
commit
9dcd0fc854
3 changed files with 31 additions and 19 deletions
|
|
@ -11,12 +11,13 @@ export async function GET() {
|
||||||
const familyId = auth.session!.familyId!;
|
const familyId = auth.session!.familyId!;
|
||||||
const info = await getStorageInfo(familyId);
|
const info = await getStorageInfo(familyId);
|
||||||
|
|
||||||
|
// Both tiers now have a real cap (free 1 GB / premium 50 GB) — show actuals.
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
usedBytes: info.usedBytes,
|
usedBytes: info.usedBytes,
|
||||||
limitBytes: info.isPaid ? null : info.limitBytes,
|
limitBytes: info.limitBytes,
|
||||||
usedFormatted: formatBytes(info.usedBytes),
|
usedFormatted: formatBytes(info.usedBytes),
|
||||||
limitFormatted: info.isPaid ? "Unlimited" : formatBytes(info.limitBytes),
|
limitFormatted: formatBytes(info.limitBytes),
|
||||||
fraction: info.isPaid ? 0 : info.fraction,
|
fraction: info.fraction,
|
||||||
approaching: info.approaching,
|
approaching: info.approaching,
|
||||||
exceeded: info.exceeded,
|
exceeded: info.exceeded,
|
||||||
isPaid: info.isPaid,
|
isPaid: info.isPaid,
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ export function StorageMeter({ info: propInfo, className = "", compact = false }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!info || info.isPaid) return null;
|
// Show the meter for both free and premium — both now have a real cap.
|
||||||
|
if (!info) return null;
|
||||||
if (compact && !info.approaching && !info.exceeded) return null;
|
if (compact && !info.approaching && !info.exceeded) return null;
|
||||||
|
|
||||||
const pct = Math.min(info.fraction * 100, 100);
|
const pct = Math.min(info.fraction * 100, 100);
|
||||||
|
|
@ -84,7 +85,13 @@ export function StorageMeter({ info: propInfo, className = "", compact = false }
|
||||||
{info.exceeded && (
|
{info.exceeded && (
|
||||||
<p className="text-xs text-red-600 dark:text-red-400">
|
<p className="text-xs text-red-600 dark:text-red-400">
|
||||||
New uploads are paused. Your existing memories are safe.{" "}
|
New uploads are paused. Your existing memories are safe.{" "}
|
||||||
<a href="/settings#upgrade" className="underline font-medium">Upgrade</a> or delete some memories to continue.
|
{info.isPaid ? (
|
||||||
|
<>Delete some memories to free up space.</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<a href="/settings#upgrade" className="underline font-medium">Upgrade</a> or delete some memories to continue.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{info.approaching && !info.exceeded && (
|
{info.approaching && !info.exceeded && (
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ export { formatBytes } from "@/lib/format-bytes";
|
||||||
export const FREE_STORAGE_LIMIT_BYTES = 1_073_741_824; // 1 GiB
|
export const FREE_STORAGE_LIMIT_BYTES = 1_073_741_824; // 1 GiB
|
||||||
export const FREE_MEMBER_LIMIT = 2;
|
export const FREE_MEMBER_LIMIT = 2;
|
||||||
|
|
||||||
|
// Paid (premium) storage cap. Matches the seeded plan's storage grant (50 GiB).
|
||||||
|
// Kept here (not Infinity) so premium also has a ceiling and the meter shows X/50GB.
|
||||||
|
export const PAID_STORAGE_LIMIT_BYTES = 50 * 1_073_741_824; // 50 GiB
|
||||||
|
|
||||||
// Storage threshold at which we show a "approaching limit" nudge (80 %).
|
// Storage threshold at which we show a "approaching limit" nudge (80 %).
|
||||||
export const STORAGE_WARN_THRESHOLD = 0.8;
|
export const STORAGE_WARN_THRESHOLD = 0.8;
|
||||||
|
|
||||||
|
|
@ -129,15 +133,15 @@ export async function getStorageInfo(familyId: string): Promise<StorageUsageInfo
|
||||||
`;
|
`;
|
||||||
const paid = isPaidFamily(familyRow?.tier);
|
const paid = isPaidFamily(familyRow?.tier);
|
||||||
const usedBytes = await getFamilyStorageUsage(familyId);
|
const usedBytes = await getFamilyStorageUsage(familyId);
|
||||||
const limitBytes = paid ? Infinity : FREE_STORAGE_LIMIT_BYTES;
|
const limitBytes = paid ? PAID_STORAGE_LIMIT_BYTES : FREE_STORAGE_LIMIT_BYTES;
|
||||||
const fraction = paid ? 0 : usedBytes / FREE_STORAGE_LIMIT_BYTES;
|
const fraction = limitBytes > 0 ? usedBytes / limitBytes : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
usedBytes,
|
usedBytes,
|
||||||
limitBytes,
|
limitBytes,
|
||||||
fraction,
|
fraction,
|
||||||
approaching: !paid && fraction >= STORAGE_WARN_THRESHOLD && fraction < 1,
|
approaching: fraction >= STORAGE_WARN_THRESHOLD && fraction < 1,
|
||||||
exceeded: !paid && fraction >= 1,
|
exceeded: fraction >= 1,
|
||||||
isPaid: paid,
|
isPaid: paid,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -155,12 +159,8 @@ export async function checkStorageQuota(
|
||||||
SELECT tier FROM families WHERE id = ${familyId} LIMIT 1
|
SELECT tier FROM families WHERE id = ${familyId} LIMIT 1
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (isPaidFamily(familyRow?.tier)) {
|
const paid = isPaidFamily(familyRow?.tier);
|
||||||
const usedBytes = await getFamilyStorageUsage(familyId);
|
const limitBytes = paid ? PAID_STORAGE_LIMIT_BYTES : FREE_STORAGE_LIMIT_BYTES;
|
||||||
return { allowed: true, usedBytes, limitBytes: Infinity };
|
|
||||||
}
|
|
||||||
|
|
||||||
const limitBytes = FREE_STORAGE_LIMIT_BYTES;
|
|
||||||
const usedBytes = await getFamilyStorageUsage(familyId);
|
const usedBytes = await getFamilyStorageUsage(familyId);
|
||||||
|
|
||||||
if (wouldExceedQuota(usedBytes, declaredBytes, limitBytes)) {
|
if (wouldExceedQuota(usedBytes, declaredBytes, limitBytes)) {
|
||||||
|
|
@ -169,7 +169,9 @@ export async function checkStorageQuota(
|
||||||
reason: "storage_quota_exceeded",
|
reason: "storage_quota_exceeded",
|
||||||
usedBytes,
|
usedBytes,
|
||||||
limitBytes,
|
limitBytes,
|
||||||
message: `Storage quota exceeded. You have used ${formatBytes(usedBytes)} of your ${formatBytes(limitBytes)} limit. Delete some memories or upgrade to continue uploading.`,
|
message: paid
|
||||||
|
? `Storage limit reached. You have used ${formatBytes(usedBytes)} of your ${formatBytes(limitBytes)} premium storage.`
|
||||||
|
: `Storage quota exceeded. You have used ${formatBytes(usedBytes)} of your ${formatBytes(limitBytes)} limit. Delete some memories or upgrade to continue uploading.`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,14 +196,16 @@ export async function reconcileActualSize(
|
||||||
UPDATE memories SET size_bytes = ${actualBytes} WHERE id = ${memoryId} AND family_id = ${familyId}
|
UPDATE memories SET size_bytes = ${actualBytes} WHERE id = ${memoryId} AND family_id = ${familyId}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (isPaidFamily(familyRow?.tier)) return { keep: true };
|
const limitBytes = isPaidFamily(familyRow?.tier)
|
||||||
|
? PAID_STORAGE_LIMIT_BYTES
|
||||||
|
: FREE_STORAGE_LIMIT_BYTES;
|
||||||
|
|
||||||
// Re-derive usage with the updated size
|
// Re-derive usage with the updated size
|
||||||
const usedBytes = await getFamilyStorageUsage(familyId);
|
const usedBytes = await getFamilyStorageUsage(familyId);
|
||||||
if (usedBytes > FREE_STORAGE_LIMIT_BYTES) {
|
if (usedBytes > limitBytes) {
|
||||||
return {
|
return {
|
||||||
keep: false,
|
keep: false,
|
||||||
reason: `Actual file size (${formatBytes(actualBytes)}) pushed family over the ${formatBytes(FREE_STORAGE_LIMIT_BYTES)} quota.`,
|
reason: `Actual file size (${formatBytes(actualBytes)}) pushed family over the ${formatBytes(limitBytes)} quota.`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue