checkpoint: goals feature, wallet balance, and goals/wallet detail UI
- Add goals feature (models, migrations, API, web pages) - Add reserved/centralized wallet balance service - Add wallet detail page and overview components - Add new UI components (progress, multi-select, FAB) - Remove stray empty -H/-d files from working tree
This commit is contained in:
0
apps/api/prisma/migrations/20250813164148_add_core_models/migration.sql
Normal file → Executable file
0
apps/api/prisma/migrations/20250813164148_add_core_models/migration.sql
Normal file → Executable file
0
apps/api/prisma/migrations/20251010054217_add_custom_auth_and_otp/migration.sql
Normal file → Executable file
0
apps/api/prisma/migrations/20251010054217_add_custom_auth_and_otp/migration.sql
Normal file → Executable file
0
apps/api/prisma/migrations/20251010132022_add_phone_and_whatsapp_otp/migration.sql
Normal file → Executable file
0
apps/api/prisma/migrations/20251010132022_add_phone_and_whatsapp_otp/migration.sql
Normal file → Executable file
0
apps/api/prisma/migrations/20251011070242_add_admin_subscription_api_models/migration.sql
Normal file → Executable file
0
apps/api/prisma/migrations/20251011070242_add_admin_subscription_api_models/migration.sql
Normal file → Executable file
87
apps/api/prisma/migrations/20251022141924_add_goals_feature/migration.sql
Executable file
87
apps/api/prisma/migrations/20251022141924_add_goals_feature/migration.sql
Executable file
@@ -0,0 +1,87 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "public"."Goal" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"teamId" TEXT,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"targetAmount" DECIMAL(18,2) NOT NULL,
|
||||
"currentAmount" DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||
"currency" TEXT NOT NULL DEFAULT 'IDR',
|
||||
"targetDate" TIMESTAMP(3),
|
||||
"imageUrl" TEXT,
|
||||
"category" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'active',
|
||||
"completedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Goal_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "public"."GoalAllocation" (
|
||||
"id" TEXT NOT NULL,
|
||||
"goalId" TEXT NOT NULL,
|
||||
"walletId" TEXT NOT NULL,
|
||||
"amount" DECIMAL(18,2) NOT NULL,
|
||||
"currency" TEXT NOT NULL,
|
||||
"exchangeRate" DECIMAL(18,6),
|
||||
"amountInGoalCurrency" DECIMAL(18,2) NOT NULL,
|
||||
"notes" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"createdBy" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "GoalAllocation_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "public"."GoalMilestone" (
|
||||
"id" TEXT NOT NULL,
|
||||
"goalId" TEXT NOT NULL,
|
||||
"percentage" INTEGER NOT NULL,
|
||||
"targetAmount" DECIMAL(18,2) NOT NULL,
|
||||
"achievedAt" TIMESTAMP(3),
|
||||
"notifiedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "GoalMilestone_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Goal_userId_idx" ON "public"."Goal"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Goal_status_idx" ON "public"."Goal"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Goal_teamId_idx" ON "public"."Goal"("teamId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "GoalAllocation_goalId_idx" ON "public"."GoalAllocation"("goalId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "GoalAllocation_walletId_idx" ON "public"."GoalAllocation"("walletId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "GoalAllocation_createdAt_idx" ON "public"."GoalAllocation"("createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "GoalMilestone_goalId_idx" ON "public"."GoalMilestone"("goalId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "GoalMilestone_achievedAt_idx" ON "public"."GoalMilestone"("achievedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "GoalMilestone_goalId_percentage_key" ON "public"."GoalMilestone"("goalId", "percentage");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "public"."Goal" ADD CONSTRAINT "Goal_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "public"."GoalAllocation" ADD CONSTRAINT "GoalAllocation_goalId_fkey" FOREIGN KEY ("goalId") REFERENCES "public"."Goal"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "public"."GoalAllocation" ADD CONSTRAINT "GoalAllocation_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "public"."Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "public"."GoalMilestone" ADD CONSTRAINT "GoalMilestone_goalId_fkey" FOREIGN KEY ("goalId") REFERENCES "public"."Goal"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Wallet" ADD COLUMN "reservedBalance" DECIMAL(18,2) NOT NULL DEFAULT 0;
|
||||
0
apps/api/prisma/migrations/migration_lock.toml
Normal file → Executable file
0
apps/api/prisma/migrations/migration_lock.toml
Normal file → Executable file
452
apps/api/prisma/schema.prisma
Normal file → Executable file
452
apps/api/prisma/schema.prisma
Normal file → Executable file
@@ -9,39 +9,37 @@ datasource db {
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
status String @default("active")
|
||||
email String @unique
|
||||
emailVerified Boolean @default(false)
|
||||
passwordHash String?
|
||||
name String?
|
||||
avatarUrl String?
|
||||
phone String? @unique
|
||||
defaultCurrency String?
|
||||
timeZone String?
|
||||
// OTP/MFA fields
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
status String @default("active")
|
||||
email String @unique
|
||||
name String?
|
||||
avatarUrl String?
|
||||
defaultCurrency String?
|
||||
timeZone String?
|
||||
emailVerified Boolean @default(false)
|
||||
otpEmailEnabled Boolean @default(false)
|
||||
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[]
|
||||
passwordHash String?
|
||||
otpWhatsappEnabled Boolean @default(false)
|
||||
phone String? @unique
|
||||
lastLoginAt DateTime?
|
||||
role String @default("user")
|
||||
suspendedAt DateTime?
|
||||
suspendedReason String?
|
||||
apiKeys ApiKey[]
|
||||
authAccounts AuthAccount[]
|
||||
categories Category[]
|
||||
goals Goal[]
|
||||
payments Payment[]
|
||||
Recurrence Recurrence[]
|
||||
sessions Session[]
|
||||
subscriptions Subscription?
|
||||
transactions Transaction[]
|
||||
wallets Wallet[]
|
||||
webhooks Webhook[]
|
||||
}
|
||||
|
||||
model AuthAccount {
|
||||
@@ -71,19 +69,21 @@ model Session {
|
||||
}
|
||||
|
||||
model Wallet {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
kind String
|
||||
name String
|
||||
currency String?
|
||||
unit String?
|
||||
initialAmount Decimal? @db.Decimal(18, 2)
|
||||
pricePerUnit Decimal? @db.Decimal(18, 2)
|
||||
deletedAt DateTime?
|
||||
transactions Transaction[]
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
kind String
|
||||
name String
|
||||
currency String?
|
||||
unit String?
|
||||
deletedAt DateTime?
|
||||
initialAmount Decimal? @db.Decimal(18, 2)
|
||||
pricePerUnit Decimal? @db.Decimal(18, 2)
|
||||
reservedBalance Decimal @default(0) @db.Decimal(18, 2)
|
||||
goalAllocations GoalAllocation[]
|
||||
transactions Transaction[]
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
@@ -140,36 +140,32 @@ model CurrencyRate {
|
||||
@@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[]
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
slug String @unique
|
||||
description String?
|
||||
price Decimal @db.Decimal(10, 2)
|
||||
currency String @default("IDR")
|
||||
durationType String
|
||||
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])
|
||||
@@ -178,21 +174,21 @@ model Plan {
|
||||
}
|
||||
|
||||
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?
|
||||
id String @id @default(uuid())
|
||||
userId String @unique
|
||||
planId String
|
||||
status String
|
||||
startDate DateTime
|
||||
endDate DateTime
|
||||
isTrialUsed Boolean @default(false)
|
||||
trialEndDate DateTime?
|
||||
cancelledAt DateTime?
|
||||
cancellationReason String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
payments Payment[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
payments Payment[]
|
||||
plan Plan @relation(fields: [planId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@index([userId])
|
||||
@@index([status])
|
||||
@@ -200,35 +196,35 @@ model Subscription {
|
||||
}
|
||||
|
||||
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
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
subscriptionId String?
|
||||
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?
|
||||
discountAmount Decimal? @db.Decimal(10, 2)
|
||||
notes String?
|
||||
expiresAt DateTime?
|
||||
paidAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
coupon Coupon? @relation(fields: [couponId], references: [id])
|
||||
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@index([userId])
|
||||
@@index([status])
|
||||
@@ -237,126 +233,180 @@ model Payment {
|
||||
}
|
||||
|
||||
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
|
||||
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[]
|
||||
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?
|
||||
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[]
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
name String
|
||||
keyHash String @unique
|
||||
prefix String
|
||||
scopes String[]
|
||||
lastUsedAt DateTime?
|
||||
expiresAt DateTime?
|
||||
revokedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
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())
|
||||
id String @id @default(uuid())
|
||||
apiKeyId String
|
||||
endpoint String
|
||||
method String
|
||||
statusCode Int
|
||||
responseTime Int
|
||||
timestamp DateTime @default(now())
|
||||
apiKey ApiKey @relation(fields: [apiKeyId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@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[]
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
url String
|
||||
events String[]
|
||||
secret String
|
||||
isActive Boolean @default(true)
|
||||
lastTriggeredAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
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())
|
||||
id String @id @default(uuid())
|
||||
webhookId String
|
||||
event String
|
||||
payload Json
|
||||
status String
|
||||
statusCode Int?
|
||||
response String?
|
||||
attempts Int @default(0)
|
||||
nextRetryAt DateTime?
|
||||
deliveredAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
webhook Webhook @relation(fields: [webhookId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([webhookId])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model Goal {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
teamId String?
|
||||
name String
|
||||
description String?
|
||||
targetAmount Decimal @db.Decimal(18, 2)
|
||||
currentAmount Decimal @default(0) @db.Decimal(18, 2)
|
||||
currency String @default("IDR")
|
||||
targetDate DateTime?
|
||||
imageUrl String?
|
||||
category String?
|
||||
status String @default("active")
|
||||
completedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
allocations GoalAllocation[]
|
||||
milestones GoalMilestone[]
|
||||
|
||||
@@index([userId])
|
||||
@@index([status])
|
||||
@@index([teamId])
|
||||
}
|
||||
|
||||
model GoalAllocation {
|
||||
id String @id @default(uuid())
|
||||
goalId String
|
||||
walletId String
|
||||
amount Decimal @db.Decimal(18, 2)
|
||||
currency String
|
||||
exchangeRate Decimal? @db.Decimal(18, 6)
|
||||
amountInGoalCurrency Decimal @db.Decimal(18, 2)
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
createdBy String
|
||||
goal Goal @relation(fields: [goalId], references: [id], onDelete: Cascade)
|
||||
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([goalId])
|
||||
@@index([walletId])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
model GoalMilestone {
|
||||
id String @id @default(uuid())
|
||||
goalId String
|
||||
percentage Int
|
||||
targetAmount Decimal @db.Decimal(18, 2)
|
||||
achievedAt DateTime?
|
||||
notifiedAt DateTime?
|
||||
goal Goal @relation(fields: [goalId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([goalId, percentage])
|
||||
@@index([goalId])
|
||||
@@index([achievedAt])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user