From 9022b30e1ed6a65c898021a3a60d2c52311f8e22 Mon Sep 17 00:00:00 2001 From: leninner Date: Thu, 13 Nov 2025 21:21:48 -0500 Subject: [PATCH 1/2] feat: add subs --- drizzle/0008_subscriptions.sql | 36 +++ src/app.ts | 2 + src/core/infrastructure/database/schema.ts | 47 ++++ src/core/infrastructure/scripts/plans.seed.ts | 55 +++++ .../auth/application/services/auth.service.ts | 20 ++ .../application/dtos/subscription.dto.ts | 10 + .../services/subscription.service.ts | 151 +++++++++++++ .../subscriptions/domain/entities/IPlan.ts | 10 + .../domain/entities/ISubscription.ts | 16 ++ .../domain/ports/plan-repository.port.ts | 8 + .../ports/subscription-repository.port.ts | 10 + .../adapters/plan.repository.ts | 57 +++++ .../adapters/subscription-api.adapter.ts | 37 ++++ .../adapters/subscription.repository.ts | 209 ++++++++++++++++++ .../controllers/subscription.controller.ts | 24 ++ .../controllers/subscription.routes.ts | 159 +++++++++++++ .../application/services/user.service.ts | 19 ++ tsconfig.json | 3 + 18 files changed, 873 insertions(+) create mode 100644 drizzle/0008_subscriptions.sql create mode 100644 src/core/infrastructure/scripts/plans.seed.ts create mode 100644 src/features/subscriptions/application/dtos/subscription.dto.ts create mode 100644 src/features/subscriptions/application/services/subscription.service.ts create mode 100644 src/features/subscriptions/domain/entities/IPlan.ts create mode 100644 src/features/subscriptions/domain/entities/ISubscription.ts create mode 100644 src/features/subscriptions/domain/ports/plan-repository.port.ts create mode 100644 src/features/subscriptions/domain/ports/subscription-repository.port.ts create mode 100644 src/features/subscriptions/infrastructure/adapters/plan.repository.ts create mode 100644 src/features/subscriptions/infrastructure/adapters/subscription-api.adapter.ts create mode 100644 src/features/subscriptions/infrastructure/adapters/subscription.repository.ts create mode 100644 src/features/subscriptions/infrastructure/controllers/subscription.controller.ts create mode 100644 src/features/subscriptions/infrastructure/controllers/subscription.routes.ts diff --git a/drizzle/0008_subscriptions.sql b/drizzle/0008_subscriptions.sql new file mode 100644 index 0000000..3d0a568 --- /dev/null +++ b/drizzle/0008_subscriptions.sql @@ -0,0 +1,36 @@ +CREATE TABLE IF NOT EXISTS "plans" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar NOT NULL, + "duration_days" integer NOT NULL, + "price" numeric(10, 2) NOT NULL, + "frequency" varchar NOT NULL, + "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "plans_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "subscriptions" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "plan_id" integer NOT NULL, + "frequency" varchar NOT NULL, + "start_date" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + "end_date" timestamp NOT NULL, + "retirement_date" timestamp, + "active" boolean DEFAULT true NOT NULL, + "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "sub_user_idx" ON "subscriptions" ("user_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "sub_plan_idx" ON "subscriptions" ("plan_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "sub_active_idx" ON "subscriptions" ("active");--> statement-breakpoint +ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_plan_id_plans_id_fk" FOREIGN KEY ("plan_id") REFERENCES "public"."plans"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +INSERT INTO "plans" ("name", "duration_days", "price", "frequency") VALUES +('demo', 15, 0.00, 'one-time'), +('lite', 30, 9.99, 'monthly'), +('lite', 365, 99.99, 'yearly'), +('plus', 30, 19.99, 'monthly'), +('plus', 365, 199.99, 'yearly'); + diff --git a/src/app.ts b/src/app.ts index 25f8c90..4ddaa7f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -39,6 +39,7 @@ import { ExcelService } from "./features/reports/infrastructure/services/excel.s import { CSVService } from "./features/reports/infrastructure/services/csv.service"; import reports from "./features/reports/infrastructure/controllers/report.controller"; import aiAgents from "./features/ai-agents/infrastructure/controllers/voice-command.controller"; +import subscriptions from "./features/subscriptions/infrastructure/controllers/subscription.controller"; const app = createApp(); @@ -112,6 +113,7 @@ const routes = [ reports, aiAgents, notificationSocket, + subscriptions, ] as const; app.get("/debug/db-status", (c) => { diff --git a/src/core/infrastructure/database/schema.ts b/src/core/infrastructure/database/schema.ts index 99d7fbd..f16720c 100644 --- a/src/core/infrastructure/database/schema.ts +++ b/src/core/infrastructure/database/schema.ts @@ -384,3 +384,50 @@ export const reports = pgTable("reports", { .notNull(), expires_at: timestamp("expires_at").notNull(), }); + +export const plans = pgTable("plans", { + id: serial("id").primaryKey(), + name: varchar("name").notNull().unique(), + duration_days: integer("duration_days").notNull(), + price: decimal("price", { precision: 10, scale: 2 }).notNull(), + frequency: varchar("frequency").notNull(), + created_at: timestamp("created_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updated_at: timestamp("updated_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), +}); + +export const subscriptions = pgTable( + "subscriptions", + { + id: serial("id").primaryKey(), + user_id: integer("user_id") + .references(() => users.id) + .notNull(), + plan_id: integer("plan_id") + .references(() => plans.id) + .notNull(), + frequency: varchar("frequency").notNull(), + start_date: timestamp("start_date") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + end_date: timestamp("end_date").notNull(), + retirement_date: timestamp("retirement_date"), + active: boolean("active").default(true).notNull(), + created_at: timestamp("created_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updated_at: timestamp("updated_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + }, + (table) => { + return { + user_idx: index("sub_user_idx").on(table.user_id), + plan_idx: index("sub_plan_idx").on(table.plan_id), + active_idx: index("sub_active_idx").on(table.active), + }; + } +); diff --git a/src/core/infrastructure/scripts/plans.seed.ts b/src/core/infrastructure/scripts/plans.seed.ts new file mode 100644 index 0000000..98484e1 --- /dev/null +++ b/src/core/infrastructure/scripts/plans.seed.ts @@ -0,0 +1,55 @@ +import DatabaseConnection from "@/core/infrastructure/database"; +import { plans } from "@/schema"; + +async function seedPlans() { + console.log("🌱 Iniciando seeding de planes..."); + + const db = DatabaseConnection.getInstance().db; + + try { + const existingPlans = await db.select().from(plans); + + if (existingPlans.length > 0) { + console.log(`Ya existen ${existingPlans.length} planes en la base de datos.`); + return; + } + + const plansData = [ + { name: "demo", durationDays: 15, price: "0.00", frequency: "one-time" }, + { name: "lite", durationDays: 30, price: "9.99", frequency: "monthly" }, + { name: "lite", durationDays: 365, price: "99.99", frequency: "yearly" }, + { name: "plus", durationDays: 30, price: "19.99", frequency: "monthly" }, + { name: "plus", durationDays: 365, price: "199.99", frequency: "yearly" }, + ]; + + console.log(`Creando ${plansData.length} planes...`); + + for (const plan of plansData) { + await db.insert(plans).values({ + name: plan.name, + duration_days: plan.durationDays, + price: plan.price, + frequency: plan.frequency, + }); + } + + console.log("✅ Planes creados exitosamente!"); + + } catch (error) { + console.error("❌ Error durante la creación de planes:", error); + } finally { + await DatabaseConnection.getInstance().close(); + } +} + +if (require.main === module) { + seedPlans() + .then(() => process.exit(0)) + .catch((error) => { + console.error("❌ Error fatal durante el seeding de planes:", error); + process.exit(1); + }); +} + +export default seedPlans; + diff --git a/src/features/auth/application/services/auth.service.ts b/src/features/auth/application/services/auth.service.ts index 8ef7bcf..3740c9a 100644 --- a/src/features/auth/application/services/auth.service.ts +++ b/src/features/auth/application/services/auth.service.ts @@ -11,6 +11,8 @@ import { generateToken } from "@/shared/utils/jwt.util"; import { EmailService } from "@/email/application/services/email.service"; import * as HttpStatusCodes from "stoker/http-status-codes"; import { randomBytes } from "crypto"; +import { PgSubscriptionRepository } from "@/subscriptions/infrastructure/adapters/subscription.repository"; +import { PgPlanRepository } from "@/subscriptions/infrastructure/adapters/plan.repository"; export class AuthService { private static instance: AuthService; @@ -117,6 +119,24 @@ export class AuthService { active: true, }); + // Asignar plan demo + const planRepository = PgPlanRepository.getInstance(); + const subscriptionRepository = PgSubscriptionRepository.getInstance(); + const demoPlan = await planRepository.findByName("demo"); + if (demoPlan) { + const startDate = new Date(); + const endDate = new Date(); + endDate.setDate(endDate.getDate() + demoPlan.durationDays); + await subscriptionRepository.create({ + userId: user.id, + planId: demoPlan.id, + frequency: demoPlan.frequency, + startDate, + endDate, + active: true, + }); + } + // Generar token const token = await generateToken({ id: user.id, diff --git a/src/features/subscriptions/application/dtos/subscription.dto.ts b/src/features/subscriptions/application/dtos/subscription.dto.ts new file mode 100644 index 0000000..f809a19 --- /dev/null +++ b/src/features/subscriptions/application/dtos/subscription.dto.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +export const setPlanSchema = z.object({ + planId: z.number().int().positive(), + frequency: z.string().min(1), + userId: z.number().int().positive(), +}); + +export type SetPlanDTO = z.infer; + diff --git a/src/features/subscriptions/application/services/subscription.service.ts b/src/features/subscriptions/application/services/subscription.service.ts new file mode 100644 index 0000000..e887d36 --- /dev/null +++ b/src/features/subscriptions/application/services/subscription.service.ts @@ -0,0 +1,151 @@ +import { createHandler } from "@/core/infrastructure/lib/handler.wrapper,"; +import * as HttpStatusCodes from "stoker/http-status-codes"; +import { IPlanRepository } from "@/subscriptions/domain/ports/plan-repository.port"; +import { ISubscriptionRepository } from "@/subscriptions/domain/ports/subscription-repository.port"; +import { + ListPlansRoute, + SetPlanRoute, + CancelPlanRoute, + GetUserSubscriptionRoute, +} from "@/subscriptions/infrastructure/controllers/subscription.routes"; +import { SubscriptionApiAdapter } from "@/subscriptions/infrastructure/adapters/subscription-api.adapter"; +import { IUserRepository } from "@/users/domain/ports/user-repository.port"; + +export class SubscriptionService { + private static instance: SubscriptionService; + + constructor( + private readonly planRepository: IPlanRepository, + private readonly subscriptionRepository: ISubscriptionRepository, + private readonly userRepository: IUserRepository + ) {} + + public static getInstance( + planRepository: IPlanRepository, + subscriptionRepository: ISubscriptionRepository, + userRepository: IUserRepository + ): SubscriptionService { + if (!SubscriptionService.instance) { + SubscriptionService.instance = new SubscriptionService( + planRepository, + subscriptionRepository, + userRepository + ); + } + return SubscriptionService.instance; + } + + listPlans = createHandler(async (c) => { + const plans = await this.planRepository.findAll(); + return c.json( + { + success: true, + data: SubscriptionApiAdapter.planToApiResponseList(plans), + message: "Plans retrieved successfully", + }, + HttpStatusCodes.OK + ); + }); + + setPlan = createHandler(async (c) => { + const data = c.req.valid("json"); + + const user = await this.userRepository.findById(data.userId); + if (!user) { + return c.json( + { + success: false, + data: null, + message: "User not found", + }, + HttpStatusCodes.NOT_FOUND + ); + } + + const plan = await this.planRepository.findById(data.planId); + if (!plan) { + return c.json( + { + success: false, + data: null, + message: "Plan not found", + }, + HttpStatusCodes.NOT_FOUND + ); + } + + const startDate = new Date(); + const endDate = new Date(); + endDate.setDate(endDate.getDate() + plan.durationDays); + + const subscription = await this.subscriptionRepository.create({ + userId: data.userId, + planId: data.planId, + frequency: data.frequency, + startDate, + endDate, + active: true, + }); + + return c.json( + { + success: true, + data: SubscriptionApiAdapter.toApiResponse(subscription), + message: "Plan set successfully", + }, + HttpStatusCodes.CREATED + ); + }); + + cancelPlan = createHandler(async (c) => { + const subscriptionId = c.req.param("id"); + + const subscription = await this.subscriptionRepository.findById( + Number(subscriptionId) + ); + if (!subscription) { + return c.json( + { + success: false, + data: null, + message: "Subscription not found", + }, + HttpStatusCodes.NOT_FOUND + ); + } + + const cancelled = await this.subscriptionRepository.cancel( + subscription.id, + subscription.endDate + ); + + return c.json( + { + success: true, + data: SubscriptionApiAdapter.toApiResponse(cancelled), + message: "Plan cancelled successfully", + }, + HttpStatusCodes.OK + ); + }); + + getUserSubscription = createHandler(async (c) => { + const userId = c.req.param("userId"); + + const subscription = await this.subscriptionRepository.findByUserId( + Number(userId) + ); + + return c.json( + { + success: true, + data: subscription + ? SubscriptionApiAdapter.toApiResponse(subscription) + : null, + message: "User subscription retrieved successfully", + }, + HttpStatusCodes.OK + ); + }); +} + diff --git a/src/features/subscriptions/domain/entities/IPlan.ts b/src/features/subscriptions/domain/entities/IPlan.ts new file mode 100644 index 0000000..ebcf872 --- /dev/null +++ b/src/features/subscriptions/domain/entities/IPlan.ts @@ -0,0 +1,10 @@ +export interface IPlan { + id: number; + name: string; + durationDays: number; + price: number; + frequency: string; + createdAt: Date; + updatedAt: Date; +} + diff --git a/src/features/subscriptions/domain/entities/ISubscription.ts b/src/features/subscriptions/domain/entities/ISubscription.ts new file mode 100644 index 0000000..ab2e2ee --- /dev/null +++ b/src/features/subscriptions/domain/entities/ISubscription.ts @@ -0,0 +1,16 @@ +import { IPlan } from "./IPlan"; + +export interface ISubscription { + id: number; + userId: number; + planId: number; + plan?: IPlan | null; + frequency: string; + startDate: Date; + endDate: Date; + retirementDate?: Date | null; + active: boolean; + createdAt: Date; + updatedAt: Date; +} + diff --git a/src/features/subscriptions/domain/ports/plan-repository.port.ts b/src/features/subscriptions/domain/ports/plan-repository.port.ts new file mode 100644 index 0000000..62feac9 --- /dev/null +++ b/src/features/subscriptions/domain/ports/plan-repository.port.ts @@ -0,0 +1,8 @@ +import { IPlan } from "../entities/IPlan"; + +export interface IPlanRepository { + findAll(): Promise; + findById(id: number): Promise; + findByName(name: string): Promise; +} + diff --git a/src/features/subscriptions/domain/ports/subscription-repository.port.ts b/src/features/subscriptions/domain/ports/subscription-repository.port.ts new file mode 100644 index 0000000..a44fd61 --- /dev/null +++ b/src/features/subscriptions/domain/ports/subscription-repository.port.ts @@ -0,0 +1,10 @@ +import { ISubscription } from "../entities/ISubscription"; + +export interface ISubscriptionRepository { + findByUserId(userId: number): Promise; + findById(id: number): Promise; + create(subscription: Omit): Promise; + update(id: number, subscription: Partial): Promise; + cancel(id: number, retirementDate: Date): Promise; +} + diff --git a/src/features/subscriptions/infrastructure/adapters/plan.repository.ts b/src/features/subscriptions/infrastructure/adapters/plan.repository.ts new file mode 100644 index 0000000..859a015 --- /dev/null +++ b/src/features/subscriptions/infrastructure/adapters/plan.repository.ts @@ -0,0 +1,57 @@ +import { eq } from "drizzle-orm"; +import DatabaseConnection from "@/core/infrastructure/database"; +import { plans } from "@/schema"; +import { IPlanRepository } from "@/subscriptions/domain/ports/plan-repository.port"; +import { IPlan } from "@/subscriptions/domain/entities/IPlan"; + +export class PgPlanRepository implements IPlanRepository { + private db = DatabaseConnection.getInstance().db; + private static instance: PgPlanRepository; + + private constructor() {} + + public static getInstance(): PgPlanRepository { + if (!PgPlanRepository.instance) { + PgPlanRepository.instance = new PgPlanRepository(); + } + return PgPlanRepository.instance; + } + + async findAll(): Promise { + const result = await this.db.select().from(plans); + return result.map(this.mapToEntity); + } + + async findById(id: number): Promise { + const result = await this.db + .select() + .from(plans) + .where(eq(plans.id, id)) + .limit(1); + + return result[0] ? this.mapToEntity(result[0]) : null; + } + + async findByName(name: string): Promise { + const result = await this.db + .select() + .from(plans) + .where(eq(plans.name, name)) + .limit(1); + + return result[0] ? this.mapToEntity(result[0]) : null; + } + + private mapToEntity(raw: any): IPlan { + return { + id: raw.id, + name: raw.name, + durationDays: raw.duration_days, + price: Number(raw.price), + frequency: raw.frequency, + createdAt: raw.created_at, + updatedAt: raw.updated_at, + }; + } +} + diff --git a/src/features/subscriptions/infrastructure/adapters/subscription-api.adapter.ts b/src/features/subscriptions/infrastructure/adapters/subscription-api.adapter.ts new file mode 100644 index 0000000..36887c4 --- /dev/null +++ b/src/features/subscriptions/infrastructure/adapters/subscription-api.adapter.ts @@ -0,0 +1,37 @@ +import { ISubscription } from "@/subscriptions/domain/entities/ISubscription"; +import { IPlan } from "@/subscriptions/domain/entities/IPlan"; + +export class SubscriptionApiAdapter { + static toApiResponse(subscription: ISubscription) { + return { + id: subscription.id, + userId: subscription.userId, + planId: subscription.planId, + plan: subscription.plan ? this.planToApiResponse(subscription.plan) : null, + frequency: subscription.frequency, + startDate: subscription.startDate, + endDate: subscription.endDate, + retirementDate: subscription.retirementDate, + active: subscription.active, + createdAt: subscription.createdAt, + updatedAt: subscription.updatedAt, + }; + } + + static planToApiResponse(plan: IPlan) { + return { + id: plan.id, + name: plan.name, + durationDays: plan.durationDays, + price: plan.price, + frequency: plan.frequency, + createdAt: plan.createdAt, + updatedAt: plan.updatedAt, + }; + } + + static planToApiResponseList(plans: IPlan[]) { + return plans.map(this.planToApiResponse); + } +} + diff --git a/src/features/subscriptions/infrastructure/adapters/subscription.repository.ts b/src/features/subscriptions/infrastructure/adapters/subscription.repository.ts new file mode 100644 index 0000000..01677ea --- /dev/null +++ b/src/features/subscriptions/infrastructure/adapters/subscription.repository.ts @@ -0,0 +1,209 @@ +import { eq, and } from "drizzle-orm"; +import DatabaseConnection from "@/core/infrastructure/database"; +import { subscriptions, plans } from "@/schema"; +import { ISubscriptionRepository } from "@/subscriptions/domain/ports/subscription-repository.port"; +import { ISubscription } from "@/subscriptions/domain/entities/ISubscription"; +import { IPlan } from "@/subscriptions/domain/entities/IPlan"; + +export class PgSubscriptionRepository implements ISubscriptionRepository { + private db = DatabaseConnection.getInstance().db; + private static instance: PgSubscriptionRepository; + + private constructor() {} + + public static getInstance(): PgSubscriptionRepository { + if (!PgSubscriptionRepository.instance) { + PgSubscriptionRepository.instance = new PgSubscriptionRepository(); + } + return PgSubscriptionRepository.instance; + } + + async findByUserId(userId: number): Promise { + const result = await this.db + .select({ + id: subscriptions.id, + user_id: subscriptions.user_id, + plan_id: subscriptions.plan_id, + plan: { + id: plans.id, + name: plans.name, + duration_days: plans.duration_days, + price: plans.price, + frequency: plans.frequency, + created_at: plans.created_at, + updated_at: plans.updated_at, + }, + frequency: subscriptions.frequency, + start_date: subscriptions.start_date, + end_date: subscriptions.end_date, + retirement_date: subscriptions.retirement_date, + active: subscriptions.active, + created_at: subscriptions.created_at, + updated_at: subscriptions.updated_at, + }) + .from(subscriptions) + .leftJoin(plans, eq(subscriptions.plan_id, plans.id)) + .where( + and( + eq(subscriptions.user_id, userId), + eq(subscriptions.active, true) + ) + ) + .limit(1); + + return result[0] ? this.mapToEntity(result[0]) : null; + } + + async findById(id: number): Promise { + const result = await this.db + .select({ + id: subscriptions.id, + user_id: subscriptions.user_id, + plan_id: subscriptions.plan_id, + plan: { + id: plans.id, + name: plans.name, + duration_days: plans.duration_days, + price: plans.price, + frequency: plans.frequency, + created_at: plans.created_at, + updated_at: plans.updated_at, + }, + frequency: subscriptions.frequency, + start_date: subscriptions.start_date, + end_date: subscriptions.end_date, + retirement_date: subscriptions.retirement_date, + active: subscriptions.active, + created_at: subscriptions.created_at, + updated_at: subscriptions.updated_at, + }) + .from(subscriptions) + .leftJoin(plans, eq(subscriptions.plan_id, plans.id)) + .where(eq(subscriptions.id, id)) + .limit(1); + + return result[0] ? this.mapToEntity(result[0]) : null; + } + + async create( + subscriptionData: Omit + ): Promise { + const existing = await this.findByUserId(subscriptionData.userId); + if (existing) { + await this.db + .update(subscriptions) + .set({ active: false }) + .where(eq(subscriptions.id, existing.id)); + } + + const result = await this.db + .insert(subscriptions) + .values({ + user_id: subscriptionData.userId, + plan_id: subscriptionData.planId, + frequency: subscriptionData.frequency, + start_date: subscriptionData.startDate, + end_date: subscriptionData.endDate, + retirement_date: subscriptionData.retirementDate || null, + active: subscriptionData.active, + }) + .returning(); + + const planResult = await this.db + .select() + .from(plans) + .where(eq(plans.id, subscriptionData.planId)) + .limit(1); + + return this.mapToEntity({ + ...result[0], + plan: planResult[0] || null, + }); + } + + async update( + id: number, + subscriptionData: Partial + ): Promise { + const updateData: Record = {}; + + if (subscriptionData.planId !== undefined) + updateData.plan_id = subscriptionData.planId; + if (subscriptionData.frequency !== undefined) + updateData.frequency = subscriptionData.frequency; + if (subscriptionData.startDate !== undefined) + updateData.start_date = subscriptionData.startDate; + if (subscriptionData.endDate !== undefined) + updateData.end_date = subscriptionData.endDate; + if (subscriptionData.retirementDate !== undefined) + updateData.retirement_date = subscriptionData.retirementDate; + if (subscriptionData.active !== undefined) + updateData.active = subscriptionData.active; + + const result = await this.db + .update(subscriptions) + .set(updateData) + .where(eq(subscriptions.id, id)) + .returning(); + + const planResult = await this.db + .select() + .from(plans) + .where(eq(plans.id, result[0].plan_id)) + .limit(1); + + return this.mapToEntity({ + ...result[0], + plan: planResult[0] || null, + }); + } + + async cancel(id: number, retirementDate: Date): Promise { + const result = await this.db + .update(subscriptions) + .set({ + retirement_date: retirementDate, + active: false, + }) + .where(eq(subscriptions.id, id)) + .returning(); + + const planResult = await this.db + .select() + .from(plans) + .where(eq(plans.id, result[0].plan_id)) + .limit(1); + + return this.mapToEntity({ + ...result[0], + plan: planResult[0] || null, + }); + } + + private mapToEntity(raw: any): ISubscription { + return { + id: raw.id, + userId: raw.user_id, + planId: raw.plan_id, + plan: raw.plan + ? { + id: raw.plan.id, + name: raw.plan.name, + durationDays: raw.plan.duration_days, + price: Number(raw.plan.price), + frequency: raw.plan.frequency, + createdAt: raw.plan.created_at, + updatedAt: raw.plan.updated_at, + } + : null, + frequency: raw.frequency, + startDate: new Date(raw.start_date), + endDate: new Date(raw.end_date), + retirementDate: raw.retirement_date ? new Date(raw.retirement_date) : null, + active: raw.active, + createdAt: raw.created_at, + updatedAt: raw.updated_at, + }; + } +} + diff --git a/src/features/subscriptions/infrastructure/controllers/subscription.controller.ts b/src/features/subscriptions/infrastructure/controllers/subscription.controller.ts new file mode 100644 index 0000000..90bfd59 --- /dev/null +++ b/src/features/subscriptions/infrastructure/controllers/subscription.controller.ts @@ -0,0 +1,24 @@ +import { createRouter } from "@/core/infrastructure/lib/create-app"; +import * as routes from "@/subscriptions/infrastructure/controllers/subscription.routes"; +import { SubscriptionService } from "@/subscriptions/application/services/subscription.service"; +import { PgPlanRepository } from "@/subscriptions/infrastructure/adapters/plan.repository"; +import { PgSubscriptionRepository } from "@/subscriptions/infrastructure/adapters/subscription.repository"; +import { PgUserRepository } from "@/users/infrastructure/adapters/user.repository"; + +const planRepository = PgPlanRepository.getInstance(); +const subscriptionRepository = PgSubscriptionRepository.getInstance(); +const userRepository = PgUserRepository.getInstance(); +const subscriptionService = SubscriptionService.getInstance( + planRepository, + subscriptionRepository, + userRepository +); + +const router = createRouter() + .openapi(routes.listPlans, subscriptionService.listPlans) + .openapi(routes.setPlan, subscriptionService.setPlan) + .openapi(routes.cancelPlan, subscriptionService.cancelPlan) + .openapi(routes.getUserSubscription, subscriptionService.getUserSubscription); + +export default router; + diff --git a/src/features/subscriptions/infrastructure/controllers/subscription.routes.ts b/src/features/subscriptions/infrastructure/controllers/subscription.routes.ts new file mode 100644 index 0000000..5b1c731 --- /dev/null +++ b/src/features/subscriptions/infrastructure/controllers/subscription.routes.ts @@ -0,0 +1,159 @@ +import { createRoute } from "@hono/zod-openapi"; +import { z } from "zod"; +import * as HttpStatusCodes from "stoker/http-status-codes"; +import { jsonContentRequired } from "stoker/openapi/helpers"; +import { setPlanSchema } from "@/subscriptions/application/dtos/subscription.dto"; + +const tags = ["Subscriptions"]; + +const planSchema = z.object({ + id: z.number(), + name: z.string(), + durationDays: z.number(), + price: z.number(), + frequency: z.string(), + createdAt: z.date(), + updatedAt: z.date(), +}); + +const subscriptionSchema = z.object({ + id: z.number(), + userId: z.number(), + planId: z.number(), + plan: planSchema.nullable(), + frequency: z.string(), + startDate: z.date(), + endDate: z.date(), + retirementDate: z.date().nullable(), + active: z.boolean(), + createdAt: z.date(), + updatedAt: z.date(), +}); + +const baseResponseSchema = (schema: T) => + z.object({ + success: z.boolean(), + data: schema, + message: z.string(), + }); + +const errorResponseSchema = z.object({ + success: z.boolean(), + data: z.null(), + message: z.string(), +}); + +export const listPlans = createRoute({ + path: "/plans", + method: "get", + tags, + responses: { + [HttpStatusCodes.OK]: { + content: { + "application/json": { + schema: baseResponseSchema(z.array(planSchema)), + }, + }, + description: "Plans retrieved successfully", + }, + }, +}); + +export const setPlan = createRoute({ + path: "/subscriptions", + method: "post", + tags, + request: { + body: jsonContentRequired(setPlanSchema, "Set plan data"), + }, + responses: { + [HttpStatusCodes.CREATED]: { + content: { + "application/json": { + schema: baseResponseSchema(subscriptionSchema), + }, + }, + description: "Plan set successfully", + }, + [HttpStatusCodes.NOT_FOUND]: { + content: { + "application/json": { + schema: errorResponseSchema, + }, + }, + description: "Plan or user not found", + }, + [HttpStatusCodes.BAD_REQUEST]: { + content: { + "application/json": { + schema: errorResponseSchema, + }, + }, + description: "Invalid data", + }, + }, +}); + +export const cancelPlan = createRoute({ + path: "/subscriptions/:id/cancel", + method: "post", + tags, + request: { + params: z.object({ + id: z.string().regex(/^\d+$/).transform(Number), + }), + }, + responses: { + [HttpStatusCodes.OK]: { + content: { + "application/json": { + schema: baseResponseSchema(subscriptionSchema), + }, + }, + description: "Plan cancelled successfully", + }, + [HttpStatusCodes.NOT_FOUND]: { + content: { + "application/json": { + schema: errorResponseSchema, + }, + }, + description: "Subscription not found", + }, + }, +}); + +export const getUserSubscription = createRoute({ + path: "/users/:userId/subscription", + method: "get", + tags, + request: { + params: z.object({ + userId: z.string().regex(/^\d+$/).transform(Number), + }), + }, + responses: { + [HttpStatusCodes.OK]: { + content: { + "application/json": { + schema: baseResponseSchema(subscriptionSchema.nullable()), + }, + }, + description: "User subscription retrieved successfully", + }, + [HttpStatusCodes.NOT_FOUND]: { + content: { + "application/json": { + schema: errorResponseSchema, + }, + }, + description: "User subscription not found", + }, + }, +}); + +export type ListPlansRoute = typeof listPlans; +export type SetPlanRoute = typeof setPlan; +export type CancelPlanRoute = typeof cancelPlan; +export type GetUserSubscriptionRoute = typeof getUserSubscription; + diff --git a/src/features/users/application/services/user.service.ts b/src/features/users/application/services/user.service.ts index c1fcf32..813046f 100644 --- a/src/features/users/application/services/user.service.ts +++ b/src/features/users/application/services/user.service.ts @@ -105,6 +105,25 @@ export class UserService implements IUserService { active: true, }); + const { PgPlanRepository } = await import("@/subscriptions/infrastructure/adapters/plan.repository"); + const { PgSubscriptionRepository } = await import("@/subscriptions/infrastructure/adapters/subscription.repository"); + const planRepository = PgPlanRepository.getInstance(); + const subscriptionRepository = PgSubscriptionRepository.getInstance(); + const demoPlan = await planRepository.findByName("demo"); + if (demoPlan) { + const startDate = new Date(); + const endDate = new Date(); + endDate.setDate(endDate.getDate() + demoPlan.durationDays); + await subscriptionRepository.create({ + userId: user.id, + planId: demoPlan.id, + frequency: demoPlan.frequency, + startDate, + endDate, + active: true, + }); + } + return c.json( { success: true, diff --git a/tsconfig.json b/tsconfig.json index 70f3468..339e65b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -58,6 +58,9 @@ ], "@/notifications/*": [ "./src/features/notifications/*" + ], + "@/subscriptions/*": [ + "./src/features/subscriptions/*" ] } } From c4d3d86045b100e3c2da8a9531ab4274b6315d2e Mon Sep 17 00:00:00 2001 From: leninner Date: Tue, 25 Nov 2025 19:33:58 -0500 Subject: [PATCH 2/2] feat: update subs --- drizzle/0008_empty_guardsmen.sql | 29 + drizzle/0009_silky_jamie_braddock.sql | 1 + drizzle/0010_sharp_morgan_stark.sql | 2 + drizzle/meta/0008_snapshot.json | 2006 ++++++++++++++++ drizzle/meta/0009_snapshot.json | 1998 ++++++++++++++++ drizzle/meta/0010_snapshot.json | 2010 +++++++++++++++++ drizzle/meta/_journal.json | 21 + src/core/infrastructure/database/schema.ts | 4 +- .../infrastructure/scripts/assign-demo.ts | 35 + .../infrastructure/scripts/clear-plans.ts | 12 + src/core/infrastructure/scripts/list-plans.ts | 11 + .../scripts/list-subscriptions.ts | 24 + src/core/infrastructure/scripts/list-users.ts | 11 + src/core/infrastructure/scripts/plans.seed.ts | 77 +- .../auth/application/services/auth.service.ts | 2 +- .../services/subscription.service.ts | 3 +- .../subscriptions/domain/entities/IPlan.ts | 2 + .../adapters/plan.repository.ts | 2 + .../adapters/subscription-api.adapter.ts | 4 +- .../controllers/subscription.routes.ts | 2 + 20 files changed, 6247 insertions(+), 9 deletions(-) create mode 100644 drizzle/0008_empty_guardsmen.sql create mode 100644 drizzle/0009_silky_jamie_braddock.sql create mode 100644 drizzle/0010_sharp_morgan_stark.sql create mode 100644 drizzle/meta/0008_snapshot.json create mode 100644 drizzle/meta/0009_snapshot.json create mode 100644 drizzle/meta/0010_snapshot.json create mode 100644 src/core/infrastructure/scripts/assign-demo.ts create mode 100644 src/core/infrastructure/scripts/clear-plans.ts create mode 100644 src/core/infrastructure/scripts/list-plans.ts create mode 100644 src/core/infrastructure/scripts/list-subscriptions.ts create mode 100644 src/core/infrastructure/scripts/list-users.ts diff --git a/drizzle/0008_empty_guardsmen.sql b/drizzle/0008_empty_guardsmen.sql new file mode 100644 index 0000000..d423398 --- /dev/null +++ b/drizzle/0008_empty_guardsmen.sql @@ -0,0 +1,29 @@ +CREATE TABLE "plans" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar NOT NULL, + "duration_days" integer NOT NULL, + "price" numeric(10, 2) NOT NULL, + "frequency" varchar NOT NULL, + "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + CONSTRAINT "plans_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "subscriptions" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "plan_id" integer NOT NULL, + "frequency" varchar NOT NULL, + "start_date" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + "end_date" timestamp NOT NULL, + "retirement_date" timestamp, + "active" boolean DEFAULT true NOT NULL, + "created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_plan_id_plans_id_fk" FOREIGN KEY ("plan_id") REFERENCES "public"."plans"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "sub_user_idx" ON "subscriptions" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "sub_plan_idx" ON "subscriptions" USING btree ("plan_id");--> statement-breakpoint +CREATE INDEX "sub_active_idx" ON "subscriptions" USING btree ("active"); \ No newline at end of file diff --git a/drizzle/0009_silky_jamie_braddock.sql b/drizzle/0009_silky_jamie_braddock.sql new file mode 100644 index 0000000..58f7ebd --- /dev/null +++ b/drizzle/0009_silky_jamie_braddock.sql @@ -0,0 +1 @@ +ALTER TABLE "plans" DROP CONSTRAINT "plans_name_unique"; \ No newline at end of file diff --git a/drizzle/0010_sharp_morgan_stark.sql b/drizzle/0010_sharp_morgan_stark.sql new file mode 100644 index 0000000..f9de867 --- /dev/null +++ b/drizzle/0010_sharp_morgan_stark.sql @@ -0,0 +1,2 @@ +ALTER TABLE "plans" ADD COLUMN "description" text;--> statement-breakpoint +ALTER TABLE "plans" ADD COLUMN "features" jsonb; \ No newline at end of file diff --git a/drizzle/meta/0008_snapshot.json b/drizzle/meta/0008_snapshot.json new file mode 100644 index 0000000..d2da723 --- /dev/null +++ b/drizzle/meta/0008_snapshot.json @@ -0,0 +1,2006 @@ +{ + "id": "fe141e79-c873-4a0c-8c00-d6d977b1e32c", + "prevId": "0af304cf-d2cb-4241-afc6-947d48b98414", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.budgets": { + "name": "budgets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shared_user_id": { + "name": "shared_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "limit_amount": { + "name": "limit_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "current_amount": { + "name": "current_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "month": { + "name": "month", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "budget_user_idx": { + "name": "budget_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_category_idx": { + "name": "budget_category_idx", + "columns": [ + { + "expression": "category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_month_idx": { + "name": "budget_month_idx", + "columns": [ + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budgets_user_id_users_id_fk": { + "name": "budgets_user_id_users_id_fk", + "tableFrom": "budgets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budgets_shared_user_id_users_id_fk": { + "name": "budgets_shared_user_id_users_id_fk", + "tableFrom": "budgets", + "tableTo": "users", + "columnsFrom": [ + "shared_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budgets_category_id_categories_id_fk": { + "name": "budgets_category_id_categories_id_fk", + "tableFrom": "budgets", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "categories_name_unique": { + "name": "categories_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.debts": { + "name": "debts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_amount": { + "name": "original_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "pending_amount": { + "name": "pending_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "due_date": { + "name": "due_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "paid": { + "name": "paid", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "creditor_id": { + "name": "creditor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "debts_user_idx": { + "name": "debts_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "debts_creditor_idx": { + "name": "debts_creditor_idx", + "columns": [ + { + "expression": "creditor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "debts_due_date_idx": { + "name": "debts_due_date_idx", + "columns": [ + { + "expression": "due_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "debts_user_id_users_id_fk": { + "name": "debts_user_id_users_id_fk", + "tableFrom": "debts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "debts_creditor_id_users_id_fk": { + "name": "debts_creditor_id_users_id_fk", + "tableFrom": "debts", + "tableTo": "users", + "columnsFrom": [ + "creditor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "debts_category_id_categories_id_fk": { + "name": "debts_category_id_categories_id_fk", + "tableFrom": "debts", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.friends": { + "name": "friends", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "friend_id": { + "name": "friend_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "connection_date": { + "name": "connection_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "friends_user_idx": { + "name": "friends_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "friends_friend_idx": { + "name": "friends_friend_idx", + "columns": [ + { + "expression": "friend_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "friends_user_id_users_id_fk": { + "name": "friends_user_id_users_id_fk", + "tableFrom": "friends", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "friends_friend_id_users_id_fk": { + "name": "friends_friend_id_users_id_fk", + "tableFrom": "friends", + "tableTo": "users", + "columnsFrom": [ + "friend_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goal_contribution_schedule": { + "name": "goal_contribution_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scheduled_date": { + "name": "scheduled_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "contribution_id": { + "name": "contribution_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "gcs_goal_idx": { + "name": "gcs_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gcs_user_idx": { + "name": "gcs_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gcs_date_idx": { + "name": "gcs_date_idx", + "columns": [ + { + "expression": "scheduled_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gcs_status_idx": { + "name": "gcs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goal_contribution_schedule_goal_id_goals_id_fk": { + "name": "goal_contribution_schedule_goal_id_goals_id_fk", + "tableFrom": "goal_contribution_schedule", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goal_contribution_schedule_user_id_users_id_fk": { + "name": "goal_contribution_schedule_user_id_users_id_fk", + "tableFrom": "goal_contribution_schedule", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goal_contribution_schedule_contribution_id_goal_contributions_id_fk": { + "name": "goal_contribution_schedule_contribution_id_goal_contributions_id_fk", + "tableFrom": "goal_contribution_schedule", + "tableTo": "goal_contributions", + "columnsFrom": [ + "contribution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goal_contributions": { + "name": "goal_contributions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "gc_goal_idx": { + "name": "gc_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gc_user_idx": { + "name": "gc_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gc_date_idx": { + "name": "gc_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goal_contributions_goal_id_goals_id_fk": { + "name": "goal_contributions_goal_id_goals_id_fk", + "tableFrom": "goal_contributions", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goal_contributions_user_id_users_id_fk": { + "name": "goal_contributions_user_id_users_id_fk", + "tableFrom": "goal_contributions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shared_user_id": { + "name": "shared_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "target_amount": { + "name": "target_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "current_amount": { + "name": "current_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "contribution_frequency": { + "name": "contribution_frequency", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "contribution_amount": { + "name": "contribution_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "goals_user_idx": { + "name": "goals_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "goals_shared_user_idx": { + "name": "goals_shared_user_idx", + "columns": [ + { + "expression": "shared_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goals_user_id_users_id_fk": { + "name": "goals_user_id_users_id_fk", + "tableFrom": "goals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_shared_user_id_users_id_fk": { + "name": "goals_shared_user_id_users_id_fk", + "tableFrom": "goals", + "tableTo": "users", + "columnsFrom": [ + "shared_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_category_id_categories_id_fk": { + "name": "goals_category_id_categories_id_fk", + "tableFrom": "goals", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "subtitle": { + "name": "subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read": { + "name": "read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "notif_user_idx": { + "name": "notif_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notif_read_idx": { + "name": "notif_read_idx", + "columns": [ + { + "expression": "read", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notif_expires_idx": { + "name": "notif_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_methods": { + "name": "payment_methods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shared_user_id": { + "name": "shared_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "last_four_digits": { + "name": "last_four_digits", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "pm_user_idx": { + "name": "pm_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pm_shared_user_idx": { + "name": "pm_shared_user_idx", + "columns": [ + { + "expression": "shared_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payment_methods_user_id_users_id_fk": { + "name": "payment_methods_user_id_users_id_fk", + "tableFrom": "payment_methods", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "payment_methods_shared_user_id_users_id_fk": { + "name": "payment_methods_shared_user_id_users_id_fk", + "tableFrom": "payment_methods", + "tableTo": "users", + "columnsFrom": [ + "shared_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plans": { + "name": "plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "duration_days": { + "name": "duration_days", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "frequency": { + "name": "frequency", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plans_name_unique": { + "name": "plans_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "format": { + "name": "format", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "reports_user_id_users_id_fk": { + "name": "reports_user_id_users_id_fk", + "tableFrom": "reports", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduled_transactions": { + "name": "scheduled_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_method_id": { + "name": "payment_method_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "frequency": { + "name": "frequency", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "next_execution_date": { + "name": "next_execution_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "st_user_idx": { + "name": "st_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "st_next_date_idx": { + "name": "st_next_date_idx", + "columns": [ + { + "expression": "next_execution_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduled_transactions_user_id_users_id_fk": { + "name": "scheduled_transactions_user_id_users_id_fk", + "tableFrom": "scheduled_transactions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scheduled_transactions_category_id_categories_id_fk": { + "name": "scheduled_transactions_category_id_categories_id_fk", + "tableFrom": "scheduled_transactions", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scheduled_transactions_payment_method_id_payment_methods_id_fk": { + "name": "scheduled_transactions_payment_method_id_payment_methods_id_fk", + "tableFrom": "scheduled_transactions", + "tableTo": "payment_methods", + "columnsFrom": [ + "payment_method_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "frequency": { + "name": "frequency", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "retirement_date": { + "name": "retirement_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "sub_user_idx": { + "name": "sub_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sub_plan_idx": { + "name": "sub_plan_idx", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sub_active_idx": { + "name": "sub_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscriptions_user_id_users_id_fk": { + "name": "subscriptions_user_id_users_id_fk", + "tableFrom": "subscriptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "subscriptions_plan_id_plans_id_fk": { + "name": "subscriptions_plan_id_plans_id_fk", + "tableFrom": "subscriptions", + "tableTo": "plans", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transactions": { + "name": "transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_method_id": { + "name": "payment_method_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "scheduled_transaction_id": { + "name": "scheduled_transaction_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "debt_id": { + "name": "debt_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "contribution_id": { + "name": "contribution_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "budget_id": { + "name": "budget_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "tx_user_idx": { + "name": "tx_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tx_date_idx": { + "name": "tx_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tx_category_idx": { + "name": "tx_category_idx", + "columns": [ + { + "expression": "category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tx_budget_idx": { + "name": "tx_budget_idx", + "columns": [ + { + "expression": "budget_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactions_user_id_users_id_fk": { + "name": "transactions_user_id_users_id_fk", + "tableFrom": "transactions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactions_category_id_categories_id_fk": { + "name": "transactions_category_id_categories_id_fk", + "tableFrom": "transactions", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactions_payment_method_id_payment_methods_id_fk": { + "name": "transactions_payment_method_id_payment_methods_id_fk", + "tableFrom": "transactions", + "tableTo": "payment_methods", + "columnsFrom": [ + "payment_method_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactions_debt_id_debts_id_fk": { + "name": "transactions_debt_id_debts_id_fk", + "tableFrom": "transactions", + "tableTo": "debts", + "columnsFrom": [ + "debt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactions_contribution_id_goal_contributions_id_fk": { + "name": "transactions_contribution_id_goal_contributions_id_fk", + "tableFrom": "transactions", + "tableTo": "goal_contributions", + "columnsFrom": [ + "contribution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactions_budget_id_budgets_id_fk": { + "name": "transactions_budget_id_budgets_id_fk", + "tableFrom": "transactions", + "tableTo": "budgets", + "columnsFrom": [ + "budget_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "registration_date": { + "name": "registration_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "recovery_token": { + "name": "recovery_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "recovery_token_expires": { + "name": "recovery_token_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0009_snapshot.json b/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..ce9eadd --- /dev/null +++ b/drizzle/meta/0009_snapshot.json @@ -0,0 +1,1998 @@ +{ + "id": "34b504b0-ea8a-419f-b0b7-d05b982cbae9", + "prevId": "fe141e79-c873-4a0c-8c00-d6d977b1e32c", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.budgets": { + "name": "budgets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shared_user_id": { + "name": "shared_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "limit_amount": { + "name": "limit_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "current_amount": { + "name": "current_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "month": { + "name": "month", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "budget_user_idx": { + "name": "budget_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_category_idx": { + "name": "budget_category_idx", + "columns": [ + { + "expression": "category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_month_idx": { + "name": "budget_month_idx", + "columns": [ + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budgets_user_id_users_id_fk": { + "name": "budgets_user_id_users_id_fk", + "tableFrom": "budgets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budgets_shared_user_id_users_id_fk": { + "name": "budgets_shared_user_id_users_id_fk", + "tableFrom": "budgets", + "tableTo": "users", + "columnsFrom": [ + "shared_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budgets_category_id_categories_id_fk": { + "name": "budgets_category_id_categories_id_fk", + "tableFrom": "budgets", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "categories_name_unique": { + "name": "categories_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.debts": { + "name": "debts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_amount": { + "name": "original_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "pending_amount": { + "name": "pending_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "due_date": { + "name": "due_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "paid": { + "name": "paid", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "creditor_id": { + "name": "creditor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "debts_user_idx": { + "name": "debts_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "debts_creditor_idx": { + "name": "debts_creditor_idx", + "columns": [ + { + "expression": "creditor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "debts_due_date_idx": { + "name": "debts_due_date_idx", + "columns": [ + { + "expression": "due_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "debts_user_id_users_id_fk": { + "name": "debts_user_id_users_id_fk", + "tableFrom": "debts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "debts_creditor_id_users_id_fk": { + "name": "debts_creditor_id_users_id_fk", + "tableFrom": "debts", + "tableTo": "users", + "columnsFrom": [ + "creditor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "debts_category_id_categories_id_fk": { + "name": "debts_category_id_categories_id_fk", + "tableFrom": "debts", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.friends": { + "name": "friends", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "friend_id": { + "name": "friend_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "connection_date": { + "name": "connection_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "friends_user_idx": { + "name": "friends_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "friends_friend_idx": { + "name": "friends_friend_idx", + "columns": [ + { + "expression": "friend_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "friends_user_id_users_id_fk": { + "name": "friends_user_id_users_id_fk", + "tableFrom": "friends", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "friends_friend_id_users_id_fk": { + "name": "friends_friend_id_users_id_fk", + "tableFrom": "friends", + "tableTo": "users", + "columnsFrom": [ + "friend_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goal_contribution_schedule": { + "name": "goal_contribution_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scheduled_date": { + "name": "scheduled_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "contribution_id": { + "name": "contribution_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "gcs_goal_idx": { + "name": "gcs_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gcs_user_idx": { + "name": "gcs_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gcs_date_idx": { + "name": "gcs_date_idx", + "columns": [ + { + "expression": "scheduled_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gcs_status_idx": { + "name": "gcs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goal_contribution_schedule_goal_id_goals_id_fk": { + "name": "goal_contribution_schedule_goal_id_goals_id_fk", + "tableFrom": "goal_contribution_schedule", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goal_contribution_schedule_user_id_users_id_fk": { + "name": "goal_contribution_schedule_user_id_users_id_fk", + "tableFrom": "goal_contribution_schedule", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goal_contribution_schedule_contribution_id_goal_contributions_id_fk": { + "name": "goal_contribution_schedule_contribution_id_goal_contributions_id_fk", + "tableFrom": "goal_contribution_schedule", + "tableTo": "goal_contributions", + "columnsFrom": [ + "contribution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goal_contributions": { + "name": "goal_contributions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "gc_goal_idx": { + "name": "gc_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gc_user_idx": { + "name": "gc_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gc_date_idx": { + "name": "gc_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goal_contributions_goal_id_goals_id_fk": { + "name": "goal_contributions_goal_id_goals_id_fk", + "tableFrom": "goal_contributions", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goal_contributions_user_id_users_id_fk": { + "name": "goal_contributions_user_id_users_id_fk", + "tableFrom": "goal_contributions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shared_user_id": { + "name": "shared_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "target_amount": { + "name": "target_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "current_amount": { + "name": "current_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "contribution_frequency": { + "name": "contribution_frequency", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "contribution_amount": { + "name": "contribution_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "goals_user_idx": { + "name": "goals_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "goals_shared_user_idx": { + "name": "goals_shared_user_idx", + "columns": [ + { + "expression": "shared_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goals_user_id_users_id_fk": { + "name": "goals_user_id_users_id_fk", + "tableFrom": "goals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_shared_user_id_users_id_fk": { + "name": "goals_shared_user_id_users_id_fk", + "tableFrom": "goals", + "tableTo": "users", + "columnsFrom": [ + "shared_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_category_id_categories_id_fk": { + "name": "goals_category_id_categories_id_fk", + "tableFrom": "goals", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "subtitle": { + "name": "subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read": { + "name": "read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "notif_user_idx": { + "name": "notif_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notif_read_idx": { + "name": "notif_read_idx", + "columns": [ + { + "expression": "read", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notif_expires_idx": { + "name": "notif_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_methods": { + "name": "payment_methods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shared_user_id": { + "name": "shared_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "last_four_digits": { + "name": "last_four_digits", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "pm_user_idx": { + "name": "pm_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pm_shared_user_idx": { + "name": "pm_shared_user_idx", + "columns": [ + { + "expression": "shared_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payment_methods_user_id_users_id_fk": { + "name": "payment_methods_user_id_users_id_fk", + "tableFrom": "payment_methods", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "payment_methods_shared_user_id_users_id_fk": { + "name": "payment_methods_shared_user_id_users_id_fk", + "tableFrom": "payment_methods", + "tableTo": "users", + "columnsFrom": [ + "shared_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plans": { + "name": "plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "duration_days": { + "name": "duration_days", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "frequency": { + "name": "frequency", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "format": { + "name": "format", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "reports_user_id_users_id_fk": { + "name": "reports_user_id_users_id_fk", + "tableFrom": "reports", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduled_transactions": { + "name": "scheduled_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_method_id": { + "name": "payment_method_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "frequency": { + "name": "frequency", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "next_execution_date": { + "name": "next_execution_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "st_user_idx": { + "name": "st_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "st_next_date_idx": { + "name": "st_next_date_idx", + "columns": [ + { + "expression": "next_execution_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduled_transactions_user_id_users_id_fk": { + "name": "scheduled_transactions_user_id_users_id_fk", + "tableFrom": "scheduled_transactions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scheduled_transactions_category_id_categories_id_fk": { + "name": "scheduled_transactions_category_id_categories_id_fk", + "tableFrom": "scheduled_transactions", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scheduled_transactions_payment_method_id_payment_methods_id_fk": { + "name": "scheduled_transactions_payment_method_id_payment_methods_id_fk", + "tableFrom": "scheduled_transactions", + "tableTo": "payment_methods", + "columnsFrom": [ + "payment_method_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "frequency": { + "name": "frequency", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "retirement_date": { + "name": "retirement_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "sub_user_idx": { + "name": "sub_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sub_plan_idx": { + "name": "sub_plan_idx", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sub_active_idx": { + "name": "sub_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscriptions_user_id_users_id_fk": { + "name": "subscriptions_user_id_users_id_fk", + "tableFrom": "subscriptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "subscriptions_plan_id_plans_id_fk": { + "name": "subscriptions_plan_id_plans_id_fk", + "tableFrom": "subscriptions", + "tableTo": "plans", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transactions": { + "name": "transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_method_id": { + "name": "payment_method_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "scheduled_transaction_id": { + "name": "scheduled_transaction_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "debt_id": { + "name": "debt_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "contribution_id": { + "name": "contribution_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "budget_id": { + "name": "budget_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "tx_user_idx": { + "name": "tx_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tx_date_idx": { + "name": "tx_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tx_category_idx": { + "name": "tx_category_idx", + "columns": [ + { + "expression": "category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tx_budget_idx": { + "name": "tx_budget_idx", + "columns": [ + { + "expression": "budget_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactions_user_id_users_id_fk": { + "name": "transactions_user_id_users_id_fk", + "tableFrom": "transactions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactions_category_id_categories_id_fk": { + "name": "transactions_category_id_categories_id_fk", + "tableFrom": "transactions", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactions_payment_method_id_payment_methods_id_fk": { + "name": "transactions_payment_method_id_payment_methods_id_fk", + "tableFrom": "transactions", + "tableTo": "payment_methods", + "columnsFrom": [ + "payment_method_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactions_debt_id_debts_id_fk": { + "name": "transactions_debt_id_debts_id_fk", + "tableFrom": "transactions", + "tableTo": "debts", + "columnsFrom": [ + "debt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactions_contribution_id_goal_contributions_id_fk": { + "name": "transactions_contribution_id_goal_contributions_id_fk", + "tableFrom": "transactions", + "tableTo": "goal_contributions", + "columnsFrom": [ + "contribution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactions_budget_id_budgets_id_fk": { + "name": "transactions_budget_id_budgets_id_fk", + "tableFrom": "transactions", + "tableTo": "budgets", + "columnsFrom": [ + "budget_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "registration_date": { + "name": "registration_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "recovery_token": { + "name": "recovery_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "recovery_token_expires": { + "name": "recovery_token_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0010_snapshot.json b/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..8e96644 --- /dev/null +++ b/drizzle/meta/0010_snapshot.json @@ -0,0 +1,2010 @@ +{ + "id": "3a64d546-50f6-46eb-a64d-57fc4127847e", + "prevId": "34b504b0-ea8a-419f-b0b7-d05b982cbae9", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.budgets": { + "name": "budgets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shared_user_id": { + "name": "shared_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "limit_amount": { + "name": "limit_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "current_amount": { + "name": "current_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "month": { + "name": "month", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "budget_user_idx": { + "name": "budget_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_category_idx": { + "name": "budget_category_idx", + "columns": [ + { + "expression": "category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_month_idx": { + "name": "budget_month_idx", + "columns": [ + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budgets_user_id_users_id_fk": { + "name": "budgets_user_id_users_id_fk", + "tableFrom": "budgets", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budgets_shared_user_id_users_id_fk": { + "name": "budgets_shared_user_id_users_id_fk", + "tableFrom": "budgets", + "tableTo": "users", + "columnsFrom": [ + "shared_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budgets_category_id_categories_id_fk": { + "name": "budgets_category_id_categories_id_fk", + "tableFrom": "budgets", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "categories_name_unique": { + "name": "categories_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.debts": { + "name": "debts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_amount": { + "name": "original_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "pending_amount": { + "name": "pending_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "due_date": { + "name": "due_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "paid": { + "name": "paid", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "creditor_id": { + "name": "creditor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "debts_user_idx": { + "name": "debts_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "debts_creditor_idx": { + "name": "debts_creditor_idx", + "columns": [ + { + "expression": "creditor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "debts_due_date_idx": { + "name": "debts_due_date_idx", + "columns": [ + { + "expression": "due_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "debts_user_id_users_id_fk": { + "name": "debts_user_id_users_id_fk", + "tableFrom": "debts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "debts_creditor_id_users_id_fk": { + "name": "debts_creditor_id_users_id_fk", + "tableFrom": "debts", + "tableTo": "users", + "columnsFrom": [ + "creditor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "debts_category_id_categories_id_fk": { + "name": "debts_category_id_categories_id_fk", + "tableFrom": "debts", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.friends": { + "name": "friends", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "friend_id": { + "name": "friend_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "connection_date": { + "name": "connection_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "friends_user_idx": { + "name": "friends_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "friends_friend_idx": { + "name": "friends_friend_idx", + "columns": [ + { + "expression": "friend_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "friends_user_id_users_id_fk": { + "name": "friends_user_id_users_id_fk", + "tableFrom": "friends", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "friends_friend_id_users_id_fk": { + "name": "friends_friend_id_users_id_fk", + "tableFrom": "friends", + "tableTo": "users", + "columnsFrom": [ + "friend_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goal_contribution_schedule": { + "name": "goal_contribution_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scheduled_date": { + "name": "scheduled_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "contribution_id": { + "name": "contribution_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "gcs_goal_idx": { + "name": "gcs_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gcs_user_idx": { + "name": "gcs_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gcs_date_idx": { + "name": "gcs_date_idx", + "columns": [ + { + "expression": "scheduled_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gcs_status_idx": { + "name": "gcs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goal_contribution_schedule_goal_id_goals_id_fk": { + "name": "goal_contribution_schedule_goal_id_goals_id_fk", + "tableFrom": "goal_contribution_schedule", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goal_contribution_schedule_user_id_users_id_fk": { + "name": "goal_contribution_schedule_user_id_users_id_fk", + "tableFrom": "goal_contribution_schedule", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goal_contribution_schedule_contribution_id_goal_contributions_id_fk": { + "name": "goal_contribution_schedule_contribution_id_goal_contributions_id_fk", + "tableFrom": "goal_contribution_schedule", + "tableTo": "goal_contributions", + "columnsFrom": [ + "contribution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goal_contributions": { + "name": "goal_contributions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "gc_goal_idx": { + "name": "gc_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gc_user_idx": { + "name": "gc_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gc_date_idx": { + "name": "gc_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goal_contributions_goal_id_goals_id_fk": { + "name": "goal_contributions_goal_id_goals_id_fk", + "tableFrom": "goal_contributions", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goal_contributions_user_id_users_id_fk": { + "name": "goal_contributions_user_id_users_id_fk", + "tableFrom": "goal_contributions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shared_user_id": { + "name": "shared_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "target_amount": { + "name": "target_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "current_amount": { + "name": "current_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "contribution_frequency": { + "name": "contribution_frequency", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "contribution_amount": { + "name": "contribution_amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "goals_user_idx": { + "name": "goals_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "goals_shared_user_idx": { + "name": "goals_shared_user_idx", + "columns": [ + { + "expression": "shared_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goals_user_id_users_id_fk": { + "name": "goals_user_id_users_id_fk", + "tableFrom": "goals", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_shared_user_id_users_id_fk": { + "name": "goals_shared_user_id_users_id_fk", + "tableFrom": "goals", + "tableTo": "users", + "columnsFrom": [ + "shared_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_category_id_categories_id_fk": { + "name": "goals_category_id_categories_id_fk", + "tableFrom": "goals", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "subtitle": { + "name": "subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read": { + "name": "read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "notif_user_idx": { + "name": "notif_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notif_read_idx": { + "name": "notif_read_idx", + "columns": [ + { + "expression": "read", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notif_expires_idx": { + "name": "notif_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_methods": { + "name": "payment_methods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shared_user_id": { + "name": "shared_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "last_four_digits": { + "name": "last_four_digits", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "pm_user_idx": { + "name": "pm_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pm_shared_user_idx": { + "name": "pm_shared_user_idx", + "columns": [ + { + "expression": "shared_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payment_methods_user_id_users_id_fk": { + "name": "payment_methods_user_id_users_id_fk", + "tableFrom": "payment_methods", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "payment_methods_shared_user_id_users_id_fk": { + "name": "payment_methods_shared_user_id_users_id_fk", + "tableFrom": "payment_methods", + "tableTo": "users", + "columnsFrom": [ + "shared_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plans": { + "name": "plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "duration_days": { + "name": "duration_days", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "frequency": { + "name": "frequency", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "features": { + "name": "features", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "format": { + "name": "format", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "reports_user_id_users_id_fk": { + "name": "reports_user_id_users_id_fk", + "tableFrom": "reports", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduled_transactions": { + "name": "scheduled_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_method_id": { + "name": "payment_method_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "frequency": { + "name": "frequency", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "next_execution_date": { + "name": "next_execution_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "st_user_idx": { + "name": "st_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "st_next_date_idx": { + "name": "st_next_date_idx", + "columns": [ + { + "expression": "next_execution_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduled_transactions_user_id_users_id_fk": { + "name": "scheduled_transactions_user_id_users_id_fk", + "tableFrom": "scheduled_transactions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scheduled_transactions_category_id_categories_id_fk": { + "name": "scheduled_transactions_category_id_categories_id_fk", + "tableFrom": "scheduled_transactions", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "scheduled_transactions_payment_method_id_payment_methods_id_fk": { + "name": "scheduled_transactions_payment_method_id_payment_methods_id_fk", + "tableFrom": "scheduled_transactions", + "tableTo": "payment_methods", + "columnsFrom": [ + "payment_method_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "frequency": { + "name": "frequency", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "retirement_date": { + "name": "retirement_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "sub_user_idx": { + "name": "sub_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sub_plan_idx": { + "name": "sub_plan_idx", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sub_active_idx": { + "name": "sub_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscriptions_user_id_users_id_fk": { + "name": "subscriptions_user_id_users_id_fk", + "tableFrom": "subscriptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "subscriptions_plan_id_plans_id_fk": { + "name": "subscriptions_plan_id_plans_id_fk", + "tableFrom": "subscriptions", + "tableTo": "plans", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transactions": { + "name": "transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_method_id": { + "name": "payment_method_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "scheduled_transaction_id": { + "name": "scheduled_transaction_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "debt_id": { + "name": "debt_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "contribution_id": { + "name": "contribution_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "budget_id": { + "name": "budget_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "tx_user_idx": { + "name": "tx_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tx_date_idx": { + "name": "tx_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tx_category_idx": { + "name": "tx_category_idx", + "columns": [ + { + "expression": "category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tx_budget_idx": { + "name": "tx_budget_idx", + "columns": [ + { + "expression": "budget_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactions_user_id_users_id_fk": { + "name": "transactions_user_id_users_id_fk", + "tableFrom": "transactions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactions_category_id_categories_id_fk": { + "name": "transactions_category_id_categories_id_fk", + "tableFrom": "transactions", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactions_payment_method_id_payment_methods_id_fk": { + "name": "transactions_payment_method_id_payment_methods_id_fk", + "tableFrom": "transactions", + "tableTo": "payment_methods", + "columnsFrom": [ + "payment_method_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactions_debt_id_debts_id_fk": { + "name": "transactions_debt_id_debts_id_fk", + "tableFrom": "transactions", + "tableTo": "debts", + "columnsFrom": [ + "debt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactions_contribution_id_goal_contributions_id_fk": { + "name": "transactions_contribution_id_goal_contributions_id_fk", + "tableFrom": "transactions", + "tableTo": "goal_contributions", + "columnsFrom": [ + "contribution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactions_budget_id_budgets_id_fk": { + "name": "transactions_budget_id_budgets_id_fk", + "tableFrom": "transactions", + "tableTo": "budgets", + "columnsFrom": [ + "budget_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "registration_date": { + "name": "registration_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "recovery_token": { + "name": "recovery_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "recovery_token_expires": { + "name": "recovery_token_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index bf31a60..8679f97 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -57,6 +57,27 @@ "when": 1745570066019, "tag": "0007_acoustic_doctor_faustus", "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1764112994585, + "tag": "0008_empty_guardsmen", + "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1764113188585, + "tag": "0009_silky_jamie_braddock", + "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1764113325919, + "tag": "0010_sharp_morgan_stark", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/core/infrastructure/database/schema.ts b/src/core/infrastructure/database/schema.ts index f16720c..dfafa14 100644 --- a/src/core/infrastructure/database/schema.ts +++ b/src/core/infrastructure/database/schema.ts @@ -387,10 +387,12 @@ export const reports = pgTable("reports", { export const plans = pgTable("plans", { id: serial("id").primaryKey(), - name: varchar("name").notNull().unique(), + name: varchar("name").notNull(), duration_days: integer("duration_days").notNull(), price: decimal("price", { precision: 10, scale: 2 }).notNull(), frequency: varchar("frequency").notNull(), + description: text("description"), + features: jsonb("features").$type(), created_at: timestamp("created_at") .default(sql`CURRENT_TIMESTAMP`) .notNull(), diff --git a/src/core/infrastructure/scripts/assign-demo.ts b/src/core/infrastructure/scripts/assign-demo.ts new file mode 100644 index 0000000..13309da --- /dev/null +++ b/src/core/infrastructure/scripts/assign-demo.ts @@ -0,0 +1,35 @@ +import DatabaseConnection from "@/core/infrastructure/database"; +import { subscriptions, plans } from "@/schema"; +import { eq } from "drizzle-orm"; + +async function assignDemoPlan(userId: number) { + const db = DatabaseConnection.getInstance().db; + + // Find Plan Demo + const demoPlan = await db.query.plans.findFirst({ + where: eq(plans.name, "Plan Demo") + }); + + if (!demoPlan) { + console.error("Plan Demo not found!"); + process.exit(1); + } + + const startDate = new Date(); + const endDate = new Date(); + endDate.setDate(endDate.getDate() + demoPlan.duration_days); + + await db.insert(subscriptions).values({ + user_id: userId, + plan_id: demoPlan.id, + frequency: demoPlan.frequency, + start_date: startDate, + end_date: endDate, + active: true, + }); + + console.log(`Assigned Plan Demo to user ${userId}`); + await DatabaseConnection.getInstance().close(); +} + +assignDemoPlan(1); diff --git a/src/core/infrastructure/scripts/clear-plans.ts b/src/core/infrastructure/scripts/clear-plans.ts new file mode 100644 index 0000000..23f1e59 --- /dev/null +++ b/src/core/infrastructure/scripts/clear-plans.ts @@ -0,0 +1,12 @@ +import DatabaseConnection from "@/core/infrastructure/database"; +import { plans } from "@/schema"; +import { sql } from "drizzle-orm"; + +async function clearPlans() { + const db = DatabaseConnection.getInstance().db; + await db.execute(sql`TRUNCATE TABLE ${plans} RESTART IDENTITY CASCADE`); + console.log("Plans table truncated."); + await DatabaseConnection.getInstance().close(); +} + +clearPlans(); diff --git a/src/core/infrastructure/scripts/list-plans.ts b/src/core/infrastructure/scripts/list-plans.ts new file mode 100644 index 0000000..fae410d --- /dev/null +++ b/src/core/infrastructure/scripts/list-plans.ts @@ -0,0 +1,11 @@ +import DatabaseConnection from "@/core/infrastructure/database"; +import { plans } from "@/schema"; + +async function listPlans() { + const db = DatabaseConnection.getInstance().db; + const allPlans = await db.select().from(plans); + console.log(JSON.stringify(allPlans, null, 2)); + await DatabaseConnection.getInstance().close(); +} + +listPlans(); diff --git a/src/core/infrastructure/scripts/list-subscriptions.ts b/src/core/infrastructure/scripts/list-subscriptions.ts new file mode 100644 index 0000000..8f09bed --- /dev/null +++ b/src/core/infrastructure/scripts/list-subscriptions.ts @@ -0,0 +1,24 @@ +import DatabaseConnection from "@/core/infrastructure/database"; +import { subscriptions, users, plans } from "@/schema"; +import { eq } from "drizzle-orm"; + +async function listSubscriptions() { + const db = DatabaseConnection.getInstance().db; + const subs = await db + .select({ + subscriptionId: subscriptions.id, + user: users.email, + plan: plans.name, + active: subscriptions.active, + startDate: subscriptions.start_date, + endDate: subscriptions.end_date, + }) + .from(subscriptions) + .leftJoin(users, eq(subscriptions.user_id, users.id)) + .leftJoin(plans, eq(subscriptions.plan_id, plans.id)); + + console.log(JSON.stringify(subs, null, 2)); + await DatabaseConnection.getInstance().close(); +} + +listSubscriptions(); diff --git a/src/core/infrastructure/scripts/list-users.ts b/src/core/infrastructure/scripts/list-users.ts new file mode 100644 index 0000000..bbf3eb1 --- /dev/null +++ b/src/core/infrastructure/scripts/list-users.ts @@ -0,0 +1,11 @@ +import DatabaseConnection from "@/core/infrastructure/database"; +import { users } from "@/schema"; + +async function listUsers() { + const db = DatabaseConnection.getInstance().db; + const allUsers = await db.select().from(users); + console.log(JSON.stringify(allUsers, null, 2)); + await DatabaseConnection.getInstance().close(); +} + +listUsers(); diff --git a/src/core/infrastructure/scripts/plans.seed.ts b/src/core/infrastructure/scripts/plans.seed.ts index 98484e1..8eb560f 100644 --- a/src/core/infrastructure/scripts/plans.seed.ts +++ b/src/core/infrastructure/scripts/plans.seed.ts @@ -15,11 +15,76 @@ async function seedPlans() { } const plansData = [ - { name: "demo", durationDays: 15, price: "0.00", frequency: "one-time" }, - { name: "lite", durationDays: 30, price: "9.99", frequency: "monthly" }, - { name: "lite", durationDays: 365, price: "99.99", frequency: "yearly" }, - { name: "plus", durationDays: 30, price: "19.99", frequency: "monthly" }, - { name: "plus", durationDays: 365, price: "199.99", frequency: "yearly" }, + { + name: "Plan Demo", + durationDays: 15, + price: "0.00", + frequency: "one-time", + description: "Prueba las funcionalidades básicas", + features: ["Acceso limitado", "Prueba de concepto"] + }, + { + name: "Plan Lite", + durationDays: 30, + price: "9.99", + frequency: "monthly", + description: "Perfecto para comenzar con la gestión financiera inteligente", + features: [ + "Entrada de voz", + "Acceso ilimitado a IA", + "Soporte básico", + "Acceso a la app móvil", + "Informes exportables" + ] + }, + { + name: "Plan Lite Anual", + durationDays: 365, + price: "99.99", + frequency: "yearly", + description: "Perfecto para comenzar con la gestión financiera inteligente", + features: [ + "Entrada de voz", + "Acceso ilimitado a IA", + "Soporte básico", + "Acceso a la app móvil", + "Informes exportables" + ] + }, + { + name: "Plan Plus", + durationDays: 30, + price: "19.99", + frequency: "monthly", + description: "Para usuarios avanzados que quieren insights avanzados y soporte prioritario", + features: [ + "Entrada de voz", + "Acceso ilimitado a IA", + "Recomendaciones avanzadas", + "Informes detallados", + "Soporte prioritario", + "Categorías personalizadas", + "Alertas de presupuesto", + "Sincronización multi-dispositivo" + ] + }, + { + name: "Plan Plus Anual", + durationDays: 365, + price: "199.99", + frequency: "yearly", + description: "Para usuarios avanzados que quieren insights avanzados y soporte prioritario", + features: [ + "Entrada de voz", + "Acceso ilimitado a IA", + "Recomendaciones avanzadas", + "Informes detallados", + "Soporte prioritario", + "Categorías personalizadas", + "Alertas de presupuesto", + "Sincronización multi-dispositivo" + ] + }, ]; console.log(`Creando ${plansData.length} planes...`); @@ -30,6 +95,8 @@ async function seedPlans() { duration_days: plan.durationDays, price: plan.price, frequency: plan.frequency, + description: plan.description, + features: plan.features, }); } diff --git a/src/features/auth/application/services/auth.service.ts b/src/features/auth/application/services/auth.service.ts index 3740c9a..fd2870a 100644 --- a/src/features/auth/application/services/auth.service.ts +++ b/src/features/auth/application/services/auth.service.ts @@ -122,7 +122,7 @@ export class AuthService { // Asignar plan demo const planRepository = PgPlanRepository.getInstance(); const subscriptionRepository = PgSubscriptionRepository.getInstance(); - const demoPlan = await planRepository.findByName("demo"); + const demoPlan = await planRepository.findByName("Plan Demo"); if (demoPlan) { const startDate = new Date(); const endDate = new Date(); diff --git a/src/features/subscriptions/application/services/subscription.service.ts b/src/features/subscriptions/application/services/subscription.service.ts index e887d36..fe5e625 100644 --- a/src/features/subscriptions/application/services/subscription.service.ts +++ b/src/features/subscriptions/application/services/subscription.service.ts @@ -37,10 +37,11 @@ export class SubscriptionService { listPlans = createHandler(async (c) => { const plans = await this.planRepository.findAll(); + const visiblePlans = plans.filter((p) => p.name !== "Plan Demo"); return c.json( { success: true, - data: SubscriptionApiAdapter.planToApiResponseList(plans), + data: SubscriptionApiAdapter.planToApiResponseList(visiblePlans), message: "Plans retrieved successfully", }, HttpStatusCodes.OK diff --git a/src/features/subscriptions/domain/entities/IPlan.ts b/src/features/subscriptions/domain/entities/IPlan.ts index ebcf872..89ac463 100644 --- a/src/features/subscriptions/domain/entities/IPlan.ts +++ b/src/features/subscriptions/domain/entities/IPlan.ts @@ -4,6 +4,8 @@ export interface IPlan { durationDays: number; price: number; frequency: string; + description: string | null; + features: string[] | null; createdAt: Date; updatedAt: Date; } diff --git a/src/features/subscriptions/infrastructure/adapters/plan.repository.ts b/src/features/subscriptions/infrastructure/adapters/plan.repository.ts index 859a015..8a39b15 100644 --- a/src/features/subscriptions/infrastructure/adapters/plan.repository.ts +++ b/src/features/subscriptions/infrastructure/adapters/plan.repository.ts @@ -49,6 +49,8 @@ export class PgPlanRepository implements IPlanRepository { durationDays: raw.duration_days, price: Number(raw.price), frequency: raw.frequency, + description: raw.description, + features: raw.features, createdAt: raw.created_at, updatedAt: raw.updated_at, }; diff --git a/src/features/subscriptions/infrastructure/adapters/subscription-api.adapter.ts b/src/features/subscriptions/infrastructure/adapters/subscription-api.adapter.ts index 36887c4..63ab6c4 100644 --- a/src/features/subscriptions/infrastructure/adapters/subscription-api.adapter.ts +++ b/src/features/subscriptions/infrastructure/adapters/subscription-api.adapter.ts @@ -11,7 +11,7 @@ export class SubscriptionApiAdapter { frequency: subscription.frequency, startDate: subscription.startDate, endDate: subscription.endDate, - retirementDate: subscription.retirementDate, + retirementDate: subscription.retirementDate || null, active: subscription.active, createdAt: subscription.createdAt, updatedAt: subscription.updatedAt, @@ -25,6 +25,8 @@ export class SubscriptionApiAdapter { durationDays: plan.durationDays, price: plan.price, frequency: plan.frequency, + description: plan.description, + features: plan.features, createdAt: plan.createdAt, updatedAt: plan.updatedAt, }; diff --git a/src/features/subscriptions/infrastructure/controllers/subscription.routes.ts b/src/features/subscriptions/infrastructure/controllers/subscription.routes.ts index 5b1c731..f63661f 100644 --- a/src/features/subscriptions/infrastructure/controllers/subscription.routes.ts +++ b/src/features/subscriptions/infrastructure/controllers/subscription.routes.ts @@ -12,6 +12,8 @@ const planSchema = z.object({ durationDays: z.number(), price: z.number(), frequency: z.string(), + description: z.string().nullable(), + features: z.array(z.string()).nullable(), createdAt: z.date(), updatedAt: z.date(), });