feat(db): implement PostgreSQL database integration with Prisma ORM

Agent 2 - Database Integration (P0 Critical):

- Add Prisma ORM with PostgreSQL for persistent data storage
- Create comprehensive database schema with 20+ models:
  - User & authentication models
  - Plant & lineage tracking
  - Transport events & supply chain
  - Vertical farming (farms, zones, batches, recipes)
  - Demand & market matching
  - Audit logging & blockchain storage

- Implement complete database service layer (lib/db/):
  - users.ts: User CRUD with search and stats
  - plants.ts: Plant operations with lineage tracking
  - transport.ts: Transport events and carbon tracking
  - farms.ts: Vertical farm and crop batch management
  - demand.ts: Consumer preferences and market matching
  - audit.ts: Audit logging and blockchain integrity

- Add PlantChainDB for database-backed blockchain
- Create development seed script with sample data
- Add database documentation (docs/DATABASE.md)
- Update package.json with Prisma dependencies and scripts

This provides the foundation for all other agents to build upon
with persistent, scalable data storage.
This commit is contained in:
Claude 2025-11-23 03:56:40 +00:00
parent 705105d9b6
commit 3d2ccdc29a
No known key found for this signature in database
16 changed files with 5529 additions and 1 deletions

View file

@ -1,5 +1,17 @@
# LocalGreenChain Environment Variables
# ===========================================
# DATABASE CONFIGURATION (Required for Agent 2)
# ===========================================
# PostgreSQL connection string
# Format: postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA
DATABASE_URL="postgresql://postgres:password@localhost:5432/localgreenchain?schema=public"
# ===========================================
# EXTERNAL SERVICES
# ===========================================
# Plants.net API (optional)
PLANTS_NET_API_KEY=your_api_key_here

363
docs/DATABASE.md Normal file
View file

@ -0,0 +1,363 @@
# LocalGreenChain Database Integration
This document describes the PostgreSQL database integration for LocalGreenChain, implemented as part of Agent 2's deployment.
## Overview
The database layer provides persistent storage for all LocalGreenChain entities using PostgreSQL and Prisma ORM. This replaces the previous file-based JSON storage with a robust, scalable database solution.
## Quick Start
### 1. Install Dependencies
```bash
bun install
```
### 2. Configure Database
Copy the environment template and configure your database connection:
```bash
cp .env.example .env
```
Edit `.env` and set your PostgreSQL connection string:
```env
DATABASE_URL="postgresql://user:password@localhost:5432/localgreenchain?schema=public"
```
### 3. Generate Prisma Client
```bash
bun run db:generate
```
### 4. Run Migrations
For development:
```bash
bun run db:migrate
```
For production:
```bash
bun run db:migrate:prod
```
### 5. Seed Database (Optional)
```bash
bun run db:seed
```
## Database Schema
The schema includes the following main entities:
### Core Entities
| Entity | Description |
|--------|-------------|
| `User` | User accounts with authentication and profiles |
| `Plant` | Plant records with lineage tracking |
| `TransportEvent` | Supply chain transport events |
| `SeedBatch` | Seed batch tracking |
| `HarvestBatch` | Harvest batch records |
### Vertical Farming
| Entity | Description |
|--------|-------------|
| `VerticalFarm` | Vertical farm facilities |
| `GrowingZone` | Individual growing zones |
| `CropBatch` | Active crop batches |
| `GrowingRecipe` | Growing recipes/protocols |
| `ResourceUsage` | Energy and resource tracking |
| `FarmAnalytics` | Farm performance analytics |
### Demand & Market
| Entity | Description |
|--------|-------------|
| `ConsumerPreference` | Consumer food preferences |
| `DemandSignal` | Aggregated demand signals |
| `SupplyCommitment` | Grower supply commitments |
| `MarketMatch` | Matched supply and demand |
| `SeasonalPlan` | Grower seasonal plans |
| `DemandForecast` | Demand predictions |
| `PlantingRecommendation` | Planting recommendations |
### Audit & Blockchain
| Entity | Description |
|--------|-------------|
| `AuditLog` | System audit trail |
| `BlockchainBlock` | Blockchain block storage |
## Usage
### Importing the Database Layer
```typescript
import * as db from '@/lib/db';
// or import specific functions
import { createPlant, getPlantById, getPlantLineage } from '@/lib/db';
```
### Common Operations
#### Users
```typescript
// Create a user
const user = await db.createUser({
email: 'grower@example.com',
name: 'John Farmer',
userType: 'GROWER',
city: 'San Francisco',
country: 'USA',
});
// Get user by email
const user = await db.getUserByEmail('grower@example.com');
// Get user with their plants
const userWithPlants = await db.getUserWithPlants(userId);
```
#### Plants
```typescript
// Create a plant
const plant = await db.createPlant({
commonName: 'Cherry Tomato',
scientificName: 'Solanum lycopersicum',
plantedDate: new Date(),
latitude: 37.7749,
longitude: -122.4194,
ownerId: userId,
});
// Clone a plant
const clone = await db.clonePlant(parentId, newOwnerId, 'CLONE');
// Get plant lineage
const lineage = await db.getPlantLineage(plantId);
// Returns: { plant, ancestors, descendants, siblings }
// Find nearby plants
const nearby = await db.getNearbyPlants({
latitude: 37.7749,
longitude: -122.4194,
radiusKm: 10,
});
```
#### Transport Events
```typescript
// Create a transport event
const event = await db.createTransportEvent({
eventType: 'DISTRIBUTION',
fromLatitude: 37.8044,
fromLongitude: -122.2712,
fromLocationType: 'VERTICAL_FARM',
toLatitude: 37.7849,
toLongitude: -122.4094,
toLocationType: 'MARKET',
durationMinutes: 25,
transportMethod: 'ELECTRIC_TRUCK',
senderId: farmerId,
receiverId: distributorId,
});
// Get plant journey
const journey = await db.getPlantJourney(plantId);
// Get environmental impact
const impact = await db.getEnvironmentalImpact({ userId });
```
#### Vertical Farms
```typescript
// Create a farm
const farm = await db.createVerticalFarm({
name: 'Urban Greens',
ownerId: userId,
latitude: 37.8044,
longitude: -122.2712,
address: '123 Farm St',
city: 'Oakland',
country: 'USA',
specs: { totalAreaSqm: 500, numberOfLevels: 5 },
});
// Create a growing zone
const zone = await db.createGrowingZone({
name: 'Zone A',
farmId: farm.id,
level: 1,
areaSqm: 80,
growingMethod: 'NFT',
plantPositions: 400,
});
// Create a crop batch
const batch = await db.createCropBatch({
farmId: farm.id,
zoneId: zone.id,
cropType: 'Lettuce',
plantCount: 400,
plantingDate: new Date(),
expectedHarvestDate: new Date(Date.now() + 28 * 24 * 60 * 60 * 1000),
expectedYieldKg: 60,
});
```
#### Demand & Market
```typescript
// Set consumer preferences
await db.upsertConsumerPreference({
consumerId: userId,
latitude: 37.7849,
longitude: -122.4094,
preferredCategories: ['leafy_greens', 'herbs'],
certificationPreferences: ['organic', 'local'],
});
// Create supply commitment
const commitment = await db.createSupplyCommitment({
growerId: farmerId,
produceType: 'Butterhead Lettuce',
committedQuantityKg: 60,
availableFrom: new Date(),
availableUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
pricePerKg: 8.5,
deliveryRadiusKm: 25,
deliveryMethods: ['grower_delivery', 'customer_pickup'],
});
// Find matching supply
const matches = await db.findMatchingSupply(
'Lettuce',
{ latitude: 37.7849, longitude: -122.4094, radiusKm: 20 },
new Date()
);
```
### Using the Database-Backed Blockchain
The database layer includes a `PlantChainDB` class that provides blockchain functionality with PostgreSQL persistence:
```typescript
import { getBlockchainDB } from '@/lib/blockchain/manager';
// Get the database-backed blockchain
const chain = await getBlockchainDB();
// Register a plant (creates both DB record and blockchain block)
const { plant, block } = await chain.registerPlant({
commonName: 'Tomato',
latitude: 37.7749,
longitude: -122.4194,
ownerId: userId,
});
// Clone a plant
const { plant: clone, block: cloneBlock } = await chain.clonePlant(
parentId,
newOwnerId,
'CLONE'
);
// Verify blockchain integrity
const isValid = await chain.isChainValid();
```
## NPM Scripts
| Script | Description |
|--------|-------------|
| `bun run db:generate` | Generate Prisma client |
| `bun run db:push` | Push schema to database (dev) |
| `bun run db:migrate` | Create and run migration (dev) |
| `bun run db:migrate:prod` | Run migrations (production) |
| `bun run db:seed` | Seed database with test data |
| `bun run db:studio` | Open Prisma Studio GUI |
## Architecture
```
lib/db/
├── prisma.ts # Prisma client singleton
├── types.ts # Type definitions and utilities
├── users.ts # User CRUD operations
├── plants.ts # Plant operations with lineage
├── transport.ts # Transport event operations
├── farms.ts # Vertical farm operations
├── demand.ts # Demand and market operations
├── audit.ts # Audit logging and blockchain
└── index.ts # Central exports
prisma/
├── schema.prisma # Database schema
└── seed.ts # Seed script
```
## Migration from File Storage
If you have existing data in JSON files, you can migrate to the database:
1. Ensure database is configured and migrations are run
2. Load existing JSON data
3. Use the database service layer to insert records
4. Verify data integrity
5. Remove old JSON files
## Performance Considerations
- The schema includes strategic indexes on frequently queried fields
- Pagination is supported for large result sets
- Location-based queries use in-memory filtering (consider PostGIS for large scale)
- Blockchain integrity verification scans all blocks (cache results for performance)
## Troubleshooting
### Connection Issues
```bash
# Test database connection
bunx prisma db pull
```
### Migration Issues
```bash
# Reset database (WARNING: deletes all data)
bunx prisma migrate reset
# Generate new migration
bunx prisma migrate dev --name your_migration_name
```
### Type Issues
```bash
# Regenerate Prisma client
bun run db:generate
```
## Security Notes
- Never commit `.env` files with real credentials
- Use environment variables for all sensitive configuration
- Database user should have minimal required permissions
- Enable SSL for production database connections
---
*Implemented by Agent 2 - Database Integration*

View file

@ -0,0 +1,419 @@
/**
* PlantChainDB - Database-backed blockchain for plant lineage tracking
* This implementation uses PostgreSQL via Prisma for persistence while
* maintaining blockchain data integrity guarantees
*/
import { PlantBlock } from './PlantBlock';
import type { PlantData, PlantLineage, NearbyPlant, PlantNetwork } from './types';
import * as db from '../db';
import type { Plant, BlockchainBlock } from '../db/types';
/**
* Convert database Plant model to PlantData format
*/
function plantToPlantData(plant: Plant & { owner?: { id: string; name: string; email: string } }): PlantData {
return {
id: plant.id,
commonName: plant.commonName,
scientificName: plant.scientificName || undefined,
species: plant.species || undefined,
genus: plant.genus || undefined,
family: plant.family || undefined,
parentPlantId: plant.parentPlantId || undefined,
propagationType: plant.propagationType.toLowerCase() as PlantData['propagationType'],
generation: plant.generation,
plantedDate: plant.plantedDate.toISOString(),
harvestedDate: plant.harvestedDate?.toISOString(),
status: plant.status.toLowerCase() as PlantData['status'],
location: {
latitude: plant.latitude,
longitude: plant.longitude,
address: plant.address || undefined,
city: plant.city || undefined,
country: plant.country || undefined,
},
owner: plant.owner ? {
id: plant.owner.id,
name: plant.owner.name,
email: plant.owner.email,
} : {
id: plant.ownerId,
name: 'Unknown',
email: 'unknown@localgreenchain.io',
},
childPlants: [], // Will be populated separately if needed
environment: plant.environment as PlantData['environment'],
growthMetrics: plant.growthMetrics as PlantData['growthMetrics'],
notes: plant.notes || undefined,
images: plant.images || undefined,
plantsNetId: plant.plantsNetId || undefined,
registeredAt: plant.registeredAt.toISOString(),
updatedAt: plant.updatedAt.toISOString(),
};
}
/**
* PlantChainDB - Database-backed plant blockchain
*/
export class PlantChainDB {
public difficulty: number;
constructor(difficulty: number = 4) {
this.difficulty = difficulty;
}
/**
* Initialize the chain with genesis block if needed
*/
async initialize(): Promise<void> {
const latestBlock = await db.getLatestBlockchainBlock();
if (!latestBlock) {
await this.createGenesisBlock();
}
}
/**
* Create the genesis block
*/
private async createGenesisBlock(): Promise<BlockchainBlock> {
const genesisPlant: PlantData = {
id: 'genesis-plant-0',
commonName: 'Genesis Plant',
scientificName: 'Blockchain primordialis',
propagationType: 'original',
generation: 0,
plantedDate: new Date().toISOString(),
status: 'mature',
location: {
latitude: 0,
longitude: 0,
address: 'The Beginning',
},
owner: {
id: 'system',
name: 'LocalGreenChain',
email: 'system@localgreenchain.org',
},
childPlants: [],
registeredAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const block = new PlantBlock(0, new Date().toISOString(), genesisPlant, '0');
return db.createBlockchainBlock({
index: 0,
timestamp: new Date(),
previousHash: '0',
hash: block.hash,
nonce: 0,
blockType: 'genesis',
content: { plant: genesisPlant },
});
}
/**
* Get the latest block
*/
async getLatestBlock(): Promise<BlockchainBlock | null> {
return db.getLatestBlockchainBlock();
}
/**
* Register a new plant and add to blockchain
*/
async registerPlant(plantData: {
id?: string;
commonName: string;
scientificName?: string;
species?: string;
genus?: string;
family?: string;
plantedDate?: Date;
status?: 'SPROUTED' | 'GROWING' | 'MATURE' | 'FLOWERING' | 'FRUITING' | 'DORMANT' | 'DECEASED';
latitude: number;
longitude: number;
address?: string;
city?: string;
country?: string;
ownerId: string;
notes?: string;
images?: string[];
plantsNetId?: string;
}): Promise<{ plant: Plant; block: BlockchainBlock }> {
// Create plant in database
const plant = await db.createPlant({
...plantData,
plantedDate: plantData.plantedDate || new Date(),
propagationType: 'ORIGINAL',
generation: 0,
});
// Get latest block for previous hash
const latestBlock = await this.getLatestBlock();
const previousHash = latestBlock?.hash || '0';
const nextIndex = (latestBlock?.index ?? -1) + 1;
// Create PlantBlock for mining
const plantDataForBlock = await this.getPlantData(plant.id);
const newBlock = new PlantBlock(
nextIndex,
new Date().toISOString(),
plantDataForBlock!,
previousHash
);
newBlock.mineBlock(this.difficulty);
// Store block in database
const blockchainBlock = await db.createBlockchainBlock({
index: nextIndex,
timestamp: new Date(),
previousHash,
hash: newBlock.hash,
nonce: newBlock.nonce,
blockType: 'plant',
plantId: plant.id,
content: { plant: plantDataForBlock, action: 'REGISTER' },
});
// Update plant with blockchain reference
await db.updatePlantBlockchain(plant.id, nextIndex, newBlock.hash);
return { plant, block: blockchainBlock };
}
/**
* Clone a plant and add to blockchain
*/
async clonePlant(
parentPlantId: string,
ownerId: string,
propagationType: 'SEED' | 'CLONE' | 'CUTTING' | 'DIVISION' | 'GRAFTING',
overrides?: {
latitude?: number;
longitude?: number;
address?: string;
city?: string;
country?: string;
notes?: string;
}
): Promise<{ plant: Plant; block: BlockchainBlock }> {
// Create cloned plant in database
const plant = await db.clonePlant(
parentPlantId,
ownerId,
propagationType,
overrides
);
// Get latest block for previous hash
const latestBlock = await this.getLatestBlock();
const previousHash = latestBlock?.hash || '0';
const nextIndex = (latestBlock?.index ?? -1) + 1;
// Create PlantBlock for mining
const plantDataForBlock = await this.getPlantData(plant.id);
const newBlock = new PlantBlock(
nextIndex,
new Date().toISOString(),
plantDataForBlock!,
previousHash
);
newBlock.mineBlock(this.difficulty);
// Store block in database
const blockchainBlock = await db.createBlockchainBlock({
index: nextIndex,
timestamp: new Date(),
previousHash,
hash: newBlock.hash,
nonce: newBlock.nonce,
blockType: 'plant',
plantId: plant.id,
content: {
plant: plantDataForBlock,
action: 'CLONE',
parentPlantId,
},
});
// Update plant with blockchain reference
await db.updatePlantBlockchain(plant.id, nextIndex, newBlock.hash);
return { plant, block: blockchainBlock };
}
/**
* Update plant status and add to blockchain
*/
async updatePlantStatus(
plantId: string,
status: 'SPROUTED' | 'GROWING' | 'MATURE' | 'FLOWERING' | 'FRUITING' | 'DORMANT' | 'DECEASED',
harvestedDate?: Date
): Promise<{ plant: Plant; block: BlockchainBlock }> {
// Update plant in database
const plant = await db.updatePlantStatus(plantId, status, harvestedDate);
// Get latest block for previous hash
const latestBlock = await this.getLatestBlock();
const previousHash = latestBlock?.hash || '0';
const nextIndex = (latestBlock?.index ?? -1) + 1;
// Create PlantBlock for mining
const plantDataForBlock = await this.getPlantData(plant.id);
const newBlock = new PlantBlock(
nextIndex,
new Date().toISOString(),
plantDataForBlock!,
previousHash
);
newBlock.mineBlock(this.difficulty);
// Store block in database
const blockchainBlock = await db.createBlockchainBlock({
index: nextIndex,
timestamp: new Date(),
previousHash,
hash: newBlock.hash,
nonce: newBlock.nonce,
blockType: 'plant',
plantId: plant.id,
content: {
plant: plantDataForBlock,
action: 'UPDATE_STATUS',
newStatus: status,
},
});
// Update plant with blockchain reference
await db.updatePlantBlockchain(plant.id, nextIndex, newBlock.hash);
return { plant, block: blockchainBlock };
}
/**
* Get plant data in PlantData format
*/
async getPlantData(plantId: string): Promise<PlantData | null> {
const plant = await db.getPlantWithOwner(plantId);
if (!plant) return null;
const plantData = plantToPlantData(plant as Plant & { owner: { id: string; name: string; email: string } });
// Get child plant IDs
const lineage = await db.getPlantLineage(plantId);
plantData.childPlants = lineage.descendants.map(d => d.id);
return plantData;
}
/**
* Get a plant by ID
*/
async getPlant(plantId: string): Promise<Plant | null> {
return db.getPlantById(plantId);
}
/**
* Get plant with owner
*/
async getPlantWithOwner(plantId: string) {
return db.getPlantWithOwner(plantId);
}
/**
* Get complete lineage for a plant
*/
async getPlantLineage(plantId: string): Promise<PlantLineage | null> {
const result = await db.getPlantLineage(plantId);
if (!result.plant) return null;
const plant = result.plant as Plant & { owner: { id: string; name: string; email: string } };
return {
plantId,
ancestors: result.ancestors.map(p => plantToPlantData(p as Plant & { owner: { id: string; name: string; email: string } })),
descendants: result.descendants.map(p => plantToPlantData(p as Plant & { owner: { id: string; name: string; email: string } })),
siblings: result.siblings.map(p => plantToPlantData(p as Plant & { owner: { id: string; name: string; email: string } })),
generation: plant.generation,
};
}
/**
* Find plants near a location
*/
async findNearbyPlants(
latitude: number,
longitude: number,
radiusKm: number = 50,
excludeOwnerId?: string
): Promise<NearbyPlant[]> {
const plants = await db.getNearbyPlants(
{ latitude, longitude, radiusKm },
excludeOwnerId
);
return plants.map(p => ({
plant: plantToPlantData(p as Plant & { owner: { id: string; name: string; email: string } }),
distance: p.distance,
owner: {
id: p.ownerId,
name: 'Unknown',
email: 'unknown@localgreenchain.io',
},
}));
}
/**
* Get network statistics
*/
async getNetworkStats(): Promise<PlantNetwork> {
return db.getPlantNetworkStats();
}
/**
* Validate the blockchain integrity
*/
async isChainValid(): Promise<boolean> {
const result = await db.verifyBlockchainIntegrity();
return result.isValid;
}
/**
* Get blockchain statistics
*/
async getBlockchainStats() {
return db.getBlockchainStats();
}
/**
* Search plants
*/
async searchPlants(query: string, options?: { page?: number; limit?: number }) {
return db.searchPlants(query, options);
}
/**
* Get plants by owner
*/
async getPlantsByOwner(ownerId: string) {
return db.getPlantsByOwner(ownerId);
}
}
// Singleton instance
let plantChainInstance: PlantChainDB | null = null;
/**
* Get the singleton PlantChainDB instance
*/
export async function getPlantChain(): Promise<PlantChainDB> {
if (!plantChainInstance) {
plantChainInstance = new PlantChainDB();
await plantChainInstance.initialize();
}
return plantChainInstance;
}
export default PlantChainDB;

View file

@ -1,14 +1,22 @@
/**
* Blockchain Manager
* Singleton to manage the global plant blockchain instance
*
* Supports two modes:
* 1. File-based (legacy): Uses JSON file storage
* 2. Database-backed: Uses PostgreSQL via Prisma (recommended)
*/
import { PlantChain } from './PlantChain';
import { PlantChainDB, getPlantChain as getDBPlantChain } from './PlantChainDB';
import fs from 'fs';
import path from 'path';
const BLOCKCHAIN_FILE = path.join(process.cwd(), 'data', 'plantchain.json');
// Flag to determine storage mode
const USE_DATABASE = process.env.DATABASE_URL ? true : false;
class BlockchainManager {
private static instance: BlockchainManager;
private plantChain: PlantChain;
@ -104,3 +112,21 @@ export function getBlockchain(): PlantChain {
export function saveBlockchain(): void {
BlockchainManager.getInstance().saveBlockchain();
}
/**
* Get the database-backed blockchain instance
* Use this for production applications with PostgreSQL
*/
export async function getBlockchainDB(): Promise<PlantChainDB> {
return getDBPlantChain();
}
/**
* Check if using database storage
*/
export function isUsingDatabase(): boolean {
return USE_DATABASE;
}
// Export the DB class for type usage
export { PlantChainDB };

296
lib/db/audit.ts Normal file
View file

@ -0,0 +1,296 @@
/**
* Audit & Blockchain Database Service
* Operations for audit logging and blockchain block storage
*/
import prisma from './prisma';
import type { AuditLog, BlockchainBlock, Prisma } from '@prisma/client';
import type { PaginationOptions, PaginatedResult, DateRangeFilter } from './types';
import { createPaginatedResult } from './types';
// ============================================
// AUDIT LOG OPERATIONS
// ============================================
// Create an audit log entry
export async function createAuditLog(data: {
userId?: string;
action: string;
entityType: string;
entityId?: string;
previousValue?: Record<string, unknown>;
newValue?: Record<string, unknown>;
metadata?: Record<string, unknown>;
ipAddress?: string;
userAgent?: string;
}): Promise<AuditLog> {
return prisma.auditLog.create({ data });
}
// Get audit logs with pagination
export async function getAuditLogs(
options: PaginationOptions = {},
filters?: {
userId?: string;
action?: string;
entityType?: string;
entityId?: string;
dateRange?: DateRangeFilter;
}
): Promise<PaginatedResult<AuditLog>> {
const page = options.page || 1;
const limit = options.limit || 50;
const skip = (page - 1) * limit;
const where: Prisma.AuditLogWhereInput = {};
if (filters?.userId) where.userId = filters.userId;
if (filters?.action) where.action = { contains: filters.action, mode: 'insensitive' };
if (filters?.entityType) where.entityType = filters.entityType;
if (filters?.entityId) where.entityId = filters.entityId;
if (filters?.dateRange) {
where.timestamp = {
gte: filters.dateRange.start,
lte: filters.dateRange.end,
};
}
const [logs, total] = await Promise.all([
prisma.auditLog.findMany({
where,
skip,
take: limit,
orderBy: { timestamp: 'desc' },
include: { user: true },
}),
prisma.auditLog.count({ where }),
]);
return createPaginatedResult(logs, total, page, limit);
}
// Get audit logs for entity
export async function getEntityAuditLogs(
entityType: string,
entityId: string
): Promise<AuditLog[]> {
return prisma.auditLog.findMany({
where: { entityType, entityId },
orderBy: { timestamp: 'desc' },
include: { user: true },
});
}
// Get audit logs by user
export async function getUserAuditLogs(
userId: string,
options: PaginationOptions = {}
): Promise<PaginatedResult<AuditLog>> {
const page = options.page || 1;
const limit = options.limit || 50;
const skip = (page - 1) * limit;
const [logs, total] = await Promise.all([
prisma.auditLog.findMany({
where: { userId },
skip,
take: limit,
orderBy: { timestamp: 'desc' },
}),
prisma.auditLog.count({ where: { userId } }),
]);
return createPaginatedResult(logs, total, page, limit);
}
// Get recent audit logs summary
export async function getAuditLogsSummary(hours: number = 24) {
const since = new Date(Date.now() - hours * 60 * 60 * 1000);
const [
totalLogs,
actionBreakdown,
entityBreakdown,
recentLogs,
] = await Promise.all([
prisma.auditLog.count({
where: { timestamp: { gte: since } },
}),
prisma.auditLog.groupBy({
by: ['action'],
_count: { action: true },
where: { timestamp: { gte: since } },
}),
prisma.auditLog.groupBy({
by: ['entityType'],
_count: { entityType: true },
where: { timestamp: { gte: since } },
}),
prisma.auditLog.findMany({
where: { timestamp: { gte: since } },
orderBy: { timestamp: 'desc' },
take: 10,
include: { user: true },
}),
]);
return {
totalLogs,
actionBreakdown: Object.fromEntries(
actionBreakdown.map(a => [a.action, a._count.action])
),
entityBreakdown: Object.fromEntries(
entityBreakdown.map(e => [e.entityType, e._count.entityType])
),
recentLogs,
};
}
// ============================================
// BLOCKCHAIN BLOCK OPERATIONS
// ============================================
// Create a blockchain block
export async function createBlockchainBlock(data: {
index: number;
timestamp: Date;
previousHash: string;
hash: string;
nonce: number;
blockType: string;
plantId?: string;
transportEventId?: string;
content: Record<string, unknown>;
}): Promise<BlockchainBlock> {
return prisma.blockchainBlock.create({ data });
}
// Get blockchain block by index
export async function getBlockchainBlockByIndex(index: number): Promise<BlockchainBlock | null> {
return prisma.blockchainBlock.findUnique({
where: { index },
});
}
// Get blockchain block by hash
export async function getBlockchainBlockByHash(hash: string): Promise<BlockchainBlock | null> {
return prisma.blockchainBlock.findUnique({
where: { hash },
});
}
// Get latest blockchain block
export async function getLatestBlockchainBlock(): Promise<BlockchainBlock | null> {
return prisma.blockchainBlock.findFirst({
orderBy: { index: 'desc' },
});
}
// Get blockchain blocks with pagination
export async function getBlockchainBlocks(
options: PaginationOptions = {},
filters?: {
blockType?: string;
plantId?: string;
transportEventId?: string;
}
): Promise<PaginatedResult<BlockchainBlock>> {
const page = options.page || 1;
const limit = options.limit || 50;
const skip = (page - 1) * limit;
const where: Prisma.BlockchainBlockWhereInput = {};
if (filters?.blockType) where.blockType = filters.blockType;
if (filters?.plantId) where.plantId = filters.plantId;
if (filters?.transportEventId) where.transportEventId = filters.transportEventId;
const [blocks, total] = await Promise.all([
prisma.blockchainBlock.findMany({
where,
skip,
take: limit,
orderBy: { index: 'desc' },
}),
prisma.blockchainBlock.count({ where }),
]);
return createPaginatedResult(blocks, total, page, limit);
}
// Verify blockchain integrity
export async function verifyBlockchainIntegrity(): Promise<{
isValid: boolean;
invalidBlocks: number[];
totalBlocks: number;
}> {
const blocks = await prisma.blockchainBlock.findMany({
orderBy: { index: 'asc' },
});
const invalidBlocks: number[] = [];
for (let i = 1; i < blocks.length; i++) {
const currentBlock = blocks[i];
const previousBlock = blocks[i - 1];
// Check if previous hash matches
if (currentBlock.previousHash !== previousBlock.hash) {
invalidBlocks.push(currentBlock.index);
}
}
return {
isValid: invalidBlocks.length === 0,
invalidBlocks,
totalBlocks: blocks.length,
};
}
// Get blockchain statistics
export async function getBlockchainStats() {
const [
totalBlocks,
blocksByType,
latestBlock,
oldestBlock,
] = await Promise.all([
prisma.blockchainBlock.count(),
prisma.blockchainBlock.groupBy({
by: ['blockType'],
_count: { blockType: true },
}),
prisma.blockchainBlock.findFirst({ orderBy: { index: 'desc' } }),
prisma.blockchainBlock.findFirst({ orderBy: { index: 'asc' } }),
]);
return {
totalBlocks,
blocksByType: Object.fromEntries(
blocksByType.map(b => [b.blockType, b._count.blockType])
),
latestBlockIndex: latestBlock?.index ?? -1,
latestBlockHash: latestBlock?.hash,
oldestBlockTimestamp: oldestBlock?.timestamp,
latestBlockTimestamp: latestBlock?.timestamp,
};
}
// Helper to log entity changes
export async function logEntityChange<T extends Record<string, unknown>>(
userId: string | undefined,
action: 'CREATE' | 'UPDATE' | 'DELETE',
entityType: string,
entityId: string,
previousValue: T | null,
newValue: T | null,
metadata?: Record<string, unknown>
): Promise<AuditLog> {
return createAuditLog({
userId,
action,
entityType,
entityId,
previousValue: previousValue || undefined,
newValue: newValue || undefined,
metadata,
});
}

597
lib/db/demand.ts Normal file
View file

@ -0,0 +1,597 @@
/**
* Demand & Market Matching Database Service
* CRUD operations for consumer preferences, demand signals, and market matching
*/
import prisma from './prisma';
import type {
ConsumerPreference,
DemandSignal,
SupplyCommitment,
MarketMatch,
SeasonalPlan,
DemandForecast,
PlantingRecommendation,
SupplyStatus,
CommitmentStatus,
MatchStatus,
PlanStatus,
Prisma,
} from '@prisma/client';
import type { PaginationOptions, PaginatedResult, LocationFilter, DateRangeFilter } from './types';
import { createPaginatedResult, calculateDistanceKm } from './types';
// ============================================
// CONSUMER PREFERENCE OPERATIONS
// ============================================
// Create or update consumer preference
export async function upsertConsumerPreference(data: {
consumerId: string;
latitude: number;
longitude: number;
maxDeliveryRadiusKm?: number;
city?: string;
region?: string;
dietaryType?: string[];
allergies?: string[];
dislikes?: string[];
preferredCategories?: string[];
preferredItems?: Record<string, unknown>[];
certificationPreferences?: string[];
freshnessImportance?: number;
priceImportance?: number;
sustainabilityImportance?: number;
deliveryPreferences?: Record<string, unknown>;
householdSize?: number;
weeklyBudget?: number;
currency?: string;
}): Promise<ConsumerPreference> {
return prisma.consumerPreference.upsert({
where: { consumerId: data.consumerId },
update: {
...data,
updatedAt: new Date(),
},
create: {
...data,
maxDeliveryRadiusKm: data.maxDeliveryRadiusKm || 50,
dietaryType: data.dietaryType || [],
allergies: data.allergies || [],
dislikes: data.dislikes || [],
preferredCategories: data.preferredCategories || [],
certificationPreferences: data.certificationPreferences || [],
freshnessImportance: data.freshnessImportance || 3,
priceImportance: data.priceImportance || 3,
sustainabilityImportance: data.sustainabilityImportance || 3,
householdSize: data.householdSize || 1,
currency: data.currency || 'USD',
},
});
}
// Get consumer preference by consumer ID
export async function getConsumerPreference(consumerId: string): Promise<ConsumerPreference | null> {
return prisma.consumerPreference.findUnique({
where: { consumerId },
});
}
// Get consumer preferences near location
export async function getNearbyConsumerPreferences(
location: LocationFilter
): Promise<ConsumerPreference[]> {
const preferences = await prisma.consumerPreference.findMany();
return preferences.filter(pref => {
const distance = calculateDistanceKm(
location.latitude,
location.longitude,
pref.latitude,
pref.longitude
);
return distance <= location.radiusKm;
});
}
// ============================================
// DEMAND SIGNAL OPERATIONS
// ============================================
// Create a demand signal
export async function createDemandSignal(data: {
centerLat: number;
centerLon: number;
radiusKm: number;
regionName: string;
periodStart: Date;
periodEnd: Date;
seasonalPeriod: string;
demandItems: Record<string, unknown>[];
totalConsumers: number;
totalWeeklyDemandKg: number;
confidenceLevel: number;
currentSupplyKg?: number;
supplyGapKg?: number;
supplyStatus?: SupplyStatus;
}): Promise<DemandSignal> {
return prisma.demandSignal.create({
data: {
...data,
supplyStatus: data.supplyStatus || 'BALANCED',
},
});
}
// Get demand signal by ID
export async function getDemandSignalById(id: string): Promise<DemandSignal | null> {
return prisma.demandSignal.findUnique({
where: { id },
});
}
// Get demand signals with pagination
export async function getDemandSignals(
options: PaginationOptions = {},
filters?: {
regionName?: string;
supplyStatus?: SupplyStatus;
dateRange?: DateRangeFilter;
}
): Promise<PaginatedResult<DemandSignal>> {
const page = options.page || 1;
const limit = options.limit || 20;
const skip = (page - 1) * limit;
const where: Prisma.DemandSignalWhereInput = {};
if (filters?.regionName) where.regionName = { contains: filters.regionName, mode: 'insensitive' };
if (filters?.supplyStatus) where.supplyStatus = filters.supplyStatus;
if (filters?.dateRange) {
where.periodStart = { gte: filters.dateRange.start };
where.periodEnd = { lte: filters.dateRange.end };
}
const [signals, total] = await Promise.all([
prisma.demandSignal.findMany({
where,
skip,
take: limit,
orderBy: { timestamp: 'desc' },
}),
prisma.demandSignal.count({ where }),
]);
return createPaginatedResult(signals, total, page, limit);
}
// Get active demand signals for region
export async function getActiveDemandSignals(
location: LocationFilter
): Promise<DemandSignal[]> {
const now = new Date();
const signals = await prisma.demandSignal.findMany({
where: {
periodEnd: { gte: now },
},
orderBy: { timestamp: 'desc' },
});
return signals.filter(signal => {
const distance = calculateDistanceKm(
location.latitude,
location.longitude,
signal.centerLat,
signal.centerLon
);
return distance <= signal.radiusKm;
});
}
// ============================================
// SUPPLY COMMITMENT OPERATIONS
// ============================================
// Create a supply commitment
export async function createSupplyCommitment(data: {
growerId: string;
produceType: string;
variety?: string;
committedQuantityKg: number;
availableFrom: Date;
availableUntil: Date;
pricePerKg: number;
currency?: string;
minimumOrderKg?: number;
bulkDiscountThreshold?: number;
bulkDiscountPercent?: number;
certifications?: string[];
freshnessGuaranteeHours?: number;
deliveryRadiusKm: number;
deliveryMethods: string[];
}): Promise<SupplyCommitment> {
return prisma.supplyCommitment.create({
data: {
...data,
currency: data.currency || 'USD',
minimumOrderKg: data.minimumOrderKg || 0,
certifications: data.certifications || [],
status: 'AVAILABLE',
remainingKg: data.committedQuantityKg,
},
});
}
// Get supply commitment by ID
export async function getSupplyCommitmentById(id: string): Promise<SupplyCommitment | null> {
return prisma.supplyCommitment.findUnique({
where: { id },
});
}
// Update supply commitment
export async function updateSupplyCommitment(
id: string,
data: Prisma.SupplyCommitmentUpdateInput
): Promise<SupplyCommitment> {
return prisma.supplyCommitment.update({
where: { id },
data,
});
}
// Reduce commitment quantity
export async function reduceCommitmentQuantity(
id: string,
quantityKg: number
): Promise<SupplyCommitment> {
const commitment = await prisma.supplyCommitment.findUnique({
where: { id },
});
if (!commitment) throw new Error('Commitment not found');
const newRemainingKg = commitment.remainingKg - quantityKg;
let newStatus: CommitmentStatus = commitment.status;
if (newRemainingKg <= 0) {
newStatus = 'FULLY_COMMITTED';
} else if (newRemainingKg < commitment.committedQuantityKg) {
newStatus = 'PARTIALLY_COMMITTED';
}
return prisma.supplyCommitment.update({
where: { id },
data: {
remainingKg: Math.max(0, newRemainingKg),
status: newStatus,
},
});
}
// Get supply commitments with pagination
export async function getSupplyCommitments(
options: PaginationOptions = {},
filters?: {
growerId?: string;
produceType?: string;
status?: CommitmentStatus;
dateRange?: DateRangeFilter;
}
): Promise<PaginatedResult<SupplyCommitment>> {
const page = options.page || 1;
const limit = options.limit || 20;
const skip = (page - 1) * limit;
const where: Prisma.SupplyCommitmentWhereInput = {};
if (filters?.growerId) where.growerId = filters.growerId;
if (filters?.produceType) where.produceType = { contains: filters.produceType, mode: 'insensitive' };
if (filters?.status) where.status = filters.status;
if (filters?.dateRange) {
where.availableFrom = { lte: filters.dateRange.end };
where.availableUntil = { gte: filters.dateRange.start };
}
const [commitments, total] = await Promise.all([
prisma.supplyCommitment.findMany({
where,
skip,
take: limit,
orderBy: { timestamp: 'desc' },
include: { grower: true },
}),
prisma.supplyCommitment.count({ where }),
]);
return createPaginatedResult(commitments, total, page, limit);
}
// Find matching supply for demand
export async function findMatchingSupply(
produceType: string,
location: LocationFilter,
requiredDate: Date
): Promise<SupplyCommitment[]> {
const commitments = await prisma.supplyCommitment.findMany({
where: {
produceType: { contains: produceType, mode: 'insensitive' },
status: { in: ['AVAILABLE', 'PARTIALLY_COMMITTED'] },
availableFrom: { lte: requiredDate },
availableUntil: { gte: requiredDate },
remainingKg: { gt: 0 },
},
include: { grower: true },
});
// Filter by delivery radius
return commitments.filter(c => {
if (!c.grower.latitude || !c.grower.longitude) return false;
const distance = calculateDistanceKm(
location.latitude,
location.longitude,
c.grower.latitude,
c.grower.longitude
);
return distance <= c.deliveryRadiusKm;
});
}
// ============================================
// MARKET MATCH OPERATIONS
// ============================================
// Create a market match
export async function createMarketMatch(data: {
consumerId: string;
growerId: string;
demandSignalId: string;
supplyCommitmentId: string;
produceType: string;
matchedQuantityKg: number;
agreedPricePerKg: number;
totalPrice: number;
currency?: string;
deliveryDate: Date;
deliveryMethod: string;
deliveryLatitude?: number;
deliveryLongitude?: number;
deliveryAddress?: string;
}): Promise<MarketMatch> {
// Reduce the supply commitment
await reduceCommitmentQuantity(data.supplyCommitmentId, data.matchedQuantityKg);
return prisma.marketMatch.create({
data: {
...data,
currency: data.currency || 'USD',
status: 'PENDING',
},
});
}
// Get market match by ID
export async function getMarketMatchById(id: string): Promise<MarketMatch | null> {
return prisma.marketMatch.findUnique({
where: { id },
});
}
// Get market match with details
export async function getMarketMatchWithDetails(id: string) {
return prisma.marketMatch.findUnique({
where: { id },
include: {
demandSignal: true,
supplyCommitment: {
include: { grower: true },
},
},
});
}
// Update market match status
export async function updateMarketMatchStatus(
id: string,
status: MatchStatus,
extras?: {
consumerRating?: number;
growerRating?: number;
feedback?: string;
}
): Promise<MarketMatch> {
return prisma.marketMatch.update({
where: { id },
data: { status, ...extras },
});
}
// Get market matches with pagination
export async function getMarketMatches(
options: PaginationOptions = {},
filters?: {
consumerId?: string;
growerId?: string;
status?: MatchStatus;
dateRange?: DateRangeFilter;
}
): Promise<PaginatedResult<MarketMatch>> {
const page = options.page || 1;
const limit = options.limit || 20;
const skip = (page - 1) * limit;
const where: Prisma.MarketMatchWhereInput = {};
if (filters?.consumerId) where.consumerId = filters.consumerId;
if (filters?.growerId) where.growerId = filters.growerId;
if (filters?.status) where.status = filters.status;
if (filters?.dateRange) {
where.deliveryDate = {
gte: filters.dateRange.start,
lte: filters.dateRange.end,
};
}
const [matches, total] = await Promise.all([
prisma.marketMatch.findMany({
where,
skip,
take: limit,
orderBy: { deliveryDate: 'desc' },
include: {
demandSignal: true,
supplyCommitment: true,
},
}),
prisma.marketMatch.count({ where }),
]);
return createPaginatedResult(matches, total, page, limit);
}
// ============================================
// SEASONAL PLAN OPERATIONS
// ============================================
// Create a seasonal plan
export async function createSeasonalPlan(data: {
growerId: string;
year: number;
season: string;
location: Record<string, unknown>;
growingCapacity: Record<string, unknown>;
plannedCrops: Record<string, unknown>[];
expectedTotalYieldKg?: number;
expectedRevenue?: number;
expectedCarbonFootprintKg?: number;
}): Promise<SeasonalPlan> {
return prisma.seasonalPlan.create({
data: {
...data,
status: 'DRAFT',
},
});
}
// Get seasonal plan by ID
export async function getSeasonalPlanById(id: string): Promise<SeasonalPlan | null> {
return prisma.seasonalPlan.findUnique({
where: { id },
});
}
// Update seasonal plan
export async function updateSeasonalPlan(
id: string,
data: Prisma.SeasonalPlanUpdateInput
): Promise<SeasonalPlan> {
return prisma.seasonalPlan.update({
where: { id },
data,
});
}
// Update seasonal plan status
export async function updateSeasonalPlanStatus(
id: string,
status: PlanStatus,
completionPercentage?: number
): Promise<SeasonalPlan> {
return prisma.seasonalPlan.update({
where: { id },
data: { status, completionPercentage },
});
}
// Get seasonal plans by grower
export async function getSeasonalPlansByGrower(
growerId: string,
year?: number
): Promise<SeasonalPlan[]> {
return prisma.seasonalPlan.findMany({
where: {
growerId,
...(year && { year }),
},
orderBy: [{ year: 'desc' }, { season: 'asc' }],
});
}
// ============================================
// DEMAND FORECAST OPERATIONS
// ============================================
// Create a demand forecast
export async function createDemandForecast(data: {
region: string;
forecastPeriodStart: Date;
forecastPeriodEnd: Date;
forecasts: Record<string, unknown>[];
modelVersion: string;
dataPointsUsed: number;
lastTrainingDate?: Date;
}): Promise<DemandForecast> {
return prisma.demandForecast.create({ data });
}
// Get latest demand forecast for region
export async function getLatestDemandForecast(region: string): Promise<DemandForecast | null> {
return prisma.demandForecast.findFirst({
where: { region },
orderBy: { generatedAt: 'desc' },
});
}
// ============================================
// PLANTING RECOMMENDATION OPERATIONS
// ============================================
// Create a planting recommendation
export async function createPlantingRecommendation(data: {
growerId: string;
produceType: string;
variety?: string;
category: string;
recommendedQuantity: number;
quantityUnit: string;
expectedYieldKg: number;
yieldConfidence: number;
plantByDate: Date;
expectedHarvestStart: Date;
expectedHarvestEnd: Date;
growingDays: number;
projectedDemandKg?: number;
projectedPricePerKg?: number;
projectedRevenue?: number;
marketConfidence?: number;
riskFactors?: Record<string, unknown>[];
overallRisk?: string;
demandSignalIds: string[];
explanation?: string;
}): Promise<PlantingRecommendation> {
return prisma.plantingRecommendation.create({
data: {
...data,
overallRisk: data.overallRisk || 'medium',
},
});
}
// Get planting recommendations for grower
export async function getPlantingRecommendations(
growerId: string,
options: PaginationOptions = {}
): Promise<PaginatedResult<PlantingRecommendation>> {
const page = options.page || 1;
const limit = options.limit || 20;
const skip = (page - 1) * limit;
const [recommendations, total] = await Promise.all([
prisma.plantingRecommendation.findMany({
where: { growerId },
skip,
take: limit,
orderBy: { plantByDate: 'asc' },
}),
prisma.plantingRecommendation.count({ where: { growerId } }),
]);
return createPaginatedResult(recommendations, total, page, limit);
}

568
lib/db/farms.ts Normal file
View file

@ -0,0 +1,568 @@
/**
* Vertical Farm Database Service
* CRUD operations for vertical farms, zones, and crop batches
*/
import prisma from './prisma';
import type {
VerticalFarm,
GrowingZone,
CropBatch,
GrowingRecipe,
FarmStatus,
ZoneStatus,
CropBatchStatus,
Prisma,
} from '@prisma/client';
import type { PaginationOptions, PaginatedResult } from './types';
import { createPaginatedResult } from './types';
// ============================================
// VERTICAL FARM OPERATIONS
// ============================================
// Create a vertical farm
export async function createVerticalFarm(data: {
name: string;
ownerId: string;
latitude: number;
longitude: number;
address: string;
city: string;
country: string;
timezone?: string;
specs: Record<string, unknown>;
environmentalControl?: Record<string, unknown>;
irrigationSystem?: Record<string, unknown>;
lightingSystem?: Record<string, unknown>;
nutrientSystem?: Record<string, unknown>;
automationLevel?: 'MANUAL' | 'SEMI_AUTOMATED' | 'FULLY_AUTOMATED';
automationSystems?: Record<string, unknown>;
}): Promise<VerticalFarm> {
return prisma.verticalFarm.create({
data: {
...data,
timezone: data.timezone || 'UTC',
automationLevel: data.automationLevel || 'MANUAL',
status: 'OFFLINE',
},
});
}
// Get vertical farm by ID
export async function getVerticalFarmById(id: string): Promise<VerticalFarm | null> {
return prisma.verticalFarm.findUnique({
where: { id },
});
}
// Get vertical farm with zones
export async function getVerticalFarmWithZones(id: string) {
return prisma.verticalFarm.findUnique({
where: { id },
include: {
owner: true,
zones: true,
cropBatches: {
where: { status: { not: 'COMPLETED' } },
},
},
});
}
// Update vertical farm
export async function updateVerticalFarm(
id: string,
data: Prisma.VerticalFarmUpdateInput
): Promise<VerticalFarm> {
return prisma.verticalFarm.update({
where: { id },
data,
});
}
// Update farm status
export async function updateFarmStatus(
id: string,
status: FarmStatus
): Promise<VerticalFarm> {
return prisma.verticalFarm.update({
where: { id },
data: { status },
});
}
// Delete vertical farm
export async function deleteVerticalFarm(id: string): Promise<VerticalFarm> {
return prisma.verticalFarm.delete({
where: { id },
});
}
// Get farms with pagination
export async function getVerticalFarms(
options: PaginationOptions = {},
filters?: {
ownerId?: string;
status?: FarmStatus;
city?: string;
country?: string;
}
): Promise<PaginatedResult<VerticalFarm>> {
const page = options.page || 1;
const limit = options.limit || 20;
const skip = (page - 1) * limit;
const where: Prisma.VerticalFarmWhereInput = {};
if (filters?.ownerId) where.ownerId = filters.ownerId;
if (filters?.status) where.status = filters.status;
if (filters?.city) where.city = { contains: filters.city, mode: 'insensitive' };
if (filters?.country) where.country = { contains: filters.country, mode: 'insensitive' };
const [farms, total] = await Promise.all([
prisma.verticalFarm.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
include: {
owner: true,
zones: true,
},
}),
prisma.verticalFarm.count({ where }),
]);
return createPaginatedResult(farms, total, page, limit);
}
// Get farms by owner
export async function getVerticalFarmsByOwner(ownerId: string): Promise<VerticalFarm[]> {
return prisma.verticalFarm.findMany({
where: { ownerId },
include: { zones: true },
orderBy: { name: 'asc' },
});
}
// ============================================
// GROWING ZONE OPERATIONS
// ============================================
// Create a growing zone
export async function createGrowingZone(data: {
name: string;
farmId: string;
level: number;
areaSqm: number;
lengthM?: number;
widthM?: number;
growingMethod: 'NFT' | 'DWC' | 'EBB_FLOW' | 'AEROPONICS' | 'VERTICAL_TOWERS' | 'RACK_SYSTEM';
plantPositions: number;
environmentTargets?: Record<string, unknown>;
}): Promise<GrowingZone> {
return prisma.growingZone.create({
data: {
...data,
status: 'EMPTY',
},
});
}
// Get growing zone by ID
export async function getGrowingZoneById(id: string): Promise<GrowingZone | null> {
return prisma.growingZone.findUnique({
where: { id },
});
}
// Get growing zone with current batch
export async function getGrowingZoneWithBatch(id: string) {
return prisma.growingZone.findUnique({
where: { id },
include: {
farm: true,
cropBatches: {
where: { status: { not: 'COMPLETED' } },
take: 1,
},
},
});
}
// Update growing zone
export async function updateGrowingZone(
id: string,
data: Prisma.GrowingZoneUpdateInput
): Promise<GrowingZone> {
return prisma.growingZone.update({
where: { id },
data,
});
}
// Update zone status
export async function updateZoneStatus(
id: string,
status: ZoneStatus
): Promise<GrowingZone> {
return prisma.growingZone.update({
where: { id },
data: { status },
});
}
// Delete growing zone
export async function deleteGrowingZone(id: string): Promise<GrowingZone> {
return prisma.growingZone.delete({
where: { id },
});
}
// Get zones by farm
export async function getGrowingZonesByFarm(farmId: string): Promise<GrowingZone[]> {
return prisma.growingZone.findMany({
where: { farmId },
orderBy: [{ level: 'asc' }, { name: 'asc' }],
});
}
// Update zone environment readings
export async function updateZoneEnvironment(
id: string,
readings: Record<string, unknown>
): Promise<GrowingZone> {
return prisma.growingZone.update({
where: { id },
data: { currentEnvironment: readings },
});
}
// ============================================
// CROP BATCH OPERATIONS
// ============================================
// Create a crop batch
export async function createCropBatch(data: {
farmId: string;
zoneId: string;
cropType: string;
variety?: string;
recipeId?: string;
seedBatchId?: string;
plantCount: number;
plantingDate: Date;
expectedHarvestDate: Date;
expectedYieldKg: number;
}): Promise<CropBatch> {
// Update zone status
await prisma.growingZone.update({
where: { id: data.zoneId },
data: {
status: 'PLANTED',
currentCrop: data.cropType,
plantingDate: data.plantingDate,
expectedHarvestDate: data.expectedHarvestDate,
},
});
return prisma.cropBatch.create({
data: {
...data,
currentStage: 'germinating',
currentDay: 0,
status: 'GERMINATING',
},
});
}
// Get crop batch by ID
export async function getCropBatchById(id: string): Promise<CropBatch | null> {
return prisma.cropBatch.findUnique({
where: { id },
});
}
// Get crop batch with details
export async function getCropBatchWithDetails(id: string) {
return prisma.cropBatch.findUnique({
where: { id },
include: {
farm: true,
zone: true,
recipe: true,
seedBatch: true,
plants: true,
harvestBatches: true,
},
});
}
// Update crop batch
export async function updateCropBatch(
id: string,
data: Prisma.CropBatchUpdateInput
): Promise<CropBatch> {
return prisma.cropBatch.update({
where: { id },
data,
});
}
// Update crop batch status
export async function updateCropBatchStatus(
id: string,
status: CropBatchStatus,
extras?: {
currentStage?: string;
currentDay?: number;
healthScore?: number;
actualHarvestDate?: Date;
actualYieldKg?: number;
qualityGrade?: string;
}
): Promise<CropBatch> {
const batch = await prisma.cropBatch.update({
where: { id },
data: { status, ...extras },
});
// Update zone status based on batch status
if (status === 'COMPLETED' || status === 'FAILED') {
await prisma.growingZone.update({
where: { id: batch.zoneId },
data: {
status: 'CLEANING',
currentCrop: null,
plantingDate: null,
expectedHarvestDate: null,
},
});
} else if (status === 'HARVESTING') {
await prisma.growingZone.update({
where: { id: batch.zoneId },
data: { status: 'HARVESTING' },
});
} else if (status === 'GROWING') {
await prisma.growingZone.update({
where: { id: batch.zoneId },
data: { status: 'GROWING' },
});
}
return batch;
}
// Delete crop batch
export async function deleteCropBatch(id: string): Promise<CropBatch> {
return prisma.cropBatch.delete({
where: { id },
});
}
// Get crop batches with pagination
export async function getCropBatches(
options: PaginationOptions = {},
filters?: {
farmId?: string;
zoneId?: string;
status?: CropBatchStatus;
cropType?: string;
}
): Promise<PaginatedResult<CropBatch>> {
const page = options.page || 1;
const limit = options.limit || 20;
const skip = (page - 1) * limit;
const where: Prisma.CropBatchWhereInput = {};
if (filters?.farmId) where.farmId = filters.farmId;
if (filters?.zoneId) where.zoneId = filters.zoneId;
if (filters?.status) where.status = filters.status;
if (filters?.cropType) where.cropType = { contains: filters.cropType, mode: 'insensitive' };
const [batches, total] = await Promise.all([
prisma.cropBatch.findMany({
where,
skip,
take: limit,
orderBy: { plantingDate: 'desc' },
include: {
zone: true,
recipe: true,
},
}),
prisma.cropBatch.count({ where }),
]);
return createPaginatedResult(batches, total, page, limit);
}
// Get active batches by farm
export async function getActiveCropBatchesByFarm(farmId: string): Promise<CropBatch[]> {
return prisma.cropBatch.findMany({
where: {
farmId,
status: { notIn: ['COMPLETED', 'FAILED'] },
},
include: {
zone: true,
recipe: true,
},
orderBy: { expectedHarvestDate: 'asc' },
});
}
// Add issue to crop batch
export async function addCropBatchIssue(
id: string,
issue: {
type: string;
severity: string;
description: string;
affectedPlants: number;
}
) {
const batch = await prisma.cropBatch.findUnique({
where: { id },
select: { issues: true },
});
const issues = (batch?.issues as unknown[] || []) as Record<string, unknown>[];
issues.push({
id: `issue_${Date.now()}`,
timestamp: new Date().toISOString(),
...issue,
});
return prisma.cropBatch.update({
where: { id },
data: { issues },
});
}
// ============================================
// GROWING RECIPE OPERATIONS
// ============================================
// Create a growing recipe
export async function createGrowingRecipe(data: {
name: string;
cropType: string;
variety?: string;
version?: string;
stages: Record<string, unknown>[];
expectedDays: number;
expectedYieldGrams: number;
expectedYieldPerSqm?: number;
requirements?: Record<string, unknown>;
source?: 'INTERNAL' | 'COMMUNITY' | 'COMMERCIAL';
author?: string;
}): Promise<GrowingRecipe> {
return prisma.growingRecipe.create({
data: {
...data,
version: data.version || '1.0',
source: data.source || 'INTERNAL',
timesUsed: 0,
},
});
}
// Get growing recipe by ID
export async function getGrowingRecipeById(id: string): Promise<GrowingRecipe | null> {
return prisma.growingRecipe.findUnique({
where: { id },
});
}
// Get recipes by crop type
export async function getGrowingRecipesByCrop(cropType: string): Promise<GrowingRecipe[]> {
return prisma.growingRecipe.findMany({
where: { cropType: { contains: cropType, mode: 'insensitive' } },
orderBy: { rating: 'desc' },
});
}
// Increment recipe usage
export async function incrementRecipeUsage(id: string): Promise<GrowingRecipe> {
return prisma.growingRecipe.update({
where: { id },
data: { timesUsed: { increment: 1 } },
});
}
// ============================================
// RESOURCE & ANALYTICS OPERATIONS
// ============================================
// Record resource usage
export async function recordResourceUsage(data: {
farmId: string;
periodStart: Date;
periodEnd: Date;
electricityKwh: number;
electricityCostUsd?: number;
renewablePercent?: number;
peakDemandKw?: number;
waterUsageL: number;
waterCostUsd?: number;
waterRecycledPercent?: number;
co2UsedKg?: number;
co2CostUsd?: number;
nutrientsUsedL?: number;
nutrientCostUsd?: number;
}) {
return prisma.resourceUsage.create({ data });
}
// Get resource usage for farm
export async function getResourceUsage(farmId: string, periodStart: Date, periodEnd: Date) {
return prisma.resourceUsage.findMany({
where: {
farmId,
periodStart: { gte: periodStart },
periodEnd: { lte: periodEnd },
},
orderBy: { periodStart: 'asc' },
});
}
// Record farm analytics
export async function recordFarmAnalytics(data: {
farmId: string;
period: string;
totalYieldKg: number;
yieldPerSqmPerYear?: number;
cropCyclesCompleted: number;
averageCyclesDays?: number;
averageQualityScore?: number;
gradeAPercent?: number;
wastagePercent?: number;
cropSuccessRate?: number;
spaceUtilization?: number;
laborHoursPerKg?: number;
revenueUsd?: number;
costUsd?: number;
profitMarginPercent?: number;
revenuePerSqm?: number;
carbonFootprintKgPerKg?: number;
waterUseLPerKg?: number;
energyUseKwhPerKg?: number;
topCropsByYield?: Record<string, unknown>[];
topCropsByRevenue?: Record<string, unknown>[];
topCropsByEfficiency?: Record<string, unknown>[];
}) {
return prisma.farmAnalytics.create({ data });
}
// Get farm analytics
export async function getFarmAnalytics(farmId: string, period?: string) {
const where: Prisma.FarmAnalyticsWhereInput = { farmId };
if (period) where.period = period;
return prisma.farmAnalytics.findMany({
where,
orderBy: { generatedAt: 'desc' },
take: period ? 1 : 12,
});
}

156
lib/db/index.ts Normal file
View file

@ -0,0 +1,156 @@
/**
* LocalGreenChain Database Service Layer
* Central export for all database operations
*/
// Prisma client singleton
export { default as prisma, prisma as db } from './prisma';
// Types and utilities
export * from './types';
// User operations
export * as users from './users';
export {
createUser,
getUserById,
getUserByEmail,
getUserByWalletAddress,
updateUser,
deleteUser,
getUsers,
getUsersByType,
updateLastLogin,
searchUsers,
getUserWithPlants,
getUserWithFarms,
getUserStats,
} from './users';
// Plant operations
export * as plants from './plants';
export {
createPlant,
getPlantById,
getPlantWithOwner,
getPlantWithLineage,
updatePlant,
updatePlantStatus,
deletePlant,
getPlants,
getPlantsByOwner,
getNearbyPlants,
searchPlants,
getPlantLineage,
clonePlant,
getPlantNetworkStats,
updatePlantBlockchain,
} from './plants';
// Transport operations
export * as transport from './transport';
export {
createTransportEvent,
getTransportEventById,
getTransportEventWithDetails,
updateTransportEvent,
updateTransportEventStatus,
deleteTransportEvent,
getTransportEvents,
getTransportEventsByPlant,
getPlantJourney,
getEnvironmentalImpact,
getUserCarbonFootprint,
createSeedBatch,
getSeedBatchById,
createHarvestBatch,
getHarvestBatchById,
} from './transport';
// Vertical farm operations
export * as farms from './farms';
export {
createVerticalFarm,
getVerticalFarmById,
getVerticalFarmWithZones,
updateVerticalFarm,
updateFarmStatus,
deleteVerticalFarm,
getVerticalFarms,
getVerticalFarmsByOwner,
createGrowingZone,
getGrowingZoneById,
getGrowingZoneWithBatch,
updateGrowingZone,
updateZoneStatus,
deleteGrowingZone,
getGrowingZonesByFarm,
updateZoneEnvironment,
createCropBatch,
getCropBatchById,
getCropBatchWithDetails,
updateCropBatch,
updateCropBatchStatus,
deleteCropBatch,
getCropBatches,
getActiveCropBatchesByFarm,
addCropBatchIssue,
createGrowingRecipe,
getGrowingRecipeById,
getGrowingRecipesByCrop,
incrementRecipeUsage,
recordResourceUsage,
getResourceUsage,
recordFarmAnalytics,
getFarmAnalytics,
} from './farms';
// Demand and market operations
export * as demand from './demand';
export {
upsertConsumerPreference,
getConsumerPreference,
getNearbyConsumerPreferences,
createDemandSignal,
getDemandSignalById,
getDemandSignals,
getActiveDemandSignals,
createSupplyCommitment,
getSupplyCommitmentById,
updateSupplyCommitment,
reduceCommitmentQuantity,
getSupplyCommitments,
findMatchingSupply,
createMarketMatch,
getMarketMatchById,
getMarketMatchWithDetails,
updateMarketMatchStatus,
getMarketMatches,
createSeasonalPlan,
getSeasonalPlanById,
updateSeasonalPlan,
updateSeasonalPlanStatus,
getSeasonalPlansByGrower,
createDemandForecast,
getLatestDemandForecast,
createPlantingRecommendation,
getPlantingRecommendations,
} from './demand';
// Audit and blockchain operations
export * as audit from './audit';
export {
createAuditLog,
getAuditLogs,
getEntityAuditLogs,
getUserAuditLogs,
getAuditLogsSummary,
createBlockchainBlock,
getBlockchainBlockByIndex,
getBlockchainBlockByHash,
getLatestBlockchainBlock,
getBlockchainBlocks,
verifyBlockchainIntegrity,
getBlockchainStats,
logEntityChange,
} from './audit';

378
lib/db/plants.ts Normal file
View file

@ -0,0 +1,378 @@
/**
* Plant Database Service
* CRUD operations for plants and lineage tracking
*/
import prisma from './prisma';
import type { Plant, PlantStatus, PropagationType, Prisma } from '@prisma/client';
import type { PaginationOptions, PaginatedResult, LocationFilter, PlantWithLineage } from './types';
import { createPaginatedResult, calculateDistanceKm } from './types';
// Create a new plant
export async function createPlant(data: {
commonName: string;
scientificName?: string;
species?: string;
genus?: string;
family?: string;
parentPlantId?: string;
propagationType?: PropagationType;
generation?: number;
plantedDate: Date;
status?: PlantStatus;
latitude: number;
longitude: number;
address?: string;
city?: string;
country?: string;
ownerId: string;
environment?: Record<string, unknown>;
growthMetrics?: Record<string, unknown>;
notes?: string;
images?: string[];
plantsNetId?: string;
}): Promise<Plant> {
// Determine generation based on parent
let generation = data.generation || 0;
if (data.parentPlantId && generation === 0) {
const parent = await prisma.plant.findUnique({
where: { id: data.parentPlantId },
select: { generation: true },
});
if (parent) {
generation = parent.generation + 1;
}
}
return prisma.plant.create({
data: {
...data,
generation,
propagationType: data.propagationType || 'ORIGINAL',
status: data.status || 'SPROUTED',
},
});
}
// Get plant by ID
export async function getPlantById(id: string): Promise<Plant | null> {
return prisma.plant.findUnique({
where: { id },
});
}
// Get plant with owner
export async function getPlantWithOwner(id: string) {
return prisma.plant.findUnique({
where: { id },
include: { owner: true },
});
}
// Get plant with full lineage
export async function getPlantWithLineage(id: string): Promise<PlantWithLineage | null> {
return prisma.plant.findUnique({
where: { id },
include: {
owner: true,
parentPlant: true,
childPlants: true,
},
});
}
// Update plant
export async function updatePlant(
id: string,
data: Prisma.PlantUpdateInput
): Promise<Plant> {
return prisma.plant.update({
where: { id },
data,
});
}
// Update plant status
export async function updatePlantStatus(
id: string,
status: PlantStatus,
harvestedDate?: Date
): Promise<Plant> {
return prisma.plant.update({
where: { id },
data: {
status,
...(harvestedDate && { harvestedDate }),
},
});
}
// Delete plant
export async function deletePlant(id: string): Promise<Plant> {
return prisma.plant.delete({
where: { id },
});
}
// Get plants with pagination
export async function getPlants(
options: PaginationOptions = {},
filters?: {
ownerId?: string;
status?: PlantStatus;
commonName?: string;
species?: string;
}
): Promise<PaginatedResult<Plant>> {
const page = options.page || 1;
const limit = options.limit || 20;
const skip = (page - 1) * limit;
const where: Prisma.PlantWhereInput = {};
if (filters?.ownerId) where.ownerId = filters.ownerId;
if (filters?.status) where.status = filters.status;
if (filters?.commonName) where.commonName = { contains: filters.commonName, mode: 'insensitive' };
if (filters?.species) where.species = { contains: filters.species, mode: 'insensitive' };
const [plants, total] = await Promise.all([
prisma.plant.findMany({
where,
skip,
take: limit,
orderBy: { registeredAt: 'desc' },
include: { owner: true },
}),
prisma.plant.count({ where }),
]);
return createPaginatedResult(plants, total, page, limit);
}
// Get plants by owner
export async function getPlantsByOwner(ownerId: string): Promise<Plant[]> {
return prisma.plant.findMany({
where: { ownerId },
orderBy: { registeredAt: 'desc' },
});
}
// Get plants near location
export async function getNearbyPlants(
location: LocationFilter,
excludeOwnerId?: string
): Promise<Array<Plant & { distance: number }>> {
// Get all plants and filter by distance
// Note: For production, consider using PostGIS for efficient geo queries
const plants = await prisma.plant.findMany({
where: excludeOwnerId ? { ownerId: { not: excludeOwnerId } } : undefined,
include: { owner: true },
});
const plantsWithDistance = plants
.map(plant => ({
...plant,
distance: calculateDistanceKm(
location.latitude,
location.longitude,
plant.latitude,
plant.longitude
),
}))
.filter(plant => plant.distance <= location.radiusKm)
.sort((a, b) => a.distance - b.distance);
return plantsWithDistance;
}
// Search plants
export async function searchPlants(
query: string,
options: PaginationOptions = {}
): Promise<PaginatedResult<Plant>> {
const page = options.page || 1;
const limit = options.limit || 20;
const skip = (page - 1) * limit;
const where: Prisma.PlantWhereInput = {
OR: [
{ commonName: { contains: query, mode: 'insensitive' } },
{ scientificName: { contains: query, mode: 'insensitive' } },
{ species: { contains: query, mode: 'insensitive' } },
{ genus: { contains: query, mode: 'insensitive' } },
{ family: { contains: query, mode: 'insensitive' } },
],
};
const [plants, total] = await Promise.all([
prisma.plant.findMany({
where,
skip,
take: limit,
orderBy: { registeredAt: 'desc' },
include: { owner: true },
}),
prisma.plant.count({ where }),
]);
return createPaginatedResult(plants, total, page, limit);
}
// Get full lineage (ancestors and descendants)
export async function getPlantLineage(id: string): Promise<{
plant: Plant | null;
ancestors: Plant[];
descendants: Plant[];
siblings: Plant[];
}> {
const plant = await prisma.plant.findUnique({
where: { id },
include: {
parentPlant: true,
childPlants: true,
},
});
if (!plant) {
return { plant: null, ancestors: [], descendants: [], siblings: [] };
}
// Get ancestors recursively
const ancestors: Plant[] = [];
let currentParentId = plant.parentPlantId;
while (currentParentId) {
const parent = await prisma.plant.findUnique({
where: { id: currentParentId },
});
if (parent) {
ancestors.push(parent);
currentParentId = parent.parentPlantId;
} else {
break;
}
}
// Get all descendants recursively
const descendants = await getDescendants(id);
// Get siblings (other plants from the same parent)
let siblings: Plant[] = [];
if (plant.parentPlantId) {
siblings = await prisma.plant.findMany({
where: {
parentPlantId: plant.parentPlantId,
id: { not: id },
},
});
}
return {
plant,
ancestors,
descendants,
siblings,
};
}
// Helper to get all descendants
async function getDescendants(plantId: string): Promise<Plant[]> {
const children = await prisma.plant.findMany({
where: { parentPlantId: plantId },
});
const allDescendants: Plant[] = [...children];
for (const child of children) {
const grandchildren = await getDescendants(child.id);
allDescendants.push(...grandchildren);
}
return allDescendants;
}
// Clone a plant
export async function clonePlant(
parentId: string,
ownerId: string,
propagationType: PropagationType = 'CLONE',
overrides?: Partial<{
latitude: number;
longitude: number;
address: string;
city: string;
country: string;
notes: string;
}>
): Promise<Plant> {
const parent = await prisma.plant.findUnique({
where: { id: parentId },
});
if (!parent) {
throw new Error('Parent plant not found');
}
return prisma.plant.create({
data: {
commonName: parent.commonName,
scientificName: parent.scientificName,
species: parent.species,
genus: parent.genus,
family: parent.family,
parentPlantId: parentId,
propagationType,
generation: parent.generation + 1,
plantedDate: new Date(),
status: 'SPROUTED',
latitude: overrides?.latitude || parent.latitude,
longitude: overrides?.longitude || parent.longitude,
address: overrides?.address,
city: overrides?.city || parent.city,
country: overrides?.country || parent.country,
ownerId,
notes: overrides?.notes,
plantsNetId: parent.plantsNetId,
},
});
}
// Get plant network statistics
export async function getPlantNetworkStats() {
const [totalPlants, speciesDistribution, countryDistribution, ownerCount] = await Promise.all([
prisma.plant.count(),
prisma.plant.groupBy({
by: ['species'],
_count: { species: true },
where: { species: { not: null } },
}),
prisma.plant.groupBy({
by: ['country'],
_count: { country: true },
where: { country: { not: null } },
}),
prisma.user.count({
where: { ownedPlants: { some: {} } },
}),
]);
return {
totalPlants,
totalOwners: ownerCount,
species: Object.fromEntries(
speciesDistribution.map(s => [s.species || 'unknown', s._count.species])
),
globalDistribution: Object.fromEntries(
countryDistribution.map(c => [c.country || 'unknown', c._count.country])
),
};
}
// Store blockchain data for a plant
export async function updatePlantBlockchain(
id: string,
blockIndex: number,
blockHash: string
): Promise<Plant> {
return prisma.plant.update({
where: { id },
data: { blockIndex, blockHash },
});
}

27
lib/db/prisma.ts Normal file
View file

@ -0,0 +1,27 @@
/**
* Prisma Client Singleton
* Prevents multiple instances during development hot-reloading
*/
import { PrismaClient } from '@prisma/client';
declare global {
// eslint-disable-next-line no-var
var prisma: PrismaClient | undefined;
}
const prismaClientSingleton = () => {
return new PrismaClient({
log: process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
});
};
export const prisma = globalThis.prisma ?? prismaClientSingleton();
if (process.env.NODE_ENV !== 'production') {
globalThis.prisma = prisma;
}
export default prisma;

410
lib/db/transport.ts Normal file
View file

@ -0,0 +1,410 @@
/**
* Transport Event Database Service
* CRUD operations for transport events and supply chain tracking
*/
import prisma from './prisma';
import type {
TransportEvent,
TransportEventType,
TransportMethod,
TransportStatus,
Prisma,
} from '@prisma/client';
import type { PaginationOptions, PaginatedResult, DateRangeFilter } from './types';
import { createPaginatedResult, calculateDistanceKm } from './types';
// Carbon emission factors (kg CO2 per km per kg of cargo)
const CARBON_FACTORS: Record<TransportMethod, number> = {
WALKING: 0,
BICYCLE: 0,
ELECTRIC_VEHICLE: 0.02,
HYBRID_VEHICLE: 0.08,
GASOLINE_VEHICLE: 0.12,
DIESEL_TRUCK: 0.15,
ELECTRIC_TRUCK: 0.03,
REFRIGERATED_TRUCK: 0.25,
RAIL: 0.01,
SHIP: 0.008,
AIR: 0.5,
DRONE: 0.01,
LOCAL_DELIVERY: 0.05,
CUSTOMER_PICKUP: 0.1,
};
// Create a transport event
export async function createTransportEvent(data: {
eventType: TransportEventType;
fromLatitude: number;
fromLongitude: number;
fromAddress?: string;
fromCity?: string;
fromCountry?: string;
fromLocationType: 'FARM' | 'GREENHOUSE' | 'VERTICAL_FARM' | 'WAREHOUSE' | 'HUB' | 'MARKET' | 'CONSUMER' | 'SEED_BANK' | 'OTHER';
fromFacilityId?: string;
fromFacilityName?: string;
toLatitude: number;
toLongitude: number;
toAddress?: string;
toCity?: string;
toCountry?: string;
toLocationType: 'FARM' | 'GREENHOUSE' | 'VERTICAL_FARM' | 'WAREHOUSE' | 'HUB' | 'MARKET' | 'CONSUMER' | 'SEED_BANK' | 'OTHER';
toFacilityId?: string;
toFacilityName?: string;
durationMinutes: number;
transportMethod: TransportMethod;
senderId: string;
receiverId: string;
notes?: string;
photos?: string[];
documents?: string[];
eventData?: Record<string, unknown>;
plantIds?: string[];
seedBatchId?: string;
harvestBatchId?: string;
cargoWeightKg?: number;
}): Promise<TransportEvent> {
// Calculate distance
const distanceKm = calculateDistanceKm(
data.fromLatitude,
data.fromLongitude,
data.toLatitude,
data.toLongitude
);
// Calculate carbon footprint
const cargoWeight = data.cargoWeightKg || 1;
const carbonFootprintKg = distanceKm * CARBON_FACTORS[data.transportMethod] * cargoWeight;
return prisma.transportEvent.create({
data: {
eventType: data.eventType,
fromLatitude: data.fromLatitude,
fromLongitude: data.fromLongitude,
fromAddress: data.fromAddress,
fromCity: data.fromCity,
fromCountry: data.fromCountry,
fromLocationType: data.fromLocationType,
fromFacilityId: data.fromFacilityId,
fromFacilityName: data.fromFacilityName,
toLatitude: data.toLatitude,
toLongitude: data.toLongitude,
toAddress: data.toAddress,
toCity: data.toCity,
toCountry: data.toCountry,
toLocationType: data.toLocationType,
toFacilityId: data.toFacilityId,
toFacilityName: data.toFacilityName,
distanceKm,
durationMinutes: data.durationMinutes,
transportMethod: data.transportMethod,
carbonFootprintKg,
senderId: data.senderId,
receiverId: data.receiverId,
status: 'PENDING',
notes: data.notes,
photos: data.photos || [],
documents: data.documents || [],
eventData: data.eventData,
seedBatchId: data.seedBatchId,
harvestBatchId: data.harvestBatchId,
plants: data.plantIds ? { connect: data.plantIds.map(id => ({ id })) } : undefined,
},
});
}
// Get transport event by ID
export async function getTransportEventById(id: string): Promise<TransportEvent | null> {
return prisma.transportEvent.findUnique({
where: { id },
});
}
// Get transport event with related data
export async function getTransportEventWithDetails(id: string) {
return prisma.transportEvent.findUnique({
where: { id },
include: {
sender: true,
receiver: true,
plants: true,
seedBatch: true,
harvestBatch: true,
},
});
}
// Update transport event
export async function updateTransportEvent(
id: string,
data: Prisma.TransportEventUpdateInput
): Promise<TransportEvent> {
return prisma.transportEvent.update({
where: { id },
data,
});
}
// Update transport event status
export async function updateTransportEventStatus(
id: string,
status: TransportStatus,
signature?: { type: 'sender' | 'receiver' | 'verifier'; signature: string }
): Promise<TransportEvent> {
const updateData: Prisma.TransportEventUpdateInput = { status };
if (signature) {
if (signature.type === 'sender') updateData.senderSignature = signature.signature;
if (signature.type === 'receiver') updateData.receiverSignature = signature.signature;
if (signature.type === 'verifier') updateData.verifierSignature = signature.signature;
}
return prisma.transportEvent.update({
where: { id },
data: updateData,
});
}
// Delete transport event
export async function deleteTransportEvent(id: string): Promise<TransportEvent> {
return prisma.transportEvent.delete({
where: { id },
});
}
// Get transport events with pagination
export async function getTransportEvents(
options: PaginationOptions = {},
filters?: {
eventType?: TransportEventType;
senderId?: string;
receiverId?: string;
status?: TransportStatus;
dateRange?: DateRangeFilter;
}
): Promise<PaginatedResult<TransportEvent>> {
const page = options.page || 1;
const limit = options.limit || 20;
const skip = (page - 1) * limit;
const where: Prisma.TransportEventWhereInput = {};
if (filters?.eventType) where.eventType = filters.eventType;
if (filters?.senderId) where.senderId = filters.senderId;
if (filters?.receiverId) where.receiverId = filters.receiverId;
if (filters?.status) where.status = filters.status;
if (filters?.dateRange) {
where.timestamp = {
gte: filters.dateRange.start,
lte: filters.dateRange.end,
};
}
const [events, total] = await Promise.all([
prisma.transportEvent.findMany({
where,
skip,
take: limit,
orderBy: { timestamp: 'desc' },
include: {
sender: true,
receiver: true,
},
}),
prisma.transportEvent.count({ where }),
]);
return createPaginatedResult(events, total, page, limit);
}
// Get transport events by plant
export async function getTransportEventsByPlant(plantId: string): Promise<TransportEvent[]> {
return prisma.transportEvent.findMany({
where: {
plants: {
some: { id: plantId },
},
},
orderBy: { timestamp: 'asc' },
include: {
sender: true,
receiver: true,
},
});
}
// Get plant journey (complete transport history)
export async function getPlantJourney(plantId: string) {
const plant = await prisma.plant.findUnique({
where: { id: plantId },
include: { owner: true },
});
if (!plant) return null;
const events = await prisma.transportEvent.findMany({
where: {
plants: {
some: { id: plantId },
},
},
orderBy: { timestamp: 'asc' },
include: {
sender: true,
receiver: true,
},
});
// Calculate totals
const totalFoodMiles = events.reduce((sum, e) => sum + e.distanceKm, 0);
const totalCarbonKg = events.reduce((sum, e) => sum + e.carbonFootprintKg, 0);
const daysInTransit = events.reduce((sum, e) => sum + e.durationMinutes / 1440, 0);
return {
plantId,
plant,
events,
totalFoodMiles,
totalCarbonKg,
daysInTransit,
generation: plant.generation,
};
}
// Get environmental impact summary
export async function getEnvironmentalImpact(filters?: {
userId?: string;
dateRange?: DateRangeFilter;
}) {
const where: Prisma.TransportEventWhereInput = {};
if (filters?.userId) {
where.OR = [
{ senderId: filters.userId },
{ receiverId: filters.userId },
];
}
if (filters?.dateRange) {
where.timestamp = {
gte: filters.dateRange.start,
lte: filters.dateRange.end,
};
}
const events = await prisma.transportEvent.findMany({ where });
const totalCarbonKg = events.reduce((sum, e) => sum + e.carbonFootprintKg, 0);
const totalFoodMiles = events.reduce((sum, e) => sum + e.distanceKm, 0);
// Breakdown by method
const breakdownByMethod: Record<string, { distance: number; carbon: number }> = {};
events.forEach(e => {
if (!breakdownByMethod[e.transportMethod]) {
breakdownByMethod[e.transportMethod] = { distance: 0, carbon: 0 };
}
breakdownByMethod[e.transportMethod].distance += e.distanceKm;
breakdownByMethod[e.transportMethod].carbon += e.carbonFootprintKg;
});
// Breakdown by event type
const breakdownByEventType: Record<string, { count: number; carbon: number }> = {};
events.forEach(e => {
if (!breakdownByEventType[e.eventType]) {
breakdownByEventType[e.eventType] = { count: 0, carbon: 0 };
}
breakdownByEventType[e.eventType].count += 1;
breakdownByEventType[e.eventType].carbon += e.carbonFootprintKg;
});
return {
totalCarbonKg,
totalFoodMiles,
eventCount: events.length,
breakdownByMethod,
breakdownByEventType,
};
}
// Get user's carbon footprint
export async function getUserCarbonFootprint(userId: string) {
const events = await prisma.transportEvent.findMany({
where: {
OR: [
{ senderId: userId },
{ receiverId: userId },
],
},
});
const sent = events.filter(e => e.senderId === userId);
const received = events.filter(e => e.receiverId === userId);
return {
totalCarbonKg: events.reduce((sum, e) => sum + e.carbonFootprintKg, 0),
totalDistanceKm: events.reduce((sum, e) => sum + e.distanceKm, 0),
sentEvents: sent.length,
receivedEvents: received.length,
sentCarbonKg: sent.reduce((sum, e) => sum + e.carbonFootprintKg, 0),
receivedCarbonKg: received.reduce((sum, e) => sum + e.carbonFootprintKg, 0),
};
}
// Seed Batch operations
export async function createSeedBatch(data: {
species: string;
variety?: string;
quantity: number;
quantityUnit?: string;
generation?: number;
germinationRate?: number;
purityPercentage?: number;
harvestDate?: Date;
expirationDate?: Date;
certifications?: string[];
}) {
return prisma.seedBatch.create({
data: {
...data,
quantityUnit: data.quantityUnit || 'seeds',
generation: data.generation || 0,
status: 'AVAILABLE',
},
});
}
export async function getSeedBatchById(id: string) {
return prisma.seedBatch.findUnique({
where: { id },
});
}
// Harvest Batch operations
export async function createHarvestBatch(data: {
produceType: string;
harvestType?: string;
grossWeight: number;
netWeight: number;
weightUnit?: string;
itemCount?: number;
qualityGrade?: string;
qualityNotes?: string;
packagingType?: string;
shelfLifeHours?: number;
cropBatchId?: string;
}) {
return prisma.harvestBatch.create({
data: {
...data,
harvestType: data.harvestType || 'full',
weightUnit: data.weightUnit || 'kg',
},
});
}
export async function getHarvestBatchById(id: string) {
return prisma.harvestBatch.findUnique({
where: { id },
include: {
transportEvents: true,
cropBatch: true,
},
});
}

150
lib/db/types.ts Normal file
View file

@ -0,0 +1,150 @@
/**
* Database Types and Utilities
* Type helpers for working with Prisma and the database
*/
import type { Prisma } from '@prisma/client';
// Re-export Prisma types for convenience
export type {
User,
Plant,
TransportEvent,
VerticalFarm,
GrowingZone,
CropBatch,
GrowingRecipe,
SeedBatch,
HarvestBatch,
ConsumerPreference,
DemandSignal,
SupplyCommitment,
MarketMatch,
SeasonalPlan,
DemandForecast,
PlantingRecommendation,
ResourceUsage,
FarmAnalytics,
AuditLog,
BlockchainBlock,
} from '@prisma/client';
// Common query options
export interface PaginationOptions {
page?: number;
limit?: number;
cursor?: string;
}
export interface SortOptions {
field: string;
direction: 'asc' | 'desc';
}
// Location-based query options
export interface LocationFilter {
latitude: number;
longitude: number;
radiusKm: number;
}
// Date range filter
export interface DateRangeFilter {
start: Date;
end: Date;
}
// Create input types for common operations
export type CreateUserInput = Prisma.UserCreateInput;
export type UpdateUserInput = Prisma.UserUpdateInput;
export type CreatePlantInput = Prisma.PlantCreateInput;
export type UpdatePlantInput = Prisma.PlantUpdateInput;
export type CreateTransportEventInput = Prisma.TransportEventCreateInput;
export type UpdateTransportEventInput = Prisma.TransportEventUpdateInput;
export type CreateVerticalFarmInput = Prisma.VerticalFarmCreateInput;
export type UpdateVerticalFarmInput = Prisma.VerticalFarmUpdateInput;
export type CreateCropBatchInput = Prisma.CropBatchCreateInput;
export type UpdateCropBatchInput = Prisma.CropBatchUpdateInput;
// Result types with includes
export type PlantWithOwner = Prisma.PlantGetPayload<{
include: { owner: true };
}>;
export type PlantWithLineage = Prisma.PlantGetPayload<{
include: {
owner: true;
parentPlant: true;
childPlants: true;
};
}>;
export type TransportEventWithParties = Prisma.TransportEventGetPayload<{
include: {
sender: true;
receiver: true;
plants: true;
};
}>;
export type VerticalFarmWithZones = Prisma.VerticalFarmGetPayload<{
include: {
owner: true;
zones: true;
cropBatches: true;
};
}>;
// Utility type for pagination results
export interface PaginatedResult<T> {
items: T[];
total: number;
page: number;
limit: number;
totalPages: number;
hasMore: boolean;
}
// Helper function to calculate distance between two points (Haversine formula)
export function calculateDistanceKm(
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number {
const R = 6371; // Earth's radius in km
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
function toRad(deg: number): number {
return deg * (Math.PI / 180);
}
// Helper to create pagination result
export function createPaginatedResult<T>(
items: T[],
total: number,
page: number,
limit: number
): PaginatedResult<T> {
const totalPages = Math.ceil(total / limit);
return {
items,
total,
page,
limit,
totalPages,
hasMore: page < totalPages,
};
}

199
lib/db/users.ts Normal file
View file

@ -0,0 +1,199 @@
/**
* User Database Service
* CRUD operations for users
*/
import prisma from './prisma';
import type { User, UserType, Prisma } from '@prisma/client';
import type { PaginationOptions, PaginatedResult } from './types';
import { createPaginatedResult } from './types';
// Create a new user
export async function createUser(data: {
email: string;
name: string;
walletAddress?: string;
passwordHash?: string;
avatarUrl?: string;
bio?: string;
latitude?: number;
longitude?: number;
address?: string;
city?: string;
country?: string;
userType?: UserType;
}): Promise<User> {
return prisma.user.create({
data: {
...data,
userType: data.userType || 'CONSUMER',
},
});
}
// Get user by ID
export async function getUserById(id: string): Promise<User | null> {
return prisma.user.findUnique({
where: { id },
});
}
// Get user by email
export async function getUserByEmail(email: string): Promise<User | null> {
return prisma.user.findUnique({
where: { email },
});
}
// Get user by wallet address
export async function getUserByWalletAddress(walletAddress: string): Promise<User | null> {
return prisma.user.findUnique({
where: { walletAddress },
});
}
// Update user
export async function updateUser(
id: string,
data: Prisma.UserUpdateInput
): Promise<User> {
return prisma.user.update({
where: { id },
data,
});
}
// Delete user
export async function deleteUser(id: string): Promise<User> {
return prisma.user.delete({
where: { id },
});
}
// Get all users with pagination
export async function getUsers(
options: PaginationOptions = {},
filters?: {
userType?: UserType;
city?: string;
country?: string;
}
): Promise<PaginatedResult<User>> {
const page = options.page || 1;
const limit = options.limit || 20;
const skip = (page - 1) * limit;
const where: Prisma.UserWhereInput = {};
if (filters?.userType) where.userType = filters.userType;
if (filters?.city) where.city = { contains: filters.city, mode: 'insensitive' };
if (filters?.country) where.country = { contains: filters.country, mode: 'insensitive' };
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.user.count({ where }),
]);
return createPaginatedResult(users, total, page, limit);
}
// Get users by type
export async function getUsersByType(userType: UserType): Promise<User[]> {
return prisma.user.findMany({
where: { userType },
orderBy: { name: 'asc' },
});
}
// Update last login
export async function updateLastLogin(id: string): Promise<User> {
return prisma.user.update({
where: { id },
data: { lastLoginAt: new Date() },
});
}
// Search users
export async function searchUsers(
query: string,
options: PaginationOptions = {}
): Promise<PaginatedResult<User>> {
const page = options.page || 1;
const limit = options.limit || 20;
const skip = (page - 1) * limit;
const where: Prisma.UserWhereInput = {
OR: [
{ name: { contains: query, mode: 'insensitive' } },
{ email: { contains: query, mode: 'insensitive' } },
{ city: { contains: query, mode: 'insensitive' } },
],
};
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
skip,
take: limit,
orderBy: { name: 'asc' },
}),
prisma.user.count({ where }),
]);
return createPaginatedResult(users, total, page, limit);
}
// Get user with their plants
export async function getUserWithPlants(id: string) {
return prisma.user.findUnique({
where: { id },
include: {
ownedPlants: {
orderBy: { registeredAt: 'desc' },
},
},
});
}
// Get user with their farms
export async function getUserWithFarms(id: string) {
return prisma.user.findUnique({
where: { id },
include: {
verticalFarms: {
include: {
zones: true,
},
},
},
});
}
// Get user statistics
export async function getUserStats(id: string) {
const [
plantCount,
farmCount,
transportEventsSent,
transportEventsReceived,
supplyCommitments,
] = await Promise.all([
prisma.plant.count({ where: { ownerId: id } }),
prisma.verticalFarm.count({ where: { ownerId: id } }),
prisma.transportEvent.count({ where: { senderId: id } }),
prisma.transportEvent.count({ where: { receiverId: id } }),
prisma.supplyCommitment.count({ where: { growerId: id } }),
]);
return {
plantCount,
farmCount,
transportEventsSent,
transportEventsReceived,
supplyCommitments,
};
}

View file

@ -15,9 +15,16 @@
"cy:open": "cypress open",
"cy:run": "cypress run",
"test:e2e": "start-server-and-test 'bun run preview' http://localhost:3001 cy:open",
"test:e2e:ci": "start-server-and-test 'bun run preview' http://localhost:3001 cy:run"
"test:e2e:ci": "start-server-and-test 'bun run preview' http://localhost:3001 cy:run",
"db:generate": "prisma generate",
"db:push": "prisma db push",
"db:migrate": "prisma migrate dev",
"db:migrate:prod": "prisma migrate deploy",
"db:seed": "bun run prisma/seed.ts",
"db:studio": "prisma studio"
},
"dependencies": {
"@prisma/client": "^5.7.0",
"@tailwindcss/forms": "^0.4.0",
"@tailwindcss/typography": "^0.5.1",
"@tanstack/react-query": "^4.0.10",
@ -41,6 +48,7 @@
"eslint-config-next": "^12.0.10",
"jest": "^29.5.0",
"postcss": "^8.4.5",
"prisma": "^5.7.0",
"tailwindcss": "^3.0.15",
"ts-jest": "^29.1.0",
"typescript": "^4.5.5"

1084
prisma/schema.prisma Normal file

File diff suppressed because it is too large Load diff

835
prisma/seed.ts Normal file
View file

@ -0,0 +1,835 @@
/**
* LocalGreenChain Database Seed Script
* Populates the database with development data
*
* Run with: bun run db:seed
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('Starting database seed...');
// Clean existing data (in reverse dependency order)
console.log('Cleaning existing data...');
await prisma.auditLog.deleteMany();
await prisma.blockchainBlock.deleteMany();
await prisma.marketMatch.deleteMany();
await prisma.plantingRecommendation.deleteMany();
await prisma.demandForecast.deleteMany();
await prisma.seasonalPlan.deleteMany();
await prisma.supplyCommitment.deleteMany();
await prisma.demandSignal.deleteMany();
await prisma.consumerPreference.deleteMany();
await prisma.farmAnalytics.deleteMany();
await prisma.resourceUsage.deleteMany();
await prisma.harvestBatch.deleteMany();
await prisma.cropBatch.deleteMany();
await prisma.growingRecipe.deleteMany();
await prisma.growingZone.deleteMany();
await prisma.verticalFarm.deleteMany();
await prisma.transportEvent.deleteMany();
await prisma.seedBatch.deleteMany();
await prisma.plant.deleteMany();
await prisma.user.deleteMany();
// Create users
console.log('Creating users...');
const users = await Promise.all([
prisma.user.create({
data: {
email: 'alice@localgreenchain.io',
name: 'Alice Green',
userType: 'GROWER',
latitude: 37.7749,
longitude: -122.4194,
city: 'San Francisco',
country: 'USA',
bio: 'Urban farmer specializing in leafy greens and microgreens',
walletAddress: '0x1234567890abcdef1234567890abcdef12345678',
},
}),
prisma.user.create({
data: {
email: 'bob@localgreenchain.io',
name: 'Bob Farmer',
userType: 'GROWER',
latitude: 37.8044,
longitude: -122.2712,
city: 'Oakland',
country: 'USA',
bio: 'Vertical farm operator with 5 years experience',
walletAddress: '0xabcdef1234567890abcdef1234567890abcdef12',
},
}),
prisma.user.create({
data: {
email: 'carol@localgreenchain.io',
name: 'Carol Consumer',
userType: 'CONSUMER',
latitude: 37.7849,
longitude: -122.4094,
city: 'San Francisco',
country: 'USA',
bio: 'Health-conscious consumer supporting local farms',
},
}),
prisma.user.create({
data: {
email: 'david@localgreenchain.io',
name: 'David Distributor',
userType: 'DISTRIBUTOR',
latitude: 37.7649,
longitude: -122.3994,
city: 'San Francisco',
country: 'USA',
bio: 'Local food distributor serving Bay Area restaurants',
},
}),
prisma.user.create({
data: {
email: 'eve@localgreenchain.io',
name: 'Eve Admin',
userType: 'ADMIN',
latitude: 37.7949,
longitude: -122.4294,
city: 'San Francisco',
country: 'USA',
bio: 'LocalGreenChain platform administrator',
},
}),
]);
console.log(`Created ${users.length} users`);
// Create plants
console.log('Creating plants...');
const plants = await Promise.all([
// Original tomato plant
prisma.plant.create({
data: {
commonName: 'Cherry Tomato',
scientificName: 'Solanum lycopersicum var. cerasiforme',
species: 'lycopersicum',
genus: 'Solanum',
family: 'Solanaceae',
propagationType: 'ORIGINAL',
generation: 0,
plantedDate: new Date('2024-03-15'),
status: 'MATURE',
latitude: 37.7749,
longitude: -122.4194,
city: 'San Francisco',
country: 'USA',
ownerId: users[0].id,
notes: 'Heirloom variety from local seed library',
images: ['tomato-seedling.jpg', 'tomato-mature.jpg'],
},
}),
// Basil plant
prisma.plant.create({
data: {
commonName: 'Sweet Basil',
scientificName: 'Ocimum basilicum',
species: 'basilicum',
genus: 'Ocimum',
family: 'Lamiaceae',
propagationType: 'SEED',
generation: 1,
plantedDate: new Date('2024-04-01'),
status: 'GROWING',
latitude: 37.7749,
longitude: -122.4194,
city: 'San Francisco',
country: 'USA',
ownerId: users[0].id,
notes: 'Italian large leaf variety',
},
}),
// Lettuce
prisma.plant.create({
data: {
commonName: 'Butterhead Lettuce',
scientificName: 'Lactuca sativa var. capitata',
species: 'sativa',
genus: 'Lactuca',
family: 'Asteraceae',
propagationType: 'SEED',
generation: 0,
plantedDate: new Date('2024-05-01'),
status: 'GROWING',
latitude: 37.8044,
longitude: -122.2712,
city: 'Oakland',
country: 'USA',
ownerId: users[1].id,
notes: 'Vertical farm grown with LED lighting',
},
}),
// Kale
prisma.plant.create({
data: {
commonName: 'Curly Kale',
scientificName: 'Brassica oleracea var. sabellica',
species: 'oleracea',
genus: 'Brassica',
family: 'Brassicaceae',
propagationType: 'SEED',
generation: 0,
plantedDate: new Date('2024-04-15'),
status: 'MATURE',
latitude: 37.8044,
longitude: -122.2712,
city: 'Oakland',
country: 'USA',
ownerId: users[1].id,
notes: 'Organic certified',
},
}),
// Microgreens
prisma.plant.create({
data: {
commonName: 'Sunflower Microgreens',
scientificName: 'Helianthus annuus',
species: 'annuus',
genus: 'Helianthus',
family: 'Asteraceae',
propagationType: 'SEED',
generation: 0,
plantedDate: new Date('2024-05-20'),
status: 'SPROUTED',
latitude: 37.7749,
longitude: -122.4194,
city: 'San Francisco',
country: 'USA',
ownerId: users[0].id,
notes: 'Fast-growing microgreen variety',
},
}),
]);
console.log(`Created ${plants.length} plants`);
// Create tomato clone (child plant)
const tomatoClone = await prisma.plant.create({
data: {
commonName: 'Cherry Tomato Clone',
scientificName: 'Solanum lycopersicum var. cerasiforme',
species: 'lycopersicum',
genus: 'Solanum',
family: 'Solanaceae',
parentPlantId: plants[0].id,
propagationType: 'CLONE',
generation: 1,
plantedDate: new Date('2024-04-20'),
status: 'GROWING',
latitude: 37.8044,
longitude: -122.2712,
city: 'Oakland',
country: 'USA',
ownerId: users[1].id,
notes: 'Cloned from Alice\'s heirloom tomato',
},
});
console.log('Created tomato clone');
// Create seed batches
console.log('Creating seed batches...');
const seedBatches = await Promise.all([
prisma.seedBatch.create({
data: {
species: 'Solanum lycopersicum',
variety: 'Cherry Heirloom',
quantity: 100,
quantityUnit: 'seeds',
generation: 0,
germinationRate: 0.92,
purityPercentage: 0.99,
harvestDate: new Date('2023-10-15'),
expirationDate: new Date('2026-10-15'),
certifications: ['organic', 'heirloom'],
status: 'AVAILABLE',
},
}),
prisma.seedBatch.create({
data: {
species: 'Lactuca sativa',
variety: 'Butterhead',
quantity: 500,
quantityUnit: 'seeds',
generation: 0,
germinationRate: 0.88,
harvestDate: new Date('2024-01-20'),
expirationDate: new Date('2027-01-20'),
certifications: ['organic', 'non_gmo'],
status: 'AVAILABLE',
},
}),
]);
console.log(`Created ${seedBatches.length} seed batches`);
// Create vertical farm
console.log('Creating vertical farms...');
const farm = await prisma.verticalFarm.create({
data: {
name: 'Oakland Urban Greens',
ownerId: users[1].id,
latitude: 37.8044,
longitude: -122.2712,
address: '123 Industrial Blvd',
city: 'Oakland',
country: 'USA',
timezone: 'America/Los_Angeles',
specs: {
totalAreaSqm: 500,
growingAreaSqm: 400,
numberOfLevels: 5,
ceilingHeightM: 4,
totalGrowingPositions: 2000,
currentActivePlants: 1500,
powerCapacityKw: 100,
waterStorageL: 5000,
backupPowerHours: 8,
certifications: ['gap', 'haccp'],
buildingType: 'warehouse',
insulation: 'high_efficiency',
},
environmentalControl: {
hvacUnits: [
{ id: 'hvac1', type: 'heat_pump', capacityKw: 20, status: 'running' },
{ id: 'hvac2', type: 'cooling', capacityKw: 15, status: 'running' },
],
co2Injection: { type: 'tank', capacityKg: 50, currentLevelKg: 35 },
},
lightingSystem: {
type: 'LED',
totalWattage: 50000,
efficacyUmolJ: 2.8,
},
automationLevel: 'SEMI_AUTOMATED',
status: 'OPERATIONAL',
operationalSince: new Date('2023-06-01'),
capacityUtilization: 0.75,
yieldEfficiency: 0.85,
energyEfficiencyScore: 0.78,
},
});
console.log('Created vertical farm');
// Create growing zones
console.log('Creating growing zones...');
const zones = await Promise.all([
prisma.growingZone.create({
data: {
name: 'Zone A - Leafy Greens',
farmId: farm.id,
level: 1,
areaSqm: 80,
lengthM: 10,
widthM: 8,
growingMethod: 'NFT',
plantPositions: 400,
currentCrop: 'Butterhead Lettuce',
plantingDate: new Date('2024-05-01'),
expectedHarvestDate: new Date('2024-05-28'),
environmentTargets: {
temperatureC: { min: 18, max: 24, target: 21 },
humidityPercent: { min: 60, max: 80, target: 70 },
co2Ppm: { min: 800, max: 1200, target: 1000 },
lightPpfd: { min: 200, max: 400, target: 300 },
lightHours: 16,
},
currentEnvironment: {
timestamp: new Date().toISOString(),
temperatureC: 21.5,
humidityPercent: 68,
co2Ppm: 950,
ppfd: 310,
waterTempC: 20,
ec: 1.4,
ph: 6.2,
},
status: 'GROWING',
},
}),
prisma.growingZone.create({
data: {
name: 'Zone B - Herbs',
farmId: farm.id,
level: 2,
areaSqm: 60,
lengthM: 10,
widthM: 6,
growingMethod: 'DWC',
plantPositions: 300,
currentCrop: 'Sweet Basil',
plantingDate: new Date('2024-05-10'),
expectedHarvestDate: new Date('2024-06-10'),
status: 'GROWING',
},
}),
prisma.growingZone.create({
data: {
name: 'Zone C - Microgreens',
farmId: farm.id,
level: 3,
areaSqm: 40,
growingMethod: 'RACK_SYSTEM',
plantPositions: 500,
status: 'EMPTY',
},
}),
]);
console.log(`Created ${zones.length} growing zones`);
// Create growing recipe
console.log('Creating growing recipes...');
const recipe = await prisma.growingRecipe.create({
data: {
name: 'Butterhead Lettuce - Fast Cycle',
cropType: 'Lettuce',
variety: 'Butterhead',
version: '2.0',
stages: [
{
name: 'Germination',
daysStart: 0,
daysEnd: 3,
temperature: { day: 20, night: 18 },
humidity: { day: 80, night: 85 },
lightHours: 18,
lightPpfd: 150,
},
{
name: 'Seedling',
daysStart: 4,
daysEnd: 10,
temperature: { day: 21, night: 18 },
humidity: { day: 70, night: 75 },
lightHours: 16,
lightPpfd: 250,
},
{
name: 'Vegetative',
daysStart: 11,
daysEnd: 24,
temperature: { day: 22, night: 18 },
humidity: { day: 65, night: 70 },
lightHours: 16,
lightPpfd: 350,
},
{
name: 'Harvest',
daysStart: 25,
daysEnd: 28,
temperature: { day: 18, night: 16 },
humidity: { day: 60, night: 65 },
lightHours: 12,
lightPpfd: 300,
},
],
expectedDays: 28,
expectedYieldGrams: 150,
expectedYieldPerSqm: 3.5,
requirements: {
positions: 1,
zoneType: 'NFT',
minimumPpfd: 200,
idealTemperatureC: 20,
},
source: 'INTERNAL',
author: 'Bob Farmer',
rating: 4.5,
timesUsed: 15,
},
});
console.log('Created growing recipe');
// Create crop batch
console.log('Creating crop batches...');
const cropBatch = await prisma.cropBatch.create({
data: {
farmId: farm.id,
zoneId: zones[0].id,
cropType: 'Butterhead Lettuce',
variety: 'Bibb',
recipeId: recipe.id,
seedBatchId: seedBatches[1].id,
plantCount: 400,
plantingDate: new Date('2024-05-01'),
expectedHarvestDate: new Date('2024-05-28'),
expectedYieldKg: 60,
currentStage: 'vegetative',
currentDay: 18,
healthScore: 92,
status: 'GROWING',
environmentLog: [
{
timestamp: new Date('2024-05-15').toISOString(),
readings: { temperatureC: 21, humidityPercent: 68, ppfd: 320 },
},
{
timestamp: new Date('2024-05-18').toISOString(),
readings: { temperatureC: 21.5, humidityPercent: 67, ppfd: 325 },
},
],
},
});
console.log('Created crop batch');
// Create transport events
console.log('Creating transport events...');
const transportEvents = await Promise.all([
prisma.transportEvent.create({
data: {
eventType: 'SEED_ACQUISITION',
fromLatitude: 37.7849,
fromLongitude: -122.4094,
fromLocationType: 'SEED_BANK',
fromFacilityName: 'Bay Area Seed Library',
toLatitude: 37.7749,
toLongitude: -122.4194,
toLocationType: 'FARM',
toFacilityName: 'Alice\'s Urban Farm',
distanceKm: 1.2,
durationMinutes: 15,
transportMethod: 'BICYCLE',
carbonFootprintKg: 0,
senderId: users[2].id,
receiverId: users[0].id,
status: 'VERIFIED',
notes: 'Picked up heirloom tomato seeds',
eventData: {
seedBatchId: seedBatches[0].id,
quantity: 50,
sourceType: 'seed_library',
},
},
}),
prisma.transportEvent.create({
data: {
eventType: 'DISTRIBUTION',
fromLatitude: 37.8044,
fromLongitude: -122.2712,
fromLocationType: 'VERTICAL_FARM',
fromFacilityName: 'Oakland Urban Greens',
toLatitude: 37.7849,
toLongitude: -122.4094,
toLocationType: 'MARKET',
toFacilityName: 'Ferry Building Farmers Market',
distanceKm: 8.5,
durationMinutes: 25,
transportMethod: 'ELECTRIC_TRUCK',
carbonFootprintKg: 0.51,
senderId: users[1].id,
receiverId: users[3].id,
status: 'DELIVERED',
notes: 'Weekly lettuce delivery',
eventData: {
batchIds: [cropBatch.id],
quantityKg: 45,
destinationType: 'market',
},
},
}),
]);
console.log(`Created ${transportEvents.length} transport events`);
// Create consumer preference
console.log('Creating consumer preferences...');
await prisma.consumerPreference.create({
data: {
consumerId: users[2].id,
latitude: 37.7849,
longitude: -122.4094,
maxDeliveryRadiusKm: 15,
city: 'San Francisco',
region: 'Bay Area',
dietaryType: ['vegetarian', 'flexitarian'],
allergies: [],
dislikes: ['cilantro'],
preferredCategories: ['leafy_greens', 'herbs', 'microgreens'],
preferredItems: [
{ produceType: 'Lettuce', category: 'leafy_greens', priority: 'must_have' },
{ produceType: 'Basil', category: 'herbs', priority: 'preferred' },
{ produceType: 'Kale', category: 'leafy_greens', priority: 'nice_to_have' },
],
certificationPreferences: ['organic', 'local'],
freshnessImportance: 5,
priceImportance: 3,
sustainabilityImportance: 5,
deliveryPreferences: {
method: ['farmers_market', 'home_delivery'],
frequency: 'weekly',
preferredDays: ['saturday', 'sunday'],
},
householdSize: 2,
weeklyBudget: 75,
currency: 'USD',
},
});
console.log('Created consumer preference');
// Create demand signal
console.log('Creating demand signals...');
const demandSignal = await prisma.demandSignal.create({
data: {
centerLat: 37.7849,
centerLon: -122.4094,
radiusKm: 20,
regionName: 'San Francisco Bay Area',
periodStart: new Date('2024-05-01'),
periodEnd: new Date('2024-05-31'),
seasonalPeriod: 'spring',
demandItems: [
{
produceType: 'Butterhead Lettuce',
category: 'leafy_greens',
weeklyDemandKg: 150,
monthlyDemandKg: 600,
consumerCount: 45,
aggregatePriority: 8,
urgency: 'this_week',
inSeason: true,
},
{
produceType: 'Sweet Basil',
category: 'herbs',
weeklyDemandKg: 30,
monthlyDemandKg: 120,
consumerCount: 32,
aggregatePriority: 7,
urgency: 'this_week',
inSeason: true,
},
],
totalConsumers: 78,
totalWeeklyDemandKg: 180,
confidenceLevel: 85,
currentSupplyKg: 120,
supplyGapKg: 60,
supplyStatus: 'SHORTAGE',
},
});
console.log('Created demand signal');
// Create supply commitment
console.log('Creating supply commitments...');
const supplyCommitment = await prisma.supplyCommitment.create({
data: {
growerId: users[1].id,
produceType: 'Butterhead Lettuce',
variety: 'Bibb',
committedQuantityKg: 60,
availableFrom: new Date('2024-05-28'),
availableUntil: new Date('2024-06-15'),
pricePerKg: 8.5,
currency: 'USD',
minimumOrderKg: 2,
bulkDiscountThreshold: 20,
bulkDiscountPercent: 10,
certifications: ['gap', 'local'],
freshnessGuaranteeHours: 24,
deliveryRadiusKm: 25,
deliveryMethods: ['grower_delivery', 'customer_pickup'],
status: 'AVAILABLE',
remainingKg: 60,
},
});
console.log('Created supply commitment');
// Create market match
console.log('Creating market matches...');
await prisma.marketMatch.create({
data: {
consumerId: users[2].id,
growerId: users[1].id,
demandSignalId: demandSignal.id,
supplyCommitmentId: supplyCommitment.id,
produceType: 'Butterhead Lettuce',
matchedQuantityKg: 4,
agreedPricePerKg: 8.5,
totalPrice: 34,
currency: 'USD',
deliveryDate: new Date('2024-05-30'),
deliveryMethod: 'farmers_market',
deliveryLatitude: 37.7955,
deliveryLongitude: -122.3937,
deliveryAddress: 'Ferry Building, San Francisco',
status: 'CONFIRMED',
},
});
console.log('Created market match');
// Create seasonal plan
console.log('Creating seasonal plans...');
await prisma.seasonalPlan.create({
data: {
growerId: users[1].id,
year: 2024,
season: 'summer',
location: {
latitude: 37.8044,
longitude: -122.2712,
hardinessZone: '10a',
},
growingCapacity: {
verticalFarmSqMeters: 400,
hydroponicUnits: 5,
},
plannedCrops: [
{
produceType: 'Butterhead Lettuce',
plantingDate: '2024-06-01',
quantity: 500,
expectedYieldKg: 75,
status: 'planned',
},
{
produceType: 'Sweet Basil',
plantingDate: '2024-06-15',
quantity: 300,
expectedYieldKg: 15,
status: 'planned',
},
],
expectedTotalYieldKg: 90,
expectedRevenue: 765,
status: 'DRAFT',
},
});
console.log('Created seasonal plan');
// Create audit logs
console.log('Creating audit logs...');
await Promise.all([
prisma.auditLog.create({
data: {
userId: users[0].id,
action: 'CREATE',
entityType: 'Plant',
entityId: plants[0].id,
newValue: { commonName: 'Cherry Tomato', status: 'SPROUTED' },
metadata: { source: 'web_app' },
},
}),
prisma.auditLog.create({
data: {
userId: users[1].id,
action: 'CREATE',
entityType: 'VerticalFarm',
entityId: farm.id,
newValue: { name: 'Oakland Urban Greens' },
metadata: { source: 'web_app' },
},
}),
prisma.auditLog.create({
data: {
userId: users[1].id,
action: 'UPDATE',
entityType: 'CropBatch',
entityId: cropBatch.id,
previousValue: { status: 'GERMINATING' },
newValue: { status: 'GROWING' },
metadata: { source: 'automated_agent' },
},
}),
]);
console.log('Created audit logs');
// Create blockchain blocks
console.log('Creating blockchain blocks...');
await Promise.all([
prisma.blockchainBlock.create({
data: {
index: 0,
timestamp: new Date('2024-01-01'),
previousHash: '0',
hash: '0000000000000000000000000000000000000000000000000000000000000000',
nonce: 0,
blockType: 'genesis',
content: { message: 'LocalGreenChain Genesis Block' },
},
}),
prisma.blockchainBlock.create({
data: {
index: 1,
timestamp: new Date('2024-03-15'),
previousHash: '0000000000000000000000000000000000000000000000000000000000000000',
hash: '1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890',
nonce: 42,
blockType: 'plant',
plantId: plants[0].id,
content: {
plantId: plants[0].id,
commonName: 'Cherry Tomato',
action: 'REGISTERED',
},
},
}),
]);
console.log('Created blockchain blocks');
// Create resource usage records
console.log('Creating resource usage records...');
await prisma.resourceUsage.create({
data: {
farmId: farm.id,
periodStart: new Date('2024-05-01'),
periodEnd: new Date('2024-05-15'),
electricityKwh: 1500,
electricityCostUsd: 225,
renewablePercent: 60,
peakDemandKw: 45,
waterUsageL: 3000,
waterCostUsd: 15,
waterRecycledPercent: 85,
co2UsedKg: 20,
co2CostUsd: 40,
nutrientsUsedL: 50,
nutrientCostUsd: 75,
kwhPerKgProduce: 25,
litersPerKgProduce: 50,
costPerKgProduce: 5.92,
},
});
console.log('Created resource usage records');
// Summary
console.log('\n=== Seed Complete ===');
console.log(`Users: ${users.length}`);
console.log(`Plants: ${plants.length + 1}`);
console.log(`Seed Batches: ${seedBatches.length}`);
console.log(`Vertical Farms: 1`);
console.log(`Growing Zones: ${zones.length}`);
console.log(`Growing Recipes: 1`);
console.log(`Crop Batches: 1`);
console.log(`Transport Events: ${transportEvents.length}`);
console.log('\nDatabase seeded successfully!');
}
main()
.catch((e) => {
console.error('Seed error:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});