refactor(saas): remove the legacy credits engine (FE + Java) (#6687)

Complete legacy-credits teardown ("Group 3"). The per-user/per-team
credit model is fully superseded by PAYG (`wallet_ledger`) — confirmed
no PAYG code references it. Authorized to also remove the `TeamCredit`
pool + its monthly reset.

## Frontend (saas)
- Deleted `saas/hooks/useCredits.ts`, `apiKeys/hooks/useCredits.ts`,
`types/credits.ts`, `apiKeys/UsageSection.tsx`.
- `UseSession.tsx`: removed credit members (`creditBalance`,
`creditSummary`, `hasSufficientCredits`, `updateCredits`,
`refreshCredits`, `fetchCredits`) + the credit types + global
credit-update callback. **Kept** `isPro`/`refreshProStatus` and the
Supabase auth subscription listener.
- `services/apiClient.ts`: removed the dead `x-credits-remaining`
handler + low-credit plumbing (token-refresh / PAYG / 401 logic
untouched).
- Credit refs removed from `ApiKeys.tsx`, `AppConfigModal.tsx`,
`auth/teamSession.ts`.

## Java (:saas)
**Deleted (15):** `UserCredit`(+repo),
`TeamCredit`(+repo)+`TeamCreditService`, `CreditService`,
`CreditHeaderUtils`, `CreditResetScheduler`, `CreditController`,
`CreditInterceptorConfig`, `UnifiedCreditInterceptor`,
`CreditSuccessAdvice`, `CreditErrorAdvice`, `CreditConsumptionResult` (+
the CreditController test).

**Edited — stripped legacy credit side-effects, preserved
auth/role/AI/PAYG logic:**
- `AiCreate`/`AiProxyController`: dropped the
`X-Credits-Remaining`/`X-Credit-Source` response header (its only
consumer, the desktop credit system, was already removed).
- `SaasTeamService`: dropped UserCredit/TeamCredit init on team-create +
seat-update.
- `SupabaseAuthenticationFilter` / `SupabaseSecurityConfig`: dropped
`getOrCreateUserCredits` on signup + the credit field/CORS header.
- `UserRoleService`: dropped `resetCycleAllocationForRoleChange`;
`ROLE_PRO_USER` grant/revoke preserved.
- proprietary `UserRepository`: dropped
`findUsersWithApiKeyButNoCredits()`.
- Tests updated to drop credit mocks/refs.

## Kept / scope
- `isPro` / `is_pro` RPC / `ROLE_PRO_USER` (that's the separate Group-4
/ EE effort) and **all PAYG** are untouched.
- **No DB tables dropped.** `user_credits`/`team_credits` stay until a
later **gated** migration — which this PR unblocks (the JPA entities
that pinned them are gone).

## Verify
`:saas:compileJava` + `:saas:compileTestJava` pass; FE `tsc --noEmit`
(saas) + eslint clean; 0 stray artifacts; no residual source refs to the
deleted classes.

## Follow-up (not in this PR)
`ErrorTrackingService` (+
`UserErrorTracker`/`ProcessingErrorType`/`CreditsProperties`) is now a
dead island — its only callers were the deleted interceptors. Safe to
delete, but it cascades beyond the credit scope, so it's a separate
tidy-up.

Targets `feat/desktop-cloud-saas-reuse`.
This commit is contained in:
ConnorYoh
2026-06-17 14:11:06 +00:00
committed by GitHub
parent 4f26fdeb5c
commit 20c88feabb
50 changed files with 134 additions and 9700 deletions
+4 -104
View File
@@ -14,12 +14,6 @@ import type {
User as SupabaseUser,
AuthError,
} from "@supabase/supabase-js";
import {
CreditSummary,
SubscriptionInfo,
CreditCheckResult,
} from "@app/types/credits";
import { setGlobalCreditUpdateCallback } from "@app/services/apiClient";
import { synchronizeUserUpgrade } from "@app/services/userService";
import {
syncOAuthAvatar,
@@ -70,17 +64,11 @@ interface AuthContextType {
isAnonymous: boolean;
loading: boolean;
error: AuthError | null;
creditBalance: number | null;
subscription: SubscriptionInfo | null;
creditSummary: CreditSummary | null;
isPro: boolean | null;
profilePictureUrl: string | null;
profilePictureMetadata: ProfilePictureMetadata | null;
signOut: () => Promise<void>;
refreshSession: () => Promise<void>;
hasSufficientCredits: (requiredCredits: number) => CreditCheckResult;
updateCredits: (newBalance: number) => void;
refreshCredits: () => Promise<void>;
refreshProStatus: () => Promise<void>;
refreshProfilePicture: () => Promise<void>;
refreshProfilePictureMetadata: () => Promise<void>;
@@ -93,21 +81,11 @@ const AuthContext = createContext<AuthContextType>({
isAnonymous: false,
loading: true,
error: null,
creditBalance: null,
subscription: null,
creditSummary: null,
isPro: null,
profilePictureUrl: null,
profilePictureMetadata: null,
signOut: async () => {},
refreshSession: async () => {},
hasSufficientCredits: () => ({
hasSufficientCredits: false,
currentBalance: 0,
requiredCredits: 0,
}),
updateCredits: () => {},
refreshCredits: async () => {},
refreshProStatus: async () => {},
refreshProfilePicture: async () => {},
refreshProfilePictureMetadata: async () => {},
@@ -117,13 +95,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<AuthError | null>(null);
const [creditBalance, setCreditBalance] = useState<number | null>(null);
const [subscription, setSubscription] = useState<SubscriptionInfo | null>(
null,
);
const [creditSummary, setCreditSummary] = useState<CreditSummary | null>(
null,
);
const [isPro, setIsPro] = useState<boolean | null>(null);
const [profilePictureUrl, setProfilePictureUrl] = useState<string | null>(
null,
@@ -131,19 +102,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const [profilePictureMetadata, setProfilePictureMetadata] =
useState<ProfilePictureMetadata | null>(null);
// Legacy weekly-credits feed (GET /api/v1/credits) is dead. PAYG replaces it via
// useWallet() reading /api/v1/payg/wallet. Symbols are kept as no-ops so existing
// consumers of useAuth() that still destructure creditBalance / refreshCredits
// compile cleanly; values just stay null forever and refreshCredits is a noop.
// _ underscore on the param keeps the public signature stable for callers.
const fetchCredits = useCallback(async (_sessionToUse?: Session | null) => {
/* legacy credit fetch removed — see comment above */
}, []);
const refreshCredits = useCallback(async () => {
/* legacy credit refresh removed — useWallet() replaces this */
}, []);
const fetchProStatus = useCallback(
async (sessionToUse?: Session | null) => {
const currentSession = sessionToUse ?? session;
@@ -289,46 +247,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
await fetchProfilePictureMetadata();
}, [fetchProfilePictureMetadata]);
const updateCredits = useCallback(
(newBalance: number) => {
console.debug("[Auth Debug] Updating credit balance:", {
from: creditBalance,
to: newBalance,
});
setCreditBalance(newBalance);
// Also update the creditSummary if it exists
if (creditSummary) {
const updatedSummary: CreditSummary = {
...creditSummary,
creditsRemaining: newBalance,
currentCredits: newBalance,
};
setCreditSummary(updatedSummary);
}
},
[creditSummary],
);
const hasSufficientCredits = useCallback(
(requiredCredits: number): CreditCheckResult => {
const currentBalance = creditBalance ?? 0;
const hasSufficient = currentBalance >= requiredCredits;
console.debug("[Auth Debug] Credit check:", {
requiredCredits,
currentBalance,
hasSufficient,
});
return {
hasSufficientCredits: hasSufficient,
currentBalance,
requiredCredits,
shortfall: hasSufficient ? undefined : requiredCredits - currentBalance,
};
},
[creditBalance],
);
const refreshSession = async () => {
try {
setLoading(true);
@@ -372,11 +290,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}
};
// Set up global credit update callback
useEffect(() => {
setGlobalCreditUpdateCallback(updateCredits);
}, [updateCredits]);
useEffect(() => {
let mounted = true;
@@ -399,7 +312,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
});
setSession(data.session);
// Fetch credits, pro status, profile picture metadata, and profile picture using the session from the response
// Fetch pro status, profile picture metadata, and profile picture using the session from the response
if (data.session?.user) {
// Sync OAuth avatar in background; fetch the picture once the
// sync settles instead of guessing with a fixed delay.
@@ -413,7 +326,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
})
.then(() => fetchProfilePicture(data.session));
await fetchCredits(data.session);
await fetchProStatus(data.session);
await fetchProfilePictureMetadata(data.session);
}
@@ -458,10 +370,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
// Additional handling for specific events
if (event === "SIGNED_OUT") {
console.debug("[Auth Debug] User signed out, clearing session");
// Clear credit data, pro status, profile picture, and metadata on sign out
setCreditBalance(null);
setCreditSummary(null);
setSubscription(null);
// Clear pro status, profile picture, and metadata on sign out
setIsPro(null);
setProfilePictureUrl(null);
setProfilePictureMetadata(null);
@@ -490,7 +399,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
// Fetch user data in parallel
Promise.all([
fetchCredits(newSession),
fetchProStatus(newSession),
fetchProfilePictureMetadata(newSession),
]).then(() => {
@@ -506,10 +414,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}
} else if (event === "TOKEN_REFRESHED") {
console.debug("[Auth Debug] Token refreshed");
// Optionally refresh credits, pro status, profile picture metadata, and profile picture on token refresh
// Optionally refresh pro status, profile picture metadata, and profile picture on token refresh
if (newSession?.user) {
Promise.all([
fetchCredits(newSession),
fetchProStatus(newSession),
fetchProfilePictureMetadata(newSession),
fetchProfilePicture(newSession),
@@ -547,10 +454,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
"[Auth Debug] User upgrade synchronized successfully",
);
// Refresh credits, pro status, profile picture metadata, and profile picture after upgrade
// Refresh pro status, profile picture metadata, and profile picture after upgrade
if (newSession?.user) {
return Promise.all([
fetchCredits(newSession),
fetchProStatus(newSession),
fetchProfilePictureMetadata(newSession),
fetchProfilePicture(newSession),
@@ -589,17 +495,11 @@ export function AuthProvider({ children }: { children: ReactNode }) {
isAnonymous: Boolean(user?.is_anonymous),
loading,
error,
creditBalance,
subscription,
creditSummary,
isPro,
profilePictureUrl,
profilePictureMetadata,
signOut,
refreshSession,
hasSufficientCredits,
updateCredits,
refreshCredits,
refreshProStatus,
refreshProfilePicture,
refreshProfilePictureMetadata,