feat: Add Multi-Gateway Configuration Module with Payment Settings Tab

This commit is contained in:
CycroftX
2026-02-10 11:35:23 +05:30
parent 514508df89
commit 0c8593ef22
13 changed files with 1979 additions and 25 deletions

270
MASTER_API_INVENTORY.md Normal file
View File

@@ -0,0 +1,270 @@
# Eventify Master API Inventory
> **Version:** 1.0 | **Last Updated:** 2026-02-09 | **Status:** Definitive Reference
---
## 1. Auth & Identity
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| AUTH-01 | `/api/v1/auth/login/otp/request` | POST | Inbound | App/Web -> Server | Request OTP for phone login | High |
| AUTH-02 | `/api/v1/auth/login/otp/verify` | POST | Inbound | App/Web -> Server | Verify OTP and issue tokens | High |
| AUTH-03 | `/api/v1/auth/login/password` | POST | Inbound | App/Web -> Server | Email/password login | High |
| AUTH-04 | `/api/v1/auth/register` | POST | Inbound | App/Web -> Server | New user registration | High |
| AUTH-05 | `/api/v1/auth/logout` | POST | Inbound | App/Web -> Server | Invalidate session/tokens | Medium |
| AUTH-06 | `/api/v1/auth/refresh` | POST | Inbound | App/Web -> Server | Refresh access token | High |
| AUTH-07 | `/api/v1/auth/forgot-password` | POST | Inbound | App/Web -> Server | Initiate password reset | Medium |
| AUTH-08 | `/api/v1/auth/reset-password` | POST | Inbound | App/Web -> Server | Complete password reset | Medium |
| AUTH-09 | `/api/v1/auth/mfa/setup` | POST | Inbound | App/Web -> Server | Enable 2FA (TOTP/SMS) | Medium |
| AUTH-10 | `/api/v1/auth/mfa/verify` | POST | Inbound | App/Web -> Server | Verify 2FA code | High |
| AUTH-11 | `/api/v1/auth/oauth/google` | GET | Inbound | App/Web -> Server | Google OAuth redirect | Medium |
| AUTH-12 | `/api/v1/auth/oauth/google/callback` | GET | Inbound | Google -> Server | Google OAuth callback | Medium |
| AUTH-13 | `/api/v1/auth/oauth/apple` | GET | Inbound | App -> Server | Apple Sign-In redirect | Medium |
| AUTH-14 | `/api/v1/auth/oauth/apple/callback` | POST | Inbound | Apple -> Server | Apple Sign-In callback | Medium |
| AUTH-15 | `/api/v1/auth/sessions` | GET | Inbound | App/Web -> Server | List active sessions | Low |
| AUTH-16 | `/api/v1/auth/sessions/:id` | DELETE | Inbound | App/Web -> Server | Revoke specific session | Medium |
| AUTH-17 | `/api/v1/auth/sessions/all` | DELETE | Inbound | App/Web -> Server | Revoke all sessions (except current) | Medium |
| AUTH-18 | `/api/v1/partner/auth/login` | POST | Inbound | Partner -> Server | Partner/Organizer login | High |
| AUTH-19 | `/api/v1/admin/auth/login` | POST | Inbound | Control Center -> Server | Admin login | High |
| AUTH-20 | `/api/v1/admin/auth/impersonate/:userId` | POST | Inbound | Control Center -> Server | Impersonate user session | High |
---
## 2. User Entity (CRUD & Profile)
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| USER-01 | `/api/v1/users/me` | GET | Inbound | App/Web -> Server | Get current user profile | High |
| USER-02 | `/api/v1/users/me` | PATCH | Inbound | App/Web -> Server | Update profile (name, bio, avatar) | Medium |
| USER-03 | `/api/v1/users/me/phone` | PATCH | Inbound | App/Web -> Server | Update phone number | Medium |
| USER-04 | `/api/v1/users/me/email` | PATCH | Inbound | App/Web -> Server | Update email address | Medium |
| USER-05 | `/api/v1/users/me/preferences` | PATCH | Inbound | App/Web -> Server | Update notification/privacy preferences | Low |
| USER-06 | `/api/v1/users/me/avatar` | POST | Inbound | App/Web -> Server | Upload avatar image | Low |
| USER-07 | `/api/v1/users/me/delete` | POST | Inbound | App/Web -> Server | Request account deletion (GDPR) | Medium |
| USER-08 | `/api/v1/users/:id` | GET | Inbound | App/Web -> Server | Get public user profile | Low |
| USER-09 | `/api/v1/users/:id/follow` | POST | Inbound | App -> Server | Follow another user | Low |
| USER-10 | `/api/v1/users/:id/unfollow` | POST | Inbound | App -> Server | Unfollow user | Low |
| USER-11 | `/api/v1/users/me/followers` | GET | Inbound | App -> Server | List followers | Low |
| USER-12 | `/api/v1/users/me/following` | GET | Inbound | App -> Server | List following | Low |
| USER-13 | `/api/v1/admin/users` | GET | Inbound | Control Center -> Server | List all users (paginated, filterable) | High |
| USER-14 | `/api/v1/admin/users/:id` | GET | Inbound | Control Center -> Server | Get user details (admin view) | High |
| USER-15 | `/api/v1/admin/users/:id` | PATCH | Inbound | Control Center -> Server | Update user (admin override) | High |
| USER-16 | `/api/v1/admin/users/:id/suspend` | POST | Inbound | Control Center -> Server | Suspend user account | High |
| USER-17 | `/api/v1/admin/users/:id/ban` | POST | Inbound | Control Center -> Server | Ban user permanently | High |
| USER-18 | `/api/v1/admin/users/:id/reinstate` | POST | Inbound | Control Center -> Server | Reinstate suspended/banned user | High |
| USER-19 | `/api/v1/admin/users/:id/notes` | POST | Inbound | Control Center -> Server | Add internal admin note | Low |
| USER-20 | `/api/v1/admin/users/:id/tags` | PATCH | Inbound | Control Center -> Server | Update user tags | Low |
---
## 3. Events (Discovery, CRUD, Publishing)
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| EVT-01 | `/api/v1/events` | GET | Inbound | App/Web -> Server | List/search events (public) | High |
| EVT-02 | `/api/v1/events/:id` | GET | Inbound | App/Web -> Server | Get event details | High |
| EVT-03 | `/api/v1/events/:id/tickets` | GET | Inbound | App/Web -> Server | Get ticket types for event | High |
| EVT-04 | `/api/v1/events/featured` | GET | Inbound | App/Web -> Server | Get featured/promoted events | Medium |
| EVT-05 | `/api/v1/events/nearby` | GET | Inbound | App -> Server | Get events near user (geo) | Medium |
| EVT-06 | `/api/v1/events/categories` | GET | Inbound | App/Web -> Server | List event categories | Low |
| EVT-07 | `/api/v1/events/:id/similar` | GET | Inbound | App/Web -> Server | Get similar events | Low |
| EVT-08 | `/api/v1/partner/events` | GET | Inbound | Partner -> Server | List organizer's events | High |
| EVT-09 | `/api/v1/partner/events` | POST | Inbound | Partner -> Server | Create new event | High |
| EVT-10 | `/api/v1/partner/events/:id` | GET | Inbound | Partner -> Server | Get event details (owner) | High |
| EVT-11 | `/api/v1/partner/events/:id` | PATCH | Inbound | Partner -> Server | Update event | High |
| EVT-12 | `/api/v1/partner/events/:id` | DELETE | Inbound | Partner -> Server | Delete/cancel event | High |
| EVT-13 | `/api/v1/partner/events/:id/publish` | POST | Inbound | Partner -> Server | Publish event (make live) | High |
| EVT-14 | `/api/v1/partner/events/:id/unpublish` | POST | Inbound | Partner -> Server | Unpublish/draft event | Medium |
| EVT-15 | `/api/v1/partner/events/:id/duplicate` | POST | Inbound | Partner -> Server | Clone event | Low |
| EVT-16 | `/api/v1/partner/events/:id/tickets` | POST | Inbound | Partner -> Server | Create ticket type | High |
| EVT-17 | `/api/v1/partner/events/:id/tickets/:ticketId` | PATCH | Inbound | Partner -> Server | Update ticket type | High |
| EVT-18 | `/api/v1/partner/events/:id/tickets/:ticketId` | DELETE | Inbound | Partner -> Server | Delete ticket type | Medium |
| EVT-19 | `/api/v1/admin/events` | GET | Inbound | Control Center -> Server | List all events (admin) | High |
| EVT-20 | `/api/v1/admin/events/:id` | PATCH | Inbound | Control Center -> Server | Override event details | High |
| EVT-21 | `/api/v1/admin/events/:id/approve` | POST | Inbound | Control Center -> Server | Approve pending event | High |
| EVT-22 | `/api/v1/admin/events/:id/reject` | POST | Inbound | Control Center -> Server | Reject pending event | High |
| EVT-23 | `/api/v1/admin/events/:id/feature` | POST | Inbound | Control Center -> Server | Feature event on homepage | Medium |
---
## 4. Orders & Tickets (Commerce)
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| ORD-01 | `/api/v1/orders` | POST | Inbound | App/Web -> Server | Create order (cart checkout) | High |
| ORD-02 | `/api/v1/orders/:id` | GET | Inbound | App/Web -> Server | Get order details | High |
| ORD-03 | `/api/v1/orders/:id/pay` | POST | Inbound | App/Web -> Server | Initiate payment for order | High |
| ORD-04 | `/api/v1/orders/:id/cancel` | POST | Inbound | App/Web -> Server | Cancel pending order | Medium |
| ORD-05 | `/api/v1/users/me/orders` | GET | Inbound | App/Web -> Server | List user's orders | High |
| ORD-06 | `/api/v1/users/me/tickets` | GET | Inbound | App/Web -> Server | List user's tickets (wallet) | High |
| ORD-07 | `/api/v1/tickets/:id` | GET | Inbound | App/Web -> Server | Get ticket details (for wallet) | High |
| ORD-08 | `/api/v1/tickets/:id/qr` | GET | Inbound | App -> Server | Get QR code data for ticket | High |
| ORD-09 | `/api/v1/tickets/:id/transfer` | POST | Inbound | App -> Server | Transfer ticket to another user | Medium |
| ORD-10 | `/api/v1/tickets/:id/accept-transfer` | POST | Inbound | App -> Server | Accept incoming ticket transfer | Medium |
| ORD-11 | `/api/v1/partner/orders` | GET | Inbound | Partner -> Server | List orders for organizer's events | High |
| ORD-12 | `/api/v1/partner/orders/:id` | GET | Inbound | Partner -> Server | Get order details (organizer) | High |
| ORD-13 | `/api/v1/partner/orders/:id/refund` | POST | Inbound | Partner -> Server | Initiate refund | High |
| ORD-14 | `/api/v1/partner/attendees` | GET | Inbound | Partner -> Server | List attendees for event | High |
| ORD-15 | `/api/v1/admin/orders` | GET | Inbound | Control Center -> Server | List all orders (admin) | High |
| ORD-16 | `/api/v1/admin/orders/:id` | GET | Inbound | Control Center -> Server | Get order details (admin) | High |
| ORD-17 | `/api/v1/admin/orders/:id/refund` | POST | Inbound | Control Center -> Server | Force refund (admin override) | High |
| ORD-18 | `/api/v1/admin/orders/:id/receipt` | GET | Inbound | Control Center -> Server | Generate/download receipt | Medium |
---
## 5. Payments & Payouts
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| PAY-01 | `/api/v1/payments/razorpay/create-order` | POST | Inbound | App/Web -> Server | Create Razorpay order | High |
| PAY-02 | `/api/v1/payments/razorpay/verify` | POST | Inbound | App/Web -> Server | Verify Razorpay payment signature | High |
| PAY-03 | `/api/v1/payments/stripe/create-intent` | POST | Inbound | App/Web -> Server | Create Stripe PaymentIntent | High |
| PAY-04 | `/api/v1/payments/stripe/confirm` | POST | Inbound | App/Web -> Server | Confirm Stripe payment | High |
| PAY-05 | `/api/v1/webhooks/razorpay` | POST | Inbound | Razorpay -> Server | Razorpay webhook events | High |
| PAY-06 | `/api/v1/webhooks/stripe` | POST | Inbound | Stripe -> Server | Stripe webhook events | High |
| PAY-07 | `/api/v1/partner/payouts` | GET | Inbound | Partner -> Server | List payout history | High |
| PAY-08 | `/api/v1/partner/payouts/:id` | GET | Inbound | Partner -> Server | Get payout details | High |
| PAY-09 | `/api/v1/partner/bank-account` | GET | Inbound | Partner -> Server | Get linked bank account | High |
| PAY-10 | `/api/v1/partner/bank-account` | POST | Inbound | Partner -> Server | Link bank account for payouts | High |
| PAY-11 | `/api/v1/partner/bank-account` | PATCH | Inbound | Partner -> Server | Update bank account | High |
| PAY-12 | `/api/v1/admin/payouts` | GET | Inbound | Control Center -> Server | List all payouts (admin) | High |
| PAY-13 | `/api/v1/admin/payouts/:id/approve` | POST | Inbound | Control Center -> Server | Approve payout | High |
| PAY-14 | `/api/v1/admin/payouts/:id/hold` | POST | Inbound | Control Center -> Server | Hold payout | High |
| PAY-15 | `/api/v1/admin/payouts/:id/release` | POST | Inbound | Control Center -> Server | Release held payout | High |
| PAY-16 | `Razorpay Payout API` | POST | Outbound | Server -> Razorpay | Initiate payout to organizer | High |
| PAY-17 | `Stripe Connect Payout` | POST | Outbound | Server -> Stripe | Transfer funds to connected account | High |
---
## 6. Operations (Check-in, Scanning, Staff)
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| OPS-01 | `/api/v1/partner/scan/validate` | POST | Inbound | Partner App -> Server | Validate scanned QR code | High |
| OPS-02 | `/api/v1/partner/scan/checkin` | POST | Inbound | Partner App -> Server | Check-in attendee | High |
| OPS-03 | `/api/v1/partner/scan/checkout` | POST | Inbound | Partner App -> Server | Check-out attendee (optional) | Low |
| OPS-04 | `/api/v1/partner/events/:id/checkin-stats` | GET | Inbound | Partner -> Server | Get live check-in statistics | Medium |
| OPS-05 | `/api/v1/partner/staff` | GET | Inbound | Partner -> Server | List event staff | Medium |
| OPS-06 | `/api/v1/partner/staff` | POST | Inbound | Partner -> Server | Invite staff member | Medium |
| OPS-07 | `/api/v1/partner/staff/:id` | DELETE | Inbound | Partner -> Server | Remove staff member | Medium |
| OPS-08 | `/api/v1/partner/staff/:id/permissions` | PATCH | Inbound | Partner -> Server | Update staff permissions | Medium |
| OPS-09 | `/api/v1/staff/auth/login` | POST | Inbound | Staff App -> Server | Staff member login | High |
| OPS-10 | `/api/v1/staff/events` | GET | Inbound | Staff App -> Server | List assigned events | Medium |
---
## 7. Communication (Email, Push, SMS, Chat)
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| COM-01 | `Resend API - Transactional` | POST | Outbound | Server -> Resend | Send order confirmation email | High |
| COM-02 | `Resend API - Marketing` | POST | Outbound | Server -> Resend | Send marketing/promo email | Low |
| COM-03 | `FCM - Push Notification` | POST | Outbound | Server -> FCM | Send push notification to user | Medium |
| COM-04 | `APNs - Push Notification` | POST | Outbound | Server -> APNs | Send iOS push notification | Medium |
| COM-05 | `Twilio SMS` | POST | Outbound | Server -> Twilio | Send SMS (OTP, alerts) | High |
| COM-06 | `/api/v1/users/me/devices` | POST | Inbound | App -> Server | Register device for push | Medium |
| COM-07 | `/api/v1/users/me/devices/:id` | DELETE | Inbound | App -> Server | Unregister device | Low |
| COM-08 | `/api/v1/webhooks/resend` | POST | Inbound | Resend -> Server | Email delivery status webhook | Medium |
| COM-09 | `/api/v1/webhooks/twilio` | POST | Inbound | Twilio -> Server | SMS delivery status webhook | Medium |
| COM-10 | `/api/v1/admin/notifications/send` | POST | Inbound | Control Center -> Server | Send notification to user | Medium |
| COM-11 | `/api/v1/admin/notifications/broadcast` | POST | Inbound | Control Center -> Server | Broadcast to user segment | Medium |
| COM-12 | `/api/v1/chat/conversations` | GET | Inbound | App -> Server | List user's conversations | Low |
| COM-13 | `/api/v1/chat/conversations/:id/messages` | GET | Inbound | App -> Server | Get messages in conversation | Low |
| COM-14 | `/api/v1/chat/conversations/:id/messages` | POST | Inbound | App -> Server | Send message | Low |
| COM-15 | `WebSocket /ws/chat` | WS | Bidirectional | App <-> Server | Real-time chat connection | Low |
---
## 8. Data & Analytics
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| DAT-01 | `/api/v1/partner/analytics/overview` | GET | Inbound | Partner -> Server | Get sales overview dashboard | Medium |
| DAT-02 | `/api/v1/partner/analytics/sales` | GET | Inbound | Partner -> Server | Get detailed sales data | Medium |
| DAT-03 | `/api/v1/partner/analytics/attendees` | GET | Inbound | Partner -> Server | Get attendee demographics | Low |
| DAT-04 | `/api/v1/partner/analytics/traffic` | GET | Inbound | Partner -> Server | Get page view/traffic data | Low |
| DAT-05 | `/api/v1/partner/export/attendees` | GET | Outbound | Server -> Browser | Export attendee list (CSV) | Medium |
| DAT-06 | `/api/v1/partner/export/orders` | GET | Outbound | Server -> Browser | Export orders (CSV) | Medium |
| DAT-07 | `/api/v1/admin/analytics/overview` | GET | Inbound | Control Center -> Server | Platform-wide analytics | Medium |
| DAT-08 | `/api/v1/admin/analytics/revenue` | GET | Inbound | Control Center -> Server | Revenue breakdown | High |
| DAT-09 | `/api/v1/admin/analytics/users` | GET | Inbound | Control Center -> Server | User growth metrics | Medium |
| DAT-10 | `/api/v1/admin/export/users` | GET | Outbound | Server -> Browser | Export users (CSV) | Medium |
| DAT-11 | `/api/v1/admin/export/orders` | GET | Outbound | Server -> Browser | Export orders (CSV) | Medium |
| DAT-12 | `/api/v1/admin/export/payouts` | GET | Outbound | Server -> Browser | Export payouts (CSV) | Medium |
| DAT-13 | `/api/v1/admin/audit-logs` | GET | Inbound | Control Center -> Server | Get admin activity logs | High |
---
## 9. Third-Party Integrations
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| INT-01 | `AWS S3 - Presigned URL` | POST | Outbound | Server -> S3 | Generate upload URL | Medium |
| INT-02 | `/api/v1/uploads/presigned` | GET | Inbound | App/Web -> Server | Request presigned URL for upload | Medium |
| INT-03 | `Google Maps Geocoding API` | GET | Outbound | Server -> Google | Geocode event address | Low |
| INT-04 | `Google Maps Places API` | GET | Outbound | Server -> Google | Autocomplete venue search | Low |
| INT-05 | `/api/v1/geo/ip` | GET | Inbound | App/Web -> Server | Get location from IP | Low |
| INT-06 | `MaxMind GeoIP` | GET | Internal | Server -> MaxMind DB | IP to location lookup | Low |
| INT-07 | `/api/v1/calendar/google/export` | GET | Inbound | App/Web -> Server | Generate Google Calendar link | Low |
| INT-08 | `/api/v1/calendar/ical/export` | GET | Inbound | App/Web -> Server | Generate iCal file | Low |
| INT-09 | `Sentry - Error Reporting` | POST | Outbound | Server -> Sentry | Log errors | Medium |
| INT-10 | `Slack Webhook` | POST | Outbound | Server -> Slack | Alert on critical events | Medium |
| INT-11 | `PagerDuty API` | POST | Outbound | Server -> PagerDuty | Trigger on-call alerts | High |
---
## 10. Support & Moderation
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| SUP-01 | `/api/v1/admin/tickets` | GET | Inbound | Control Center -> Server | List support tickets | Medium |
| SUP-02 | `/api/v1/admin/tickets/:id` | GET | Inbound | Control Center -> Server | Get ticket details | Medium |
| SUP-03 | `/api/v1/admin/tickets` | POST | Inbound | Control Center -> Server | Create escalation ticket | Medium |
| SUP-04 | `/api/v1/admin/tickets/:id/reply` | POST | Inbound | Control Center -> Server | Reply to ticket | Medium |
| SUP-05 | `/api/v1/admin/tickets/:id/close` | POST | Inbound | Control Center -> Server | Close ticket | Medium |
| SUP-06 | `/api/v1/admin/tickets/:id/assign` | POST | Inbound | Control Center -> Server | Assign ticket to agent | Medium |
| SUP-07 | `/api/v1/users/me/tickets` | GET | Inbound | App/Web -> Server | List user's support tickets | Low |
| SUP-08 | `/api/v1/users/me/tickets` | POST | Inbound | App/Web -> Server | Create support ticket | Low |
| SUP-09 | `/api/v1/admin/moderation/reports` | GET | Inbound | Control Center -> Server | List content reports | Medium |
| SUP-10 | `/api/v1/admin/moderation/reports/:id/action` | POST | Inbound | Control Center -> Server | Take action on report | Medium |
---
## 11. System & Utilities
| ID | Endpoint / Action Name | Method | Direction | Source -> Target | Purpose | Criticality |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| SYS-01 | `/api/v1/health` | GET | Inbound | Any -> Server | Health check | High |
| SYS-02 | `/api/v1/health/db` | GET | Inbound | Internal -> Server | Database connectivity check | High |
| SYS-03 | `/api/v1/health/redis` | GET | Inbound | Internal -> Server | Redis connectivity check | High |
| SYS-04 | `/api/v1/config/mobile` | GET | Inbound | App -> Server | Get mobile app config (feature flags) | Medium |
| SYS-05 | `/api/v1/config/web` | GET | Inbound | Web -> Server | Get web config (feature flags) | Medium |
| SYS-06 | `/api/v1/version` | GET | Inbound | Any -> Server | Get API version info | Low |
| SYS-07 | `Cron: Payout Processor` | CRON | Internal | Scheduler -> Server | Process pending payouts | High |
| SYS-08 | `Cron: Reminder Emails` | CRON | Internal | Scheduler -> Server | Send event reminder emails | Medium |
| SYS-09 | `Cron: Expired Orders Cleanup` | CRON | Internal | Scheduler -> Server | Cancel expired pending orders | Medium |
| SYS-10 | `Cron: Analytics Aggregation` | CRON | Internal | Scheduler -> Server | Pre-compute analytics | Low |
---
## Summary Statistics
| Category | Total Endpoints |
| :--- | :---: |
| Auth & Identity | 20 |
| User Entity | 20 |
| Events | 23 |
| Orders & Tickets | 18 |
| Payments & Payouts | 17 |
| Operations | 10 |
| Communication | 15 |
| Data & Analytics | 13 |
| Third-Party Integrations | 11 |
| Support & Moderation | 10 |
| System & Utilities | 10 |
| **TOTAL** | **167** |
---
> **Note:** This inventory represents the definitive API surface for the Eventify ecosystem. All development should reference this document for endpoint naming, method conventions, and integration points.

View File

@@ -0,0 +1,219 @@
'use client';
import { useState, useEffect } from 'react';
import { GatewayProvider, GatewayCredentials } from '@/lib/types/settings';
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { verifyGatewayCredentials, saveGatewayConfig } from '@/lib/actions/payment-settings';
import { toast } from 'sonner';
import { Lock, CheckBase, Loader2, Copy } from 'lucide-react';
import { decrypt } from '@/lib/payment-encryption';
interface GatewayConfigSheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
provider: GatewayProvider;
initialConfig: GatewayCredentials;
onSave: () => void;
}
export function GatewayConfigSheet({ open, onOpenChange, provider, initialConfig, onSave }: GatewayConfigSheetProps) {
const [config, setConfig] = useState<GatewayCredentials>(initialConfig);
const [loading, setLoading] = useState(false);
const [verifying, setVerifying] = useState(false);
// Reset config when provider changes
useEffect(() => {
// Decrypt sensitive fields for editing
const decrypted = { ...initialConfig };
if (decrypted.salt) decrypted.salt = decrypt(decrypted.salt);
setConfig(decrypted);
}, [provider, initialConfig, open]);
const handleVerify = async () => {
setVerifying(true);
try {
const res = await verifyGatewayCredentials(provider, config);
if (res.success) {
toast.success('Credentials Verified', { description: res.message });
} else {
toast.error('Verification Failed', { description: res.message });
}
} catch (error) {
toast.error('Verification Error');
} finally {
setVerifying(false);
}
};
const handleSave = async () => {
setLoading(true);
try {
const res = await saveGatewayConfig(provider, config);
if (res.success) {
toast.success(`Saved ${provider} configuration`);
onSave();
onOpenChange(false);
} else {
toast.error(res.message);
}
} catch (error) {
toast.error('Failed to save configuration');
} finally {
setLoading(false);
}
};
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="w-[400px] sm:w-[540px] overflow-y-auto">
<SheetHeader>
<SheetTitle className="flex items-center gap-2 capitalize">
{provider} Configuration
</SheetTitle>
<SheetDescription>
Configure API keys and secrets for {provider}.
</SheetDescription>
</SheetHeader>
<div className="py-6 space-y-6">
{/* Environment Toggle */}
<div className="flex items-center justify-between p-4 border rounded-lg bg-muted/30">
<div className="space-y-0.5">
<Label>Mode</Label>
<div className="text-xs text-muted-foreground">
{config.mode === 'live' ? 'Production / Live Traffic' : 'Sandbox / Test Mode'}
</div>
</div>
<Switch
checked={config.mode === 'live'}
onCheckedChange={(c) => setConfig({ ...config, mode: c ? 'live' : 'test' })}
/>
</div>
<Tabs defaultValue="credentials">
<TabsList className="w-full">
<TabsTrigger value="credentials" className="flex-1">Credentials</TabsTrigger>
<TabsTrigger value="features" className="flex-1">Features</TabsTrigger>
<TabsTrigger value="webhooks" className="flex-1">Webhooks</TabsTrigger>
</TabsList>
<TabsContent value="credentials" className="space-y-4 pt-4">
{/* Dynamic inputs based on provider */}
{(provider === 'razorpay' || provider === 'easebuzz') && (
<div className="space-y-2">
<Label>Key ID / Merchant Key</Label>
<Input
value={config.keyId || config.merchantId || ''}
onChange={(e) => setConfig({ ...config, keyId: e.target.value, merchantId: e.target.value })}
placeholder="rzp_test_..."
/>
</div>
)}
{(provider === 'stripe') && (
<div className="space-y-2">
<Label>Public Key</Label>
<Input
value={config.publicKey || ''}
onChange={(e) => setConfig({ ...config, publicKey: e.target.value })}
placeholder="pk_test_..."
/>
</div>
)}
{(provider === 'payu' || provider === 'easebuzz') && (
<div className="space-y-2">
<Label>Salt / Secret Key</Label>
<div className="relative">
<Input
type="password"
value={config.salt || ''}
onChange={(e) => setConfig({ ...config, salt: e.target.value })}
placeholder="Enter secret salt"
className="pr-10"
/>
<Lock className="absolute right-3 top-3 h-4 w-4 text-muted-foreground" />
</div>
</div>
)}
<Button variant="secondary" className="w-full mt-4" onClick={handleVerify} disabled={verifying}>
{verifying && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Test Credentials
</Button>
</TabsContent>
<TabsContent value="features" className="space-y-4 pt-4">
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center space-x-2 border p-3 rounded-md">
<Switch
id="feat-nb"
checked={config.features.netbanking}
onCheckedChange={(c) => setConfig({ ...config, features: { ...config.features, netbanking: c } })}
/>
<Label htmlFor="feat-nb">Netbanking</Label>
</div>
<div className="flex items-center space-x-2 border p-3 rounded-md">
<Switch
id="feat-upi"
checked={config.features.upi}
onCheckedChange={(c) => setConfig({ ...config, features: { ...config.features, upi: c } })}
/>
<Label htmlFor="feat-upi">UPI</Label>
</div>
<div className="flex items-center space-x-2 border p-3 rounded-md">
<Switch
id="feat-cards"
checked={config.features.cards}
onCheckedChange={(c) => setConfig({ ...config, features: { ...config.features, cards: c } })}
/>
<Label htmlFor="feat-cards">Cards</Label>
</div>
<div className="flex items-center space-x-2 border p-3 rounded-md">
<Switch
id="feat-emi"
checked={config.features.emi}
onCheckedChange={(c) => setConfig({ ...config, features: { ...config.features, emi: c } })}
/>
<Label htmlFor="feat-emi">EMI</Label>
</div>
</div>
</TabsContent>
<TabsContent value="webhooks" className="space-y-4 pt-4">
<div className="space-y-2">
<Label>Webhook URL</Label>
<div className="flex items-center gap-2">
<Input readOnly value={`https://api.eventify.com/webhooks/${provider}`} />
<Button variant="outline" size="icon">
<Copy className="h-4 w-4" />
</Button>
</div>
<p className="text-xs text-muted-foreground">Add this URL to your provider dashboard.</p>
</div>
<div className="space-y-2">
<Label>Webhook Secret</Label>
<Input
value={config.webhookSecret || ''}
onChange={(e) => setConfig({ ...config, webhookSecret: e.target.value })}
placeholder="whsec_..."
/>
</div>
</TabsContent>
</Tabs>
</div>
<SheetFooter>
<Button onClick={handleSave} disabled={loading} className="w-full sm:w-auto">
{loading ? 'Saving...' : 'Save Configuration'}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@@ -0,0 +1,143 @@
'use client';
import { useEffect, useState } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card } from '@/components/ui/card';
import { getSystemSettings } from '@/lib/actions/settings';
import { GlobalSettings, DEFAULT_SETTINGS } from '@/lib/types/settings';
import { OrganizationSettings } from './tabs/OrganizationSettings';
import { PublicAppConfigTab } from './tabs/PublicAppConfig';
import { PartnerGovernanceTab } from './tabs/PartnerGovernance';
import { SystemHealthTab } from './tabs/SystemHealth';
import { PaymentConfigTab } from './tabs/PaymentConfig';
import { Loader2, Settings, Smartphone, Building2, Handshake, Server, CreditCard } from 'lucide-react';
export function SettingsLayout() {
const [settings, setSettings] = useState<GlobalSettings | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadSettings() {
try {
const res = await getSystemSettings();
if (res.success) {
setSettings(res.data);
} else {
setSettings(DEFAULT_SETTINGS); // Fallback
}
} catch (err) {
console.error(err);
setSettings(DEFAULT_SETTINGS);
} finally {
setLoading(false);
}
}
loadSettings();
}, []);
const handleUpdate = (section: keyof GlobalSettings, data: any) => {
if (!settings) return;
setSettings({
...settings,
[section]: { ...settings[section], ...data }
});
};
if (loading || !settings) {
return (
<div className="flex flex-col items-center justify-center h-[60vh] text-muted-foreground">
<Loader2 className="h-8 w-8 animate-spin mb-4" />
<p>Loading system configuration...</p>
</div>
);
}
return (
<div className="flex flex-col h-full space-y-6 container mx-auto p-6 max-w-7xl">
<div className="flex items-center justify-between pb-2">
<div>
<h1 className="text-3xl font-bold tracking-tight">System Settings</h1>
<p className="text-muted-foreground">
Global configuration for User App, Partner Dashboard, and Backoffice.
</p>
</div>
</div>
<Tabs defaultValue="organization" orientation="vertical" className="flex-1 w-full flex flex-col md:flex-row gap-8">
<Card className="md:w-64 flex-shrink-0 h-fit bg-muted/30 p-2 border shadow-sm">
<TabsList className="bg-transparent flex flex-col h-auto items-start w-full space-y-1 p-0">
<TabsTrigger
value="organization"
className="w-full justify-start px-3 py-2.5 data-[state=active]:bg-background data-[state=active]:shadow-sm rounded-md transition-all font-medium"
>
<Building2 className="h-4 w-4 mr-2" />
Organization
</TabsTrigger>
<TabsTrigger
value="payment"
className="w-full justify-start px-3 py-2.5 data-[state=active]:bg-background data-[state=active]:shadow-sm rounded-md transition-all font-medium"
>
<CreditCard className="h-4 w-4 mr-2" />
Payment Gateways
</TabsTrigger>
<TabsTrigger
value="public-app"
className="w-full justify-start px-3 py-2.5 data-[state=active]:bg-background data-[state=active]:shadow-sm rounded-md transition-all font-medium"
>
<Smartphone className="h-4 w-4 mr-2" />
Public App
</TabsTrigger>
<TabsTrigger
value="partner-governance"
className="w-full justify-start px-3 py-2.5 data-[state=active]:bg-background data-[state=active]:shadow-sm rounded-md transition-all font-medium"
>
<Handshake className="h-4 w-4 mr-2" />
Partner Governance
</TabsTrigger>
<TabsTrigger
value="system-health"
className="w-full justify-start px-3 py-2.5 data-[state=active]:bg-background data-[state=active]:shadow-sm rounded-md transition-all font-medium"
>
<Server className="h-4 w-4 mr-2" />
System Health
</TabsTrigger>
</TabsList>
</Card>
<div className="flex-1 min-w-0">
<TabsContent value="organization" className="mt-0 space-y-4 animate-in fade-in-50 slide-in-from-bottom-2 duration-300">
<OrganizationSettings
orgConfig={settings.organization}
securityConfig={settings.security}
onUpdate={handleUpdate}
/>
</TabsContent>
<TabsContent value="payment" className="mt-0 space-y-4 animate-in fade-in-50 slide-in-from-bottom-2 duration-300">
<PaymentConfigTab
config={settings.payment}
onUpdate={(data) => handleUpdate('payment', data)}
/>
</TabsContent>
<TabsContent value="public-app" className="mt-0 space-y-4 animate-in fade-in-50 slide-in-from-bottom-2 duration-300">
<PublicAppConfigTab
config={settings.publicApp}
onUpdate={(data) => handleUpdate('publicApp', data)}
/>
</TabsContent>
<TabsContent value="partner-governance" className="mt-0 space-y-4 animate-in fade-in-50 slide-in-from-bottom-2 duration-300">
<PartnerGovernanceTab
config={settings.partner}
onUpdate={(data) => handleUpdate('partner', data)}
/>
</TabsContent>
<TabsContent value="system-health" className="mt-0 space-y-4 animate-in fade-in-50 slide-in-from-bottom-2 duration-300">
<SystemHealthTab
config={settings.system}
onUpdate={(data) => handleUpdate('system', data)}
/>
</TabsContent>
</div>
</Tabs>
</div>
);
}

View File

@@ -0,0 +1,158 @@
'use client';
import { useState } from 'react';
import { OrganizationConfig, SecurityConfig } from '@/lib/types/settings';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Switch } from '@/components/ui/switch';
import { updateSystemSetting } from '@/lib/actions/settings';
import { toast } from 'sonner';
import { Building2, ShieldCheck, Mail, Globe } from 'lucide-react';
interface OrganizationSettingsProps {
orgConfig: OrganizationConfig;
securityConfig: SecurityConfig;
onUpdate: (section: 'organization' | 'security', data: any) => void;
}
export function OrganizationSettings({ orgConfig, securityConfig, onUpdate }: OrganizationSettingsProps) {
const [loading, setLoading] = useState(false);
// Internal state for form usage
const [orgState, setOrgState] = useState(orgConfig);
const [secState, setSecState] = useState(securityConfig);
const handleSaveOrg = async () => {
setLoading(true);
try {
const res = await updateSystemSetting('organization', orgState);
if (res.success) {
toast.success('Organization profile updated');
onUpdate('organization', res.updatedSettings.organization);
}
} catch (error) {
toast.error('Failed to update organization settings');
} finally {
setLoading(false);
}
};
const handleSaveSecurity = async () => {
setLoading(true);
try {
const res = await updateSystemSetting('security', secState);
if (res.success) {
toast.success('Security policy updated');
onUpdate('security', res.updatedSettings.security);
}
} catch (error) {
toast.error('Failed to update security settings');
} finally {
setLoading(false);
}
};
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Building2 className="h-5 w-5 text-primary" />
Organization Profile
</CardTitle>
<CardDescription>
General branding and contact details for the control center and emails.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Brand Name</Label>
<Input
value={orgState.brandName}
onChange={(e) => setOrgState({ ...orgState, brandName: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label>Support Email</Label>
<div className="relative">
<Mail className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
className="pl-9"
value={orgState.supportEmail}
onChange={(e) => setOrgState({ ...orgState, supportEmail: e.target.value })}
/>
</div>
</div>
</div>
<div className="space-y-2">
<Label>Legal Address</Label>
<Input
value={orgState.legalAddress}
onChange={(e) => setOrgState({ ...orgState, legalAddress: e.target.value })}
/>
</div>
<div className="flex justify-end mt-4">
<Button onClick={handleSaveOrg} disabled={loading}>
{loading ? 'Saving...' : 'Save Profile'}
</Button>
</div>
</CardContent>
</Card>
<Card className="border-orange-200 bg-orange-50/10">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ShieldCheck className="h-5 w-5 text-orange-600" />
Security Policy
</CardTitle>
<CardDescription>
Enforce security rules for all Control Center staff members.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between p-4 border rounded-lg bg-card">
<div className="space-y-0.5">
<Label className="text-base">Enforce 2FA</Label>
<p className="text-sm text-muted-foreground">
Require Two-Factor Authentication for all admin accounts.
</p>
</div>
<Switch
checked={secState.enforce2FA}
onCheckedChange={(checked) => setSecState({ ...secState, enforce2FA: checked })}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Session Timeout (Minutes)</Label>
<Input
type="number"
value={secState.sessionTimeoutMinutes}
onChange={(e) => setSecState({ ...secState, sessionTimeoutMinutes: parseInt(e.target.value) })}
/>
</div>
<div className="space-y-2">
<Label>Password Expiration (Days)</Label>
<Input
type="number"
value={secState.passwordExpirationDays}
onChange={(e) => setSecState({ ...secState, passwordExpirationDays: parseInt(e.target.value) })}
/>
</div>
</div>
<div className="flex justify-end">
<Button variant="outline" onClick={handleSaveSecurity} disabled={loading} className="border-orange-200 text-orange-700 hover:bg-orange-50 hover:text-orange-800">
{loading ? 'Saving...' : 'Update Security Policy'}
</Button>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,173 @@
'use client';
import { useState } from 'react';
import { PartnerConfig } from '@/lib/types/settings';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Switch } from '@/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { updateSystemSetting } from '@/lib/actions/settings';
import { toast } from 'sonner';
import { Handshake, Banknote, FileCheck } from 'lucide-react';
interface PartnerGovernanceProps {
config: PartnerConfig;
onUpdate: (data: PartnerConfig) => void;
}
export function PartnerGovernanceTab({ config, onUpdate }: PartnerGovernanceProps) {
const [loading, setLoading] = useState(false);
const [state, setState] = useState(config);
const handleSave = async () => {
setLoading(true);
try {
const res = await updateSystemSetting('partner', state);
if (res.success && res.updatedSettings.partner) {
toast.success('Partner governance rules updated');
onUpdate(res.updatedSettings.partner);
}
} catch (error) {
toast.error('Failed to update partner settings');
} finally {
setLoading(false);
}
};
const toggleKycDoc = (doc: 'pan' | 'gst' | 'aadhaar' | 'cheque') => {
const current = state.allowedKycDocs;
const updated = current.includes(doc)
? current.filter(d => d !== doc)
: [...current, doc];
setState({ ...state, allowedKycDocs: updated });
};
return (
<div className="space-y-6">
<div className="grid md:grid-cols-2 gap-6">
{/* Onboarding Rules */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Handshake className="h-5 w-5 text-indigo-500" />
Onboarding & Approval
</CardTitle>
<CardDescription>
Set rules for new organizer accounts and events.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between py-2 border-b">
<div className="space-y-0.5">
<Label>Require KYC Approval</Label>
<p className="text-xs text-muted-foreground">Block payouts until verified.</p>
</div>
<Switch
checked={state.requireKyc}
onCheckedChange={(c) => setState({ ...state, requireKyc: c })}
/>
</div>
<div className="flex items-center justify-between py-2">
<div className="space-y-0.5">
<Label>Manual Event Approval</Label>
<p className="text-xs text-muted-foreground">Admins must approve live events.</p>
</div>
<Switch
checked={state.manualEventApproval}
onCheckedChange={(c) => setState({ ...state, manualEventApproval: c })}
/>
</div>
</CardContent>
</Card>
{/* Commission Model */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Banknote className="h-5 w-5 text-green-600" />
Commission & Payouts
</CardTitle>
<CardDescription>
Define the default revenue share model.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Default Commission (%)</Label>
<Input
type="number"
value={state.defaultCommissionPercent}
onChange={(e) => setState({ ...state, defaultCommissionPercent: Number(e.target.value) })}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Payout Schedule</Label>
<Select
value={state.payoutSchedule}
onValueChange={(v: any) => setState({ ...state, payoutSchedule: v })}
>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="daily">Daily (T+1)</SelectItem>
<SelectItem value="weekly">Weekly</SelectItem>
<SelectItem value="monthly">Monthly</SelectItem>
<SelectItem value="manual">Manual Request</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Min. Payout Amount ()</Label>
<Input
type="number"
value={state.minPayoutAmount}
onChange={(e) => setState({ ...state, minPayoutAmount: Number(e.target.value) })}
/>
</div>
</div>
</CardContent>
</Card>
</div>
{/* KYC Requirements */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileCheck className="h-5 w-5 text-slate-500" />
KYC Requirements
</CardTitle>
<CardDescription>
Select documents required for organizer verification.
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{['pan', 'gst', 'aadhaar', 'cheque'].map((doc) => (
<div key={doc} className="flex items-center space-x-2 border p-3 rounded-md">
<Checkbox
id={`doc-${doc}`}
checked={state.allowedKycDocs.includes(doc as any)}
onCheckedChange={() => toggleKycDoc(doc as any)}
/>
<Label htmlFor={`doc-${doc}`} className="uppercase text-xs font-semibold">
{doc}
</Label>
</div>
))}
</div>
</CardContent>
</Card>
<div className="flex justify-end">
<Button onClick={handleSave} disabled={loading}>
{loading ? 'Saving...' : 'Update Governance Rules'}
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,196 @@
'use client';
import { useState } from 'react';
import { PaymentConfig, GatewayProvider, GatewayCredentials } from '@/lib/types/settings';
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Label } from '@/components/ui/label';
import { GatewayConfigSheet } from '@/features/settings/components/GatewayConfigSheet';
import { updateRoutingRules } from '@/lib/actions/payment-settings';
import { toast } from 'sonner';
import { CreditCard, Globe, Settings2, ShieldCheck } from 'lucide-react';
interface PaymentConfigTabProps {
config: PaymentConfig;
onUpdate: (data: PaymentConfig) => void;
}
const PROVIDERS: { id: GatewayProvider; name: string }[] = [
{ id: 'razorpay', name: 'Razorpay' },
{ id: 'stripe', name: 'Stripe' },
{ id: 'payu', name: 'PayU' },
{ id: 'easebuzz', name: 'Easebuzz' },
{ id: 'worldline', name: 'Worldline' },
];
export function PaymentConfigTab({ config, onUpdate }: PaymentConfigTabProps) {
const [selectedProvider, setSelectedProvider] = useState<GatewayProvider | null>(null);
const [sheetOpen, setSheetOpen] = useState(false);
const [routingState, setRoutingState] = useState(config);
const handleOpenConfig = (provider: GatewayProvider) => {
setSelectedProvider(provider);
setSheetOpen(true);
};
const handleRoutingUpdate = async (key: keyof PaymentConfig, value: string) => {
const newState = { ...routingState, [key]: value };
setRoutingState(newState);
try {
const res = await updateRoutingRules({
defaultGateway: newState.defaultGateway,
fallbackGateway: newState.fallbackGateway,
internationalGateway: newState.internationalGateway
});
if (res.success) {
toast.success('Routing rules updated');
// Optimistic update handled by local state, but we should propagate up
onUpdate(newState);
}
} catch (error) {
toast.error('Failed to update routing');
}
};
return (
<div className="space-y-8">
{/* Gateway Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{PROVIDERS.map((provider) => {
const gwConfig = config.gateways[provider.id];
const isActive = gwConfig?.enabled;
const isLive = gwConfig?.mode === 'live';
return (
<Card key={provider.id} className={`group hover:shadow-md transition-shadow ${isActive ? 'border-primary/50 bg-primary/5' : 'opacity-75'}`}>
<CardHeader className="flex flex-row items-start justify-between pb-2">
<CardTitle className="text-lg font-bold">{provider.name}</CardTitle>
{isActive ? (
<Badge variant={isLive ? 'default' : 'secondary'} className={isLive ? 'bg-emerald-600' : ''}>
{isLive ? 'LIVE' : 'TEST'}
</Badge>
) : (
<Badge variant="outline">Disabled</Badge>
)}
</CardHeader>
<CardContent>
<div className="text-sm text-muted-foreground space-y-1">
{isActive ? (
<>
<div className="flex items-center gap-2">
<ShieldCheck className="h-4 w-4 text-emerald-500" />
Verified
</div>
<div className="flex gap-2 mt-2">
{gwConfig.features.upi && <Badge variant="outline" className="text-[10px]">UPI</Badge>}
{gwConfig.features.cards && <Badge variant="outline" className="text-[10px]">Cards</Badge>}
{gwConfig.features.emi && <Badge variant="outline" className="text-[10px]">EMI</Badge>}
</div>
</>
) : (
<div className="flex items-center gap-2 text-muted-foreground">
Not configured
</div>
)}
</div>
</CardContent>
<CardFooter>
<Button
variant={isActive ? "outline" : "secondary"}
className="w-full"
onClick={() => handleOpenConfig(provider.id)}
>
<Settings2 className="h-4 w-4 mr-2" />
Configure
</Button>
</CardFooter>
</Card>
);
})}
</div>
{/* Routing Logic */}
<Card className="border-indigo-200 bg-indigo-50/10">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-indigo-700">
<Globe className="h-5 w-5" />
Smart Routing Logic
</CardTitle>
<CardDescription>
Define how transactions are routed across active gateways.
</CardDescription>
</CardHeader>
<CardContent className="grid md:grid-cols-3 gap-6">
<div className="space-y-2">
<Label>Default Gateway</Label>
<Select
value={routingState.defaultGateway}
onValueChange={(v) => handleRoutingUpdate('defaultGateway', v)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{PROVIDERS.map(p => <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>)}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">Used for 90% of traffic.</p>
</div>
<div className="space-y-2">
<Label>Fallback Gateway</Label>
<Select
value={routingState.fallbackGateway}
onValueChange={(v) => handleRoutingUpdate('fallbackGateway', v)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{PROVIDERS.map(p => <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>)}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">Used if default fails.</p>
</div>
<div className="space-y-2">
<Label>International (Non-INR)</Label>
<Select
value={routingState.internationalGateway}
onValueChange={(v) => handleRoutingUpdate('internationalGateway', v)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{PROVIDERS.map(p => <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>)}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">Cards issued outside India.</p>
</div>
</CardContent>
</Card>
{/* Config Sheet Instance */}
{selectedProvider && config.gateways[selectedProvider] && (
<GatewayConfigSheet
open={sheetOpen}
onOpenChange={setSheetOpen}
provider={selectedProvider}
initialConfig={config.gateways[selectedProvider]}
onSave={() => {
// Refresh parent data? Handled via Actions and upper state update usually,
// but for now we rely on the parent's polling or manual refresh,
// effectively we might want to trigger a callback here.
// Ideally onUpdate would trigger a re-fetch.
window.location.reload(); // Quick dirty refresh to sync state in this demo
}}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,193 @@
'use client';
import { useState } from 'react';
import { PublicAppConfig } from '@/lib/types/settings';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Switch } from '@/components/ui/switch';
import { updateSystemSetting } from '@/lib/actions/settings';
import { toast } from 'sonner';
import { Smartphone, Zap, Percent, Link as LinkIcon } from 'lucide-react';
interface PublicAppConfigProps {
config: PublicAppConfig;
onUpdate: (data: PublicAppConfig) => void;
}
export function PublicAppConfigTab({ config, onUpdate }: PublicAppConfigProps) {
const [loading, setLoading] = useState(false);
const [state, setState] = useState(config);
const handleSave = async () => {
setLoading(true);
try {
const res = await updateSystemSetting('publicApp', state);
if (res.success && res.updatedSettings.publicApp) {
toast.success('Public App configuration updated');
onUpdate(res.updatedSettings.publicApp);
}
} catch (error) {
toast.error('Failed to update app config');
} finally {
setLoading(false);
}
};
// Helper to update deeply nested feature flags
const toggleFeature = (key: keyof PublicAppConfig['betaFeatures']) => {
setState(prev => ({
...prev,
betaFeatures: {
...prev.betaFeatures,
[key]: !prev.betaFeatures[key]
}
}));
};
return (
<div className="space-y-6">
<div className="grid md:grid-cols-2 gap-6">
{/* Feature Flags */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Zap className="h-5 w-5 text-yellow-500" />
Feature Flags
</CardTitle>
<CardDescription>
Toggle beta features for end-users instantly.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between py-2 border-b">
<div className="space-y-0.5">
<Label>Crypto Payments</Label>
<p className="text-xs text-muted-foreground">Accept ETH/SOL via Solana Pay</p>
</div>
<Switch
checked={state.betaFeatures.cryptoPayments}
onCheckedChange={() => toggleFeature('cryptoPayments')}
/>
</div>
<div className="flex items-center justify-between py-2 border-b">
<div className="space-y-0.5">
<Label>AI Recommendations</Label>
<p className="text-xs text-muted-foreground">Show "Events you might like"</p>
</div>
<Switch
checked={state.betaFeatures.aiRecommendations}
onCheckedChange={() => toggleFeature('aiRecommendations')}
/>
</div>
<div className="flex items-center justify-between py-2">
<div className="space-y-0.5">
<Label>Social Login</Label>
<p className="text-xs text-muted-foreground">Enable Google/Apple Auth</p>
</div>
<Switch
checked={state.betaFeatures.socialLogin}
onCheckedChange={() => toggleFeature('socialLogin')}
/>
</div>
</CardContent>
</Card>
{/* Commercials */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Percent className="h-5 w-5 text-emerald-600" />
Booking Fees & Tax
</CardTitle>
<CardDescription>
Set the global platform fee charged to users.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Platform Fee (Flat )</Label>
<Input
type="number"
value={state.fees.platformFeeFlat}
onChange={(e) => setState({
...state,
fees: { ...state.fees, platformFeeFlat: Number(e.target.value) }
})}
/>
<p className="text-xs text-muted-foreground">
Added to every ticket purchase.
</p>
</div>
<div className="space-y-2">
<Label>GST / Tax Rate (%)</Label>
<Input
type="number"
value={state.fees.taxRatePercent}
onChange={(e) => setState({
...state,
fees: { ...state.fees, taxRatePercent: Number(e.target.value) }
})}
/>
</div>
</CardContent>
</Card>
</div>
{/* Links */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<LinkIcon className="h-5 w-5 text-blue-500" />
Support Links
</CardTitle>
<CardDescription>
Update external URLs for help, terms, and privacy policies.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label>Help Center URL</Label>
<Input
value={state.links.helpCenterUrl}
onChange={(e) => setState({
...state,
links: { ...state.links, helpCenterUrl: e.target.value }
})}
/>
</div>
<div className="space-y-2">
<Label>Terms & Conditions URL</Label>
<Input
value={state.links.termsUrl}
onChange={(e) => setState({
...state,
links: { ...state.links, termsUrl: e.target.value }
})}
/>
</div>
<div className="space-y-2">
<Label>Privacy Policy URL</Label>
<Input
value={state.links.privacyUrl}
onChange={(e) => setState({
...state,
links: { ...state.links, privacyUrl: e.target.value }
})}
/>
</div>
</div>
</CardContent>
</Card>
<div className="flex justify-end sticky bottom-6 z-10">
<Button size="lg" onClick={handleSave} disabled={loading} className="shadow-lg">
{loading ? 'Publishing Updates...' : 'Publish Public App Config'}
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,244 @@
'use client';
import { useState } from 'react';
import { SystemConfig } from '@/lib/types/settings';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
import { Switch } from '@/components/ui/switch';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { updateSystemSetting, purgeSystemCache } from '@/lib/actions/settings';
import { toast } from 'sonner';
import { Server, CreditCard, Radio, Trash2, AlertTriangle, Activity } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
interface SystemHealthProps {
config: SystemConfig;
onUpdate: (data: SystemConfig) => void;
}
export function SystemHealthTab({ config, onUpdate }: SystemHealthProps) {
const [loading, setLoading] = useState(false);
const [state, setState] = useState(config);
const [purgeLoading, setPurgeLoading] = useState(false);
const handleSave = async () => {
setLoading(true);
try {
const res = await updateSystemSetting('system', state);
if (res.success && res.updatedSettings.system) {
toast.success('System configuration updated');
onUpdate(res.updatedSettings.system);
}
} catch (error) {
toast.error('Failed to update system config');
} finally {
setLoading(false);
}
};
const handlePurgeCache = async () => {
setPurgeLoading(true);
try {
const res = await purgeSystemCache();
if (res.success) {
toast.success('Cache purged successfully', {
description: 'Changes are propagating to edge locations.'
});
}
} catch (error) {
toast.error('Failed to purge cache');
} finally {
setPurgeLoading(false);
}
};
return (
<div className="space-y-6">
{/* System Status Banner */}
<div className="grid md:grid-cols-3 gap-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">API Status</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2">
<div className="h-2.5 w-2.5 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-2xl font-bold">Operational</span>
</div>
<p className="text-xs text-muted-foreground mt-1">Uptime: 99.99%</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">Cache Status</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<span className="text-2xl font-bold">Healthy</span>
<Badge variant="outline">TTL: {state.cache.ttl}s</Badge>
</div>
<p className="text-xs text-muted-foreground mt-1">
Last Purged: {state.cache.lastPurgedAt ? new Date(state.cache.lastPurgedAt).toLocaleTimeString() : 'Never'}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">Database</CardTitle>
</CardHeader>
<CardContent>
<span className="text-2xl font-bold">Connected</span>
<p className="text-xs text-muted-foreground mt-1">Latency: 24ms</p>
</CardContent>
</Card>
</div>
{/* Payment Gateways */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CreditCard className="h-5 w-5 text-primary" />
Payment Gateways
</CardTitle>
<CardDescription>
Manage gateway keys and active modes (Test vs Live).
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Stripe */}
<div className="p-4 border rounded-lg bg-card/50">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<h4 className="font-semibold">Stripe</h4>
{state.gateways.stripe.mode === 'live' ?
<Badge className="bg-emerald-500">Live</Badge> :
<Badge variant="secondary">Test Mode</Badge>
}
</div>
<Switch
checked={state.gateways.stripe.enabled}
onCheckedChange={(c) => setState({
...state,
gateways: { ...state.gateways, stripe: { ...state.gateways.stripe, enabled: c } }
})}
/>
</div>
<div className="grid gap-2">
<Label>Public Key</Label>
<Input
type="password"
value={state.gateways.stripe.publicKey}
readOnly // For demo safety
className="font-mono text-xs"
/>
</div>
<div className="flex items-center gap-2 mt-4">
<Label className="text-xs">Mode:</Label>
<div className="flex items-center space-x-2">
<Switch
checked={state.gateways.stripe.mode === 'live'}
onCheckedChange={(c) => setState({
...state,
gateways: { ...state.gateways, stripe: { ...state.gateways.stripe, mode: c ? 'live' : 'test' } }
})}
/>
<span className={state.gateways.stripe.mode === 'live' ? 'font-bold text-red-600' : 'text-muted-foreground'}>
{state.gateways.stripe.mode === 'live' ? 'LIVE TRAFFIC' : 'Test Mode'}
</span>
</div>
</div>
</div>
{/* Razorpay */}
<div className="p-4 border rounded-lg bg-card/50">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<h4 className="font-semibold">Razorpay</h4>
{state.gateways.razorpay.mode === 'live' ?
<Badge className="bg-emerald-500">Live</Badge> :
<Badge variant="secondary">Test Mode</Badge>
}
</div>
<Switch
checked={state.gateways.razorpay.enabled}
onCheckedChange={(c) => setState({
...state,
gateways: { ...state.gateways, razorpay: { ...state.gateways.razorpay, enabled: c } }
})}
/>
</div>
<div className="grid gap-2">
<Label>Key ID</Label>
<Input
type="password"
value={state.gateways.razorpay.keyId}
readOnly
className="font-mono text-xs"
/>
</div>
</div>
</CardContent>
<CardFooter className="justify-end border-t pt-4">
<Button onClick={handleSave} disabled={loading}>Save Gateway Config</Button>
</CardFooter>
</Card>
{/* Danger Zone */}
<h3 className="text-lg font-semibold text-red-600 flex items-center gap-2 mt-8">
<AlertTriangle className="h-5 w-5" />
Danger Zone
</h3>
<div className="grid md:grid-cols-2 gap-6">
<Card className="border-red-200 bg-red-50/10">
<CardHeader>
<CardTitle className="text-base text-red-700">Maintenance Mode</CardTitle>
<CardDescription>
Disable the public facing app temporarily. Only admins can access.
</CardDescription>
</CardHeader>
<CardContent className="flex justify-between items-center">
<div className="space-y-1">
<Label>Status</Label>
<p className="text-sm font-medium">
{false ? <span className="text-red-600">Active - App Offline</span> : <span className="text-emerald-600">Inactive - App Live</span>}
</p>
</div>
<Button variant="destructive">Enable Maintenance</Button>
</CardContent>
</Card>
<Card className="border-orange-200 bg-orange-50/10">
<CardHeader>
<CardTitle className="text-base text-orange-700">Cache Control</CardTitle>
<CardDescription>
Force purge the CDN cache for all static pages.
</CardDescription>
</CardHeader>
<CardContent className="flex justify-between items-center">
<div className="space-y-1">
<Label>Metrics</Label>
<p className="text-xs text-muted-foreground">Items cached: ~14.2k</p>
</div>
<Button
variant="outline"
className="border-orange-300 text-orange-700 hover:bg-orange-100"
onClick={handlePurgeCache}
disabled={purgeLoading}
>
{purgeLoading ? (
<Activity className="h-4 w-4 mr-2 animate-spin" />
) : (
<Trash2 className="h-4 w-4 mr-2" />
)}
Purge Cache
</Button>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,79 @@
import { GatewayProvider, GatewayCredentials, PaymentConfig, GlobalSettings } from '../types/settings';
import { updateSystemSetting, getSystemSettings } from './settings';
import { encrypt, decrypt } from '../payment-encryption';
export async function verifyGatewayCredentials(
provider: GatewayProvider,
credentials: Partial<GatewayCredentials>
): Promise<{ success: boolean; message: string }> {
// Simulate API call to provider
await new Promise(resolve => setTimeout(resolve, 1500));
if (provider === 'payu' && (!credentials.merchantId || !credentials.salt)) {
return { success: false, message: 'Invalid PayU credentials: Merchant ID and Salt are required.' };
}
if (provider === 'razorpay' && !credentials.keyId) {
return { success: false, message: 'Invalid Razorpay Key ID.' };
}
// Success simulation
return { success: true, message: `Successfully connected to ${provider.toUpperCase()} [${credentials.mode} mode]` };
}
export async function saveGatewayConfig(
provider: GatewayProvider,
config: GatewayCredentials
): Promise<{ success: boolean; message: string }> {
// Fetch current settings first
const currentRes = await getSystemSettings();
if (!currentRes.success) return { success: false, message: 'Failed to retrieve current settings' };
const settings = currentRes.data;
// Encrypt sensitive fields
const encryptedConfig = { ...config };
if (config.salt) encryptedConfig.salt = encrypt(config.salt);
// Note: We don't encrypt public keys usually, but secrets yes.
// For demo simplicity, we encrypt 'salt' and assume others are public/safe or handled similarly.
// Update specific gateway in payment config
const newPaymentConfig: PaymentConfig = {
...settings.payment,
gateways: {
...settings.payment.gateways,
[provider]: encryptedConfig
}
};
// Use our main update action
const updateRes = await updateSystemSetting('payment', newPaymentConfig);
return {
success: updateRes.success,
message: `Saved configuration for ${provider}`
};
}
export async function updateRoutingRules(
rules: Pick<PaymentConfig, 'defaultGateway' | 'fallbackGateway' | 'internationalGateway'>
): Promise<{ success: boolean; message: string }> {
// Fetch current settings first
const currentRes = await getSystemSettings();
if (!currentRes.success) return { success: false, message: 'Failed to retrieve settings' };
const settings = currentRes.data;
const newPaymentConfig: PaymentConfig = {
...settings.payment,
...rules
};
const updateRes = await updateSystemSetting('payment', newPaymentConfig);
return {
success: updateRes.success,
message: 'Routing logic updated successfully'
};
}

View File

@@ -0,0 +1,95 @@
import { GlobalSettings, DEFAULT_SETTINGS } from '../types/settings';
// Simulate a database/store with a singleton object
// In a real app, this would be a DB table 'system_config'
let MOCK_DB_SETTINGS: GlobalSettings = { ...DEFAULT_SETTINGS };
// Load from localStorage if available (client-side only trick for persistence in this demo)
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('mock_system_settings');
if (saved) {
try {
const parsed = JSON.parse(saved);
MOCK_DB_SETTINGS = { ...DEFAULT_SETTINGS, ...parsed };
} catch (e) {
console.error("Failed to parse saved settings", e);
}
}
}
export async function getSystemSettings(): Promise<{ success: boolean; data: GlobalSettings }> {
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 500));
// In a real app, we would fetch from DB here
// return db.systemConfig.findFirst();
// For now, return our in-memory/local storage mock
// We try to read from localStorage again to sync tabs
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('mock_system_settings');
if (saved) {
try {
const parsed = JSON.parse(saved);
// Deep merge or at least top-level merge to ensure new sections (like 'payment') are added
MOCK_DB_SETTINGS = {
...DEFAULT_SETTINGS,
...parsed,
};
} catch (e) { /* ignore */ }
}
}
return { success: true, data: MOCK_DB_SETTINGS };
}
export async function updateSystemSetting<K extends keyof GlobalSettings>(
section: K,
data: Partial<GlobalSettings[K]>
): Promise<{ success: boolean; message: string; updatedSettings: GlobalSettings }> {
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 800));
// Update the specific section
MOCK_DB_SETTINGS = {
...MOCK_DB_SETTINGS,
[section]: {
...MOCK_DB_SETTINGS[section],
...data
}
};
// Persist to localStorage for demo
if (typeof window !== 'undefined') {
localStorage.setItem('mock_system_settings', JSON.stringify(MOCK_DB_SETTINGS));
}
console.log(`[AUDIT] System Setting Updated: ${section}`, data);
// In Next.js we would maintain revalidatePath here
// revalidatePath('/settings');
return {
success: true,
message: 'Settings updated successfully.',
updatedSettings: MOCK_DB_SETTINGS
};
}
export async function purgeSystemCache(): Promise<{ success: boolean; message: string }> {
await new Promise(resolve => setTimeout(resolve, 1500));
console.log(`[AUDIT] System Cache Purged by Admin`);
// Update last purged time
MOCK_DB_SETTINGS.system.cache.lastPurgedAt = new Date().toISOString();
if (typeof window !== 'undefined') {
localStorage.setItem('mock_system_settings', JSON.stringify(MOCK_DB_SETTINGS));
}
return {
success: true,
message: 'System cache purged. Changes are now live on Public App.'
};
}

View File

@@ -0,0 +1,33 @@
// In a real Node.js environment, we would use 'crypto'
// import crypto from 'crypto';
// Since this is a Vite client-side demo, we'll mock the encryption
// to simulate the security layer.
export function encrypt(text: string): string {
if (!text) return '';
try {
// Simple base64 encoding simulation + prefix
return `enc_${btoa(text)}`;
} catch (e) {
console.error("Encryption failed", e);
return text;
}
}
export function decrypt(text: string): string {
if (!text) return '';
if (!text.startsWith('enc_')) return text; // Return as-is if not encrypted
try {
const payload = text.replace('enc_', '');
return atob(payload);
} catch (e) {
console.error("Decryption failed", e);
return text;
}
}
export function maskKey(key: string): string {
if (!key || key.length < 8) return '********';
return `******${key.slice(-4)}`;
}

173
src/lib/types/settings.ts Normal file
View File

@@ -0,0 +1,173 @@
export interface OrganizationConfig {
brandName: string;
supportEmail: string;
legalAddress: string;
logoUrl?: string; // For emails
socialLinks: {
twitter?: string;
linkedin?: string;
instagram?: string;
};
}
export interface SecurityConfig {
enforce2FA: boolean;
sessionTimeoutMinutes: number;
passwordExpirationDays: number;
}
export interface PublicAppConfig {
maintenanceMode: boolean;
betaFeatures: {
cryptoPayments: boolean;
aiRecommendations: boolean;
socialLogin: boolean;
};
fees: {
platformFeeFlat: number; // e.g. 20 (INR)
taxRatePercent: number; // e.g. 18 (GST)
};
banners: {
id: string;
imageUrl: string;
linkUrl: string;
active: boolean;
}[];
links: {
helpCenterUrl: string;
termsUrl: string;
privacyUrl: string;
};
}
export interface PartnerConfig {
requireKyc: boolean;
manualEventApproval: boolean;
defaultCommissionPercent: number;
payoutSchedule: 'daily' | 'weekly' | 'monthly' | 'manual';
minPayoutAmount: number;
allowedKycDocs: ('pan' | 'gst' | 'aadhaar' | 'cheque')[];
}
export type GatewayProvider = 'razorpay' | 'stripe' | 'payu' | 'easebuzz' | 'worldline';
export interface GatewayCredentials {
enabled: boolean;
mode: 'test' | 'live';
merchantId?: string;
publicKey?: string; // For Stripe
keyId?: string; // For Razorpay
salt?: string; // For PayU/Easebuzz
webhookSecret?: string;
features: {
netbanking: boolean;
upi: boolean;
cards: boolean;
emi: boolean;
wallet: boolean;
};
}
export interface PaymentConfig {
defaultGateway: GatewayProvider;
fallbackGateway: GatewayProvider;
internationalGateway: GatewayProvider;
gateways: Record<GatewayProvider, GatewayCredentials>;
}
export interface GlobalSettings {
organization: OrganizationConfig;
security: SecurityConfig;
publicApp: PublicAppConfig;
partner: PartnerConfig;
system: SystemConfig;
payment: PaymentConfig; // New section
}
export const DEFAULT_SETTINGS: GlobalSettings = {
organization: {
brandName: 'Eventify',
supportEmail: 'support@eventify.com',
legalAddress: '123 Tech Park, Bangalore, India',
socialLinks: {}
},
security: {
enforce2FA: false,
sessionTimeoutMinutes: 60,
passwordExpirationDays: 90
},
publicApp: {
maintenanceMode: false,
betaFeatures: {
cryptoPayments: false,
aiRecommendations: true,
socialLogin: true
},
fees: {
platformFeeFlat: 20,
taxRatePercent: 18
},
banners: [],
links: {
helpCenterUrl: 'https://help.eventify.com',
termsUrl: 'https://eventify.com/terms',
privacyUrl: 'https://eventify.com/privacy'
}
},
partner: {
requireKyc: true,
manualEventApproval: false,
defaultCommissionPercent: 5,
payoutSchedule: 'weekly',
minPayoutAmount: 1000,
allowedKycDocs: ['pan', 'gst', 'cheque']
},
system: {
gateways: { // Legacy structure, will migrate to 'payment'
stripe: { enabled: true, mode: 'test', publicKey: 'pk_test_...' },
razorpay: { enabled: true, mode: 'test', keyId: 'rzp_test_...' }
},
cache: {
ttl: 3600
}
},
payment: {
defaultGateway: 'razorpay',
fallbackGateway: 'stripe',
internationalGateway: 'stripe',
gateways: {
razorpay: {
enabled: true,
mode: 'test',
keyId: 'rzp_test_123456',
features: { netbanking: true, upi: true, cards: true, emi: false, wallet: true }
},
stripe: {
enabled: true,
mode: 'test',
publicKey: 'pk_test_123456',
features: { netbanking: false, upi: false, cards: true, emi: false, wallet: false }
},
payu: {
enabled: false,
mode: 'test',
merchantId: '',
salt: '',
features: { netbanking: true, upi: true, cards: true, emi: true, wallet: true }
},
easebuzz: {
enabled: false,
mode: 'test',
merchantId: '',
salt: '',
features: { netbanking: true, upi: true, cards: true, emi: true, wallet: true }
},
worldline: {
enabled: false,
mode: 'test',
merchantId: '',
features: { netbanking: true, upi: true, cards: true, emi: false, wallet: false }
}
}
}
};

View File

@@ -1,32 +1,10 @@
import { AppLayout } from '@/components/layout/AppLayout'; import { AppLayout } from '@/components/layout/AppLayout';
import { OrganizationProfileCard } from '@/features/settings/components/OrganizationProfileCard'; import { SettingsLayout } from '@/features/settings/components/SettingsLayout';
import { PayoutBankingCard } from '@/features/settings/components/PayoutBankingCard';
import { TeamSecurityCard } from '@/features/settings/components/TeamSecurityCard';
import { DeveloperSection } from '@/features/settings/components/DeveloperSection';
import { PaymentGatewayCard } from '@/features/settings/components/PaymentGatewayCard';
export default function Settings() { export default function Settings() {
return ( return (
<AppLayout <AppLayout>
title="Settings" <SettingsLayout />
description="Manage platform configuration and critical operations."
>
<div className="space-y-6 max-w-[1200px]">
{/* Top Grid: Identity & Banking */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 h-auto lg:h-[400px]">
<OrganizationProfileCard />
<PayoutBankingCard />
</div>
{/* Middle Grid: Gateways & Security */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 h-auto">
<PaymentGatewayCard />
<TeamSecurityCard />
</div>
{/* Collapsible Developer Section */}
<DeveloperSection />
</div>
</AppLayout> </AppLayout>
); );
} }