Develop a full-stack MVP application named 'ATM Assurance' using Next.js 14 App Router, Drizzle ORM, and PostgreSQL to help users securely record ATM transactions, store evidence, and manage financial disputes. The application must feature robust user authentication and a multi-page structure. The primary goal is to provide undeniable proof for ATM transactions in case of disputes.
Technology Stack:
- Frontend: Next.js 14 (App Router), React, TypeScript, Tailwind CSS for styling.
- Backend: Next.js 14 API Routes, Drizzle ORM.
- Database: PostgreSQL (using DrizzleKit for schema migrations and management).
- Authentication: Simple JWT-based authentication for MVP (or NextAuth.js if feasible within MVP scope).
- File Storage: Simulate cloud storage for evidence files by storing URLs in the database; actual file upload to a cloud service (e.g., AWS S3, Cloudinary) is beyond MVP but URLs should be ready for it.
Database Schema (Drizzle ORM - `db/schema.ts`):
```typescript
import { pgTable, serial, text, timestamp, numeric, boolean, integer } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').unique().notNull(),
passwordHash: text('password_hash').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export const transactions = pgTable('transactions', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
amount: numeric('amount', { precision: 10, scale: 2 }).notNull(),
type: text('type', { enum: ['withdrawal', 'deposit'] }).notNull(),
transactionDate: timestamp('transaction_date').notNull(),
atmDetails: text('atm_details'),
bankName: text('bank_name'),
notes: text('notes'),
isDisputed: boolean('is_disputed').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export const evidence = pgTable('evidence', {
id: serial('id').primaryKey(),
transactionId: integer('transaction_id').notNull().references(() => transactions.id, { onDelete: 'cascade' }),
fileUrl: text('file_url').notNull(),
fileType: text('file_type', { enum: ['image', 'video'] }).notNull(),
description: text('description'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const disputes = pgTable('disputes', {
id: serial('id').primaryKey(),
transactionId: integer('transaction_id').notNull().references(() => transactions.id, { onDelete: 'cascade' }),
status: text('status', { enum: ['pending', 'resolved', 'declined'] }).default('pending').notNull(),
caseId: text('case_id'),
submittedDate: timestamp('submitted_date').defaultNow().notNull(),
resolutionDate: timestamp('resolution_date'),
resolutionDetails: text('resolution_details'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').notNull(),
});
export const userRelations = relations(users, ({ many }) => ({
transactions: many(transactions),
}));
export const transactionRelations = relations(transactions, ({ one, many }) => ({
user: one(users, {
fields: [transactions.userId],
references: [users.id],
}),
evidence: many(evidence),
disputes: many(disputes),
}));
export const evidenceRelations = relations(evidence, ({ one }) => ({
transaction: one(transactions, {
fields: [evidence.transactionId],
references: [transactions.id],
}),
}));
export const disputeRelations = relations(disputes, ({ one }) => ({
transaction: one(transactions, {
fields: [disputes.transactionId],
references: [transactions.id],
}),
}));
```
Next.js App Router Structure:
```
/app
├── layout.tsx
├── page.tsx (Public Landing Page)
├── auth
│ ├── login
│ │ └── page.tsx (User Login Form)
│ └── register
│ └── page.tsx (User Registration Form)
├── dashboard
│ ├── page.tsx (Authenticated User Dashboard: Overview of transactions and dispute summary)
│ ├── transactions
│ │ ├── page.tsx (List all user transactions)
│ │ ├── new
│ │ │ └── page.tsx (Form to add a new transaction record)
│ │ └── [id]
│ │ ├── page.tsx (View detailed transaction, display/upload evidence)
│ │ └── edit
│ │ └── page.tsx (Form to edit an existing transaction)
│ ├── disputes
│ │ ├── page.tsx (List all user disputes)
│ │ └── [id]
│ │ └── page.tsx (View dispute details, potentially update resolution details)
│ └── reports
│ └── [transactionId]
│ └── page.tsx (Generate and display a printable report for a specific transaction with evidence)
├── api
│ ├── auth
│ │ ├── register
│ │ │ └── route.ts (POST: Create new user with hashed password)
│ │ └── login
│ │ └── route.ts (POST: Authenticate user, create and return JWT/session token)
│ ├── transactions
│ │ ├── route.ts (GET: Retrieve all transactions for authenticated user; POST: Create new transaction)
│ │ └── [id]
│ │ └── route.ts (GET: Retrieve single transaction; PUT: Update transaction; DELETE: Delete transaction)
│ ├── evidence
│ │ ├── route.ts (POST: Upload evidence file URL and metadata for a transaction)
│ │ └── [id]
│ │ └── route.ts (DELETE: Delete evidence record)
│ ├── disputes
│ │ ├── route.ts (GET: Retrieve all disputes for authenticated user; POST: Create new dispute)
│ │ └── [id]
│ │ └── route.ts (GET: Retrieve single dispute; PUT: Update dispute status/details)
│ └── users
│ └── [id]
│ └── route.ts (GET: Get user profile - basic for MVP)
├── lib
│ ├── db.ts (Drizzle ORM client initialization)
│ ├── auth.ts (Authentication utility functions, token handling)
│ └── utils.ts (General utility functions)
└── components
└── ... (reusable UI components like forms, cards, navigation)