208 lines
7.1 KiB
PL/PgSQL
208 lines
7.1 KiB
PL/PgSQL
-- ============================================================================
|
|
-- ERP GENERIC - DATABASE PREREQUISITES
|
|
-- ============================================================================
|
|
-- Version: 1.0.0
|
|
-- Description: Extensions, common types, and utility functions
|
|
-- Execute: FIRST (before any schema)
|
|
-- ============================================================================
|
|
|
|
-- Enable required extensions
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- UUID generation
|
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- Password hashing
|
|
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- Trigram similarity (fuzzy search)
|
|
CREATE EXTENSION IF NOT EXISTS "unaccent"; -- Remove accents for search
|
|
|
|
-- ============================================================================
|
|
-- UTILITY FUNCTIONS
|
|
-- ============================================================================
|
|
|
|
-- Function: Update updated_at timestamp
|
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION update_updated_at_column() IS
|
|
'Generic trigger function to auto-update updated_at timestamp on row modification';
|
|
|
|
-- Function: Normalize text for search (remove accents, lowercase)
|
|
CREATE OR REPLACE FUNCTION normalize_search_text(p_text TEXT)
|
|
RETURNS TEXT AS $$
|
|
BEGIN
|
|
RETURN LOWER(unaccent(COALESCE(p_text, '')));
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
COMMENT ON FUNCTION normalize_search_text(TEXT) IS
|
|
'Normalize text for search by removing accents and converting to lowercase';
|
|
|
|
-- Function: Generate random alphanumeric code
|
|
CREATE OR REPLACE FUNCTION generate_random_code(p_length INTEGER DEFAULT 8)
|
|
RETURNS TEXT AS $$
|
|
DECLARE
|
|
chars TEXT := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
result TEXT := '';
|
|
i INTEGER;
|
|
BEGIN
|
|
FOR i IN 1..p_length LOOP
|
|
result := result || substr(chars, floor(random() * length(chars) + 1)::INTEGER, 1);
|
|
END LOOP;
|
|
RETURN result;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION generate_random_code(INTEGER) IS
|
|
'Generate random alphanumeric code of specified length (default 8)';
|
|
|
|
-- Function: Validate email format
|
|
CREATE OR REPLACE FUNCTION is_valid_email(p_email TEXT)
|
|
RETURNS BOOLEAN AS $$
|
|
BEGIN
|
|
RETURN p_email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$';
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
COMMENT ON FUNCTION is_valid_email(TEXT) IS
|
|
'Validate email format using regex';
|
|
|
|
-- Function: Validate phone number format (basic)
|
|
CREATE OR REPLACE FUNCTION is_valid_phone(p_phone TEXT)
|
|
RETURNS BOOLEAN AS $$
|
|
BEGIN
|
|
-- Basic validation: only digits, spaces, dashes, parentheses, plus sign
|
|
RETURN p_phone ~ '^[\d\s\-\(\)\+]+$' AND length(regexp_replace(p_phone, '[^\d]', '', 'g')) >= 7;
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
COMMENT ON FUNCTION is_valid_phone(TEXT) IS
|
|
'Validate phone number format (at least 7 digits)';
|
|
|
|
-- Function: Clean phone number (keep only digits)
|
|
CREATE OR REPLACE FUNCTION clean_phone(p_phone TEXT)
|
|
RETURNS TEXT AS $$
|
|
BEGIN
|
|
RETURN regexp_replace(COALESCE(p_phone, ''), '[^\d]', '', 'g');
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
COMMENT ON FUNCTION clean_phone(TEXT) IS
|
|
'Remove non-numeric characters from phone number';
|
|
|
|
-- Function: Calculate age from date
|
|
CREATE OR REPLACE FUNCTION calculate_age(p_birthdate DATE)
|
|
RETURNS INTEGER AS $$
|
|
BEGIN
|
|
IF p_birthdate IS NULL THEN
|
|
RETURN NULL;
|
|
END IF;
|
|
RETURN EXTRACT(YEAR FROM age(CURRENT_DATE, p_birthdate))::INTEGER;
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
COMMENT ON FUNCTION calculate_age(DATE) IS
|
|
'Calculate age in years from birthdate';
|
|
|
|
-- Function: Get current fiscal year start
|
|
CREATE OR REPLACE FUNCTION get_fiscal_year_start(p_date DATE DEFAULT CURRENT_DATE)
|
|
RETURNS DATE AS $$
|
|
BEGIN
|
|
-- Assuming fiscal year starts January 1st
|
|
-- Modify if different fiscal year start is needed
|
|
RETURN DATE_TRUNC('year', p_date)::DATE;
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
COMMENT ON FUNCTION get_fiscal_year_start(DATE) IS
|
|
'Get the start date of fiscal year for a given date (default: January 1st)';
|
|
|
|
-- Function: Round to decimal places
|
|
CREATE OR REPLACE FUNCTION round_currency(p_amount NUMERIC, p_decimals INTEGER DEFAULT 2)
|
|
RETURNS NUMERIC AS $$
|
|
BEGIN
|
|
RETURN ROUND(COALESCE(p_amount, 0), p_decimals);
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
COMMENT ON FUNCTION round_currency(NUMERIC, INTEGER) IS
|
|
'Round numeric value to specified decimal places (default 2 for currency)';
|
|
|
|
-- ============================================================================
|
|
-- COMMON TYPES
|
|
-- ============================================================================
|
|
|
|
-- Type: Money with currency (for multi-currency support)
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'money_amount') THEN
|
|
CREATE TYPE money_amount AS (
|
|
amount NUMERIC(15, 2),
|
|
currency_code CHAR(3)
|
|
);
|
|
END IF;
|
|
END $$;
|
|
|
|
COMMENT ON TYPE money_amount IS
|
|
'Composite type for storing monetary values with currency code';
|
|
|
|
-- Type: Address components
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'address_components') THEN
|
|
CREATE TYPE address_components AS (
|
|
street VARCHAR(255),
|
|
street2 VARCHAR(255),
|
|
city VARCHAR(100),
|
|
state VARCHAR(100),
|
|
zip VARCHAR(20),
|
|
country_code CHAR(2)
|
|
);
|
|
END IF;
|
|
END $$;
|
|
|
|
COMMENT ON TYPE address_components IS
|
|
'Composite type for address components (street, city, state, zip, country)';
|
|
|
|
-- ============================================================================
|
|
-- SCHEMA CREATION
|
|
-- ============================================================================
|
|
|
|
-- Create all schemas upfront to avoid circular dependency issues
|
|
CREATE SCHEMA IF NOT EXISTS auth;
|
|
CREATE SCHEMA IF NOT EXISTS core;
|
|
CREATE SCHEMA IF NOT EXISTS analytics;
|
|
CREATE SCHEMA IF NOT EXISTS financial;
|
|
CREATE SCHEMA IF NOT EXISTS inventory;
|
|
CREATE SCHEMA IF NOT EXISTS purchase;
|
|
CREATE SCHEMA IF NOT EXISTS sales;
|
|
CREATE SCHEMA IF NOT EXISTS projects;
|
|
CREATE SCHEMA IF NOT EXISTS system;
|
|
|
|
-- Set search path to include all schemas
|
|
ALTER DATABASE erp_generic SET search_path TO public, auth, core, analytics, financial, inventory, purchase, sales, projects, system;
|
|
|
|
-- Grant usage on schemas to public role (will be refined per-user later)
|
|
GRANT USAGE ON SCHEMA auth TO PUBLIC;
|
|
GRANT USAGE ON SCHEMA core TO PUBLIC;
|
|
GRANT USAGE ON SCHEMA analytics TO PUBLIC;
|
|
GRANT USAGE ON SCHEMA financial TO PUBLIC;
|
|
GRANT USAGE ON SCHEMA inventory TO PUBLIC;
|
|
GRANT USAGE ON SCHEMA purchase TO PUBLIC;
|
|
GRANT USAGE ON SCHEMA sales TO PUBLIC;
|
|
GRANT USAGE ON SCHEMA projects TO PUBLIC;
|
|
GRANT USAGE ON SCHEMA system TO PUBLIC;
|
|
|
|
-- ============================================================================
|
|
-- PREREQUISITES COMPLETE
|
|
-- ============================================================================
|
|
|
|
DO $$
|
|
BEGIN
|
|
RAISE NOTICE 'Prerequisites installed successfully!';
|
|
RAISE NOTICE 'Extensions: uuid-ossp, pgcrypto, pg_trgm, unaccent';
|
|
RAISE NOTICE 'Schemas created: auth, core, analytics, financial, inventory, purchase, sales, projects, system';
|
|
RAISE NOTICE 'Utility functions: 9 functions installed';
|
|
END $$;
|