DuOrigin v2 - React + NestJS + Prisma + EUDR API Integration
This commit is contained in:
192
prisma/README.md
Normal file
192
prisma/README.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# DuOrigin v2 - Prisma Database Layer
|
||||
|
||||
Este diretório contém o schema Prisma e scripts de seed para o DuOrigin v2.
|
||||
|
||||
## 📁 Estrutura
|
||||
|
||||
```
|
||||
prisma/
|
||||
├── schema.prisma # Schema do banco de dados
|
||||
├── seed.ts # Script de seed com dados demo
|
||||
└── README.md # Esta documentação
|
||||
```
|
||||
|
||||
## 🗄️ Modelos de Dados
|
||||
|
||||
| Modelo | Descrição | Tabela |
|
||||
|--------|-----------|--------|
|
||||
| `User` | Usuários do sistema (autenticação) | `users` |
|
||||
| `Company` | Empresas/operadores EUDR | `companies` |
|
||||
| `Producer` | Produtores rurais | `producers` |
|
||||
| `Area` | Propriedades/glebas com geolocalização | `areas` |
|
||||
| `Product` | Commodities EUDR (soja, café, etc.) | `products` |
|
||||
| `Lot` | Lotes de produto para rastreabilidade | `lots` |
|
||||
| `DdsStatement` | Declarações de Due Diligence | `dds_statements` |
|
||||
| `AuditLog` | Trilha de auditoria | `audit_logs` |
|
||||
|
||||
## 🔗 Relacionamentos
|
||||
|
||||
```
|
||||
Company
|
||||
└── Producer (1:N)
|
||||
├── Area (1:N)
|
||||
│ └── Lot (1:N)
|
||||
└── Lot (1:N)
|
||||
|
||||
Product
|
||||
└── Lot (1:N)
|
||||
|
||||
Company
|
||||
└── DdsStatement (1:N)
|
||||
└── lot_ids (JSON array)
|
||||
|
||||
User
|
||||
└── AuditLog (1:N)
|
||||
```
|
||||
|
||||
## 🚀 Setup
|
||||
|
||||
### 1. Configurar Variável de Ambiente
|
||||
|
||||
Crie ou edite `.env` na raiz do projeto:
|
||||
|
||||
```env
|
||||
# Via túnel SSH (recomendado para dev local)
|
||||
DATABASE_URL="postgresql://duorigin:DuOrigin2026!@localhost:5433/duorigin"
|
||||
|
||||
# Conexão direta (se PostgreSQL aceitar conexões externas)
|
||||
# DATABASE_URL="postgresql://duorigin:DuOrigin2026!@198.199.84.130:5432/duorigin"
|
||||
```
|
||||
|
||||
### 1.1 Criar Túnel SSH (se necessário)
|
||||
|
||||
Se o PostgreSQL não aceita conexões externas, crie um túnel SSH:
|
||||
|
||||
```bash
|
||||
# Criar túnel (porta local 5433 -> servidor 5432)
|
||||
ssh -i ~/.ssh/digitalocean_jarvis -L 5433:localhost:5432 -fN root@198.199.84.130
|
||||
|
||||
# Verificar se túnel está ativo
|
||||
ss -tlnp | grep 5433
|
||||
|
||||
# Matar túnel quando terminar
|
||||
pkill -f "ssh.*5433.*5432"
|
||||
```
|
||||
|
||||
### 2. Gerar Prisma Client
|
||||
|
||||
```bash
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
### 3. Aplicar Schema (se banco novo)
|
||||
|
||||
```bash
|
||||
# Criar migration inicial
|
||||
npx prisma migrate dev --name init
|
||||
|
||||
# Ou sincronizar sem migration (dev)
|
||||
npx prisma db push
|
||||
```
|
||||
|
||||
### 4. Popular com Dados Demo
|
||||
|
||||
```bash
|
||||
npx prisma db seed
|
||||
```
|
||||
|
||||
## 📊 Dados de Demonstração
|
||||
|
||||
O seed cria os seguintes dados:
|
||||
|
||||
### Usuários
|
||||
| Email | Senha | Role |
|
||||
|-------|-------|------|
|
||||
| demo@duorigin.com | DuoDemo2026 | admin |
|
||||
| operador@duorigin.com | DuoDemo2026 | operator |
|
||||
|
||||
### Empresas (3)
|
||||
- AgroCerrado Exportações Ltda (GO)
|
||||
- Fazendas Unidas do Brasil S.A. (MT)
|
||||
- Cooperativa Agrícola Planalto Central (MG)
|
||||
|
||||
### Produtores (5)
|
||||
Produtores rurais vinculados às empresas, com CAR válido.
|
||||
|
||||
### Áreas (5)
|
||||
Propriedades rurais no Cerrado com coordenadas reais:
|
||||
- 3 áreas com risco baixo (sem desmatamento)
|
||||
- 1 área com risco médio (alerta)
|
||||
- 1 área com risco alto (desmatamento confirmado)
|
||||
|
||||
### Lotes (10)
|
||||
Lotes de soja e café em diferentes status:
|
||||
- 4 aprovados
|
||||
- 2 em revisão
|
||||
- 2 rejeitados
|
||||
- 2 pendentes
|
||||
|
||||
### Declarações DDS (5)
|
||||
Due Diligence Statements em vários status:
|
||||
- 2 aprovadas (com EU reference)
|
||||
- 1 submetida (aguardando)
|
||||
- 1 rejeitada
|
||||
- 1 rascunho
|
||||
|
||||
## 🛠️ Comandos Úteis
|
||||
|
||||
```bash
|
||||
# Abrir Prisma Studio (GUI)
|
||||
npx prisma studio
|
||||
|
||||
# Resetar banco e re-seed
|
||||
npx prisma migrate reset
|
||||
|
||||
# Verificar schema
|
||||
npx prisma validate
|
||||
|
||||
# Formatar schema
|
||||
npx prisma format
|
||||
|
||||
# Gerar diagrama ERD
|
||||
npx prisma-erd-generator
|
||||
```
|
||||
|
||||
## 📦 Uso no Código
|
||||
|
||||
```typescript
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
// Buscar todos os lotes com risco alto
|
||||
const highRiskLots = await prisma.lot.findMany({
|
||||
where: { risk_score: { gte: 70 } },
|
||||
include: {
|
||||
area: true,
|
||||
producer: true,
|
||||
product: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Buscar empresa com produtores e áreas
|
||||
const company = await prisma.company.findUnique({
|
||||
where: { id: 1 },
|
||||
include: {
|
||||
producers: {
|
||||
include: { areas: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 🔒 Segurança
|
||||
|
||||
- Nunca commitar `.env` com credenciais reais
|
||||
- Em produção, usar SSL: `?sslmode=require`
|
||||
- Sempre usar prepared statements (Prisma faz isso automaticamente)
|
||||
|
||||
## 📝 Notas
|
||||
|
||||
- O campo `lot_ids` em `DdsStatement` é um JSON array de IDs
|
||||
- O campo `geojson` em `Area` armazena polígonos GeoJSON
|
||||
- Timestamps são `DateTime?` para compatibilidade com banco existente
|
||||
- Passwords são hasheados com bcrypt (salt rounds: 10)
|
||||
195
prisma/schema.prisma
Normal file
195
prisma/schema.prisma
Normal file
@@ -0,0 +1,195 @@
|
||||
// DuOrigin v2 - Prisma Schema
|
||||
// EUDR Compliance Platform for Brazilian Agribusiness
|
||||
// Generated from production PostgreSQL database (jarvis-do)
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Users & Authentication
|
||||
// ============================================
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
hashed_password String
|
||||
full_name String?
|
||||
role String? @default("operator") // admin, operator, viewer
|
||||
is_active Boolean? @default(true)
|
||||
created_at DateTime? @default(now())
|
||||
|
||||
// Relations
|
||||
auditLogs AuditLog[]
|
||||
|
||||
@@index([id])
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Companies (Empresas)
|
||||
// ============================================
|
||||
|
||||
model Company {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
cnpj String? @unique
|
||||
country String? @default("BR")
|
||||
state String?
|
||||
city String?
|
||||
eu_operator_id String? // EU Operator ID for EUDR
|
||||
created_at DateTime? @default(now())
|
||||
|
||||
// Relations
|
||||
producers Producer[]
|
||||
ddsStatements DdsStatement[]
|
||||
|
||||
@@index([id])
|
||||
@@index([cnpj])
|
||||
@@map("companies")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Producers (Produtores Rurais)
|
||||
// ============================================
|
||||
|
||||
model Producer {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
cpf_cnpj String? @unique
|
||||
company_id Int?
|
||||
state String?
|
||||
city String?
|
||||
car_code String? // CAR - Cadastro Ambiental Rural
|
||||
created_at DateTime? @default(now())
|
||||
|
||||
// Relations
|
||||
company Company? @relation(fields: [company_id], references: [id])
|
||||
areas Area[]
|
||||
lots Lot[]
|
||||
|
||||
@@index([id])
|
||||
@@index([cpf_cnpj])
|
||||
@@map("producers")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Areas (Propriedades Rurais / Glebas)
|
||||
// ============================================
|
||||
|
||||
model Area {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
producer_id Int?
|
||||
geojson String? @db.Text // GeoJSON polygon
|
||||
area_ha Float? // Area in hectares
|
||||
biome String? // Cerrado, Amazônia, Mata Atlântica, etc.
|
||||
risk_level String? // low, medium, high, critical
|
||||
deforestation_flag String? // none, alert, confirmed
|
||||
lat_center Float? // Latitude center point
|
||||
lon_center Float? // Longitude center point
|
||||
created_at DateTime? @default(now())
|
||||
|
||||
// Relations
|
||||
producer Producer? @relation(fields: [producer_id], references: [id])
|
||||
lots Lot[]
|
||||
|
||||
@@index([id])
|
||||
@@map("areas")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Products (Commodities EUDR)
|
||||
// ============================================
|
||||
|
||||
model Product {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
hs_code String? // Harmonized System code
|
||||
eudr_category String? // cattle, cocoa, coffee, oil palm, rubber, soya, wood
|
||||
description String?
|
||||
created_at DateTime? @default(now())
|
||||
|
||||
// Relations
|
||||
lots Lot[]
|
||||
|
||||
@@index([id])
|
||||
@@map("products")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Lots (Lotes de Produto)
|
||||
// ============================================
|
||||
|
||||
model Lot {
|
||||
id Int @id @default(autoincrement())
|
||||
reference String @unique
|
||||
product_id Int?
|
||||
area_id Int?
|
||||
producer_id Int?
|
||||
quantity_kg Float?
|
||||
harvest_date String?
|
||||
risk_score Float? // 0-100 risk score
|
||||
status String? @default("pending") // pending, approved, rejected, review
|
||||
created_at DateTime? @default(now())
|
||||
|
||||
// Relations
|
||||
product Product? @relation(fields: [product_id], references: [id])
|
||||
area Area? @relation(fields: [area_id], references: [id])
|
||||
producer Producer? @relation(fields: [producer_id], references: [id])
|
||||
|
||||
@@index([id])
|
||||
@@index([reference])
|
||||
@@map("lots")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DDS Statements (Due Diligence Statements)
|
||||
// ============================================
|
||||
|
||||
model DdsStatement {
|
||||
id Int @id @default(autoincrement())
|
||||
reference_number String @unique
|
||||
company_id Int?
|
||||
status String? @default("draft") // draft, submitted, approved, rejected
|
||||
risk_assessment String? // low, medium, high
|
||||
lot_ids String? @db.Text // JSON array of lot IDs
|
||||
submission_date String?
|
||||
eu_reference String? // EU system reference after submission
|
||||
notes String? @db.Text
|
||||
created_at DateTime? @default(now())
|
||||
updated_at DateTime? @updatedAt
|
||||
|
||||
// Relations
|
||||
company Company? @relation(fields: [company_id], references: [id])
|
||||
|
||||
@@index([id])
|
||||
@@index([reference_number])
|
||||
@@map("dds_statements")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Audit Logs (Trilha de Auditoria)
|
||||
// ============================================
|
||||
|
||||
model AuditLog {
|
||||
id Int @id @default(autoincrement())
|
||||
entity_type String // User, Company, Producer, Area, Lot, DdsStatement
|
||||
entity_id Int?
|
||||
action String // create, update, delete, login, export
|
||||
user_id Int?
|
||||
details String? @db.Text // JSON with additional details
|
||||
ip_address String?
|
||||
created_at DateTime? @default(now())
|
||||
|
||||
// Relations
|
||||
user User? @relation(fields: [user_id], references: [id])
|
||||
|
||||
@@index([id])
|
||||
@@map("audit_logs")
|
||||
}
|
||||
104
prisma/seed.ts
Normal file
104
prisma/seed.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
async function main() {
|
||||
console.log('🌱 Seeding database...')
|
||||
|
||||
// Create admin user
|
||||
const adminPassword = await bcrypt.hash('admin123', 10)
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { email: 'admin@duorigin.com' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'admin@duorigin.com',
|
||||
hashedPassword: adminPassword,
|
||||
fullName: 'Administrador',
|
||||
role: 'admin',
|
||||
isActive: true,
|
||||
},
|
||||
})
|
||||
console.log('✅ Admin user created:', admin.email)
|
||||
|
||||
// Create operator user
|
||||
const operatorPassword = await bcrypt.hash('operator123', 10)
|
||||
const operator = await prisma.user.upsert({
|
||||
where: { email: 'operador@duorigin.com' },
|
||||
update: {},
|
||||
create: {
|
||||
email: 'operador@duorigin.com',
|
||||
hashedPassword: operatorPassword,
|
||||
fullName: 'Operador Teste',
|
||||
role: 'operator',
|
||||
isActive: true,
|
||||
},
|
||||
})
|
||||
console.log('✅ Operator user created:', operator.email)
|
||||
|
||||
// Create sample company
|
||||
const company = await prisma.company.upsert({
|
||||
where: { cnpj: '12345678000190' },
|
||||
update: {},
|
||||
create: {
|
||||
name: 'Fazenda Modelo LTDA',
|
||||
cnpj: '12345678000190',
|
||||
country: 'BR',
|
||||
state: 'MT',
|
||||
city: 'Cuiabá',
|
||||
euOperatorId: 'EU-OP-BR-001',
|
||||
},
|
||||
})
|
||||
console.log('✅ Company created:', company.name)
|
||||
|
||||
// Create sample propriedade
|
||||
const propriedade = await prisma.propriedade.upsert({
|
||||
where: { cpfCnpj: '11122233344' },
|
||||
update: {},
|
||||
create: {
|
||||
name: 'Fazenda Santa Clara',
|
||||
cpfCnpj: '11122233344',
|
||||
companyId: company.id,
|
||||
state: 'MT',
|
||||
city: 'Sorriso',
|
||||
carCode: 'MT-5107008-001',
|
||||
},
|
||||
})
|
||||
console.log('✅ Propriedade created:', propriedade.name)
|
||||
|
||||
// Create sample avaliacao
|
||||
const avaliacao = await prisma.avaliacao.create({
|
||||
data: {
|
||||
name: 'Área de Soja - Talhão 1',
|
||||
propriedadeId: propriedade.id,
|
||||
areaHa: 150.5,
|
||||
biome: 'Cerrado',
|
||||
riskLevel: 'low',
|
||||
deforestationFlag: 'clean',
|
||||
latCenter: -12.5489,
|
||||
lonCenter: -55.7189,
|
||||
geojson: JSON.stringify({
|
||||
type: 'Polygon',
|
||||
coordinates: [[
|
||||
[-55.72, -12.54],
|
||||
[-55.71, -12.54],
|
||||
[-55.71, -12.55],
|
||||
[-55.72, -12.55],
|
||||
[-55.72, -12.54],
|
||||
]],
|
||||
}),
|
||||
},
|
||||
})
|
||||
console.log('✅ Avaliacao created:', avaliacao.name)
|
||||
|
||||
console.log('🎉 Seed completed!')
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('❌ Seed error:', e)
|
||||
process.exit(1)
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
Reference in New Issue
Block a user