mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
Phase 3.A des feedback-rewards-and-identity-Plans. Direkter Reziprozitäts- Loop: User kriegt sofort etwas zurück fürs Mitwirken, Originalwunsch- Eulen werden beim Ship belohnt, Reagierer kriegen einen Anteil. mana-credits: - Neuer Endpoint POST /api/v1/internal/credits/grant + grantCredits() Service-Methode mit Idempotency via metadata.referenceId. - transaction_type-Enum erweitert um 'grant' (eigener Typ statt Mismatch mit 'refund'). - Migration 0001_grant_transaction_type.sql + partial-Index auf metadata->>'referenceId' für O(log n) Idempotency-Lookup. mana-analytics: - FeedbackService stempelt sofort +5 Credits beim createFeedback (top- level only, Replies bekommen nichts), wenn Mindest-20-Zeichen erfüllt und Rate-Limit (10/User/24h via feedback_grant_log) nicht überschritten. - adminUpdate triggert beim FRISCHEN Übergang nach 'completed': +500 Credits an Original-Wisher + +25 an alle, die mit 👍 oder 🚀 reagiert haben. Doppel-Pay strukturell unmöglich via referenceId (`<id>_shipped`, `<id>_reaction_<userId>`). - Founder-Whitelist via FEEDBACK_FOUNDER_USER_IDS env (verhindert Self-Reward). - Drop voteCount-Spalte (durch reactions/score seit 0002 ersetzt). - Migration 0003_grant_log_drop_vote_count.sql idempotent, lokal + prod eingespielt. Plan: docs/plans/feedback-rewards-and-identity.md (Phase 3.A-3.F). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
85 lines
3 KiB
TypeScript
85 lines
3 KiB
TypeScript
/**
|
|
* Internal routes — service-to-service endpoints (X-Service-Key auth)
|
|
*/
|
|
|
|
import { Hono } from 'hono';
|
|
import type { CreditsService } from '../services/credits';
|
|
import type { GiftCodeService } from '../services/gift-code';
|
|
import type { SyncBillingService } from '../services/sync-billing';
|
|
import {
|
|
internalUseCreditsSchema,
|
|
internalRefundSchema,
|
|
internalInitSchema,
|
|
internalRedeemPendingSchema,
|
|
internalReserveSchema,
|
|
internalCommitSchema,
|
|
internalRefundReservationSchema,
|
|
internalGrantSchema,
|
|
} from '../lib/validation';
|
|
|
|
export function createInternalRoutes(
|
|
creditsService: CreditsService,
|
|
giftCodeService: GiftCodeService,
|
|
syncBillingService: SyncBillingService
|
|
) {
|
|
return new Hono()
|
|
.get('/credits/balance/:userId', async (c) => {
|
|
const balance = await creditsService.getBalance(c.req.param('userId'));
|
|
return c.json(balance);
|
|
})
|
|
.post('/credits/use', async (c) => {
|
|
const body = internalUseCreditsSchema.parse(await c.req.json());
|
|
const { userId, ...params } = body;
|
|
const result = await creditsService.useCredits(userId, params);
|
|
return c.json(result);
|
|
})
|
|
.post('/credits/refund', async (c) => {
|
|
const body = internalRefundSchema.parse(await c.req.json());
|
|
const result = await creditsService.refundCredits(
|
|
body.userId,
|
|
body.amount,
|
|
body.description,
|
|
body.appId,
|
|
body.metadata
|
|
);
|
|
return c.json(result);
|
|
})
|
|
.post('/credits/grant', async (c) => {
|
|
const body = internalGrantSchema.parse(await c.req.json());
|
|
const result = await creditsService.grantCredits(body);
|
|
return c.json(result);
|
|
})
|
|
.post('/credits/init', async (c) => {
|
|
const body = internalInitSchema.parse(await c.req.json());
|
|
const balance = await creditsService.initializeBalance(body.userId);
|
|
return c.json(balance);
|
|
})
|
|
.post('/credits/reserve', async (c) => {
|
|
const body = internalReserveSchema.parse(await c.req.json());
|
|
const result = await creditsService.reserve(body.userId, body.amount, body.reason);
|
|
return c.json(result);
|
|
})
|
|
.post('/credits/commit', async (c) => {
|
|
const body = internalCommitSchema.parse(await c.req.json());
|
|
const result = await creditsService.commitReservation(body.reservationId, body.description);
|
|
return c.json(result);
|
|
})
|
|
.post('/credits/refund-reservation', async (c) => {
|
|
const body = internalRefundReservationSchema.parse(await c.req.json());
|
|
const result = await creditsService.refundReservation(body.reservationId);
|
|
return c.json(result);
|
|
})
|
|
.post('/gifts/redeem-pending', async (c) => {
|
|
const body = internalRedeemPendingSchema.parse(await c.req.json());
|
|
const result = await giftCodeService.redeemPendingForUser(body.userId, body.email);
|
|
return c.json(result);
|
|
})
|
|
.get('/sync/status/:userId', async (c) => {
|
|
const status = await syncBillingService.getSyncStatus(c.req.param('userId'));
|
|
return c.json(status);
|
|
})
|
|
.post('/sync/charge-recurring', async (c) => {
|
|
const result = await syncBillingService.chargeRecurring();
|
|
return c.json(result);
|
|
});
|
|
}
|