Skip to content

feat/fix: AI credits, image validation, per-tenant moderation, atomic persistence - #1

Closed
popsman01 wants to merge 4 commits into
masterfrom
fix/ai-result-atomic-persist
Closed

feat/fix: AI credits, image validation, per-tenant moderation, atomic persistence#1
popsman01 wants to merge 4 commits into
masterfrom
fix/ai-result-atomic-persist

Conversation

@popsman01

Copy link
Copy Markdown
Owner

Summary

This PR resolves four issues across the AI and billing subsystems.


hman38705#627 — Deduct credits proportional to actual AI token usage

Files: backend/src/services/AIService.ts, backend/src/services/BillingService.ts, backend/src/routes/ai.ts, backend/src/workers/index.ts

  • Added BillingService.deductCreditsForTokens(userId, tokens) — 1 credit per token consumed.
  • Updated AIService.generateContent to return { text, totalTokens } and read res.usageMetadata.totalTokenCount from the Gemini response.
  • Credit deduction now happens post-call inside the service using the actual token count, replacing the flat-rate requireCredits('ai:generate') pre-call middleware on /ai/analyze-image.
  • Updated all callers (generateCaption, generateReplies, analyzeContent, workers) to destructure { text } from the new return type.
  • Tests: 6 tests — short/long token deduction, insufficient credits error, no-userId skip.

Closes hman38705#627


hman38705#628 — Validate image size before sending to Gemini API

Files: backend/src/services/geminiService.ts, backend/jest.config.js

  • Added ValidationError class (extends GeminiServiceError, code VALIDATION_ERROR).
  • analyzeImage now computes the decoded byte size from the base64 string (accounting for padding) and throws ValidationError for images exceeding the 20 MB limit before any API call is made.
  • Added a dedicated gemini-validation Jest project that bypasses the global geminiService stub so tests run against the real implementation.
  • Tests: 3 tests — below limit, exactly at limit, above limit.

Closes hman38705#628


hman38705#629 — Support per-tenant moderation sensitivity thresholds

Files: backend/src/services/ModerationService.ts, backend/src/services/DynamicConfigService.ts

  • Added ConfigKey.MODERATION_SENSITIVITY to the DynamicConfigService enum.
  • getSensitivity(tenantId?) now checks dynamicConfigService.get(tenant:${tenantId}:MODERATION_SENSITIVITY) first, then falls back to process.env.MODERATION_SENSITIVITY.
  • Threaded tenantId through ModerationService.moderate(text, tenantId?) — fully backward-compatible (parameter is optional).
  • Tests: 4 tests — high/low tenant thresholds, env fallback, two tenants receiving different thresholds for identical content.

Closes hman38705#629


hman38705#630 — Persist AI generation result inside the job transaction

Files: backend/src/workers/index.ts

  • Imported withTransaction and TxClient from backend/src/lib/transaction.ts.
  • persistAIResult now accepts an optional TxClient and uses upsert keyed on jobId (update: {} is a no-op) — prevents duplicate records when a retried job re-runs after a crash between generation and persistence.
  • All 5 AI processors (generate-caption, generate-hashtags, generate-content, analyze-sentiment, translate-content) now call withTransaction(async (tx) => persistAIResult(job, output, tx)).
  • Tests: 3 tests — success path, crash/error propagation, no-duplicate on retry.

Closes hman38705#630


Testing

All new tests pass:

  • aiProportionalCredits.test.ts — 6 passing
  • geminiImageValidation.test.ts — 3 passing
  • moderationPerTenant.test.ts — 4 passing
  • aiAtomicPersist.test.ts — 3 passing

…#627)

- Add BillingService.deductCreditsForTokens(userId, tokens) — 1 credit per token
- Update AIService.generateContent to return {text, totalTokens} and deduct
  credits post-call using res.usageMetadata.totalTokenCount
- Remove requireCredits('ai:generate') pre-call middleware from /ai/analyze-image
- Update workers to destructure {text} from generateContent result
- Add 6 tests: short/long token deduction, insufficient credits, no-userId skip
- Add ValidationError class (extends GeminiServiceError, code: VALIDATION_ERROR)
- Reject images > 20 MB before the API call with a clear error message
- Add gemini-validation Jest project to bypass the global geminiService mock
- Add 3 tests: below limit, at limit, above limit
…#629)

- Add ConfigKey.MODERATION_SENSITIVITY to DynamicConfigService
- Update getSensitivity(tenantId?) to check tenant:ID:MODERATION_SENSITIVITY
  in DynamicConfigService before falling back to env var
- Thread tenantId through ModerationService.moderate(text, tenantId?)
- Add 4 tests: high/low tenant thresholds, env fallback, cross-tenant diff
- Import withTransaction and TxClient in workers/index.ts
- Update persistAIResult to accept optional TxClient and use upsert
  keyed on jobId (update:{} no-op prevents duplicates on retry)
- Wrap persistence in withTransaction in all 5 AI processors
- Add 3 tests: success path, crash propagation, no-duplicate on retry
@popsman01 popsman01 closed this Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant