blob: a665cd1b70b9b6972dd0aa7beb4808680da22c4b [file]
#!/usr/bin/env bash
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/common.sh"
ensure_docker_prerequisites
load_environment
if [[ -n "${SOURCE_DB_CONTAINER:-}" ]]; then
SOURCE_DB_CONTAINER="${SOURCE_DB_CONTAINER}"
else
SOURCE_DB_CONTAINER=""
if [[ -n "${SOURCE_DB_HOST:-}" ]]; then
SOURCE_DB_CONTAINER="$(docker compose -f "${COMPOSE_FILE}" ps -q "${SOURCE_DB_HOST}" 2>/dev/null || true)"
fi
SOURCE_DB_CONTAINER="${SOURCE_DB_CONTAINER:-fineract-db-1}"
fi
log() { echo "[bootstrap-source] $*"; }
fail() { echo "[bootstrap-source] ERROR: $*" >&2; exit 1; }
run_sql() {
local sql="$1"
docker exec -i "${SOURCE_DB_CONTAINER}" \
env PGPASSWORD="${SOURCE_BOOTSTRAP_PASSWORD}" \
psql -v ON_ERROR_STOP=1 \
-U "${SOURCE_BOOTSTRAP_USER}" \
-d "${SOURCE_DB_NAME}" \
-c "${sql}"
}
run_sql_stdin() {
docker exec -i "${SOURCE_DB_CONTAINER}" \
env PGPASSWORD="${SOURCE_BOOTSTRAP_PASSWORD}" \
psql -v ON_ERROR_STOP=1 \
-U "${SOURCE_BOOTSTRAP_USER}" \
-d "${SOURCE_DB_NAME}"
}
log "Checking connection to '${SOURCE_DB_CONTAINER}'..."
docker exec "${SOURCE_DB_CONTAINER}" \
env PGPASSWORD="${SOURCE_BOOTSTRAP_PASSWORD}" \
pg_isready -U "${SOURCE_BOOTSTRAP_USER}" -d postgres \
|| fail "Cannot reach ${SOURCE_DB_CONTAINER}. Is it running?"
log "Connection OK"
log "Creating compatibility views in schema '${SOURCE_DB_SCHEMA}'..."
run_sql_stdin <<SQL
BEGIN;
CREATE SCHEMA IF NOT EXISTS ${SOURCE_DB_SCHEMA};
-- m_office: no created_on_utc in real Fineract schema, use epoch fallback
CREATE OR REPLACE VIEW ${SOURCE_DB_SCHEMA}.m_office AS
SELECT
id, parent_id, hierarchy, external_id, name, opening_date,
'1970-01-01 00:00:00+00'::timestamptz AS created_on_utc,
'1970-01-01 00:00:00+00'::timestamptz AS last_modified_on_utc
FROM public.m_office;
-- m_currency: no audit columns in real Fineract schema
CREATE OR REPLACE VIEW ${SOURCE_DB_SCHEMA}.m_currency AS
SELECT
id, code, decimal_places, currency_multiplesof, display_symbol,
name, internationalized_name_code,
'1970-01-01 00:00:00+00'::timestamptz AS created_on_utc,
'1970-01-01 00:00:00+00'::timestamptz AS last_modified_on_utc
FROM public.m_currency;
CREATE OR REPLACE VIEW ${SOURCE_DB_SCHEMA}.m_client AS
SELECT
id, account_no, external_id, status_enum, activation_date,
office_joining_date, office_id, staff_id, gender_cv_id,
date_of_birth, legal_form_enum, submittedon_date, updated_on,
created_on_utc, last_modified_on_utc
FROM public.m_client;
-- m_product_loan: no created_on_utc in real Fineract schema
CREATE OR REPLACE VIEW ${SOURCE_DB_SCHEMA}.m_product_loan AS
SELECT
id, short_name, currency_code, currency_digits, currency_multiplesof,
principal_amount, min_principal_amount, max_principal_amount,
arrearstolerance_amount, name, description,
nominal_interest_rate_per_period, annual_nominal_interest_rate,
repay_every, repayment_period_frequency_enum, number_of_repayments,
overdue_days_for_npa, start_date, close_date,
'1970-01-01 00:00:00+00'::timestamptz AS created_on_utc,
'1970-01-01 00:00:00+00'::timestamptz AS last_modified_on_utc
FROM public.m_product_loan;
CREATE OR REPLACE VIEW ${SOURCE_DB_SCHEMA}.m_loan AS
SELECT
id, account_no, external_id, client_id, product_id,
loan_status_id, loan_type_enum, currency_code, currency_digits,
currency_multiplesof, principal_amount_proposed, principal_amount,
approved_principal, net_disbursal_amount,
annual_nominal_interest_rate, nominal_interest_rate_per_period,
interest_method_enum, interest_calculated_in_period_enum,
term_frequency, term_period_frequency_enum, repay_every,
repayment_period_frequency_enum, number_of_repayments,
amortization_method_enum, submittedon_date, approvedon_date,
expected_disbursedon_date, expected_firstrepaymenton_date,
disbursedon_date, expected_maturedon_date, maturedon_date,
principal_disbursed_derived, principal_repaid_derived,
principal_writtenoff_derived, principal_outstanding_derived,
interest_charged_derived, interest_repaid_derived,
interest_writtenoff_derived, interest_outstanding_derived,
fee_charges_outstanding_derived, penalty_charges_outstanding_derived,
total_expected_repayment_derived, total_repayment_derived,
total_writtenoff_derived, total_outstanding_derived,
loan_counter, is_npa,
created_on_utc, last_modified_on_utc
FROM public.m_loan;
-- Support both Fineract schemas: some expose submitted_on_date while others
-- require transaction_date as its compatibility value.
CREATE OR REPLACE VIEW ${SOURCE_DB_SCHEMA}.m_loan_transaction AS
SELECT
id, loan_id, office_id, is_reversed, transaction_type_enum,
transaction_date, amount, principal_portion_derived,
interest_portion_derived, fee_charges_portion_derived,
penalty_charges_portion_derived, overpayment_portion_derived,
outstanding_loan_balance_derived,
COALESCE(
(to_jsonb(m_loan_transaction) ->> 'submitted_on_date')::date,
transaction_date
) AS submitted_on_date,
created_on_utc, last_modified_on_utc
FROM public.m_loan_transaction;
CREATE OR REPLACE VIEW ${SOURCE_DB_SCHEMA}.m_delinquency_range AS
SELECT id, classification, min_age_days, max_age_days,
created_on_utc, last_modified_on_utc
FROM public.m_delinquency_range;
CREATE OR REPLACE VIEW ${SOURCE_DB_SCHEMA}.m_delinquency_bucket AS
SELECT id, name, created_on_utc, last_modified_on_utc
FROM public.m_delinquency_bucket;
CREATE OR REPLACE VIEW ${SOURCE_DB_SCHEMA}.m_delinquency_bucket_mappings AS
SELECT id, delinquency_range_id, delinquency_bucket_id,
created_on_utc, last_modified_on_utc
FROM public.m_delinquency_bucket_mappings;
CREATE OR REPLACE VIEW ${SOURCE_DB_SCHEMA}.m_loan_delinquency_tag_history AS
SELECT id, delinquency_range_id, loan_id, addedon_date, liftedon_date,
created_on_utc, last_modified_on_utc
FROM public.m_loan_delinquency_tag_history;
CREATE OR REPLACE VIEW ${SOURCE_DB_SCHEMA}.m_loan_repayment_schedule AS
SELECT id, loan_id, fromdate, duedate, installment,
principal_amount, principal_completed_derived, principal_writtenoff_derived,
interest_amount, interest_completed_derived, interest_writtenoff_derived, interest_waived_derived,
fee_charges_amount, fee_charges_completed_derived, fee_charges_writtenoff_derived, fee_charges_waived_derived,
penalty_charges_amount, penalty_charges_completed_derived, penalty_charges_writtenoff_derived, penalty_charges_waived_derived,
total_paid_in_advance_derived, total_paid_late_derived,
completed_derived, obligations_met_on_date, is_re_aged,
COALESCE(
(to_jsonb(m_loan_repayment_schedule) ->> 'last_modified_on_utc')::timestamptz,
(to_jsonb(m_loan_repayment_schedule) ->> 'lastmodified_date')::timestamptz
) AS lastmodified_date
FROM public.m_loan_repayment_schedule;
-- Fineract variants use either lastmodified_date or last_modified_on_utc.
-- Both views expose a TIMESTAMPTZ lastmodified_date cursor to the extractor.
-- batch_job_execution: real schema uses timestamp without time zone
-- Drop first because CREATE OR REPLACE VIEW cannot rename or reorder an
-- existing view's columns. Keeping this in the transaction makes a failed
-- bootstrap leave the previous compatibility schema untouched.
DROP VIEW IF EXISTS ${SOURCE_DB_SCHEMA}.batch_job_execution;
CREATE OR REPLACE VIEW ${SOURCE_DB_SCHEMA}.batch_job_execution AS
SELECT
bje.job_execution_id,
bji.job_name,
bje.status,
bje.start_time ::timestamptz AS start_time,
bje.end_time ::timestamptz AS end_time,
bje.exit_code,
bje.exit_message,
bje.create_time ::timestamptz AS created_on_utc,
bje.last_updated ::timestamptz AS last_modified_on_utc
FROM public.batch_job_execution bje
INNER JOIN public.batch_job_instance bji
ON bje.job_instance_id = bji.job_instance_id;
COMMIT;
SQL
log "Compatibility views created in schema '${SOURCE_DB_SCHEMA}'"
log "Creating replica user if not exists..."
run_sql "DO \$\$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${SOURCE_REPLICA_USER}') THEN
EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', '${SOURCE_REPLICA_USER}', '${SOURCE_REPLICA_PASSWORD}');
END IF;
END
\$\$;"
log "Granting read access to '${SOURCE_REPLICA_USER}'..."
run_sql_stdin <<SQL
GRANT USAGE ON SCHEMA public TO ${SOURCE_REPLICA_USER};
GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${SOURCE_REPLICA_USER};
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ${SOURCE_REPLICA_USER};
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO ${SOURCE_REPLICA_USER};
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO ${SOURCE_REPLICA_USER};
GRANT USAGE ON SCHEMA ${SOURCE_DB_SCHEMA} TO ${SOURCE_REPLICA_USER};
GRANT SELECT ON ALL TABLES IN SCHEMA ${SOURCE_DB_SCHEMA} TO ${SOURCE_REPLICA_USER};
SQL
log "Read access granted to '${SOURCE_REPLICA_USER}'"
log "=== Source bootstrap complete. You can now start the BI stack. ==="
log " docker compose up -d warehouse superset dbt extractor"