feat: Goa GEL Blockchain e-Licensing Platform - Full Stack Implementation
Complete implementation of the Goa Government e-Licensing platform with: Backend: - NestJS API with JWT authentication - PostgreSQL database with Knex ORM - Redis caching and session management - MinIO document storage - Hyperledger Besu blockchain integration - Multi-department workflow system - Comprehensive API tests (266/282 passing) Frontend: - Angular 21 with standalone components - Angular Material + TailwindCSS UI - Visual workflow builder - Document upload with progress tracking - Blockchain explorer integration - Role-based dashboards (Admin, Department, Citizen) - E2E tests with Playwright (37 tests) Infrastructure: - Docker Compose orchestration - Blockscout blockchain explorer - Development and production configurations
This commit is contained in:
246
backend/src/database/migrations/20240101000000_initial_schema.ts
Normal file
246
backend/src/database/migrations/20240101000000_initial_schema.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import type { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// Enable UUID extension
|
||||
await knex.raw('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
|
||||
|
||||
// Applicants table
|
||||
await knex.schema.createTable('applicants', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
||||
table.string('digilocker_id', 255).notNullable().unique();
|
||||
table.string('name', 255).notNullable();
|
||||
table.string('email', 255).notNullable();
|
||||
table.string('phone', 20);
|
||||
table.string('wallet_address', 42);
|
||||
table.boolean('is_active').notNullable().defaultTo(true);
|
||||
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
||||
|
||||
table.index('digilocker_id', 'idx_applicant_digilocker');
|
||||
table.index('email', 'idx_applicant_email');
|
||||
});
|
||||
|
||||
// Departments table
|
||||
await knex.schema.createTable('departments', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
||||
table.string('code', 50).notNullable().unique();
|
||||
table.string('name', 255).notNullable();
|
||||
table.string('wallet_address', 42).unique();
|
||||
table.string('api_key_hash', 255);
|
||||
table.string('api_secret_hash', 255);
|
||||
table.string('webhook_url', 500);
|
||||
table.string('webhook_secret_hash', 255);
|
||||
table.boolean('is_active').notNullable().defaultTo(true);
|
||||
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
||||
|
||||
table.index('code', 'idx_department_code');
|
||||
table.index('is_active', 'idx_department_active');
|
||||
});
|
||||
|
||||
// Workflows table
|
||||
await knex.schema.createTable('workflows', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
||||
table.string('workflow_type', 100).notNullable().unique();
|
||||
table.string('name', 255).notNullable();
|
||||
table.text('description');
|
||||
table.integer('version').notNullable().defaultTo(1);
|
||||
table.jsonb('definition').notNullable();
|
||||
table.boolean('is_active').notNullable().defaultTo(true);
|
||||
table.uuid('created_by');
|
||||
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
||||
|
||||
table.index('workflow_type', 'idx_workflow_type');
|
||||
table.index('is_active', 'idx_workflow_active');
|
||||
});
|
||||
|
||||
// License Requests table
|
||||
await knex.schema.createTable('license_requests', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
||||
table.string('request_number', 50).notNullable().unique();
|
||||
table.bigInteger('token_id');
|
||||
table.uuid('applicant_id').notNullable().references('id').inTable('applicants').onDelete('CASCADE');
|
||||
table.string('request_type', 100).notNullable();
|
||||
table.uuid('workflow_id').references('id').inTable('workflows').onDelete('SET NULL');
|
||||
table.string('status', 50).notNullable().defaultTo('DRAFT');
|
||||
table.jsonb('metadata');
|
||||
table.string('current_stage_id', 100);
|
||||
table.string('blockchain_tx_hash', 66);
|
||||
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
||||
table.timestamp('submitted_at');
|
||||
table.timestamp('approved_at');
|
||||
|
||||
table.index('request_number', 'idx_request_number');
|
||||
table.index('applicant_id', 'idx_request_applicant');
|
||||
table.index('status', 'idx_request_status');
|
||||
table.index('request_type', 'idx_request_type');
|
||||
table.index('created_at', 'idx_request_created');
|
||||
table.index(['status', 'request_type'], 'idx_request_status_type');
|
||||
});
|
||||
|
||||
// Documents table
|
||||
await knex.schema.createTable('documents', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
||||
table.uuid('request_id').notNullable().references('id').inTable('license_requests').onDelete('CASCADE');
|
||||
table.string('doc_type', 100).notNullable();
|
||||
table.string('original_filename', 255).notNullable();
|
||||
table.integer('current_version').notNullable().defaultTo(1);
|
||||
table.string('current_hash', 66).notNullable();
|
||||
table.string('minio_bucket', 100).notNullable();
|
||||
table.boolean('is_active').notNullable().defaultTo(true);
|
||||
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
||||
|
||||
table.index('request_id', 'idx_document_request');
|
||||
table.index('doc_type', 'idx_document_type');
|
||||
});
|
||||
|
||||
// Document Versions table
|
||||
await knex.schema.createTable('document_versions', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
||||
table.uuid('document_id').notNullable().references('id').inTable('documents').onDelete('CASCADE');
|
||||
table.integer('version').notNullable();
|
||||
table.string('hash', 66).notNullable();
|
||||
table.string('minio_path', 500).notNullable();
|
||||
table.bigInteger('file_size').notNullable();
|
||||
table.string('mime_type', 100).notNullable();
|
||||
table.uuid('uploaded_by').notNullable();
|
||||
table.string('blockchain_tx_hash', 66);
|
||||
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
||||
|
||||
table.unique(['document_id', 'version'], { indexName: 'uq_document_version' });
|
||||
table.index('document_id', 'idx_docversion_document');
|
||||
});
|
||||
|
||||
// Approvals table
|
||||
await knex.schema.createTable('approvals', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
||||
table.uuid('request_id').notNullable().references('id').inTable('license_requests').onDelete('CASCADE');
|
||||
table.uuid('department_id').notNullable().references('id').inTable('departments').onDelete('CASCADE');
|
||||
table.string('status', 50).notNullable().defaultTo('PENDING');
|
||||
table.text('remarks');
|
||||
table.string('remarks_hash', 66);
|
||||
table.jsonb('reviewed_documents');
|
||||
table.string('blockchain_tx_hash', 66);
|
||||
table.boolean('is_active').notNullable().defaultTo(true);
|
||||
table.timestamp('invalidated_at');
|
||||
table.string('invalidation_reason', 255);
|
||||
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
||||
|
||||
table.index('request_id', 'idx_approval_request');
|
||||
table.index('department_id', 'idx_approval_department');
|
||||
table.index('status', 'idx_approval_status');
|
||||
table.index(['request_id', 'department_id'], 'idx_approval_request_dept');
|
||||
});
|
||||
|
||||
// Workflow States table
|
||||
await knex.schema.createTable('workflow_states', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
||||
table.uuid('request_id').notNullable().unique().references('id').inTable('license_requests').onDelete('CASCADE');
|
||||
table.string('current_stage_id', 100).notNullable();
|
||||
table.jsonb('completed_stages').notNullable().defaultTo('[]');
|
||||
table.jsonb('pending_approvals').notNullable().defaultTo('[]');
|
||||
table.jsonb('execution_log').notNullable().defaultTo('[]');
|
||||
table.timestamp('stage_started_at');
|
||||
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
||||
|
||||
table.index('request_id', 'idx_wfstate_request');
|
||||
});
|
||||
|
||||
// Webhooks table
|
||||
await knex.schema.createTable('webhooks', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
||||
table.uuid('department_id').notNullable().references('id').inTable('departments').onDelete('CASCADE');
|
||||
table.string('url', 500).notNullable();
|
||||
table.jsonb('events').notNullable();
|
||||
table.string('secret_hash', 255).notNullable();
|
||||
table.boolean('is_active').notNullable().defaultTo(true);
|
||||
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now());
|
||||
|
||||
table.index('department_id', 'idx_webhook_department');
|
||||
});
|
||||
|
||||
// Webhook Logs table
|
||||
await knex.schema.createTable('webhook_logs', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
||||
table.uuid('webhook_id').notNullable().references('id').inTable('webhooks').onDelete('CASCADE');
|
||||
table.string('event_type', 100).notNullable();
|
||||
table.jsonb('payload').notNullable();
|
||||
table.integer('response_status');
|
||||
table.text('response_body');
|
||||
table.integer('response_time');
|
||||
table.integer('retry_count').notNullable().defaultTo(0);
|
||||
table.string('status', 20).notNullable().defaultTo('PENDING');
|
||||
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
||||
|
||||
table.index('webhook_id', 'idx_webhooklog_webhook');
|
||||
table.index('event_type', 'idx_webhooklog_event');
|
||||
table.index('status', 'idx_webhooklog_status');
|
||||
table.index('created_at', 'idx_webhooklog_created');
|
||||
});
|
||||
|
||||
// Audit Logs table
|
||||
await knex.schema.createTable('audit_logs', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
||||
table.string('entity_type', 50).notNullable();
|
||||
table.uuid('entity_id').notNullable();
|
||||
table.string('action', 50).notNullable();
|
||||
table.string('actor_type', 50).notNullable();
|
||||
table.uuid('actor_id');
|
||||
table.jsonb('old_value');
|
||||
table.jsonb('new_value');
|
||||
table.string('ip_address', 45);
|
||||
table.text('user_agent');
|
||||
table.string('correlation_id', 100);
|
||||
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
||||
|
||||
table.index(['entity_type', 'entity_id'], 'idx_audit_entity');
|
||||
table.index('entity_type', 'idx_audit_entitytype');
|
||||
table.index('action', 'idx_audit_action');
|
||||
table.index('created_at', 'idx_audit_created');
|
||||
table.index('correlation_id', 'idx_audit_correlation');
|
||||
});
|
||||
|
||||
// Blockchain Transactions table
|
||||
await knex.schema.createTable('blockchain_transactions', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()'));
|
||||
table.string('tx_hash', 66).notNullable().unique();
|
||||
table.string('tx_type', 50).notNullable();
|
||||
table.string('related_entity_type', 50).notNullable();
|
||||
table.uuid('related_entity_id').notNullable();
|
||||
table.string('from_address', 42).notNullable();
|
||||
table.string('to_address', 42);
|
||||
table.string('status', 20).notNullable().defaultTo('PENDING');
|
||||
table.bigInteger('block_number');
|
||||
table.bigInteger('gas_used');
|
||||
table.text('error_message');
|
||||
table.timestamp('created_at').notNullable().defaultTo(knex.fn.now());
|
||||
table.timestamp('confirmed_at');
|
||||
|
||||
table.index('tx_hash', 'idx_bctx_hash');
|
||||
table.index('tx_type', 'idx_bctx_type');
|
||||
table.index('status', 'idx_bctx_status');
|
||||
table.index('related_entity_id', 'idx_bctx_entity');
|
||||
table.index('created_at', 'idx_bctx_created');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists('blockchain_transactions');
|
||||
await knex.schema.dropTableIfExists('audit_logs');
|
||||
await knex.schema.dropTableIfExists('webhook_logs');
|
||||
await knex.schema.dropTableIfExists('webhooks');
|
||||
await knex.schema.dropTableIfExists('workflow_states');
|
||||
await knex.schema.dropTableIfExists('approvals');
|
||||
await knex.schema.dropTableIfExists('document_versions');
|
||||
await knex.schema.dropTableIfExists('documents');
|
||||
await knex.schema.dropTableIfExists('license_requests');
|
||||
await knex.schema.dropTableIfExists('workflows');
|
||||
await knex.schema.dropTableIfExists('departments');
|
||||
await knex.schema.dropTableIfExists('applicants');
|
||||
}
|
||||
Reference in New Issue
Block a user