managarten/services/mana-credits/src/lib/validation.ts
Till JS 15deaf4e0a feat(services): create mana-credits service (Hono + Bun)
Extract the credit system from mana-core-auth into a standalone service.
Uses Hono framework on Bun runtime instead of NestJS.

Service includes:
- Personal credit balance with optimistic locking
- Immutable transaction ledger
- Stripe payment integration (PaymentIntents, Checkout Sessions)
- Guild shared pools with per-member spending limits
- Gift code system (simple, personalized, split, first_come, riddle)
- Service-to-service internal API (X-Service-Key auth)
- JWT validation via JWKS from mana-core-auth (jose library)

Architecture:
- 27 files, ~2.2k LOC (vs ~4.1k in NestJS)
- Drizzle ORM schemas adapted for standalone DB (no FK to auth tables)
- Zod validation instead of class-validator
- Manual service instantiation instead of NestJS DI
- Hono middleware for JWT + service key auth

Port: 3060
Database: mana_credits (separate from mana_auth)

Next steps: Update CreditClientService URL, update mana-core-auth
registration hooks, configure Docker + Cloudflare Tunnel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:08:43 +01:00

95 lines
3 KiB
TypeScript

/**
* Zod schemas for request body validation.
*/
import { z } from 'zod';
// ─── Credits ────────────────────────────────────────────────
export const useCreditsSchema = z.object({
amount: z.number().positive(),
appId: z.string().min(1),
description: z.string().min(1),
creditSource: z
.object({
type: z.literal('guild'),
guildId: z.string().min(1),
})
.optional(),
idempotencyKey: z.string().optional(),
metadata: z.record(z.unknown()).optional(),
});
export const purchaseCreditsSchema = z.object({
packageId: z.string().uuid(),
});
export const createPaymentLinkSchema = z.object({
packageId: z.string().uuid(),
quantity: z.number().int().positive().default(1),
});
// ─── Guild ──────────────────────────────────────────────────
export const fundGuildPoolSchema = z.object({
amount: z.number().positive(),
idempotencyKey: z.string().optional(),
});
export const setSpendingLimitSchema = z.object({
dailyLimit: z.number().int().positive().nullable().optional(),
monthlyLimit: z.number().int().positive().nullable().optional(),
});
// ─── Gifts ──────────────────────────────────────────────────
export const createGiftSchema = z.object({
totalCredits: z.number().int().positive().min(1).max(10000),
type: z.enum(['simple', 'personalized', 'split', 'first_come', 'riddle']).default('simple'),
totalPortions: z.number().int().positive().max(100).default(1),
targetEmail: z.string().email().optional(),
targetMatrixId: z.string().optional(),
riddleQuestion: z.string().max(200).optional(),
riddleAnswer: z.string().optional(),
message: z.string().max(500).optional(),
expirationDays: z.number().int().positive().optional(),
});
export const redeemGiftSchema = z.object({
riddleAnswer: z.string().optional(),
sourceAppId: z.string().optional(),
});
// ─── Internal (Service-to-Service) ──────────────────────────
export const internalUseCreditsSchema = z.object({
userId: z.string().min(1),
amount: z.number().positive(),
appId: z.string().min(1),
description: z.string().min(1),
creditSource: z
.object({
type: z.literal('guild'),
guildId: z.string().min(1),
})
.optional(),
idempotencyKey: z.string().optional(),
metadata: z.record(z.unknown()).optional(),
});
export const internalRefundSchema = z.object({
userId: z.string().min(1),
amount: z.number().positive(),
description: z.string().min(1),
appId: z.string().default('system'),
metadata: z.record(z.unknown()).optional(),
});
export const internalInitSchema = z.object({
userId: z.string().min(1),
});
export const internalRedeemPendingSchema = z.object({
userId: z.string().min(1),
email: z.string().email(),
});