mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 19:46:42 +02:00
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>
95 lines
3 KiB
TypeScript
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(),
|
|
});
|