feat(entitlements): delad hushålls-pott – medlemmar delar ägarens plan & skanningskvot (MIKRO 59)
CI / Typecheck, test & build (push) Failing after 43s
CI / Typecheck, test & build (push) Failing after 43s
This commit is contained in:
@@ -8,22 +8,41 @@ import {
|
|||||||
type SubscriptionSnapshot,
|
type SubscriptionSnapshot,
|
||||||
type TrialSnapshot,
|
type TrialSnapshot,
|
||||||
} from "@app/subscriptions";
|
} from "@app/subscriptions";
|
||||||
import { currentMonth } from "./helpers.js";
|
import { currentMonth, getActiveHouseholdId } from "./helpers.js";
|
||||||
import { errors } from "./errors.js";
|
import { errors } from "./errors.js";
|
||||||
|
|
||||||
|
/** Vem "äger" kvoten? Hela hushållet delar ägarens plan + pott.
|
||||||
|
* Ensam användare = sig själv (äger sitt eget hushåll). */
|
||||||
|
async function billingUserFor(db: Database, userId: string): Promise<string> {
|
||||||
|
const householdId = await getActiveHouseholdId(db, userId);
|
||||||
|
if (!householdId) return userId;
|
||||||
|
const [owner] = await db
|
||||||
|
.select({ userId: schema.householdMembers.userId })
|
||||||
|
.from(schema.householdMembers)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.householdMembers.householdId, householdId),
|
||||||
|
eq(schema.householdMembers.role, "owner"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
return owner?.userId ?? userId;
|
||||||
|
}
|
||||||
|
|
||||||
/** Ladda och beräkna entitlements för en användare. Backend = source of truth (spec §61.14). */
|
/** Ladda och beräkna entitlements för en användare. Backend = source of truth (spec §61.14). */
|
||||||
export async function loadEntitlements(db: Database, userId: string): Promise<Entitlements> {
|
export async function loadEntitlements(db: Database, userId: string): Promise<Entitlements> {
|
||||||
|
const billingUserId = await billingUserFor(db, userId);
|
||||||
const [subRow] = await db
|
const [subRow] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(schema.subscriptions)
|
.from(schema.subscriptions)
|
||||||
.where(eq(schema.subscriptions.userId, userId))
|
.where(eq(schema.subscriptions.userId, billingUserId))
|
||||||
.orderBy(desc(schema.subscriptions.updatedAt))
|
.orderBy(desc(schema.subscriptions.updatedAt))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
const [trialRow] = await db
|
const [trialRow] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(schema.trials)
|
.from(schema.trials)
|
||||||
.where(eq(schema.trials.userId, userId))
|
.where(eq(schema.trials.userId, billingUserId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
const [usageRow] = await db
|
const [usageRow] = await db
|
||||||
@@ -31,7 +50,7 @@ export async function loadEntitlements(db: Database, userId: string): Promise<En
|
|||||||
.from(schema.aiUsageCounters)
|
.from(schema.aiUsageCounters)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(schema.aiUsageCounters.userId, userId),
|
eq(schema.aiUsageCounters.userId, billingUserId),
|
||||||
eq(schema.aiUsageCounters.month, currentMonth()),
|
eq(schema.aiUsageCounters.month, currentMonth()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -69,7 +88,8 @@ export async function loadEntitlementsWithToken(
|
|||||||
|
|
||||||
/** Kräv AI-kvot och räkna upp användningen atomiskt. */
|
/** Kräv AI-kvot och räkna upp användningen atomiskt. */
|
||||||
export async function consumeAiScan(db: Database, userId: string): Promise<void> {
|
export async function consumeAiScan(db: Database, userId: string): Promise<void> {
|
||||||
const ent = await loadEntitlements(db, userId);
|
const billingUserId = await billingUserFor(db, userId);
|
||||||
|
const ent = await loadEntitlements(db, billingUserId);
|
||||||
if (ent.aiScansUsedThisMonth >= ent.aiScansPerMonth) {
|
if (ent.aiScansUsedThisMonth >= ent.aiScansPerMonth) {
|
||||||
throw errors.quotaExceeded(
|
throw errors.quotaExceeded(
|
||||||
`Månadens AI-skanningar är slut (${ent.aiScansPerMonth} st). Uppgradera för fler.`,
|
`Månadens AI-skanningar är slut (${ent.aiScansPerMonth} st). Uppgradera för fler.`,
|
||||||
@@ -78,7 +98,7 @@ export async function consumeAiScan(db: Database, userId: string): Promise<void>
|
|||||||
const month = currentMonth();
|
const month = currentMonth();
|
||||||
await db
|
await db
|
||||||
.insert(schema.aiUsageCounters)
|
.insert(schema.aiUsageCounters)
|
||||||
.values({ userId, month, aiScans: 1 })
|
.values({ userId: billingUserId, month, aiScans: 1 })
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: [schema.aiUsageCounters.userId, schema.aiUsageCounters.month],
|
target: [schema.aiUsageCounters.userId, schema.aiUsageCounters.month],
|
||||||
set: { aiScans: (await import("drizzle-orm")).sql`${schema.aiUsageCounters.aiScans} + 1` },
|
set: { aiScans: (await import("drizzle-orm")).sql`${schema.aiUsageCounters.aiScans} + 1` },
|
||||||
|
|||||||
Reference in New Issue
Block a user