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:
238
apps/api/dist/seed.js
vendored
238
apps/api/dist/seed.js
vendored
@@ -1,13 +1,233 @@
|
|||||||
"use strict";
|
"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 });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
const client_1 = require("@prisma/client");
|
const client_1 = require("@prisma/client");
|
||||||
|
const bcrypt = __importStar(require("bcrypt"));
|
||||||
const prisma = new client_1.PrismaClient();
|
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 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() {
|
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({
|
const user = await prisma.user.upsert({
|
||||||
where: { id: TEMP_USER_ID },
|
where: { id: TEMP_USER_ID },
|
||||||
update: {},
|
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()
|
main()
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
2
apps/api/dist/seed.js.map
vendored
2
apps/api/dist/seed.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@ export declare class TransactionsController {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
walletId: string;
|
||||||
|
date: Date;
|
||||||
amount: import("@prisma/client/runtime/library").Decimal;
|
amount: import("@prisma/client/runtime/library").Decimal;
|
||||||
direction: string;
|
direction: string;
|
||||||
date: Date;
|
|
||||||
memo: string | null;
|
memo: string | null;
|
||||||
walletId: string;
|
|
||||||
recurrenceId: string | null;
|
recurrenceId: string | null;
|
||||||
}[]>;
|
}[]>;
|
||||||
create(req: RequestWithUser, walletId: string, body: {
|
create(req: RequestWithUser, walletId: string, body: {
|
||||||
@@ -31,11 +31,11 @@ export declare class TransactionsController {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
walletId: string;
|
||||||
|
date: Date;
|
||||||
amount: import("@prisma/client/runtime/library").Decimal;
|
amount: import("@prisma/client/runtime/library").Decimal;
|
||||||
direction: string;
|
direction: string;
|
||||||
date: Date;
|
|
||||||
memo: string | null;
|
memo: string | null;
|
||||||
walletId: string;
|
|
||||||
recurrenceId: string | null;
|
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>;
|
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;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
walletId: string;
|
||||||
|
date: Date;
|
||||||
amount: import("@prisma/client/runtime/library").Decimal;
|
amount: import("@prisma/client/runtime/library").Decimal;
|
||||||
direction: string;
|
direction: string;
|
||||||
date: Date;
|
|
||||||
memo: string | null;
|
memo: string | null;
|
||||||
walletId: string;
|
|
||||||
recurrenceId: string | null;
|
recurrenceId: string | null;
|
||||||
}>;
|
}>;
|
||||||
delete(req: RequestWithUser, walletId: string, id: string): Promise<{
|
delete(req: RequestWithUser, walletId: string, id: string): Promise<{
|
||||||
@@ -56,11 +56,11 @@ export declare class TransactionsController {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
walletId: string;
|
||||||
|
date: Date;
|
||||||
amount: import("@prisma/client/runtime/library").Decimal;
|
amount: import("@prisma/client/runtime/library").Decimal;
|
||||||
direction: string;
|
direction: string;
|
||||||
date: Date;
|
|
||||||
memo: string | null;
|
memo: string | null;
|
||||||
walletId: string;
|
|
||||||
recurrenceId: string | null;
|
recurrenceId: string | null;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ export declare class TransactionsService {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
walletId: string;
|
||||||
|
date: Date;
|
||||||
amount: Prisma.Decimal;
|
amount: Prisma.Decimal;
|
||||||
direction: string;
|
direction: string;
|
||||||
date: Date;
|
|
||||||
memo: string | null;
|
memo: string | null;
|
||||||
walletId: string;
|
|
||||||
recurrenceId: string | null;
|
recurrenceId: string | null;
|
||||||
}[]>;
|
}[]>;
|
||||||
listAll(userId: string): Prisma.PrismaPromise<{
|
listAll(userId: string): Prisma.PrismaPromise<{
|
||||||
@@ -21,11 +21,11 @@ export declare class TransactionsService {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
walletId: string;
|
||||||
|
date: Date;
|
||||||
amount: Prisma.Decimal;
|
amount: Prisma.Decimal;
|
||||||
direction: string;
|
direction: string;
|
||||||
date: Date;
|
|
||||||
memo: string | null;
|
memo: string | null;
|
||||||
walletId: string;
|
|
||||||
recurrenceId: string | null;
|
recurrenceId: string | null;
|
||||||
}[]>;
|
}[]>;
|
||||||
listWithFilters(userId: string, walletId: string, filters: {
|
listWithFilters(userId: string, walletId: string, filters: {
|
||||||
@@ -38,11 +38,11 @@ export declare class TransactionsService {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
walletId: string;
|
||||||
|
date: Date;
|
||||||
amount: Prisma.Decimal;
|
amount: Prisma.Decimal;
|
||||||
direction: string;
|
direction: string;
|
||||||
date: Date;
|
|
||||||
memo: string | null;
|
memo: string | null;
|
||||||
walletId: string;
|
|
||||||
recurrenceId: string | null;
|
recurrenceId: string | null;
|
||||||
}[]>;
|
}[]>;
|
||||||
create(userId: string, walletId: string, input: {
|
create(userId: string, walletId: string, input: {
|
||||||
@@ -56,11 +56,11 @@ export declare class TransactionsService {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
walletId: string;
|
||||||
|
date: Date;
|
||||||
amount: Prisma.Decimal;
|
amount: Prisma.Decimal;
|
||||||
direction: string;
|
direction: string;
|
||||||
date: Date;
|
|
||||||
memo: string | null;
|
memo: string | null;
|
||||||
walletId: string;
|
|
||||||
recurrenceId: string | null;
|
recurrenceId: string | null;
|
||||||
}>;
|
}>;
|
||||||
update(userId: string, walletId: string, id: string, dto: TransactionUpdateDto): Promise<{
|
update(userId: string, walletId: string, id: string, dto: TransactionUpdateDto): Promise<{
|
||||||
@@ -68,11 +68,11 @@ export declare class TransactionsService {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
walletId: string;
|
||||||
|
date: Date;
|
||||||
amount: Prisma.Decimal;
|
amount: Prisma.Decimal;
|
||||||
direction: string;
|
direction: string;
|
||||||
date: Date;
|
|
||||||
memo: string | null;
|
memo: string | null;
|
||||||
walletId: string;
|
|
||||||
recurrenceId: string | null;
|
recurrenceId: string | null;
|
||||||
}>;
|
}>;
|
||||||
delete(userId: string, walletId: string, id: string): Promise<{
|
delete(userId: string, walletId: string, id: string): Promise<{
|
||||||
@@ -80,11 +80,11 @@ export declare class TransactionsService {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
walletId: string;
|
||||||
|
date: Date;
|
||||||
amount: Prisma.Decimal;
|
amount: Prisma.Decimal;
|
||||||
direction: string;
|
direction: string;
|
||||||
date: Date;
|
|
||||||
memo: string | null;
|
memo: string | null;
|
||||||
walletId: string;
|
|
||||||
recurrenceId: string | null;
|
recurrenceId: string | null;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
2
apps/api/dist/tsconfig.build.tsbuildinfo
vendored
2
apps/api/dist/tsconfig.build.tsbuildinfo
vendored
File diff suppressed because one or more lines are too long
4
apps/api/dist/users/users.controller.d.ts
vendored
4
apps/api/dist/users/users.controller.d.ts
vendored
@@ -25,6 +25,10 @@ export declare class UsersController {
|
|||||||
otpWhatsappEnabled: boolean;
|
otpWhatsappEnabled: boolean;
|
||||||
otpTotpEnabled: boolean;
|
otpTotpEnabled: boolean;
|
||||||
otpTotpSecret: string | null;
|
otpTotpSecret: string | null;
|
||||||
|
role: string;
|
||||||
|
suspendedAt: Date | null;
|
||||||
|
suspendedReason: string | null;
|
||||||
|
lastLoginAt: Date | null;
|
||||||
} | null>;
|
} | null>;
|
||||||
updateProfile(req: RequestWithUser, body: {
|
updateProfile(req: RequestWithUser, body: {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
4
apps/api/dist/users/users.service.d.ts
vendored
4
apps/api/dist/users/users.service.d.ts
vendored
@@ -19,6 +19,10 @@ export declare class UsersService {
|
|||||||
otpWhatsappEnabled: boolean;
|
otpWhatsappEnabled: boolean;
|
||||||
otpTotpEnabled: boolean;
|
otpTotpEnabled: boolean;
|
||||||
otpTotpSecret: string | null;
|
otpTotpSecret: string | null;
|
||||||
|
role: string;
|
||||||
|
suspendedAt: Date | null;
|
||||||
|
suspendedReason: string | null;
|
||||||
|
lastLoginAt: Date | null;
|
||||||
} | null>;
|
} | null>;
|
||||||
updateProfile(userId: string, data: {
|
updateProfile(userId: string, data: {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ export declare class WalletsController {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
walletId: string;
|
||||||
|
date: Date;
|
||||||
amount: import("@prisma/client/runtime/library").Decimal;
|
amount: import("@prisma/client/runtime/library").Decimal;
|
||||||
direction: string;
|
direction: string;
|
||||||
date: Date;
|
|
||||||
memo: string | null;
|
memo: string | null;
|
||||||
walletId: string;
|
|
||||||
recurrenceId: string | null;
|
recurrenceId: string | null;
|
||||||
}[]>;
|
}[]>;
|
||||||
create(req: RequestWithUser, body: {
|
create(req: RequestWithUser, body: {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -26,12 +26,22 @@ model User {
|
|||||||
otpWhatsappEnabled Boolean @default(false)
|
otpWhatsappEnabled Boolean @default(false)
|
||||||
otpTotpEnabled Boolean @default(false)
|
otpTotpEnabled Boolean @default(false)
|
||||||
otpTotpSecret String?
|
otpTotpSecret String?
|
||||||
|
// Admin fields
|
||||||
|
role String @default("user") // "user" | "admin"
|
||||||
|
suspendedAt DateTime?
|
||||||
|
suspendedReason String?
|
||||||
|
lastLoginAt DateTime?
|
||||||
|
// Relations
|
||||||
authAccounts AuthAccount[]
|
authAccounts AuthAccount[]
|
||||||
categories Category[]
|
categories Category[]
|
||||||
Recurrence Recurrence[]
|
Recurrence Recurrence[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
transactions Transaction[]
|
transactions Transaction[]
|
||||||
wallets Wallet[]
|
wallets Wallet[]
|
||||||
|
subscriptions Subscription[]
|
||||||
|
payments Payment[]
|
||||||
|
apiKeys ApiKey[]
|
||||||
|
webhooks Webhook[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model AuthAccount {
|
model AuthAccount {
|
||||||
@@ -129,3 +139,224 @@ model CurrencyRate {
|
|||||||
@@unique([base, quote, at])
|
@@unique([base, quote, at])
|
||||||
@@index([base, quote])
|
@@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])
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,237 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
const prisma = new PrismaClient();
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
const adminSeeder = {
|
const prisma = new PrismaClient();
|
||||||
email: 'dwindi.ramadhana@gmail.com',
|
|
||||||
password: 'tabungin2k25!@#',
|
|
||||||
}
|
|
||||||
|
|
||||||
const TEMP_USER_ID =
|
const TEMP_USER_ID =
|
||||||
process.env.TEMP_USER_ID || '16b74848-daa3-4dc9-8de2-3cf59e08f8e3';
|
process.env.TEMP_USER_ID || '16b74848-daa3-4dc9-8de2-3cf59e08f8e3';
|
||||||
|
|
||||||
|
const ADMIN_EMAIL = 'dwindi.ramadhana@gmail.com';
|
||||||
|
const ADMIN_PASSWORD = 'tabungin2k25!@#';
|
||||||
|
|
||||||
async function main() {
|
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({
|
const user = await prisma.user.upsert({
|
||||||
where: { id: TEMP_USER_ID },
|
where: { id: TEMP_USER_ID },
|
||||||
update: {},
|
update: {},
|
||||||
@@ -19,9 +241,7 @@ async function main() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// create a sample money wallet if none
|
|
||||||
const existing = await prisma.wallet.findFirst({});
|
const existing = await prisma.wallet.findFirst({});
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
await prisma.wallet.create({
|
await prisma.wallet.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -33,7 +253,19 @@ async function main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Seed complete. TEMP_USER_ID=', user.id);
|
console.log('✅ Temp user created:', 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()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user