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

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])
}