refactor(credits): route credit calls to mana-credits service

Update consumers to call the new standalone mana-credits service instead
of the credit endpoints embedded in mana-core-auth.

Changes:
- CreditClientService: Add getCreditsUrl() reading MANA_CREDITS_URL
  (falls back to MANA_CORE_AUTH_URL for backward compatibility).
  All credit calls now use /api/v1/internal/* endpoints.
- BetterAuthService: Replace direct DB inserts for credit balance and
  guild pool init with HTTP calls to mana-credits internal API.
  Replace local gift redemption with HTTP call.
- .env.development: Add MANA_CREDITS_URL=http://localhost:3060
- CLAUDE.md: Add mana-credits to services list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-27 22:13:21 +01:00
parent 64f7f768eb
commit b0009c200b
4 changed files with 59 additions and 33 deletions

View file

@ -23,7 +23,9 @@ PUBLIC_GLITCHTIP_DSN=
# Mana Core Auth Service
MANA_CORE_AUTH_URL=http://localhost:3001
# Service key for bot-to-auth communication (Matrix-SSO-Link)
# Mana Credits Service
MANA_CREDITS_URL=http://localhost:3060
# Service key for service-to-service communication
MANA_CORE_SERVICE_KEY=dev-service-key-for-bot-sso-2024
# WebAuthn / Passkeys (localhost for dev, mana.how for production)

View file

@ -130,6 +130,7 @@ manacore-monorepo/
│ └── {game-name}/ # Individual games
├── services/ # Standalone microservices
│ ├── mana-core-auth/ # Central authentication service
│ ├── mana-credits/ # Credit system (Hono + Bun, extracted from auth)
│ ├── mana-search/ # Central search & content extraction (NestJS, legacy)
│ ├── mana-search-go/ # Central search & content extraction (Go, active)
│ ├── mana-crawler/ # Web crawler service

View file

@ -35,6 +35,18 @@ export class CreditClientService {
);
}
/**
* Get the credits service URL. Uses MANA_CREDITS_URL if available,
* falls back to MANA_CORE_AUTH_URL for backward compatibility.
*/
private getCreditsUrl(): string {
return (
this.configService?.get<string>('MANA_CREDITS_URL') ||
process.env.MANA_CREDITS_URL ||
this.getAuthUrl()
);
}
private getServiceKey(): string {
return (
this.options?.serviceKey ||
@ -76,7 +88,7 @@ export class CreditClientService {
}
async getBalance(userId: string): Promise<CreditBalance> {
const authUrl = this.getAuthUrl();
const creditsUrl = this.getCreditsUrl();
const serviceKey = this.getServiceKey();
if (!serviceKey) {
@ -89,7 +101,7 @@ export class CreditClientService {
}
try {
const response = await fetch(`${authUrl}/api/v1/credits/balance/${userId}`, {
const response = await fetch(`${creditsUrl}/api/v1/internal/credits/balance/${userId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
@ -127,7 +139,7 @@ export class CreditClientService {
metadata?: Record<string, any>,
creditSource?: { type: 'personal' } | { type: 'guild'; guildId: string }
): Promise<boolean> {
const authUrl = this.getAuthUrl();
const creditsUrl = this.getCreditsUrl();
const serviceKey = this.getServiceKey();
if (!serviceKey) {
@ -136,7 +148,7 @@ export class CreditClientService {
}
try {
const response = await fetch(`${authUrl}/api/v1/credits/use`, {
const response = await fetch(`${creditsUrl}/api/v1/internal/credits/use`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -179,7 +191,7 @@ export class CreditClientService {
description: string,
metadata?: Record<string, any>
): Promise<boolean> {
const authUrl = this.getAuthUrl();
const creditsUrl = this.getCreditsUrl();
const serviceKey = this.getServiceKey();
if (!serviceKey) {
@ -188,7 +200,7 @@ export class CreditClientService {
}
try {
const response = await fetch(`${authUrl}/api/v1/credits/refund`, {
const response = await fetch(`${creditsUrl}/api/v1/internal/credits/refund`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View file

@ -158,22 +158,29 @@ export class BetterAuthService {
// Create personal credit balance
await this.createPersonalCreditBalance(user.id);
// Redeem any pending gift codes sent to this email
if (this.giftCodeService) {
try {
const giftResult = await this.giftCodeService.redeemPendingGifts(user.id, dto.email);
if (giftResult.redeemedCount > 0) {
// Redeem any pending gift codes via mana-credits service
try {
const creditsUrl = process.env.MANA_CREDITS_URL || 'http://localhost:3060';
const serviceKey = process.env.MANA_CORE_SERVICE_KEY || '';
const giftRes = await fetch(`${creditsUrl}/api/v1/internal/gifts/redeem-pending`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Service-Key': serviceKey },
body: JSON.stringify({ userId: user.id, email: dto.email }),
});
if (giftRes.ok) {
const giftResult = await giftRes.json();
if (giftResult.redeemed > 0) {
this.logger.log('Redeemed pending gifts on registration', {
userId: user.id,
redeemedCount: giftResult.redeemedCount,
redeemedCount: giftResult.redeemed,
totalCredits: giftResult.totalCredits,
});
}
} catch (error) {
this.logger.warn('Failed to redeem pending gifts (non-critical)', {
error: error instanceof Error ? error.message : 'Unknown error',
});
}
} catch (error) {
this.logger.warn('Failed to redeem pending gifts via mana-credits (non-critical)', {
error: error instanceof Error ? error.message : 'Unknown error',
});
}
return {
@ -1754,37 +1761,41 @@ export class BetterAuthService {
* @param userId - User ID
* @private
*/
/**
* Initialize credit balance via mana-credits service.
* Non-critical lazy init on first access if this fails.
*/
private async createPersonalCreditBalance(userId: string) {
const db = getDb(this.databaseUrl);
try {
await db.insert(balances).values({
userId: userId as any, // Cast to handle UUID type
balance: 0,
totalEarned: 0,
totalSpent: 0,
const creditsUrl = process.env.MANA_CREDITS_URL || 'http://localhost:3060';
const serviceKey = process.env.MANA_CORE_SERVICE_KEY || '';
await fetch(`${creditsUrl}/api/v1/internal/credits/init`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Service-Key': serviceKey },
body: JSON.stringify({ userId }),
});
} catch (error) {
this.logger.warn('Failed to create personal credit balance (non-critical)', {
this.logger.warn('Failed to init credit balance via mana-credits (non-critical)', {
error: error instanceof Error ? error.message : 'Unknown error',
});
// Don't throw - this is a non-critical operation
}
}
/**
* Initialize a guild pool for an organization.
* Non-critical if it fails, the pool can be created later.
* Initialize guild pool via mana-credits service.
* Non-critical lazy init on first access if this fails.
*/
private async initializeGuildPool(organizationId: string) {
const db = getDb(this.databaseUrl);
try {
await db.insert(guildPools).values({
organizationId,
const creditsUrl = process.env.MANA_CREDITS_URL || 'http://localhost:3060';
const serviceKey = process.env.MANA_CORE_SERVICE_KEY || '';
await fetch(`${creditsUrl}/api/v1/internal/guild-pool/init`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Service-Key': serviceKey },
body: JSON.stringify({ organizationId }),
});
} catch (error) {
this.logger.warn('Failed to initialize guild pool (non-critical)', {
this.logger.warn('Failed to init guild pool via mana-credits (non-critical)', {
organizationId,
error: error instanceof Error ? error.message : 'Unknown error',
});