mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
chore(memoro): remove old NestJS backends (Phase 8+9)
Delete apps/memoro/apps/backend/ (NestJS) and apps/memoro/apps/audio-backend/ (NestJS) — all functionality has been ported to the new Hono/Bun servers (apps/server/ and apps/audio-server/). Also clean up root and memoro package.json scripts to remove references to the old @memoro/backend and @memoro/audio-backend packages. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d097a9d8f0
commit
3fa218cbe0
430 changed files with 1190 additions and 35048 deletions
506
apps/cards/CREDIT_SYSTEM.md
Normal file
506
apps/cards/CREDIT_SYSTEM.md
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
# Manadeck Credit System
|
||||
|
||||
This document explains how the Mana Core credit system is integrated into Manadeck.
|
||||
|
||||
## Overview
|
||||
|
||||
Manadeck uses **Mana** as its credit currency to charge for operations like deck creation, card generation, and AI features. The credit system is powered by [Mana Core](https://github.com/Memo-2023/mana-core-nestjs-package), which provides:
|
||||
|
||||
- Credit validation before operations
|
||||
- Credit consumption after successful operations
|
||||
- Real-time balance tracking
|
||||
- Transaction history
|
||||
- App-level usage tracking
|
||||
|
||||
## Credit Costs
|
||||
|
||||
| Operation | Cost (Mana) | Description |
|
||||
|-----------|-------------|-------------|
|
||||
| Deck Creation | 10 | Create a new deck |
|
||||
| Card Creation | 2 | Add a single card to a deck |
|
||||
| AI Card Generation | 5 | Generate a card using AI |
|
||||
| Deck Export | 3 | Export a deck to various formats |
|
||||
|
||||
These costs are defined in `backend/src/config/credit-operations.ts`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Frontend │
|
||||
│ (React Native)
|
||||
└─────┬───────┘
|
||||
│ 1. Create Deck Request
|
||||
│ POST /api/decks
|
||||
│ { name, description }
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Backend │
|
||||
│ (NestJS) │
|
||||
├─────────────┤
|
||||
│ API │ 2. Validate Credits (10 mana)
|
||||
│ Controller │ ├─ Has credits? → Continue
|
||||
│ │ └─ No credits? → Return 400
|
||||
│ │
|
||||
│ │ 3. Create Deck (business logic)
|
||||
│ │
|
||||
│ │ 4. Consume Credits (10 mana)
|
||||
│ │
|
||||
│ │ 5. Return Success + Credits Used
|
||||
└─────┬───────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Mana Core │ - Validate balance
|
||||
│ Service │ - Deduct credits
|
||||
│ │ - Record transaction
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## Backend Integration
|
||||
|
||||
### 1. Service Key Configuration
|
||||
|
||||
The backend needs a service key from Mana Core to perform credit operations.
|
||||
|
||||
**backend/.env**:
|
||||
```env
|
||||
SERVICE_KEY=your-service-key-from-mana-core
|
||||
```
|
||||
|
||||
**backend/src/app.module.ts**:
|
||||
```typescript
|
||||
ManaCoreModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
serviceKey: configService.get<string>('SERVICE_KEY', ''),
|
||||
// ... other config
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Credit Operations
|
||||
|
||||
**backend/src/config/credit-operations.ts**:
|
||||
```typescript
|
||||
export enum CreditOperationType {
|
||||
DECK_CREATION = 'deck_creation',
|
||||
CARD_CREATION = 'card_creation',
|
||||
// Add more as needed
|
||||
}
|
||||
|
||||
export const CREDIT_COSTS: Record<CreditOperationType, number> = {
|
||||
[CreditOperationType.DECK_CREATION]: 10,
|
||||
[CreditOperationType.CARD_CREATION]: 2,
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Controller Implementation
|
||||
|
||||
**backend/src/controllers/api.controller.ts**:
|
||||
```typescript
|
||||
import { CreditClientService } from '@mana-core/nestjs-integration/services';
|
||||
import { CreditOperationType, getCreditCost } from '../config/credit-operations';
|
||||
|
||||
@Controller('api')
|
||||
@UseGuards(AuthGuard)
|
||||
export class ApiController {
|
||||
constructor(private readonly creditClient: CreditClientService) {}
|
||||
|
||||
@Post('decks')
|
||||
async createDeck(@CurrentUser() user: any, @Body() deckData: any) {
|
||||
const operationType = CreditOperationType.DECK_CREATION;
|
||||
const creditCost = getCreditCost(operationType);
|
||||
|
||||
// 1. Validate credits BEFORE operation
|
||||
const validation = await this.creditClient.validateCredits(
|
||||
user.id,
|
||||
operationType,
|
||||
creditCost,
|
||||
);
|
||||
|
||||
if (!validation.hasCredits) {
|
||||
throw new BadRequestException({
|
||||
error: 'insufficient_credits',
|
||||
message: `Insufficient mana. Required: ${creditCost}, Available: ${validation.availableCredits}`,
|
||||
requiredCredits: creditCost,
|
||||
availableCredits: validation.availableCredits,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Perform the operation
|
||||
const newDeck = await this.createDeckInDatabase(deckData);
|
||||
|
||||
// 3. Consume credits AFTER success
|
||||
await this.creditClient.consumeCredits(
|
||||
user.id,
|
||||
operationType,
|
||||
creditCost,
|
||||
`Created deck: ${deckData.name}`,
|
||||
{ deckId: newDeck.id },
|
||||
);
|
||||
|
||||
return { success: true, deck: newDeck, creditsUsed: creditCost };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Credit Balance Endpoint
|
||||
|
||||
Get user's current credit balance:
|
||||
|
||||
```typescript
|
||||
@Get('credits/balance')
|
||||
async getCreditBalance(@CurrentUser() user: any) {
|
||||
const balance = await this.creditClient.getCreditBalance(user.id);
|
||||
return {
|
||||
userId: user.id,
|
||||
balance: balance.balance || 0,
|
||||
currency: 'mana',
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
### 1. Types
|
||||
|
||||
**apps/mobile/types/credits.ts**:
|
||||
```typescript
|
||||
export interface InsufficientCreditsError {
|
||||
error: 'insufficient_credits';
|
||||
message: string;
|
||||
requiredCredits: number;
|
||||
availableCredits: number;
|
||||
operation?: string;
|
||||
}
|
||||
|
||||
export function isInsufficientCreditsError(error: any): boolean {
|
||||
return error && error.error === 'insufficient_credits';
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Credit Service
|
||||
|
||||
**apps/mobile/services/creditService.ts**:
|
||||
```typescript
|
||||
export const creditService = {
|
||||
async getBalance(): Promise<number> {
|
||||
const response = await get(`${API_URL}/api/credits/balance`);
|
||||
return response.balance || 0;
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Insufficient Credits Modal
|
||||
|
||||
**apps/mobile/components/InsufficientCreditsModal.tsx**:
|
||||
|
||||
A pre-built modal component that displays:
|
||||
- Required vs available credits
|
||||
- Shortfall amount
|
||||
- "Get More Mana" button (optional)
|
||||
- Cancel button
|
||||
|
||||
### 4. Hook for Easy Integration
|
||||
|
||||
**apps/mobile/hooks/useInsufficientCredits.ts**:
|
||||
```typescript
|
||||
export function useInsufficientCredits() {
|
||||
// ... state management
|
||||
|
||||
return {
|
||||
visible,
|
||||
requiredCredits,
|
||||
availableCredits,
|
||||
operation,
|
||||
handleCreditError, // Automatically shows modal for credit errors
|
||||
hideInsufficientCredits,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Usage Example
|
||||
|
||||
**In any screen that creates a deck**:
|
||||
```typescript
|
||||
import { useInsufficientCredits } from '../hooks/useInsufficientCredits';
|
||||
import { InsufficientCreditsModal } from '../components/InsufficientCreditsModal';
|
||||
import { creditService } from '../services/creditService';
|
||||
|
||||
function DeckCreationScreen() {
|
||||
const [creditBalance, setCreditBalance] = useState(0);
|
||||
const insufficientCredits = useInsufficientCredits();
|
||||
|
||||
// Load balance
|
||||
useEffect(() => {
|
||||
creditService.getBalance().then(setCreditBalance);
|
||||
}, []);
|
||||
|
||||
const handleCreateDeck = async () => {
|
||||
try {
|
||||
const response = await post('/api/decks', deckData);
|
||||
Alert.alert('Success', `Deck created! ${response.creditsUsed} mana used.`);
|
||||
|
||||
// Refresh balance
|
||||
const newBalance = await creditService.getBalance();
|
||||
setCreditBalance(newBalance);
|
||||
} catch (error) {
|
||||
// Automatically shows modal if it's a credit error
|
||||
if (!insufficientCredits.handleCreditError(error)) {
|
||||
// Handle other errors
|
||||
Alert.alert('Error', error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
{/* Show balance */}
|
||||
<Text>Your Mana: {creditBalance} ⚡</Text>
|
||||
|
||||
{/* Create deck button */}
|
||||
<Button title="Create Deck (10 mana)" onPress={handleCreateDeck} />
|
||||
|
||||
{/* Insufficient credits modal */}
|
||||
<InsufficientCreditsModal
|
||||
visible={insufficientCredits.visible}
|
||||
requiredCredits={insufficientCredits.requiredCredits}
|
||||
availableCredits={insufficientCredits.availableCredits}
|
||||
operation={insufficientCredits.operation}
|
||||
onClose={insufficientCredits.hideInsufficientCredits}
|
||||
onPurchase={() => {/* Navigate to purchase */}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Backend Error Response
|
||||
|
||||
When a user has insufficient credits:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "insufficient_credits",
|
||||
"message": "Insufficient mana. Required: 10, Available: 5",
|
||||
"requiredCredits": 10,
|
||||
"availableCredits": 5,
|
||||
"operation": "Create a new deck"
|
||||
}
|
||||
```
|
||||
|
||||
Status code: **400 Bad Request**
|
||||
|
||||
### Frontend Error Handling
|
||||
|
||||
The `useInsufficientCredits` hook automatically detects credit errors:
|
||||
|
||||
```typescript
|
||||
const insufficientCredits = useInsufficientCredits();
|
||||
|
||||
try {
|
||||
await createDeck();
|
||||
} catch (error) {
|
||||
// Returns true if it was a credit error (modal shown automatically)
|
||||
const wasCreditError = insufficientCredits.handleCreditError(error);
|
||||
|
||||
if (!wasCreditError) {
|
||||
// Handle other errors
|
||||
Alert.alert('Error', error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing Steps
|
||||
|
||||
1. **Check credit balance**:
|
||||
```bash
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
https://manadeck-backend-111768794939.europe-west3.run.app/api/credits/balance
|
||||
```
|
||||
|
||||
2. **Create deck with sufficient credits**:
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"Test Deck","description":"Testing"}' \
|
||||
https://manadeck-backend-111768794939.europe-west3.run.app/api/decks
|
||||
```
|
||||
|
||||
3. **Create deck with insufficient credits**:
|
||||
- Repeat step 2 until balance is < 10
|
||||
- Should return 400 with `insufficient_credits` error
|
||||
|
||||
### Unit Testing
|
||||
|
||||
Mock the `CreditClientService` in tests:
|
||||
|
||||
```typescript
|
||||
{
|
||||
provide: CreditClientService,
|
||||
useValue: {
|
||||
validateCredits: jest.fn().mockResolvedValue({
|
||||
hasCredits: true,
|
||||
availableCredits: 100,
|
||||
}),
|
||||
consumeCredits: jest.fn().mockResolvedValue({
|
||||
success: true,
|
||||
transactionId: 'txn_123',
|
||||
}),
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO
|
||||
|
||||
1. **Always validate BEFORE the operation**
|
||||
```typescript
|
||||
const validation = await creditClient.validateCredits(...);
|
||||
if (!validation.hasCredits) throw error;
|
||||
|
||||
// Then do the operation
|
||||
const result = await performOperation();
|
||||
|
||||
// Then consume
|
||||
await creditClient.consumeCredits(...);
|
||||
```
|
||||
|
||||
2. **Consume AFTER success**
|
||||
- Only consume credits if the operation succeeds
|
||||
- This prevents charging users for failed operations
|
||||
|
||||
3. **Include metadata in consumption**
|
||||
```typescript
|
||||
await creditClient.consumeCredits(
|
||||
userId,
|
||||
operationType,
|
||||
cost,
|
||||
'Human-readable description',
|
||||
{ deckId, deckName } // Useful for audit logs
|
||||
);
|
||||
```
|
||||
|
||||
4. **Refresh balance after operations**
|
||||
- Show users their updated balance
|
||||
- Prevents confusion about remaining credits
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
1. **Don't consume without validation**
|
||||
```typescript
|
||||
// BAD: What if they don't have credits?
|
||||
await performOperation();
|
||||
await creditClient.consumeCredits(...);
|
||||
```
|
||||
|
||||
2. **Don't consume before the operation**
|
||||
```typescript
|
||||
// BAD: They'll be charged even if operation fails
|
||||
await creditClient.consumeCredits(...);
|
||||
await performOperation();
|
||||
```
|
||||
|
||||
3. **Don't hardcode credit costs**
|
||||
```typescript
|
||||
// BAD
|
||||
const cost = 10;
|
||||
|
||||
// GOOD
|
||||
const cost = getCreditCost(CreditOperationType.DECK_CREATION);
|
||||
```
|
||||
|
||||
## Adding New Billable Operations
|
||||
|
||||
To add a new operation (e.g., "AI Card Description"):
|
||||
|
||||
### 1. Backend
|
||||
|
||||
**backend/src/config/credit-operations.ts**:
|
||||
```typescript
|
||||
export enum CreditOperationType {
|
||||
// ... existing
|
||||
AI_CARD_DESCRIPTION = 'ai_card_description',
|
||||
}
|
||||
|
||||
export const CREDIT_COSTS: Record<CreditOperationType, number> = {
|
||||
// ... existing
|
||||
[CreditOperationType.AI_CARD_DESCRIPTION]: 3,
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Controller
|
||||
|
||||
```typescript
|
||||
@Post('cards/:id/generate-description')
|
||||
async generateDescription(@CurrentUser() user: any, @Param('id') cardId: string) {
|
||||
const operationType = CreditOperationType.AI_CARD_DESCRIPTION;
|
||||
const cost = getCreditCost(operationType);
|
||||
|
||||
// Validate
|
||||
const validation = await this.creditClient.validateCredits(user.id, operationType, cost);
|
||||
if (!validation.hasCredits) throw insufficientCreditsError;
|
||||
|
||||
// Perform
|
||||
const description = await this.aiService.generateDescription(cardId);
|
||||
|
||||
// Consume
|
||||
await this.creditClient.consumeCredits(
|
||||
user.id,
|
||||
operationType,
|
||||
cost,
|
||||
`Generated description for card ${cardId}`,
|
||||
{ cardId, description: description.substring(0, 50) }
|
||||
);
|
||||
|
||||
return { description, creditsUsed: cost };
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Frontend
|
||||
|
||||
Use the same pattern as deck creation - the modal and hooks work for all operations automatically!
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Service key not configured" error
|
||||
|
||||
**Problem**: Credits operations fail with authentication error.
|
||||
|
||||
**Solution**: Add `SERVICE_KEY` to your `.env` file:
|
||||
```env
|
||||
SERVICE_KEY=your-actual-service-key-from-mana-core
|
||||
```
|
||||
|
||||
### Credits not being deducted
|
||||
|
||||
**Problem**: Operations succeed but credits don't decrease.
|
||||
|
||||
**Check**:
|
||||
1. Is `consumeCredits()` being called?
|
||||
2. Is it being called AFTER the operation succeeds?
|
||||
3. Check backend logs for errors
|
||||
|
||||
### Frontend not showing insufficient credits modal
|
||||
|
||||
**Problem**: Users get an error but modal doesn't appear.
|
||||
|
||||
**Check**:
|
||||
1. Is `useInsufficientCredits` hook being used?
|
||||
2. Is `handleCreditError()` being called in the catch block?
|
||||
3. Is `InsufficientCreditsModal` component rendered with hook props?
|
||||
|
||||
## Resources
|
||||
|
||||
- [Mana Core Documentation](https://docs.mana-core.com)
|
||||
- [Mana Core NestJS Package](https://github.com/Memo-2023/mana-core-nestjs-package)
|
||||
- [Integration Guide](./MANA_CORE_INTEGRATION_GUIDE.md)
|
||||
- [Example Implementation](./apps/mobile/examples/DeckCreationExample.tsx)
|
||||
Loading…
Add table
Add a link
Reference in a new issue