refactor(credits): simplify credit system — remove productivity credits, guild pools, complex gift types

The credit system was overengineered for the local-first architecture:
- Productivity micro-credits (task/event/contact creation at 0.02 credits) made no sense
  since these operations happen locally in IndexedDB with zero server cost and were never enforced
- Guild pool system (6 DB tables, spending limits, membership checks) had no active users
- Gift system had 5 types (simple/personalized/split/first_come/riddle) when 2 suffice

Now credits are only charged for operations that actually cost money: AI API calls and
premium features (sync, exports). This makes the value proposition clear to users.

Changes:
- Remove 8 productivity operations + CreditCategory.PRODUCTIVITY from @mana/credits
- Delete guild pool service, routes, schema (3 files); remove guild refs from 8 backend files
- Simplify gifts to simple + personalized only; remove bcrypt/riddle/portions logic
- Update all frontend pages (credits dashboard, gift create/redeem, public gift page)
- Update shared-hono consumeCredits() to remove creditSource parameter
- Update mana-credits CLAUDE.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-10 19:08:42 +02:00
parent 29ad31c4ed
commit e068335dd4
32 changed files with 143 additions and 922 deletions

View file

@ -23,7 +23,6 @@ export {
getOperationsByCategory,
calculateBulkCost,
isFreeOperation,
isMicroCreditOperation,
isAiOperation,
formatCreditCost,
getPricingTable,

View file

@ -58,28 +58,6 @@ export enum CreditOperationType {
AI_SUGGESTIONS = 'ai_suggestions',
AI_ENRICHMENT = 'ai_enrichment',
// -------------------------------------------------------------------------
// Productivity Operations (Micro Credits: 0.01-0.10)
// -------------------------------------------------------------------------
// Todo
TASK_CREATE = 'task_create',
PROJECT_CREATE = 'project_create',
// Calendar
EVENT_CREATE = 'event_create',
CALENDAR_CREATE = 'calendar_create',
// Contacts
CONTACT_CREATE = 'contact_create',
// Zitare
COLLECTION_CREATE = 'collection_create',
// Presi
PRESENTATION_CREATE = 'presentation_create',
SLIDE_CREATE = 'slide_create',
// -------------------------------------------------------------------------
// Premium Features (Standard Credits: 0.5-5)
// -------------------------------------------------------------------------
@ -134,20 +112,6 @@ export const CREDIT_COSTS: Record<CreditOperationType, number> = {
[CreditOperationType.AI_SUGGESTIONS]: 2,
[CreditOperationType.AI_ENRICHMENT]: 2,
// Productivity Operations (Micro Credits)
[CreditOperationType.TASK_CREATE]: 0.02,
[CreditOperationType.PROJECT_CREATE]: 0.1,
[CreditOperationType.EVENT_CREATE]: 0.02,
[CreditOperationType.CALENDAR_CREATE]: 0.1,
[CreditOperationType.CONTACT_CREATE]: 0.02,
[CreditOperationType.COLLECTION_CREATE]: 0.1,
[CreditOperationType.PRESENTATION_CREATE]: 0.5,
[CreditOperationType.SLIDE_CREATE]: 0.02,
// Premium Features
[CreditOperationType.CALDAV_SYNC]: 0.5,
[CreditOperationType.GOOGLE_SYNC]: 0.5,
@ -328,64 +292,6 @@ export const OPERATION_METADATA: Record<CreditOperationType, OperationMetadata>
app: 'contacts',
},
// Productivity - Todo
[CreditOperationType.TASK_CREATE]: {
name: 'Create Task',
description: 'Create a new task',
category: CreditCategory.PRODUCTIVITY,
app: 'todo',
},
[CreditOperationType.PROJECT_CREATE]: {
name: 'Create Project',
description: 'Create a new project',
category: CreditCategory.PRODUCTIVITY,
app: 'todo',
},
// Productivity - Calendar
[CreditOperationType.EVENT_CREATE]: {
name: 'Create Event',
description: 'Create a calendar event',
category: CreditCategory.PRODUCTIVITY,
app: 'calendar',
},
[CreditOperationType.CALENDAR_CREATE]: {
name: 'Create Calendar',
description: 'Create a new calendar',
category: CreditCategory.PRODUCTIVITY,
app: 'calendar',
},
// Productivity - Contacts
[CreditOperationType.CONTACT_CREATE]: {
name: 'Create Contact',
description: 'Create a new contact',
category: CreditCategory.PRODUCTIVITY,
app: 'contacts',
},
// Productivity - Zitare
[CreditOperationType.COLLECTION_CREATE]: {
name: 'Create Collection',
description: 'Create a quote collection',
category: CreditCategory.PRODUCTIVITY,
app: 'zitare',
},
// Productivity - Presi
[CreditOperationType.PRESENTATION_CREATE]: {
name: 'Create Presentation',
description: 'Create a new presentation',
category: CreditCategory.PRODUCTIVITY,
app: 'presi',
},
[CreditOperationType.SLIDE_CREATE]: {
name: 'Create Slide',
description: 'Add a slide to a presentation',
category: CreditCategory.PRODUCTIVITY,
app: 'presi',
},
// Premium - Sync
[CreditOperationType.CALDAV_SYNC]: {
name: 'CalDAV Sync',
@ -502,16 +408,6 @@ export function isFreeOperation(operation: CreditOperationType): boolean {
return CREDIT_COSTS[operation] === 0;
}
/**
* Check if an operation is a micro-credit operation (< 0.5 credits).
* @param operation The operation type
* @returns True if micro-credit operation
*/
export function isMicroCreditOperation(operation: CreditOperationType): boolean {
const cost = CREDIT_COSTS[operation];
return cost > 0 && cost < 0.5;
}
/**
* Check if an operation is an AI operation.
* @param operation The operation type

View file

@ -1,9 +1,5 @@
<script lang="ts">
import {
getPricingTable,
CreditCategory,
type CreditOperationType,
} from './operations';
import { getPricingTable, CreditCategory, type CreditOperationType } from './operations';
interface Props {
/** The app to show pricing for (e.g., 'todo', 'chat', 'calendar') */
@ -19,7 +15,6 @@
costLabel?: string;
freeLabel?: string;
aiLabel?: string;
productivityLabel?: string;
premiumLabel?: string;
creditsLabel?: string;
}
@ -33,7 +28,6 @@
costLabel = 'Cost',
freeLabel = 'Free',
aiLabel = 'AI Features',
productivityLabel = 'Create',
premiumLabel = 'Premium',
creditsLabel = 'Credits',
}: Props = $props();
@ -62,8 +56,6 @@
switch (category) {
case CreditCategory.AI:
return aiLabel;
case CreditCategory.PRODUCTIVITY:
return productivityLabel;
case CreditCategory.PREMIUM:
return premiumLabel;
default:
@ -75,8 +67,6 @@
switch (category) {
case CreditCategory.AI:
return 'M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z';
case CreditCategory.PRODUCTIVITY:
return 'M12 4.5v15m7.5-7.5h-15';
case CreditCategory.PREMIUM:
return 'M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z';
default:

View file

@ -11,6 +11,7 @@
],
"scripts": {
"build": "tsc",
"prepare": "tsc",
"clean": "rm -rf dist",
"test": "vitest run",
"test:watch": "vitest",

View file

@ -85,8 +85,7 @@ export async function consumeCredits(
operation: string,
amount: number,
description: string,
metadata?: Record<string, unknown>,
creditSource?: { type: 'guild'; guildId: string }
metadata?: Record<string, unknown>
): Promise<boolean> {
const result = await callCredits('/api/v1/internal/credits/use', {
method: 'POST',
@ -96,7 +95,6 @@ export async function consumeCredits(
appId: APP_ID(),
description,
metadata: { operation, ...metadata },
...(creditSource && { creditSource }),
}),
});
return !!result;

View file

@ -24,7 +24,8 @@ export type DragType =
| 'note'
| 'transaction'
| 'place'
| 'dream';
| 'dream'
| 'journal-entry';
export interface DragPayload<T = Record<string, unknown>> {
type: DragType;