mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 00:41:26 +02:00
Cloud Sync is now a paid feature: 30 credits/month (90/quarter, 360/year).
Users start in local-only mode and opt-in via Settings > Cloud Sync.
1 Credit = 1 Cent, so sync costs ~0.30€/month.
When credits run out, sync is paused (not deleted) and an in-app banner
prompts the user to top up. Local data is always preserved.
Backend (mana-credits):
- New sync_subscriptions table in credits schema
- SyncBillingService with activate/deactivate/chargeRecurring
- User-facing routes: GET/POST /api/v1/sync/{status,activate,deactivate,change-interval}
- Internal routes for server-side checks and cron triggers
Frontend (mana web):
- Sync API client + reactive sync-billing store
- syncEnabled parameter gates createUnifiedSync() — sync only starts when active
- Settings sync page with interval selection and activate/deactivate
- Pause banner in app layout when credits insufficient
Also: removed CALDAV_SYNC/GOOGLE_SYNC operations (not needed),
updated CLOUD_SYNC cost from 5 to 30 credits/month.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
76 lines
2.4 KiB
TypeScript
76 lines
2.4 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),
|
|
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),
|
|
});
|
|
|
|
// ─── Gifts ──────────────────────────────────────────────────
|
|
|
|
export const createGiftSchema = z.object({
|
|
totalCredits: z.number().int().positive().min(1).max(10000),
|
|
type: z.enum(['simple', 'personalized']).default('simple'),
|
|
targetEmail: z.string().email().optional(),
|
|
message: z.string().max(500).optional(),
|
|
expirationDays: z.number().int().positive().optional(),
|
|
});
|
|
|
|
export const redeemGiftSchema = z.object({
|
|
sourceAppId: z.string().optional(),
|
|
});
|
|
|
|
// ─── Sync ──────────────────────────────────────────────────
|
|
|
|
export const activateSyncSchema = z.object({
|
|
interval: z.enum(['monthly', 'quarterly', 'yearly']).default('monthly'),
|
|
});
|
|
|
|
export const changeSyncIntervalSchema = z.object({
|
|
interval: z.enum(['monthly', 'quarterly', 'yearly']),
|
|
});
|
|
|
|
// ─── 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),
|
|
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(),
|
|
});
|