feat: add admin dashboard schema and seeder

- Add Plan, Subscription, Payment, PaymentMethod, Coupon models
- Add ApiKey, Webhook models for API access
- Add AppConfig model for dynamic configuration
- Add role, suspendedAt fields to User model
- Create comprehensive seeder with:
  - Admin user (dwindi.ramadhana@gmail.com)
  - Default plans (Free, Pro Monthly, Pro Yearly)
  - Payment methods (BCA, Mandiri, GoPay)
  - App config (maintenance mode)
- Zero data loss migration strategy
This commit is contained in:
dwindown
2025-10-11 14:06:55 +07:00
parent 249f3a9d7d
commit c3bc181063
11 changed files with 1052 additions and 37 deletions

238
apps/api/dist/seed.js vendored
View File

@@ -1,13 +1,233 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const client_1 = require("@prisma/client");
const bcrypt = __importStar(require("bcrypt"));
const prisma = new client_1.PrismaClient();
const adminSeeder = {
email: 'dwindi.ramadhana@gmail.com',
password: 'tabungin2k25!@#',
};
const TEMP_USER_ID = process.env.TEMP_USER_ID || '16b74848-daa3-4dc9-8de2-3cf59e08f8e3';
const ADMIN_EMAIL = 'dwindi.ramadhana@gmail.com';
const ADMIN_PASSWORD = 'tabungin2k25!@#';
async function main() {
console.log('🌱 Starting seed...');
console.log('\n👤 Creating admin user...');
const passwordHash = await bcrypt.hash(ADMIN_PASSWORD, 10);
const admin = await prisma.user.upsert({
where: { email: ADMIN_EMAIL },
update: {
role: 'admin',
passwordHash,
emailVerified: true,
},
create: {
email: ADMIN_EMAIL,
passwordHash,
name: 'Dwindi Ramadhana',
role: 'admin',
emailVerified: true,
},
});
console.log('✅ Admin user created:', admin.email);
console.log('\n💰 Creating default plans...');
const freePlan = await prisma.plan.upsert({
where: { slug: 'free' },
update: {},
create: {
name: 'Free',
slug: 'free',
description: 'Perfect for getting started',
price: 0,
currency: 'IDR',
durationType: 'lifetime',
durationDays: null,
trialDays: 0,
features: {
wallets: { limit: 5, label: '5 wallets' },
goals: { limit: 3, label: '3 goals' },
team: { enabled: false, label: 'No team feature' },
api: { enabled: false, label: 'No API access' },
support: { level: 'basic', label: 'Basic support' },
export: { enabled: true, formats: ['csv'], label: 'CSV export' },
},
badge: null,
sortOrder: 1,
isActive: true,
isVisible: true,
isFeatured: false,
maxWallets: 5,
maxGoals: 3,
maxTeamMembers: 0,
apiEnabled: false,
apiRateLimit: null,
},
});
const proMonthly = await prisma.plan.upsert({
where: { slug: 'pro-monthly' },
update: {},
create: {
name: 'Pro Monthly',
slug: 'pro-monthly',
description: 'Perfect for individuals and small teams',
price: 49000,
currency: 'IDR',
durationType: 'monthly',
durationDays: 30,
trialDays: 7,
features: {
wallets: { limit: null, label: 'Unlimited wallets' },
goals: { limit: null, label: 'Unlimited goals' },
team: { enabled: true, maxMembers: 10, label: 'Team feature (10 members)' },
api: { enabled: true, rateLimit: 1000, label: 'API access (1000 req/hr)' },
support: { level: 'priority', label: 'Priority support' },
export: { enabled: true, formats: ['csv', 'excel', 'pdf'], label: 'All export formats' },
},
badge: 'Popular',
badgeColor: 'blue',
highlightColor: '#3B82F6',
sortOrder: 2,
isActive: true,
isVisible: true,
isFeatured: true,
maxWallets: null,
maxGoals: null,
maxTeamMembers: 10,
apiEnabled: true,
apiRateLimit: 1000,
},
});
const proYearly = await prisma.plan.upsert({
where: { slug: 'pro-yearly' },
update: {},
create: {
name: 'Pro Yearly',
slug: 'pro-yearly',
description: 'Best value - Save 17% with annual billing',
price: 490000,
currency: 'IDR',
durationType: 'yearly',
durationDays: 365,
trialDays: 7,
features: {
wallets: { limit: null, label: 'Unlimited wallets' },
goals: { limit: null, label: 'Unlimited goals' },
team: { enabled: true, maxMembers: 10, label: 'Team feature (10 members)' },
api: { enabled: true, rateLimit: 1000, label: 'API access (1000 req/hr)' },
support: { level: 'priority', label: 'Priority support' },
export: { enabled: true, formats: ['csv', 'excel', 'pdf'], label: 'All export formats' },
discount: { value: '17%', label: 'Save 17% (2 months free)' },
},
badge: 'Best Value',
badgeColor: 'green',
highlightColor: '#10B981',
sortOrder: 3,
isActive: true,
isVisible: true,
isFeatured: true,
maxWallets: null,
maxGoals: null,
maxTeamMembers: 10,
apiEnabled: true,
apiRateLimit: 1000,
},
});
console.log('✅ Plans created:', [freePlan.name, proMonthly.name, proYearly.name]);
console.log('\n💳 Creating default payment methods...');
const bcaMethod = await prisma.paymentMethod.upsert({
where: { id: 'bca-method' },
update: {},
create: {
id: 'bca-method',
type: 'bank_transfer',
provider: 'BCA',
accountName: 'PT Tabungin Indonesia',
accountNumber: '1234567890',
displayName: 'BCA Virtual Account',
logoUrl: '/logos/bca.png',
instructions: 'Transfer to the account above and upload proof of payment.',
isActive: true,
sortOrder: 1,
},
});
const mandiriMethod = await prisma.paymentMethod.upsert({
where: { id: 'mandiri-method' },
update: {},
create: {
id: 'mandiri-method',
type: 'bank_transfer',
provider: 'Mandiri',
accountName: 'PT Tabungin Indonesia',
accountNumber: '9876543210',
displayName: 'Mandiri Virtual Account',
logoUrl: '/logos/mandiri.png',
instructions: 'Transfer to the account above and upload proof of payment.',
isActive: true,
sortOrder: 2,
},
});
const gopayMethod = await prisma.paymentMethod.upsert({
where: { id: 'gopay-method' },
update: {},
create: {
id: 'gopay-method',
type: 'e-wallet',
provider: 'GoPay',
accountName: 'Dwindi Ramadhana',
accountNumber: '081234567890',
displayName: 'GoPay',
logoUrl: '/logos/gopay.png',
instructions: 'Send payment to the number above and upload proof.',
isActive: true,
sortOrder: 3,
},
});
console.log('✅ Payment methods created:', [bcaMethod.displayName, mandiriMethod.displayName, gopayMethod.displayName]);
console.log('\n⚙ Creating app config...');
await prisma.appConfig.upsert({
where: { key: 'MAINTENANCE_MODE' },
update: {},
create: {
key: 'MAINTENANCE_MODE',
value: 'false',
category: 'general',
label: 'Maintenance Mode',
description: 'Enable to show maintenance page to users',
type: 'boolean',
isSecret: false,
},
});
console.log('✅ App config created');
console.log('\n🔧 Creating temp user (legacy)...');
const user = await prisma.user.upsert({
where: { id: TEMP_USER_ID },
update: {},
@@ -27,7 +247,15 @@ async function main() {
},
});
}
console.log('Seed complete. TEMP_USER_ID=', user.id);
console.log('✅ Temp user created:', user.id);
console.log('\n🎉 Seed complete!');
console.log('\n📋 Summary:');
console.log(' Admin Email:', ADMIN_EMAIL);
console.log(' Admin Password:', ADMIN_PASSWORD);
console.log(' Plans:', [freePlan.name, proMonthly.name, proYearly.name].join(', '));
console.log(' Payment Methods:', [bcaMethod.displayName, mandiriMethod.displayName, gopayMethod.displayName].join(', '));
console.log('\n⚠ IMPORTANT: Change admin password after first login!');
console.log('\n🔗 Login at: http://localhost:5174/auth/login');
}
main()
.catch((e) => {

File diff suppressed because one or more lines are too long

View File

@@ -13,11 +13,11 @@ export declare class TransactionsController {
id: string;
createdAt: Date;
userId: string;
walletId: string;
date: Date;
amount: import("@prisma/client/runtime/library").Decimal;
direction: string;
date: Date;
memo: string | null;
walletId: string;
recurrenceId: string | null;
}[]>;
create(req: RequestWithUser, walletId: string, body: {
@@ -31,11 +31,11 @@ export declare class TransactionsController {
id: string;
createdAt: Date;
userId: string;
walletId: string;
date: Date;
amount: import("@prisma/client/runtime/library").Decimal;
direction: string;
date: Date;
memo: string | null;
walletId: string;
recurrenceId: string | null;
}>;
exportCsv(req: RequestWithUser, walletId: string, from: string | undefined, to: string | undefined, category: string | undefined, direction: 'in' | 'out' | undefined, res: Response): Promise<void>;
@@ -44,11 +44,11 @@ export declare class TransactionsController {
id: string;
createdAt: Date;
userId: string;
walletId: string;
date: Date;
amount: import("@prisma/client/runtime/library").Decimal;
direction: string;
date: Date;
memo: string | null;
walletId: string;
recurrenceId: string | null;
}>;
delete(req: RequestWithUser, walletId: string, id: string): Promise<{
@@ -56,11 +56,11 @@ export declare class TransactionsController {
id: string;
createdAt: Date;
userId: string;
walletId: string;
date: Date;
amount: import("@prisma/client/runtime/library").Decimal;
direction: string;
date: Date;
memo: string | null;
walletId: string;
recurrenceId: string | null;
}>;
}

View File

@@ -9,11 +9,11 @@ export declare class TransactionsService {
id: string;
createdAt: Date;
userId: string;
walletId: string;
date: Date;
amount: Prisma.Decimal;
direction: string;
date: Date;
memo: string | null;
walletId: string;
recurrenceId: string | null;
}[]>;
listAll(userId: string): Prisma.PrismaPromise<{
@@ -21,11 +21,11 @@ export declare class TransactionsService {
id: string;
createdAt: Date;
userId: string;
walletId: string;
date: Date;
amount: Prisma.Decimal;
direction: string;
date: Date;
memo: string | null;
walletId: string;
recurrenceId: string | null;
}[]>;
listWithFilters(userId: string, walletId: string, filters: {
@@ -38,11 +38,11 @@ export declare class TransactionsService {
id: string;
createdAt: Date;
userId: string;
walletId: string;
date: Date;
amount: Prisma.Decimal;
direction: string;
date: Date;
memo: string | null;
walletId: string;
recurrenceId: string | null;
}[]>;
create(userId: string, walletId: string, input: {
@@ -56,11 +56,11 @@ export declare class TransactionsService {
id: string;
createdAt: Date;
userId: string;
walletId: string;
date: Date;
amount: Prisma.Decimal;
direction: string;
date: Date;
memo: string | null;
walletId: string;
recurrenceId: string | null;
}>;
update(userId: string, walletId: string, id: string, dto: TransactionUpdateDto): Promise<{
@@ -68,11 +68,11 @@ export declare class TransactionsService {
id: string;
createdAt: Date;
userId: string;
walletId: string;
date: Date;
amount: Prisma.Decimal;
direction: string;
date: Date;
memo: string | null;
walletId: string;
recurrenceId: string | null;
}>;
delete(userId: string, walletId: string, id: string): Promise<{
@@ -80,11 +80,11 @@ export declare class TransactionsService {
id: string;
createdAt: Date;
userId: string;
walletId: string;
date: Date;
amount: Prisma.Decimal;
direction: string;
date: Date;
memo: string | null;
walletId: string;
recurrenceId: string | null;
}>;
}

File diff suppressed because one or more lines are too long

View File

@@ -25,6 +25,10 @@ export declare class UsersController {
otpWhatsappEnabled: boolean;
otpTotpEnabled: boolean;
otpTotpSecret: string | null;
role: string;
suspendedAt: Date | null;
suspendedReason: string | null;
lastLoginAt: Date | null;
} | null>;
updateProfile(req: RequestWithUser, body: {
name?: string;

View File

@@ -19,6 +19,10 @@ export declare class UsersService {
otpWhatsappEnabled: boolean;
otpTotpEnabled: boolean;
otpTotpSecret: string | null;
role: string;
suspendedAt: Date | null;
suspendedReason: string | null;
lastLoginAt: Date | null;
} | null>;
updateProfile(userId: string, data: {
name?: string;

View File

@@ -27,11 +27,11 @@ export declare class WalletsController {
id: string;
createdAt: Date;
userId: string;
walletId: string;
date: Date;
amount: import("@prisma/client/runtime/library").Decimal;
direction: string;
date: Date;
memo: string | null;
walletId: string;
recurrenceId: string | null;
}[]>;
create(req: RequestWithUser, body: {

View File

@@ -0,0 +1,316 @@
-- AlterTable
ALTER TABLE "public"."User" ADD COLUMN "lastLoginAt" TIMESTAMP(3),
ADD COLUMN "role" TEXT NOT NULL DEFAULT 'user',
ADD COLUMN "suspendedAt" TIMESTAMP(3),
ADD COLUMN "suspendedReason" TEXT;
-- CreateTable
CREATE TABLE "public"."Plan" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"price" DECIMAL(10,2) NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'IDR',
"durationType" TEXT NOT NULL,
"durationDays" INTEGER,
"trialDays" INTEGER NOT NULL DEFAULT 7,
"features" JSONB NOT NULL,
"badge" TEXT,
"badgeColor" TEXT,
"highlightColor" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"isVisible" BOOLEAN NOT NULL DEFAULT true,
"isFeatured" BOOLEAN NOT NULL DEFAULT false,
"maxWallets" INTEGER,
"maxGoals" INTEGER,
"maxTeamMembers" INTEGER,
"apiEnabled" BOOLEAN NOT NULL DEFAULT false,
"apiRateLimit" INTEGER,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Plan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."Subscription" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"planId" TEXT NOT NULL,
"status" TEXT NOT NULL,
"startDate" TIMESTAMP(3) NOT NULL,
"endDate" TIMESTAMP(3) NOT NULL,
"isTrialUsed" BOOLEAN NOT NULL DEFAULT false,
"trialEndDate" TIMESTAMP(3),
"cancelledAt" TIMESTAMP(3),
"cancellationReason" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Subscription_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."Payment" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"subscriptionId" TEXT,
"invoiceNumber" TEXT NOT NULL,
"amount" DECIMAL(10,2) NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'IDR',
"method" TEXT NOT NULL,
"tripayReference" TEXT,
"tripayFee" DECIMAL(10,2),
"totalAmount" DECIMAL(10,2) NOT NULL,
"paymentChannel" TEXT,
"paymentUrl" TEXT,
"qrUrl" TEXT,
"status" TEXT NOT NULL,
"proofImageUrl" TEXT,
"transferDate" TIMESTAMP(3),
"verifiedBy" TEXT,
"verifiedAt" TIMESTAMP(3),
"rejectionReason" TEXT,
"couponId" TEXT,
"discountAmount" DECIMAL(10,2),
"notes" TEXT,
"expiresAt" TIMESTAMP(3),
"paidAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Payment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."PaymentMethod" (
"id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"accountName" TEXT NOT NULL,
"accountNumber" TEXT NOT NULL,
"displayName" TEXT NOT NULL,
"logoUrl" TEXT,
"instructions" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PaymentMethod_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."Coupon" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"discountType" TEXT NOT NULL,
"discountValue" DECIMAL(10,2) NOT NULL,
"maxDiscount" DECIMAL(10,2),
"validFrom" TIMESTAMP(3) NOT NULL,
"validUntil" TIMESTAMP(3) NOT NULL,
"maxUses" INTEGER,
"usedCount" INTEGER NOT NULL DEFAULT 0,
"minPurchase" DECIMAL(10,2),
"applicablePlans" TEXT[],
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Coupon_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."AppConfig" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"value" TEXT NOT NULL,
"category" TEXT NOT NULL,
"label" TEXT NOT NULL,
"description" TEXT,
"type" TEXT NOT NULL,
"isSecret" BOOLEAN NOT NULL DEFAULT false,
"updatedAt" TIMESTAMP(3) NOT NULL,
"updatedBy" TEXT,
CONSTRAINT "AppConfig_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."ApiKey" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"keyHash" TEXT NOT NULL,
"prefix" TEXT NOT NULL,
"scopes" TEXT[],
"lastUsedAt" TIMESTAMP(3),
"expiresAt" TIMESTAMP(3),
"revokedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ApiKey_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."ApiKeyUsage" (
"id" TEXT NOT NULL,
"apiKeyId" TEXT NOT NULL,
"endpoint" TEXT NOT NULL,
"method" TEXT NOT NULL,
"statusCode" INTEGER NOT NULL,
"responseTime" INTEGER NOT NULL,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ApiKeyUsage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."Webhook" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"url" TEXT NOT NULL,
"events" TEXT[],
"secret" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"lastTriggeredAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Webhook_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."WebhookDelivery" (
"id" TEXT NOT NULL,
"webhookId" TEXT NOT NULL,
"event" TEXT NOT NULL,
"payload" JSONB NOT NULL,
"status" TEXT NOT NULL,
"statusCode" INTEGER,
"response" TEXT,
"attempts" INTEGER NOT NULL DEFAULT 0,
"nextRetryAt" TIMESTAMP(3),
"deliveredAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "WebhookDelivery_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Plan_slug_key" ON "public"."Plan"("slug");
-- CreateIndex
CREATE INDEX "Plan_slug_idx" ON "public"."Plan"("slug");
-- CreateIndex
CREATE INDEX "Plan_isActive_idx" ON "public"."Plan"("isActive");
-- CreateIndex
CREATE INDEX "Plan_isVisible_idx" ON "public"."Plan"("isVisible");
-- CreateIndex
CREATE INDEX "Plan_sortOrder_idx" ON "public"."Plan"("sortOrder");
-- CreateIndex
CREATE UNIQUE INDEX "Subscription_userId_key" ON "public"."Subscription"("userId");
-- CreateIndex
CREATE INDEX "Subscription_userId_idx" ON "public"."Subscription"("userId");
-- CreateIndex
CREATE INDEX "Subscription_status_idx" ON "public"."Subscription"("status");
-- CreateIndex
CREATE INDEX "Subscription_endDate_idx" ON "public"."Subscription"("endDate");
-- CreateIndex
CREATE UNIQUE INDEX "Payment_invoiceNumber_key" ON "public"."Payment"("invoiceNumber");
-- CreateIndex
CREATE UNIQUE INDEX "Payment_tripayReference_key" ON "public"."Payment"("tripayReference");
-- CreateIndex
CREATE INDEX "Payment_userId_idx" ON "public"."Payment"("userId");
-- CreateIndex
CREATE INDEX "Payment_status_idx" ON "public"."Payment"("status");
-- CreateIndex
CREATE INDEX "Payment_invoiceNumber_idx" ON "public"."Payment"("invoiceNumber");
-- CreateIndex
CREATE INDEX "Payment_tripayReference_idx" ON "public"."Payment"("tripayReference");
-- CreateIndex
CREATE INDEX "PaymentMethod_isActive_idx" ON "public"."PaymentMethod"("isActive");
-- CreateIndex
CREATE INDEX "PaymentMethod_sortOrder_idx" ON "public"."PaymentMethod"("sortOrder");
-- CreateIndex
CREATE UNIQUE INDEX "Coupon_code_key" ON "public"."Coupon"("code");
-- CreateIndex
CREATE INDEX "Coupon_code_idx" ON "public"."Coupon"("code");
-- CreateIndex
CREATE INDEX "Coupon_isActive_idx" ON "public"."Coupon"("isActive");
-- CreateIndex
CREATE UNIQUE INDEX "AppConfig_key_key" ON "public"."AppConfig"("key");
-- CreateIndex
CREATE INDEX "AppConfig_category_idx" ON "public"."AppConfig"("category");
-- CreateIndex
CREATE UNIQUE INDEX "ApiKey_keyHash_key" ON "public"."ApiKey"("keyHash");
-- CreateIndex
CREATE INDEX "ApiKey_userId_idx" ON "public"."ApiKey"("userId");
-- CreateIndex
CREATE INDEX "ApiKey_keyHash_idx" ON "public"."ApiKey"("keyHash");
-- CreateIndex
CREATE INDEX "ApiKeyUsage_apiKeyId_timestamp_idx" ON "public"."ApiKeyUsage"("apiKeyId", "timestamp");
-- CreateIndex
CREATE INDEX "Webhook_userId_idx" ON "public"."Webhook"("userId");
-- CreateIndex
CREATE INDEX "WebhookDelivery_webhookId_idx" ON "public"."WebhookDelivery"("webhookId");
-- CreateIndex
CREATE INDEX "WebhookDelivery_status_idx" ON "public"."WebhookDelivery"("status");
-- AddForeignKey
ALTER TABLE "public"."Subscription" ADD CONSTRAINT "Subscription_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."Subscription" ADD CONSTRAINT "Subscription_planId_fkey" FOREIGN KEY ("planId") REFERENCES "public"."Plan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."Payment" ADD CONSTRAINT "Payment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."Payment" ADD CONSTRAINT "Payment_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "public"."Subscription"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."Payment" ADD CONSTRAINT "Payment_couponId_fkey" FOREIGN KEY ("couponId") REFERENCES "public"."Coupon"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."ApiKey" ADD CONSTRAINT "ApiKey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."ApiKeyUsage" ADD CONSTRAINT "ApiKeyUsage_apiKeyId_fkey" FOREIGN KEY ("apiKeyId") REFERENCES "public"."ApiKey"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."Webhook" ADD CONSTRAINT "Webhook_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "public"."WebhookDelivery" ADD CONSTRAINT "WebhookDelivery_webhookId_fkey" FOREIGN KEY ("webhookId") REFERENCES "public"."Webhook"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -26,12 +26,22 @@ model User {
otpWhatsappEnabled Boolean @default(false)
otpTotpEnabled Boolean @default(false)
otpTotpSecret String?
// Admin fields
role String @default("user") // "user" | "admin"
suspendedAt DateTime?
suspendedReason String?
lastLoginAt DateTime?
// Relations
authAccounts AuthAccount[]
categories Category[]
Recurrence Recurrence[]
sessions Session[]
transactions Transaction[]
wallets Wallet[]
subscriptions Subscription[]
payments Payment[]
apiKeys ApiKey[]
webhooks Webhook[]
}
model AuthAccount {
@@ -129,3 +139,224 @@ model CurrencyRate {
@@unique([base, quote, at])
@@index([base, quote])
}
// ============================================
// SUBSCRIPTION & PAYMENT MODELS
// ============================================
model Plan {
id String @id @default(uuid())
name String
slug String @unique
description String?
price Decimal @db.Decimal(10, 2)
currency String @default("IDR")
durationType String // "monthly" | "yearly" | "lifetime" | "custom"
durationDays Int?
trialDays Int @default(7)
features Json
badge String?
badgeColor String?
highlightColor String?
sortOrder Int @default(0)
isActive Boolean @default(true)
isVisible Boolean @default(true)
isFeatured Boolean @default(false)
maxWallets Int?
maxGoals Int?
maxTeamMembers Int?
apiEnabled Boolean @default(false)
apiRateLimit Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
subscriptions Subscription[]
@@index([slug])
@@index([isActive])
@@index([isVisible])
@@index([sortOrder])
}
model Subscription {
id String @id @default(uuid())
userId String @unique
user User @relation(fields: [userId], references: [id])
planId String
plan Plan @relation(fields: [planId], references: [id])
status String // "active" | "expired" | "cancelled" | "grace_period"
startDate DateTime
endDate DateTime
isTrialUsed Boolean @default(false)
trialEndDate DateTime?
cancelledAt DateTime?
cancellationReason String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
payments Payment[]
@@index([userId])
@@index([status])
@@index([endDate])
}
model Payment {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id])
subscriptionId String?
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
invoiceNumber String @unique
amount Decimal @db.Decimal(10, 2)
currency String @default("IDR")
method String
tripayReference String? @unique
tripayFee Decimal? @db.Decimal(10, 2)
totalAmount Decimal @db.Decimal(10, 2)
paymentChannel String?
paymentUrl String?
qrUrl String?
status String
proofImageUrl String?
transferDate DateTime?
verifiedBy String?
verifiedAt DateTime?
rejectionReason String?
couponId String?
coupon Coupon? @relation(fields: [couponId], references: [id])
discountAmount Decimal? @db.Decimal(10, 2)
notes String?
expiresAt DateTime?
paidAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([status])
@@index([invoiceNumber])
@@index([tripayReference])
}
model PaymentMethod {
id String @id @default(uuid())
type String
provider String
accountName String
accountNumber String
displayName String
logoUrl String?
instructions String?
isActive Boolean @default(true)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive])
@@index([sortOrder])
}
model Coupon {
id String @id @default(uuid())
code String @unique
name String
description String?
discountType String
discountValue Decimal @db.Decimal(10, 2)
maxDiscount Decimal? @db.Decimal(10, 2)
validFrom DateTime
validUntil DateTime
maxUses Int?
usedCount Int @default(0)
minPurchase Decimal? @db.Decimal(10, 2)
applicablePlans String[]
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
payments Payment[]
@@index([code])
@@index([isActive])
}
model AppConfig {
id String @id @default(uuid())
key String @unique
value String
category String
label String
description String?
type String
isSecret Boolean @default(false)
updatedAt DateTime @updatedAt
updatedBy String?
@@index([category])
}
// ============================================
// API & WEBHOOK MODELS
// ============================================
model ApiKey {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id])
name String
keyHash String @unique
prefix String
scopes String[]
lastUsedAt DateTime?
expiresAt DateTime?
revokedAt DateTime?
createdAt DateTime @default(now())
usage ApiKeyUsage[]
@@index([userId])
@@index([keyHash])
}
model ApiKeyUsage {
id String @id @default(uuid())
apiKeyId String
apiKey ApiKey @relation(fields: [apiKeyId], references: [id], onDelete: Cascade)
endpoint String
method String
statusCode Int
responseTime Int
timestamp DateTime @default(now())
@@index([apiKeyId, timestamp])
}
model Webhook {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id])
url String
events String[]
secret String
isActive Boolean @default(true)
lastTriggeredAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deliveries WebhookDelivery[]
@@index([userId])
}
model WebhookDelivery {
id String @id @default(uuid())
webhookId String
webhook Webhook @relation(fields: [webhookId], references: [id], onDelete: Cascade)
event String
payload Json
status String
statusCode Int?
response String?
attempts Int @default(0)
nextRetryAt DateTime?
deliveredAt DateTime?
createdAt DateTime @default(now())
@@index([webhookId])
@@index([status])
}

View File

@@ -1,15 +1,237 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
import * as bcrypt from 'bcrypt';
const adminSeeder = {
email: 'dwindi.ramadhana@gmail.com',
password: 'tabungin2k25!@#',
}
const prisma = new PrismaClient();
const TEMP_USER_ID =
process.env.TEMP_USER_ID || '16b74848-daa3-4dc9-8de2-3cf59e08f8e3';
const ADMIN_EMAIL = 'dwindi.ramadhana@gmail.com';
const ADMIN_PASSWORD = 'tabungin2k25!@#';
async function main() {
console.log('🌱 Starting seed...');
// ============================================
// 1. CREATE ADMIN USER
// ============================================
console.log('\n👤 Creating admin user...');
const passwordHash = await bcrypt.hash(ADMIN_PASSWORD, 10);
const admin = await prisma.user.upsert({
where: { email: ADMIN_EMAIL },
update: {
role: 'admin',
passwordHash,
emailVerified: true,
},
create: {
email: ADMIN_EMAIL,
passwordHash,
name: 'Dwindi Ramadhana',
role: 'admin',
emailVerified: true,
},
});
console.log('✅ Admin user created:', admin.email);
// ============================================
// 2. CREATE DEFAULT PLANS
// ============================================
console.log('\n💰 Creating default plans...');
const freePlan = await prisma.plan.upsert({
where: { slug: 'free' },
update: {},
create: {
name: 'Free',
slug: 'free',
description: 'Perfect for getting started',
price: 0,
currency: 'IDR',
durationType: 'lifetime',
durationDays: null,
trialDays: 0,
features: {
wallets: { limit: 5, label: '5 wallets' },
goals: { limit: 3, label: '3 goals' },
team: { enabled: false, label: 'No team feature' },
api: { enabled: false, label: 'No API access' },
support: { level: 'basic', label: 'Basic support' },
export: { enabled: true, formats: ['csv'], label: 'CSV export' },
},
badge: null,
sortOrder: 1,
isActive: true,
isVisible: true,
isFeatured: false,
maxWallets: 5,
maxGoals: 3,
maxTeamMembers: 0,
apiEnabled: false,
apiRateLimit: null,
},
});
const proMonthly = await prisma.plan.upsert({
where: { slug: 'pro-monthly' },
update: {},
create: {
name: 'Pro Monthly',
slug: 'pro-monthly',
description: 'Perfect for individuals and small teams',
price: 49000,
currency: 'IDR',
durationType: 'monthly',
durationDays: 30,
trialDays: 7,
features: {
wallets: { limit: null, label: 'Unlimited wallets' },
goals: { limit: null, label: 'Unlimited goals' },
team: { enabled: true, maxMembers: 10, label: 'Team feature (10 members)' },
api: { enabled: true, rateLimit: 1000, label: 'API access (1000 req/hr)' },
support: { level: 'priority', label: 'Priority support' },
export: { enabled: true, formats: ['csv', 'excel', 'pdf'], label: 'All export formats' },
},
badge: 'Popular',
badgeColor: 'blue',
highlightColor: '#3B82F6',
sortOrder: 2,
isActive: true,
isVisible: true,
isFeatured: true,
maxWallets: null,
maxGoals: null,
maxTeamMembers: 10,
apiEnabled: true,
apiRateLimit: 1000,
},
});
const proYearly = await prisma.plan.upsert({
where: { slug: 'pro-yearly' },
update: {},
create: {
name: 'Pro Yearly',
slug: 'pro-yearly',
description: 'Best value - Save 17% with annual billing',
price: 490000,
currency: 'IDR',
durationType: 'yearly',
durationDays: 365,
trialDays: 7,
features: {
wallets: { limit: null, label: 'Unlimited wallets' },
goals: { limit: null, label: 'Unlimited goals' },
team: { enabled: true, maxMembers: 10, label: 'Team feature (10 members)' },
api: { enabled: true, rateLimit: 1000, label: 'API access (1000 req/hr)' },
support: { level: 'priority', label: 'Priority support' },
export: { enabled: true, formats: ['csv', 'excel', 'pdf'], label: 'All export formats' },
discount: { value: '17%', label: 'Save 17% (2 months free)' },
},
badge: 'Best Value',
badgeColor: 'green',
highlightColor: '#10B981',
sortOrder: 3,
isActive: true,
isVisible: true,
isFeatured: true,
maxWallets: null,
maxGoals: null,
maxTeamMembers: 10,
apiEnabled: true,
apiRateLimit: 1000,
},
});
console.log('✅ Plans created:', [freePlan.name, proMonthly.name, proYearly.name]);
// ============================================
// 3. CREATE DEFAULT PAYMENT METHODS
// ============================================
console.log('\n💳 Creating default payment methods...');
const bcaMethod = await prisma.paymentMethod.upsert({
where: { id: 'bca-method' },
update: {},
create: {
id: 'bca-method',
type: 'bank_transfer',
provider: 'BCA',
accountName: 'PT Tabungin Indonesia',
accountNumber: '1234567890',
displayName: 'BCA Virtual Account',
logoUrl: '/logos/bca.png',
instructions: 'Transfer to the account above and upload proof of payment.',
isActive: true,
sortOrder: 1,
},
});
const mandiriMethod = await prisma.paymentMethod.upsert({
where: { id: 'mandiri-method' },
update: {},
create: {
id: 'mandiri-method',
type: 'bank_transfer',
provider: 'Mandiri',
accountName: 'PT Tabungin Indonesia',
accountNumber: '9876543210',
displayName: 'Mandiri Virtual Account',
logoUrl: '/logos/mandiri.png',
instructions: 'Transfer to the account above and upload proof of payment.',
isActive: true,
sortOrder: 2,
},
});
const gopayMethod = await prisma.paymentMethod.upsert({
where: { id: 'gopay-method' },
update: {},
create: {
id: 'gopay-method',
type: 'e-wallet',
provider: 'GoPay',
accountName: 'Dwindi Ramadhana',
accountNumber: '081234567890',
displayName: 'GoPay',
logoUrl: '/logos/gopay.png',
instructions: 'Send payment to the number above and upload proof.',
isActive: true,
sortOrder: 3,
},
});
console.log('✅ Payment methods created:', [bcaMethod.displayName, mandiriMethod.displayName, gopayMethod.displayName]);
// ============================================
// 4. CREATE APP CONFIG (Optional)
// ============================================
console.log('\n⚙ Creating app config...');
await prisma.appConfig.upsert({
where: { key: 'MAINTENANCE_MODE' },
update: {},
create: {
key: 'MAINTENANCE_MODE',
value: 'false',
category: 'general',
label: 'Maintenance Mode',
description: 'Enable to show maintenance page to users',
type: 'boolean',
isSecret: false,
},
});
console.log('✅ App config created');
// ============================================
// 5. CREATE TEMP USER & WALLET (Legacy)
// ============================================
console.log('\n🔧 Creating temp user (legacy)...');
const user = await prisma.user.upsert({
where: { id: TEMP_USER_ID },
update: {},
@@ -19,9 +241,7 @@ async function main() {
},
});
// create a sample money wallet if none
const existing = await prisma.wallet.findFirst({});
if (!existing) {
await prisma.wallet.create({
data: {
@@ -32,8 +252,20 @@ async function main() {
},
});
}
console.log('✅ Temp user created:', user.id);
console.log('Seed complete. TEMP_USER_ID=', user.id);
// ============================================
// SUMMARY
// ============================================
console.log('\n🎉 Seed complete!');
console.log('\n📋 Summary:');
console.log(' Admin Email:', ADMIN_EMAIL);
console.log(' Admin Password:', ADMIN_PASSWORD);
console.log(' Plans:', [freePlan.name, proMonthly.name, proYearly.name].join(', '));
console.log(' Payment Methods:', [bcaMethod.displayName, mandiriMethod.displayName, gopayMethod.displayName].join(', '));
console.log('\n⚠ IMPORTANT: Change admin password after first login!');
console.log('\n🔗 Login at: http://localhost:5174/auth/login');
}
main()