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 info = await getStorageInfo(familyId);
|
||||
|
||||
// Both tiers now have a real cap (free 1 GB / premium 50 GB) — show actuals.
|
||||
return NextResponse.json({
|
||||
usedBytes: info.usedBytes,
|
||||
limitBytes: info.isPaid ? null : info.limitBytes,
|
||||
limitBytes: info.limitBytes,
|
||||
usedFormatted: formatBytes(info.usedBytes),
|
||||
limitFormatted: info.isPaid ? "Unlimited" : formatBytes(info.limitBytes),
|
||||
fraction: info.isPaid ? 0 : info.fraction,
|
||||
limitFormatted: formatBytes(info.limitBytes),
|
||||
fraction: info.fraction,
|
||||
approaching: info.approaching,
|
||||
exceeded: info.exceeded,
|
||||
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;
|
||||
|
||||
const pct = Math.min(info.fraction * 100, 100);
|
||||
|
|
@ -84,7 +85,13 @@ export function StorageMeter({ info: propInfo, className = "", compact = false }
|
|||
{info.exceeded && (
|
||||
<p className="text-xs text-red-600 dark:text-red-400">
|
||||
New uploads are paused. Your existing memories are safe.{" "}
|
||||
{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>
|
||||
)}
|
||||
{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_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 %).
|
||||
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 usedBytes = await getFamilyStorageUsage(familyId);
|
||||
const limitBytes = paid ? Infinity : FREE_STORAGE_LIMIT_BYTES;
|
||||
const fraction = paid ? 0 : usedBytes / FREE_STORAGE_LIMIT_BYTES;
|
||||
const limitBytes = paid ? PAID_STORAGE_LIMIT_BYTES : FREE_STORAGE_LIMIT_BYTES;
|
||||
const fraction = limitBytes > 0 ? usedBytes / limitBytes : 0;
|
||||
|
||||
return {
|
||||
usedBytes,
|
||||
limitBytes,
|
||||
fraction,
|
||||
approaching: !paid && fraction >= STORAGE_WARN_THRESHOLD && fraction < 1,
|
||||
exceeded: !paid && fraction >= 1,
|
||||
approaching: fraction >= STORAGE_WARN_THRESHOLD && fraction < 1,
|
||||
exceeded: fraction >= 1,
|
||||
isPaid: paid,
|
||||
};
|
||||
}
|
||||
|
|
@ -155,12 +159,8 @@ export async function checkStorageQuota(
|
|||
SELECT tier FROM families WHERE id = ${familyId} LIMIT 1
|
||||
`;
|
||||
|
||||
if (isPaidFamily(familyRow?.tier)) {
|
||||
const usedBytes = await getFamilyStorageUsage(familyId);
|
||||
return { allowed: true, usedBytes, limitBytes: Infinity };
|
||||
}
|
||||
|
||||
const limitBytes = FREE_STORAGE_LIMIT_BYTES;
|
||||
const paid = isPaidFamily(familyRow?.tier);
|
||||
const limitBytes = paid ? PAID_STORAGE_LIMIT_BYTES : FREE_STORAGE_LIMIT_BYTES;
|
||||
const usedBytes = await getFamilyStorageUsage(familyId);
|
||||
|
||||
if (wouldExceedQuota(usedBytes, declaredBytes, limitBytes)) {
|
||||
|
|
@ -169,7 +169,9 @@ export async function checkStorageQuota(
|
|||
reason: "storage_quota_exceeded",
|
||||
usedBytes,
|
||||
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}
|
||||
`;
|
||||
|
||||
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
|
||||
const usedBytes = await getFamilyStorageUsage(familyId);
|
||||
if (usedBytes > FREE_STORAGE_LIMIT_BYTES) {
|
||||
if (usedBytes > limitBytes) {
|
||||
return {
|
||||
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