loan management

get_charset_collate(); $customers = $wpdb->prefix . 'lms_customers'; $loans = $wpdb->prefix . 'lms_loans'; $settings = $wpdb->prefix . 'lms_tenure_settings'; $investors = $wpdb->prefix . 'lms_investors'; $installments = $wpdb->prefix . 'lms_installments'; $payments = $wpdb->prefix . 'lms_payments'; $savings = $wpdb->prefix . 'lms_savings_transactions'; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( "CREATE TABLE $customers ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, customer_code VARCHAR(30) NOT NULL, name VARCHAR(150) NOT NULL, guardian_name VARCHAR(150) DEFAULT '', mobile VARCHAR(30) NOT NULL, address TEXT, aadhaar VARCHAR(30) DEFAULT '', pan VARCHAR(30) DEFAULT '', status VARCHAR(20) NOT NULL DEFAULT 'Active', created_at DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY customer_code (customer_code), KEY mobile (mobile) ) $charset;" ); dbDelta( "CREATE TABLE $loans ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, loan_code VARCHAR(30) NOT NULL, customer_id BIGINT UNSIGNED NOT NULL, loan_amount DECIMAL(12,2) NOT NULL, tenure_weeks INT NOT NULL, weekly_emi DECIMAL(12,2) NOT NULL, processing_fee DECIMAL(12,2) NOT NULL, processing_paid_amount DECIMAL(12,2) NOT NULL DEFAULT 0, total_emi DECIMAL(12,2) NOT NULL, total_collection DECIMAL(12,2) NOT NULL, loan_date DATE NOT NULL DEFAULT '0000-00-00', emi_start_date DATE NOT NULL DEFAULT '0000-00-00', loan_end_date DATE NOT NULL DEFAULT '0000-00-00', start_date DATE NOT NULL DEFAULT '0000-00-00', status VARCHAR(20) NOT NULL DEFAULT 'Active', created_at DATETIME NOT NULL, closed_at DATETIME NULL, PRIMARY KEY (id), UNIQUE KEY loan_code (loan_code), KEY customer_id (customer_id), KEY status (status) ) $charset;" ); /* Migrate existing loans to the separate Loan Date / EMI Start Date / Loan End Date fields. */ $wpdb->query( "UPDATE $loans SET emi_start_date = CASE WHEN emi_start_date = '0000-00-00' THEN start_date ELSE emi_start_date END WHERE start_date <> '0000-00-00'" ); $wpdb->query( "UPDATE $loans SET loan_date = CASE WHEN loan_date = '0000-00-00' THEN start_date ELSE loan_date END WHERE start_date <> '0000-00-00'" ); $wpdb->query( "UPDATE $loans SET loan_end_date = DATE_ADD(emi_start_date, INTERVAL GREATEST(tenure_weeks - 1, 0) WEEK) WHERE emi_start_date <> '0000-00-00' AND (loan_end_date = '0000-00-00' OR loan_end_date IS NULL)" ); /* v1.7.0: Existing loans already treated processing fee as part of the old total collection, so mark their legacy processing fee as already paid once. New loans start with processing_paid_amount = 0 and must be collected/tracked. */ if ( ! get_option( 'lms_v170_processing_migration_done' ) ) { $wpdb->query( "UPDATE $loans SET processing_paid_amount = processing_fee WHERE processing_paid_amount = 0" ); update_option( 'lms_v170_processing_migration_done', 1 ); } dbDelta( "CREATE TABLE $settings ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, loan_amount DECIMAL(12,2) NOT NULL DEFAULT 0, tenure_weeks INT NOT NULL, emi_rate DECIMAL(12,6) NOT NULL DEFAULT 0, emi_frequency VARCHAR(20) NOT NULL DEFAULT 'weekly', weekly_emi DECIMAL(12,2) NOT NULL DEFAULT 0, monthly_emi DECIMAL(12,2) NOT NULL DEFAULT 0, processing_fee DECIMAL(12,2) NOT NULL DEFAULT 0, processing_multiplier DECIMAL(12,4) NOT NULL DEFAULT 1, active TINYINT(1) NOT NULL DEFAULT 1, PRIMARY KEY (id), UNIQUE KEY tenure_weeks (tenure_weeks) ) $charset;" ); // v1.8.0: add the new Loan Settings fields to installations created by older versions. $wpdb->query( "UPDATE $settings SET weekly_emi = ROUND(loan_amount * emi_rate / 100, 2) WHERE weekly_emi = 0 AND loan_amount > 0" ); dbDelta( "CREATE TABLE $investors ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, investor_type VARCHAR(20) NOT NULL DEFAULT 'Investor', name VARCHAR(150) NOT NULL, email VARCHAR(190) NOT NULL DEFAULT '', phone VARCHAR(50) NOT NULL DEFAULT '', facebook VARCHAR(255) NOT NULL DEFAULT '', instagram VARCHAR(255) NOT NULL DEFAULT '', x_url VARCHAR(255) NOT NULL DEFAULT '', image_url TEXT, amount DECIMAL(12,2) NOT NULL DEFAULT 0, interest_rate_monthly DECIMAL(8,4) NOT NULL DEFAULT 0, term_months INT NOT NULL DEFAULT 0, start_date DATE NULL, end_date DATE NULL, notes TEXT, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY investor_type (investor_type) ) $charset;" ); $investor_transactions = $wpdb->prefix . 'lms_investor_transactions'; dbDelta( "CREATE TABLE $investor_transactions ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, investor_id BIGINT UNSIGNED NOT NULL, transaction_type VARCHAR(30) NOT NULL DEFAULT 'deposit', amount DECIMAL(12,2) NOT NULL DEFAULT 0, transaction_date DATE NOT NULL, method VARCHAR(30) NOT NULL DEFAULT 'Cash', note TEXT, created_by BIGINT UNSIGNED NOT NULL DEFAULT 0, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY investor_id (investor_id), KEY transaction_type (transaction_type), KEY transaction_date (transaction_date) ) $charset;" ); dbDelta( "CREATE TABLE $installments ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, loan_id BIGINT UNSIGNED NOT NULL, installment_no INT NOT NULL, due_date DATE NOT NULL, amount DECIMAL(12,2) NOT NULL, paid_amount DECIMAL(12,2) NOT NULL DEFAULT 0, paid_date DATE NULL, status VARCHAR(20) NOT NULL DEFAULT 'Due', PRIMARY KEY (id), KEY loan_id (loan_id), KEY due_date (due_date), KEY status (status) ) $charset;" ); dbDelta( "CREATE TABLE $payments ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, loan_id BIGINT UNSIGNED NOT NULL, installment_id BIGINT UNSIGNED NULL, payment_type VARCHAR(30) NOT NULL DEFAULT 'EMI', amount DECIMAL(12,2) NOT NULL, payment_date DATE NOT NULL, method VARCHAR(30) NOT NULL DEFAULT 'Cash', note TEXT, created_by BIGINT UNSIGNED NOT NULL DEFAULT 0, savings_transaction_id BIGINT UNSIGNED NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY loan_id (loan_id), KEY installment_id (installment_id), KEY payment_date (payment_date) ) $charset;" ); dbDelta( "CREATE TABLE $savings ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, customer_id BIGINT UNSIGNED NOT NULL, transaction_type VARCHAR(20) NOT NULL, withdrawal_type VARCHAR(20) NOT NULL DEFAULT '', amount DECIMAL(12,2) NOT NULL, transaction_date DATE NOT NULL, loan_id BIGINT UNSIGNED NULL, installment_id BIGINT UNSIGNED NULL, payment_id BIGINT UNSIGNED NULL, method VARCHAR(30) NOT NULL DEFAULT 'Cash', note TEXT, created_by BIGINT UNSIGNED NOT NULL DEFAULT 0, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY customer_id (customer_id), KEY transaction_type (transaction_type), KEY transaction_date (transaction_date), KEY loan_id (loan_id), KEY payment_id (payment_id) ) $charset;" ); $collectors = $wpdb->prefix . 'lms_emi_collectors'; $collector_transactions = $wpdb->prefix . 'lms_emi_collector_transactions'; dbDelta( "CREATE TABLE $collectors ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NULL, name VARCHAR(150) NOT NULL, mobile VARCHAR(50) NOT NULL DEFAULT '', address TEXT, status VARCHAR(20) NOT NULL DEFAULT 'Active', created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY status (status) ) $charset;" ); dbDelta( "CREATE TABLE $collector_transactions ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, collector_id BIGINT UNSIGNED NOT NULL, transaction_type VARCHAR(30) NOT NULL, amount DECIMAL(12,2) NOT NULL DEFAULT 0, transaction_date DATE NOT NULL, method VARCHAR(30) NOT NULL DEFAULT 'Cash', reference_payment_id BIGINT UNSIGNED NULL, transfer_to_collector_id BIGINT UNSIGNED NULL, transfer_group_key VARCHAR(64) NULL, note TEXT, created_by BIGINT UNSIGNED NOT NULL DEFAULT 0, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY collector_id (collector_id), KEY transaction_type (transaction_type), KEY transaction_date (transaction_date), KEY reference_payment_id (reference_payment_id) ) $charset;" ); /* v2.2.5: attach EMI/processing collections to custom EMI collector profiles. */ /* Default rules supplied by the business owner. 46 weeks: 2.5% weekly EMI rate, fee = 1 x EMI. 92 weeks: 1.38125% weekly EMI rate, fee = 2 x EMI. Example: 80,000 x 1.38125% = 1,105; fee = 2,210. */ if ( (int) $wpdb->get_var( "SELECT COUNT(*) FROM $settings" ) === 0 ) { $wpdb->insert( $settings, array( 'tenure_weeks' => 46, 'emi_rate' => 2.5, 'processing_multiplier' => 1, 'active' => 1, ), array( '%d', '%f', '%f', '%d' ) ); $wpdb->insert( $settings, array( 'tenure_weeks' => 92, 'emi_rate' => 1.38125, 'processing_multiplier' => 2, 'active' => 1, ), array( '%d', '%f', '%f', '%d' ) ); } $admin = get_role( 'administrator' ); if ( $admin ) { foreach ( array( 'lms_view_dashboard', 'lms_manage_customers', 'lms_edit_customer', 'lms_manage_loans', 'lms_collect_emi', 'lms_manage_investors', 'lms_manage_settings', ) as $cap ) { $admin->add_cap( $cap ); } } } register_activation_hook( __FILE__, 'lms_activate' ); /* ========================================================= * FOUNDER / INVESTOR DATA MIGRATION + HELPERS * ========================================================= */ function lms_ensure_investor_schema() { global $wpdb; if ( get_option( 'lms_investor_v210_schema_done' ) ) { return; } require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $charset = $wpdb->get_charset_collate(); $table = $wpdb->prefix . 'lms_investors'; $tx = $wpdb->prefix . 'lms_investor_transactions'; dbDelta( "CREATE TABLE $table ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, investor_type VARCHAR(20) NOT NULL DEFAULT 'Investor', name VARCHAR(150) NOT NULL, email VARCHAR(190) NOT NULL DEFAULT '', phone VARCHAR(50) NOT NULL DEFAULT '', facebook VARCHAR(255) NOT NULL DEFAULT '', instagram VARCHAR(255) NOT NULL DEFAULT '', x_url VARCHAR(255) NOT NULL DEFAULT '', image_url TEXT, amount DECIMAL(12,2) NOT NULL DEFAULT 0, interest_rate_monthly DECIMAL(8,4) NOT NULL DEFAULT 0, term_months INT NOT NULL DEFAULT 0, start_date DATE NULL, end_date DATE NULL, notes TEXT, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY investor_type (investor_type) ) $charset;" ); $collector_columns = $wpdb->get_col( "SHOW COLUMNS FROM $collectors", 0 ); if ( ! in_array( 'user_id', $collector_columns, true ) ) { $wpdb->query( "ALTER TABLE $collectors ADD COLUMN user_id BIGINT UNSIGNED NULL AFTER id" ); } dbDelta( "CREATE TABLE $tx ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, investor_id BIGINT UNSIGNED NOT NULL, transaction_type VARCHAR(30) NOT NULL DEFAULT 'deposit', amount DECIMAL(12,2) NOT NULL DEFAULT 0, transaction_date DATE NOT NULL, method VARCHAR(30) NOT NULL DEFAULT 'Cash', note TEXT, created_by BIGINT UNSIGNED NOT NULL DEFAULT 0, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY investor_id (investor_id), KEY transaction_type (transaction_type), KEY transaction_date (transaction_date) ) $charset;" ); // Preserve the old amount field as an opening deposit exactly once. $legacy_rows = $wpdb->get_results( "SELECT * FROM $table WHERE amount > 0 ORDER BY id ASC" ); foreach ( $legacy_rows as $row ) { $has_tx = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $tx WHERE investor_id=%d", $row->id ) ); if ( ! $has_tx ) { $wpdb->insert( $tx, array( 'investor_id' => (int) $row->id, 'transaction_type' => 'deposit', 'amount' => (float) $row->amount, 'transaction_date' => current_time( 'Y-m-d' ), 'method' => 'Cash', 'note' => 'Opening balance migrated from previous version', 'created_by' => get_current_user_id(), 'created_at' => current_time( 'mysql' ), ), array( '%d','%s','%f','%s','%s','%s','%d','%s' ) ); } } update_option( 'lms_investor_v210_schema_done', 1 ); } add_action( 'admin_init', 'lms_ensure_investor_schema', 5 ); function lms_investor_history_edit_delete_enabled() { return (bool) get_option( 'lms_investor_history_edit_delete_enabled', 1 ); } function lms_collector_account_edit_delete_enabled() { return (bool) get_option( 'lms_collector_account_edit_delete_enabled', 1 ); } /* ========================================================= * EMI / ORGANIZATION BANK CONTROLS * ========================================================= */ function lms_emi_show_collector_count() { return (bool) get_option( 'lms_emi_show_collector_count', 1 ); } function lms_emi_show_collector_manage() { return (bool) get_option( 'lms_emi_show_collector_manage', 1 ); } function lms_emi_show_collector_actions() { return (bool) get_option( 'lms_emi_show_collector_actions', 1 ); } function lms_emi_show_collector_history() { return (bool) get_option( 'lms_emi_show_collector_history', 1 ); } function lms_emi_show_processing_fee() { return (bool) get_option( 'lms_emi_show_processing_fee', 1 ); } function lms_emi_show_collection() { return (bool) get_option( 'lms_emi_show_collection', 1 ); } function lms_emi_show_bulk() { return (bool) get_option( 'lms_emi_show_bulk', 1 ); } function lms_emi_show_org_bank() { return (bool) get_option( 'lms_emi_show_org_bank', 1 ); } function lms_emi_show_org_bank_actions() { return (bool) get_option( 'lms_emi_show_org_bank_actions', 1 ); } function lms_emi_org_bank_edit_delete_enabled() { return (bool) get_option( 'lms_emi_org_bank_edit_delete_enabled', 1 ); } function lms_emi_org_bank_funding_enabled() { return (bool) get_option( 'lms_emi_org_bank_funding_enabled', 0 ); } function lms_organization_name() { return (string) get_option( 'lms_organization_name', 'My Organization' ); } function lms_investor_transactions_total( $investor_id, $type = '' ) { global $wpdb; $tx = $wpdb->prefix . 'lms_investor_transactions'; if ( in_array( $type, array( 'deposit', 'withdrawal', 'capital_withdrawal', 'interest_payout' ), true ) ) { return (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(amount),0) FROM $tx WHERE investor_id=%d AND transaction_type=%s", $investor_id, $type ) ); } return (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(CASE WHEN transaction_type='deposit' THEN amount WHEN transaction_type IN ('withdrawal','capital_withdrawal','interest_payout') THEN -amount ELSE 0 END),0) FROM $tx WHERE investor_id=%d", $investor_id ) ); } function lms_investor_daily_interest_between( $principal, $start, $end, $monthly_rate ) { $principal = (float) $principal; $monthly_rate = (float) $monthly_rate; if ( $principal <= 0 || $monthly_rate <= 0 || ! $start || ! $end ) { return 0; } try { $cursor = new DateTime( $start ); $finish = new DateTime( $end ); } catch ( Exception $e ) { return 0; } if ( $finish <= $cursor ) { return 0; } $total = 0.0; // Count each calendar day from deposit date up to, but not including, the end date. while ( $cursor < $finish ) { $days_in_month = (int) $cursor->format( 't' ); $month_end = new DateTime( $cursor->format('Y-m-t') . ' 00:00:00' ); $month_end->modify( '+1 day' ); $segment_end = $finish < $month_end ? $finish : $month_end; $days = (int) $cursor->diff( $segment_end )->days; if ( $days > 0 ) { $total += $principal * $monthly_rate / 100 * ( $days / $days_in_month ); } $cursor = $segment_end; } return $total; } function lms_investor_accrued_interest_at( $investor_id, $as_of = null ) { global $wpdb; $investors = $wpdb->prefix . 'lms_investors'; $tx_table = $wpdb->prefix . 'lms_investor_transactions'; $investor = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $investors WHERE id=%d", $investor_id ) ); if ( ! $investor || $investor->investor_type !== 'Investor' ) { return 0; } $as_of = $as_of ?: current_time( 'Y-m-d' ); $txs = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $tx_table WHERE investor_id=%d AND transaction_date<=%s ORDER BY transaction_date ASC, id ASC", $investor_id, $as_of ) ); $lots = array(); $gross_interest = 0.0; $interest_paid = 0.0; foreach ( $txs as $t ) { $amount = max( 0, (float) $t->amount ); if ( $t->transaction_type === 'deposit' ) { $lots[] = array( 'amount' => $amount, 'date' => $t->transaction_date ); } elseif ( in_array( $t->transaction_type, array('capital_withdrawal','withdrawal'), true ) ) { $left = $amount; foreach ( $lots as $i => &$lot ) { if ( $left <= 0 ) { break; } $take = min( $left, $lot['amount'] ); $gross_interest += lms_investor_daily_interest_between( $take, $lot['date'], $t->transaction_date, $investor->interest_rate_monthly ); $lot['amount'] -= $take; $left -= $take; } unset( $lot ); } elseif ( $t->transaction_type === 'interest_payout' ) { $interest_paid += $amount; } } foreach ( $lots as $lot ) { if ( $lot['amount'] > 0 ) { $gross_interest += lms_investor_daily_interest_between( $lot['amount'], $lot['date'], $as_of, $investor->interest_rate_monthly ); } } return max( 0, round( $gross_interest - $interest_paid, 2 ) ); } function lms_investor_financials( $investor_id, $as_of = null ) { global $wpdb; $investors = $wpdb->prefix . 'lms_investors'; $tx_table = $wpdb->prefix . 'lms_investor_transactions'; $investor = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $investors WHERE id=%d", $investor_id ) ); if ( ! $investor ) { return array( 'invested' => 0, 'withdrawn' => 0, 'capital_withdrawn' => 0, 'interest_paid' => 0, 'balance' => 0, 'interest' => 0, 'total' => 0, 'days' => 0, 'investor' => null ); } $as_of = $as_of ?: current_time( 'Y-m-d' ); $deposit = (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(amount),0) FROM $tx_table WHERE investor_id=%d AND transaction_type='deposit' AND transaction_date<=%s", $investor_id, $as_of ) ); $capital_withdrawn = (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(amount),0) FROM $tx_table WHERE investor_id=%d AND transaction_type IN ('capital_withdrawal','withdrawal') AND transaction_date<=%s", $investor_id, $as_of ) ); $interest_paid = (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(amount),0) FROM $tx_table WHERE investor_id=%d AND transaction_type='interest_payout' AND transaction_date<=%s", $investor_id, $as_of ) ); $balance = max( 0, $deposit - $capital_withdrawn ); $interest = $investor->investor_type === 'Investor' ? lms_investor_accrued_interest_at( $investor_id, $as_of ) : 0; $start = $investor->start_date ?: $as_of; try { $days = max( 0, ( new DateTime( $start ) )->diff( new DateTime( $as_of ) )->days ); } catch ( Exception $e ) { $days = 0; } return array( 'invested' => $deposit, 'withdrawn' => $capital_withdrawn + $interest_paid, 'capital_withdrawn' => $capital_withdrawn, 'interest_paid' => $interest_paid, 'balance' => $balance, 'interest' => $interest, 'total' => $balance + $interest, 'days' => $days, 'investor' => $investor, ); } /* ========================================================= * HELPERS * ========================================================= */ function lms_money( $amount ) { return '₹' . number_format_i18n( (float) $amount, 2 ); } function lms_admin_only() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( 'Only administrators can access this section.' ); } } function lms_customer_identity_controls_enabled() { return (bool) get_option( 'lms_customer_identity_edit_delete_enabled', false ); } function lms_loan_id_edit_enabled() { return (bool) get_option( 'lms_loan_id_edit_enabled', false ); } function lms_next_customer_code() { global $wpdb; $table = $wpdb->prefix . 'lms_customers'; // New customers always receive the next number after the highest existing code. // Deleted gaps are NOT auto-reused; they can be assigned manually from the profile editor. $max = (int) $wpdb->get_var( "SELECT MAX(CAST(SUBSTRING(customer_code, 5) AS UNSIGNED)) FROM $table WHERE customer_code REGEXP '^CUS-[0-9]+$'" ); $n = max( 1, $max + 1 ); return 'CUS-' . str_pad( $n, 5, '0', STR_PAD_LEFT ); } function lms_next_customer_id() { global $wpdb; $table = $wpdb->prefix . 'lms_customers'; // New customers always get the next serial number after the highest existing ID. // Deleted gaps are intentionally left alone and may be assigned manually by an admin. $max = (int) $wpdb->get_var( "SELECT MAX(id) FROM $table" ); return max( 1, $max + 1 ); } function lms_available_customer_ids() { global $wpdb; $table = $wpdb->prefix . 'lms_customers'; $ids = array_map( 'intval', (array) $wpdb->get_col( "SELECT id FROM $table WHERE id > 0 ORDER BY id ASC" ) ); if ( empty( $ids ) ) { return array(); } $max = max( $ids ); $used = array_fill_keys( $ids, true ); $gaps = array(); for ( $i = 1; $i < $max; $i++ ) { if ( ! isset( $used[ $i ] ) ) { $gaps[] = $i; } } return $gaps; } function lms_available_customer_codes() { global $wpdb; $table = $wpdb->prefix . 'lms_customers'; $rows = (array) $wpdb->get_col( "SELECT customer_code FROM $table WHERE customer_code REGEXP '^CUS-[0-9]+$' ORDER BY customer_code ASC" ); $used = array(); $max = 0; foreach ( $rows as $code ) { if ( preg_match( '/^CUS-(\d+)$/', (string) $code, $m ) ) { $n = (int) $m[1]; $used[ $n ] = true; $max = max( $max, $n ); } } $gaps = array(); for ( $i = 1; $i < $max; $i++ ) { if ( ! isset( $used[ $i ] ) ) { $gaps[] = 'CUS-' . str_pad( $i, 5, '0', STR_PAD_LEFT ); } } return $gaps; } function lms_next_loan_code() { global $wpdb; $table = $wpdb->prefix . 'lms_loans'; $codes = $wpdb->get_col( "SELECT loan_code FROM $table WHERE loan_code LIKE 'LN-%' ORDER BY loan_code ASC" ); $used = array(); foreach ( $codes as $code ) { if ( preg_match( '/^LN-(\d+)$/', (string) $code, $m ) ) { $used[(int) $m[1]] = true; } } $n = 1; while ( isset( $used[$n] ) ) { $n++; } return 'LN-' . str_pad( $n, 5, '0', STR_PAD_LEFT ); } function lms_refresh_installment_statuses() { global $wpdb; $table = $wpdb->prefix . 'lms_installments'; $wpdb->query( "UPDATE $table SET status = 'Overdue' WHERE paid_amount < amount AND due_date < CURDATE() AND status <> 'Paid'" ); $wpdb->query( "UPDATE $table SET status = 'Due' WHERE paid_amount < amount AND due_date >= CURDATE() AND status <> 'Paid'" ); } function lms_recalculate_installment_payment_state( $installment_id ) { global $wpdb; $installments = $wpdb->prefix . 'lms_installments'; $payments = $wpdb->prefix . 'lms_payments'; $installment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $installments WHERE id=%d", $installment_id ) ); if ( ! $installment ) { return false; } $paid_amount = (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(amount),0) FROM $payments WHERE installment_id=%d", $installment_id ) ); $paid_amount = min( max( $paid_amount, 0 ), (float) $installment->amount ); $last_payment_date = $wpdb->get_var( $wpdb->prepare( "SELECT payment_date FROM $payments WHERE installment_id=%d ORDER BY id DESC LIMIT 1", $installment_id ) ); if ( $paid_amount >= (float) $installment->amount ) { $status = 'Paid'; } elseif ( $installment->due_date < current_time( 'Y-m-d' ) ) { $status = 'Overdue'; } else { $status = $paid_amount > 0 ? 'Partial' : 'Due'; } $wpdb->update( $installments, array( 'paid_amount' => $paid_amount, 'paid_date' => $last_payment_date ? $last_payment_date : null, 'status' => $status, ), array( 'id' => $installment_id ), array( '%f', '%s', '%s' ), array( '%d' ) ); return true; } function lms_recalculate_processing_fee_state( $loan_id ) { global $wpdb; $loans = $wpdb->prefix . 'lms_loans'; $payments = $wpdb->prefix . 'lms_payments'; $loan = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $loans WHERE id=%d", $loan_id ) ); if ( ! $loan ) { return false; } $paid = (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(amount),0) FROM $payments WHERE loan_id=%d AND payment_type='Processing Fee'", $loan_id ) ); $paid = min( max( $paid, 0 ), (float) $loan->processing_fee ); $wpdb->update( $loans, array( 'processing_paid_amount' => $paid ), array( 'id' => $loan_id ), array( '%f' ), array( '%d' ) ); return true; } function lms_get_processing_fee_paid( $loan_id ) { global $wpdb; $payments = $wpdb->prefix . 'lms_payments'; return (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(amount),0) FROM $payments WHERE loan_id=%d AND payment_type='Processing Fee'", $loan_id ) ); } function lms_get_savings_balance( $customer_id ) { global $wpdb; $table = $wpdb->prefix . 'lms_savings_transactions'; return (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(CASE WHEN transaction_type='deposit' THEN amount ELSE -amount END),0) FROM $table WHERE customer_id=%d", $customer_id ) ); } function lms_savings_add_transaction( $data ) { global $wpdb; $table = $wpdb->prefix . 'lms_savings_transactions'; $defaults = array( 'customer_id' => 0, 'transaction_type' => 'deposit', 'withdrawal_type' => '', 'amount' => 0, 'transaction_date' => current_time( 'Y-m-d' ), 'loan_id' => null, 'installment_id' => null, 'payment_id' => null, 'method' => 'Cash', 'note' => '', 'created_by' => get_current_user_id(), 'created_at' => current_time( 'mysql' ), ); $data = wp_parse_args( $data, $defaults ); $ok = $wpdb->insert( $table, $data, array( '%d','%s','%s','%f','%s','%d','%d','%d','%s','%s','%d','%s' ) ); return false === $ok ? false : (int) $wpdb->insert_id; } /* ========================================================= * SAVINGS HISTORY EDIT / DELETE CONTROL * ========================================================= */ function lms_savings_history_edit_delete_enabled() { return (bool) get_option( 'lms_savings_history_edit_delete_enabled', 1 ); } /* ========================================================= * EMI PAYMENT HISTORY CONTROLS * ========================================================= */ function lms_emi_history_edit_enabled() { return (bool) get_option( 'lms_emi_history_edit_enabled', 1 ); } function lms_emi_history_delete_enabled() { return (bool) get_option( 'lms_emi_history_delete_enabled', 1 ); } function lms_emi_history_mark_unpaid_enabled() { return (bool) get_option( 'lms_emi_history_mark_unpaid_enabled', 1 ); } function lms_emi_history_bulk_enabled() { return (bool) get_option( 'lms_emi_history_bulk_enabled', 1 ); } function lms_get_customer_next_emis( $customer_id ) { global $wpdb; $installments = $wpdb->prefix . 'lms_installments'; $loans = $wpdb->prefix . 'lms_loans'; $rows = $wpdb->get_results( $wpdb->prepare( "SELECT i.*, l.loan_code, l.status AS loan_status FROM $installments i INNER JOIN $loans l ON l.id=i.loan_id WHERE l.customer_id=%d AND i.paid_amount < i.amount ORDER BY i.due_date ASC, i.id ASC", $customer_id ) ); $next = array(); foreach ( $rows as $row ) { if ( ! isset( $next[ (int) $row->loan_id ] ) ) { $next[ (int) $row->loan_id ] = $row; } } return array_values( $next ); } function lms_maybe_close_loan( $loan_id ) { global $wpdb; $loans = $wpdb->prefix . 'lms_loans'; $installments = $wpdb->prefix . 'lms_installments'; $remaining_emi = (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(GREATEST(amount-paid_amount,0)),0) FROM $installments WHERE loan_id=%d", $loan_id ) ); $loan = $wpdb->get_row( $wpdb->prepare( "SELECT processing_fee FROM $loans WHERE id=%d", $loan_id ) ); $processing_paid = lms_get_processing_fee_paid( $loan_id ); $processing_remaining = $loan ? max( 0, (float) $loan->processing_fee - $processing_paid ) : 0; if ( $remaining_emi <= 0 && $processing_remaining <= 0 ) { $wpdb->update( $loans, array( 'status' => 'Closed', 'closed_at' => current_time( 'mysql' ), ), array( 'id' => $loan_id ), array( '%s', '%s' ), array( '%d' ) ); } else { // If a payment was edited/deleted after a loan was closed, reopen it. $wpdb->update( $loans, array( 'status' => 'Active', 'closed_at' => null, ), array( 'id' => $loan_id ), array( '%s', '%s' ), array( '%d' ) ); } } /* ========================================================= * GLOBAL ADMIN UI - v2.2.6 * Clean, modern, simple WordPress admin presentation. * ========================================================= */ function lms_admin_ui_styles() { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || strpos( (string) $screen->id, 'loan-management' ) === false && strpos( (string) $screen->id, 'lms-' ) === false ) { return; } ?> prefix . 'lms_customers'; $loans = $wpdb->prefix . 'lms_loans'; $installments = $wpdb->prefix . 'lms_installments'; $investors = $wpdb->prefix . 'lms_investors'; $savings = $wpdb->prefix . 'lms_savings_transactions'; $active_loans = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $loans WHERE status='Active'" ); $closed_loans = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $loans WHERE status='Closed'" ); $market_outstanding = (float) $wpdb->get_var( "SELECT COALESCE(SUM(GREATEST(amount-paid_amount,0)),0) FROM $installments i INNER JOIN $loans l ON l.id = i.loan_id WHERE l.status='Active'" ); lms_ensure_investor_schema(); $investor_transactions = $wpdb->prefix . 'lms_investor_transactions'; $total_invested = (float) $wpdb->get_var( "SELECT COALESCE(SUM(CASE WHEN transaction_type='deposit' THEN amount WHEN transaction_type IN ('capital_withdrawal','withdrawal') THEN -amount ELSE 0 END),0) FROM $investor_transactions" ); $closed_profit = (float) $wpdb->get_var( "SELECT COALESCE(SUM(total_collection - loan_amount),0) FROM $loans WHERE status='Closed'" ); /* * CASH AVAILABLE * ---------------- * Total Invested = money put into the business. * Every loan disbursement reduces cash. * Every EMI/payment collected increases cash. * * Example: * Invested = 57,500 * Loan disbursed = 10,000 * Cash available = 47,500 * * When that loan is fully closed and 11,750 is collected: * 47,500 + 11,750 = 59,250 * * Therefore cash is calculated from the actual money movement, not * by adding the closed-loan collection on top of total invested. */ $total_loan_disbursed = (float) $wpdb->get_var( "SELECT COALESCE(SUM(loan_amount),0) FROM $loans" ); // Savings-account EMI payments are internal transfers, so they must not // increase business cash a second time. Deposits add cash; cash withdrawals reduce it. $total_payment_received = (float) $wpdb->get_var( "SELECT COALESCE(SUM(amount),0) FROM $wpdb->prefix" . "lms_payments WHERE method <> 'Savings Account'" ); $savings_deposits = (float) $wpdb->get_var( "SELECT COALESCE(SUM(amount),0) FROM $savings WHERE transaction_type='deposit'" ); $savings_cash_withdrawals = (float) $wpdb->get_var( "SELECT COALESCE(SUM(amount),0) FROM $savings WHERE transaction_type='withdrawal' AND withdrawal_type='cash'" ); $investor_interest_paid = (float) $wpdb->get_var( "SELECT COALESCE(SUM(amount),0) FROM $investor_transactions WHERE transaction_type='interest_payout'" ); $cash_available = max( 0, $total_invested - $total_loan_disbursed + $total_payment_received + $savings_deposits - $savings_cash_withdrawals - $investor_interest_paid ); $founders = $wpdb->get_results( "SELECT * FROM $investors WHERE investor_type='Founder' ORDER BY id ASC" ); $other_investors = $wpdb->get_results( "SELECT * FROM $investors WHERE investor_type='Investor' ORDER BY id ASC" ); ?>

Admin Dashboard

Loan Management System — Pabitra Sarkar

Founders

No founders added yet.

👤

name ); ?>

id )['balance'] ) ); ?>
Founder Investment

Other Investors

No other investors added yet.

💼

name ); ?>

id )['balance'] ) ); ?>
Investor
prefix . 'lms_loans'; $installments = $wpdb->prefix . 'lms_installments'; $active_loans = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $loans WHERE status='Active'" ); $closed_loans = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $loans WHERE status='Closed'" ); $market_outstanding = (float) $wpdb->get_var( "SELECT COALESCE(SUM(GREATEST(i.amount-i.paid_amount,0)),0) FROM $installments i INNER JOIN $loans l ON l.id=i.loan_id WHERE l.status='Active'" ); ?>

Dashboard

Active Loans

Closed Loans

Market Outstanding

Name and mobile are required.

'; } else { $wpdb->insert( $wpdb->prefix . 'lms_customers', array( 'id' => lms_next_customer_id(), 'customer_code' => lms_next_customer_code(), 'name' => $name, 'guardian_name' => $guardian, 'mobile' => $mobile, 'address' => $address, 'aadhaar' => $aadhaar, 'pan' => $pan, 'status' => 'Active', 'created_at' => current_time( 'mysql' ), ), array( '%d','%s','%s','%s','%s','%s','%s','%s','%s','%s' ) ); $message = '

Customer saved successfully.

'; } } ?>

Add New Customer

Customer Name *
Father / Husband Name
Mobile Number *
Address
Aadhaar Number
PAN Number
prefix . 'lms_customers'; $loans = $wpdb->prefix . 'lms_loans'; if ( isset( $_POST['lms_delete_customer_from_list'] ) && check_admin_referer( 'lms_delete_customer_action', 'lms_delete_customer_nonce' ) ) { lms_admin_only(); if ( ! lms_customer_identity_controls_enabled() ) { wp_die( 'Customer deletion is disabled in Settings.' ); } $delete_id = absint( $_POST['customer_id'] ?? 0 ); $loan_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $loans WHERE customer_id=%d", $delete_id ) ); if ( $loan_count > 0 ) { echo '

This customer cannot be deleted because loan history exists.

'; } else { $wpdb->delete( $customers, array( 'id' => $delete_id ), array( '%d' ) ); echo '

Customer deleted successfully. Its ID and Customer Code are now available for reuse.

'; } } $search = sanitize_text_field( wp_unslash( $_GET['customer_search'] ?? '' ) ); if ( $search !== '' ) { $like = '%' . $wpdb->esc_like( $search ) . '%'; $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $customers WHERE name LIKE %s OR customer_code LIKE %s OR mobile LIKE %s ORDER BY id DESC", $like, $like, $like ) ); } else { $rows = $wpdb->get_results( "SELECT * FROM $customers ORDER BY id DESC" ); } ?>

Customers

Clear

+ Add New Customer

ID Name Customer Code Mobile Status Action
No customers found.
id ); ?> name ); ?> customer_code ); ?> mobile ); ?> status ); ?> Edit
get_charset_collate(); $collectors = $wpdb->prefix . 'lms_emi_collectors'; $tx = $wpdb->prefix . 'lms_emi_collector_transactions'; $payments = $wpdb->prefix . 'lms_payments'; dbDelta( "CREATE TABLE $collectors ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(150) NOT NULL, mobile VARCHAR(50) NOT NULL DEFAULT '', address TEXT, status VARCHAR(20) NOT NULL DEFAULT 'Active', created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY status (status), KEY user_id (user_id) ) $charset;" ); dbDelta( "CREATE TABLE $tx ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, collector_id BIGINT UNSIGNED NOT NULL, transaction_type VARCHAR(30) NOT NULL, amount DECIMAL(12,2) NOT NULL DEFAULT 0, transaction_date DATE NOT NULL, method VARCHAR(30) NOT NULL DEFAULT 'Cash', reference_payment_id BIGINT UNSIGNED NULL, transfer_to_collector_id BIGINT UNSIGNED NULL, note TEXT, created_by BIGINT UNSIGNED NOT NULL DEFAULT 0, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY collector_id (collector_id), KEY transaction_type (transaction_type), KEY transaction_date (transaction_date), KEY reference_payment_id (reference_payment_id) ) $charset;" ); $collector_tx_columns = $wpdb->get_col( "SHOW COLUMNS FROM $tx", 0 ); if ( ! in_array( 'transfer_group_key', $collector_tx_columns, true ) ) { $wpdb->query( "ALTER TABLE $tx ADD COLUMN transfer_group_key VARCHAR(64) NULL AFTER transfer_to_collector_id" ); } $collector_tx_indexes = $wpdb->get_results( "SHOW INDEX FROM $tx" ); $has_transfer_group_index = false; foreach ( $collector_tx_indexes as $idx ) { if ( isset($idx->Key_name) && $idx->Key_name === 'transfer_group_key' ) { $has_transfer_group_index = true; break; } } if ( ! $has_transfer_group_index ) { $wpdb->query( "ALTER TABLE $tx ADD KEY transfer_group_key (transfer_group_key)" ); } $columns = $wpdb->get_col( "SHOW COLUMNS FROM $payments", 0 ); if ( ! in_array( 'collector_id', $columns, true ) ) { $wpdb->query( "ALTER TABLE $payments ADD COLUMN collector_id BIGINT UNSIGNED NULL AFTER created_by" ); } $indexes = $wpdb->get_results( "SHOW INDEX FROM $payments" ); $has_index = false; foreach ( $indexes as $idx ) { if ( isset($idx->Key_name) && $idx->Key_name === 'collector_id' ) { $has_index = true; break; } } if ( ! $has_index ) { $wpdb->query( "ALTER TABLE $payments ADD KEY collector_id (collector_id)" ); } $bank = $wpdb->prefix . 'lms_organization_bank_transactions'; dbDelta( "CREATE TABLE $bank ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, transaction_type VARCHAR(30) NOT NULL, amount DECIMAL(12,2) NOT NULL DEFAULT 0, transaction_date DATE NOT NULL, method VARCHAR(40) NOT NULL DEFAULT 'Bank', reference_collector_tx_id BIGINT UNSIGNED NULL, reference_loan_id BIGINT UNSIGNED NULL, note TEXT, created_by BIGINT UNSIGNED NOT NULL DEFAULT 0, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY transaction_type (transaction_type), KEY transaction_date (transaction_date), KEY reference_collector_tx_id (reference_collector_tx_id), KEY reference_loan_id (reference_loan_id) ) $charset;" ); $loan_table = $wpdb->prefix . 'lms_loans'; $loan_columns = $wpdb->get_col( "SHOW COLUMNS FROM $loan_table", 0 ); if ( ! in_array( 'funding_source', $loan_columns, true ) ) { $wpdb->query( "ALTER TABLE $loan_table ADD COLUMN funding_source VARCHAR(30) NOT NULL DEFAULT 'system' AFTER loan_amount" ); } } function lms_get_emi_collectors( $active_only = true ) { global $wpdb; lms_ensure_collector_schema(); $table = $wpdb->prefix . 'lms_emi_collectors'; $where = $active_only ? "WHERE c.status='Active'" : ''; return $wpdb->get_results( "SELECT c.*, u.display_name AS user_display_name, u.user_login, u.user_email FROM $table c LEFT JOIN {$wpdb->users} u ON u.ID=c.user_id $where ORDER BY COALESCE(u.display_name,c.name) ASC, c.id ASC" ); } function lms_current_user_collector_id() { global $wpdb; lms_ensure_collector_schema(); $user_id = get_current_user_id(); if ( $user_id <= 0 ) { return 0; } $table = $wpdb->prefix . 'lms_emi_collectors'; return (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table WHERE user_id=%d AND status='Active' LIMIT 1", $user_id ) ); } function lms_get_collector_balance( $collector_id ) { global $wpdb; lms_ensure_collector_schema(); $tx = $wpdb->prefix . 'lms_emi_collector_transactions'; $payments = $wpdb->prefix . 'lms_payments'; $payment_total = (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(amount),0) FROM $payments WHERE collector_id=%d", $collector_id ) ); $other_total = (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(CASE WHEN transaction_type IN ('manual_deposit','transfer_in') THEN amount WHEN transaction_type IN ('transfer_out','bank_deposit') THEN -amount ELSE 0 END),0) FROM $tx WHERE collector_id=%d", $collector_id ) ); return $payment_total + $other_total; } function lms_add_collector_transaction( $data ) { global $wpdb; lms_ensure_collector_schema(); $table = $wpdb->prefix . 'lms_emi_collector_transactions'; $defaults = array( 'collector_id'=>0,'transaction_type'=>'manual_deposit','amount'=>0,'transaction_date'=>current_time('Y-m-d'), 'method'=>'Cash','reference_payment_id'=>null,'transfer_to_collector_id'=>null,'transfer_group_key'=>null,'note'=>'', 'created_by'=>get_current_user_id(),'created_at'=>current_time('mysql') ); $row = wp_parse_args( $data, $defaults ); if ( (int)$row['collector_id'] <= 0 || (float)$row['amount'] <= 0 ) return false; $ok = $wpdb->insert( $table, $row, array('%d','%s','%f','%s','%s','%d','%d','%s','%s','%d','%s') ); return false === $ok ? false : (int)$wpdb->insert_id; } function lms_get_organization_bank_balance() { global $wpdb; lms_ensure_collector_schema(); $bank = $wpdb->prefix . 'lms_organization_bank_transactions'; return (float) $wpdb->get_var( "SELECT COALESCE(SUM(CASE WHEN transaction_type='deposit' THEN amount WHEN transaction_type='withdrawal' THEN -amount ELSE 0 END),0) FROM $bank" ); } function lms_add_organization_bank_transaction( $data ) { global $wpdb; lms_ensure_collector_schema(); $bank = $wpdb->prefix . 'lms_organization_bank_transactions'; $defaults = array('transaction_type'=>'deposit','amount'=>0,'transaction_date'=>current_time('Y-m-d'),'method'=>'Bank','reference_collector_tx_id'=>null,'reference_loan_id'=>null,'note'=>'','created_by'=>get_current_user_id(),'created_at'=>current_time('mysql')); $row = wp_parse_args($data,$defaults); if(!in_array($row['transaction_type'],array('deposit','withdrawal'),true) || (float)$row['amount']<=0) return false; $ok=$wpdb->insert($bank,$row,array('%s','%f','%s','%s','%d','%d','%s','%d','%s')); return false===$ok ? false : (int)$wpdb->insert_id; } function lms_organization_bank_tx_for_collector_tx($collector_tx_id) { global $wpdb; lms_ensure_collector_schema(); $bank=$wpdb->prefix.'lms_organization_bank_transactions'; return $wpdb->get_row($wpdb->prepare("SELECT * FROM $bank WHERE reference_collector_tx_id=%d LIMIT 1",absint($collector_tx_id))); } function lms_record_organization_bank_loan_withdrawal($loan_id,$amount,$date) { return lms_add_organization_bank_transaction(array('transaction_type'=>'withdrawal','amount'=>$amount,'transaction_date'=>$date,'method'=>'Loan Disbursement','reference_loan_id'=>absint($loan_id),'note'=>'Loan disbursement from '.lms_organization_name().' Bank')); } function lms_record_organization_bank_loan_refund($loan_id,$amount,$date) { return lms_add_organization_bank_transaction(array('transaction_type'=>'deposit','amount'=>$amount,'transaction_date'=>$date,'method'=>'Loan Reversal','reference_loan_id'=>absint($loan_id),'note'=>'Loan reversal/refund to '.lms_organization_name().' Bank')); } function lms_record_collector_payment_transaction( $payment_id, $collector_id, $payment_type, $amount, $date, $method, $note = '' ) { global $wpdb; $payment_id = absint( $payment_id ); $collector_id = absint( $collector_id ); $amount = (float) $amount; if ( $payment_id <= 0 || $collector_id <= 0 || $amount <= 0 ) { return false; } $table = $wpdb->prefix . 'lms_emi_collector_transactions'; $existing = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table WHERE reference_payment_id=%d LIMIT 1", $payment_id ) ); if ( $existing ) { return (int) $existing; } $label = $payment_type === 'Processing Fee' ? 'Processing Fee Collection' : 'EMI Collection'; $tx_note = $note !== '' ? $note : $label; return lms_add_collector_transaction( array( 'collector_id' => $collector_id, 'transaction_type' => 'collection', 'amount' => $amount, 'transaction_date' => $date, 'method' => $method, 'reference_payment_id' => $payment_id, 'note' => $tx_note, 'created_by' => get_current_user_id(), ) ); } function lms_sync_collector_payment_transactions() { global $wpdb; $payments = $wpdb->prefix . 'lms_payments'; $tx = $wpdb->prefix . 'lms_emi_collector_transactions'; $rows = $wpdb->get_results( "SELECT id, collector_id, payment_type, amount, payment_date, method, note FROM $payments WHERE collector_id IS NOT NULL AND collector_id > 0 AND amount > 0 ORDER BY id ASC" ); foreach ( (array) $rows as $row ) { $exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $tx WHERE reference_payment_id=%d LIMIT 1", (int) $row->id ) ); if ( ! $exists ) { lms_record_collector_payment_transaction( (int) $row->id, (int) $row->collector_id, $row->payment_type, (float) $row->amount, $row->payment_date, $row->method, $row->note ); } } } function lms_collector_name( $collector_id ) { global $wpdb; lms_ensure_collector_schema(); $table = $wpdb->prefix . 'lms_emi_collectors'; $name = $wpdb->get_var( $wpdb->prepare("SELECT name FROM $table WHERE id=%d", $collector_id) ); return $name ? $name : '—'; } /* ========================================================= * CUSTOMER PROFILE / CONTACT / LOANS / EMI * ========================================================= */ add_action( 'wp_ajax_lms_get_payment_for_edit', 'lms_ajax_get_payment_for_edit' ); function lms_ajax_get_payment_for_edit() { if ( ! current_user_can( 'lms_collect_emi' ) || ! wp_verify_nonce( $_GET['_wpnonce'] ?? '', 'lms_get_payment_for_edit' ) ) { wp_send_json_error( 'Permission denied.' ); } global $wpdb; $payment_id = absint( $_GET['payment_id'] ?? 0 ); $customer_id = absint( $_GET['customer_id'] ?? 0 ); $payments = $wpdb->prefix . 'lms_payments'; $loans = $wpdb->prefix . 'lms_loans'; $p = $wpdb->get_row( $wpdb->prepare( "SELECT p.* FROM $payments p INNER JOIN $loans l ON l.id=p.loan_id WHERE p.id=%d AND l.customer_id=%d", $payment_id, $customer_id ) ); if ( ! $p ) wp_send_json_error( 'Payment history record not found.' ); wp_send_json_success( array( 'amount'=>(float)$p->amount, 'payment_date'=>$p->payment_date, 'created_by'=>(int)$p->created_by, 'collector_id'=>(int)$p->collector_id, 'method'=>$p->method, 'note'=>$p->note ) ); } function lms_customer_profile_page() { if ( ! current_user_can( 'lms_manage_customers' ) ) { wp_die( 'You do not have permission to view customers.' ); } global $wpdb; $customer_id = absint( $_GET['customer_id'] ?? $_POST['customer_id'] ?? 0 ); if ( ! $customer_id ) { wp_die( 'Customer not found.' ); } $customers = $wpdb->prefix . 'lms_customers'; $loans = $wpdb->prefix . 'lms_loans'; $installments = $wpdb->prefix . 'lms_installments'; $payments = $wpdb->prefix . 'lms_payments'; $savings = $wpdb->prefix . 'lms_savings_transactions'; $customer = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $customers WHERE id=%d", $customer_id ) ); if ( ! $customer ) { wp_die( 'Customer not found.' ); } $message = ''; /* ===================================================== * EMI PAYMENT HISTORY - EDIT / DELETE * Users who can collect EMI can also correct payment history. * Every change recalculates the related installment and loan. * ===================================================== */ if ( isset( $_POST['lms_edit_payment'] ) && check_admin_referer( 'lms_edit_payment_action', 'lms_edit_payment_nonce' ) ) { if ( ! current_user_can( 'lms_collect_emi' ) || ! lms_emi_history_edit_enabled() ) { wp_die( 'EMI payment editing is currently disabled.' ); } $payment_id = absint( $_POST['payment_id'] ?? 0 ); $amount = (float) ( $_POST['payment_amount'] ?? 0 ); $payment_date = sanitize_text_field( wp_unslash( $_POST['payment_date'] ?? '' ) ); $created_by = absint( $_POST['collection_person'] ?? get_current_user_id() ); $edit_collector_id = absint( $_POST['collection_collector_id'] ?? 0 ); $edit_collector = $edit_collector_id > 0 ? $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}lms_emi_collectors WHERE id=%d AND status='Active'", $edit_collector_id ) ) : null; if ( $edit_collector && ! empty( $edit_collector->user_id ) ) $created_by = (int) $edit_collector->user_id; $method = sanitize_text_field( wp_unslash( $_POST['payment_method'] ?? 'Cash' ) ); $note = sanitize_textarea_field( wp_unslash( $_POST['payment_note'] ?? '' ) ); $payment = $wpdb->get_row( $wpdb->prepare( "SELECT p.*, i.amount AS installment_amount, i.installment_no, l.loan_code, l.customer_id, l.processing_fee FROM $payments p LEFT JOIN $installments i ON i.id=p.installment_id INNER JOIN $loans l ON l.id=p.loan_id WHERE p.id=%d AND l.customer_id=%d", $payment_id, $customer_id ) ); if ( ! $payment ) { $message = '

Payment history record not found.

'; } elseif ( $amount <= 0 ) { $message = '

Payment amount must be greater than 0.

'; } elseif ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $payment_date ) ) { $message = '

Invalid payment date.

'; } elseif ( $payment->method !== 'Savings Account' && ! $edit_collector && (int) $payment->collector_id <= 0 ) { $message = '

Please select a valid EMI Collector for this payment.

'; } else { // Savings-funded EMI payments remain linked to their savings transaction. // When their amount is edited, the savings debit is edited too. if ( $payment->method === 'Savings Account' && $payment->savings_transaction_id ) { $available_without_this = lms_get_savings_balance( $customer_id ) + (float) $payment->amount; if ( $amount > $available_without_this + 0.00001 ) { $message = '

Insufficient savings balance for this edited amount.

'; } } if ( $message !== '' ) { // Keep the existing payment unchanged. } else { $max_allowed = 0; if ( $payment->payment_type === 'Processing Fee' ) { $other_paid = (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(amount),0) FROM $payments WHERE loan_id=%d AND payment_type='Processing Fee' AND id<>%d", $payment->loan_id, $payment_id ) ); $max_allowed = max( 0, (float) $payment->processing_fee - $other_paid ); } else { $other_paid = (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(amount),0) FROM $payments WHERE installment_id=%d AND payment_type='EMI' AND id<>%d", $payment->installment_id, $payment_id ) ); $max_allowed = max( 0, (float) $payment->installment_amount - $other_paid ); } if ( $amount > $max_allowed ) { $message = '

Payment amount cannot be greater than the remaining amount allowed for this payment.

'; } else { $allowed_methods = array( 'Cash','UPI','Bank','Other','Savings Account' ); if ( $payment->method === 'Savings Account' ) { $allowed_methods[] = 'Savings Account'; } $new_method = in_array( $method, $allowed_methods, true ) ? $method : 'Cash'; // Do not silently move an existing normal payment into savings. if ( $payment->method !== 'Savings Account' && $new_method === 'Savings Account' ) { $new_method = $payment->method; } if ( $payment->method === 'Savings Account' && $new_method !== 'Savings Account' ) { $new_method = 'Savings Account'; } $updated = $wpdb->update( $payments, array( 'amount' => $amount, 'payment_date' => $payment_date, 'method' => $new_method, 'note' => $note, 'created_by' => $created_by > 0 ? $created_by : get_current_user_id(), 'collector_id' => $new_method === 'Savings Account' ? null : ( $edit_collector_id > 0 ? $edit_collector_id : (int) $payment->collector_id ), ), array( 'id' => $payment_id ), array( '%f','%s','%s','%s','%d' ), array( '%d' ) ); if ( false === $updated ) { $message = '

Payment could not be updated.

'; } else { $collector_tx_table = $wpdb->prefix . 'lms_emi_collector_transactions'; if ( $new_method === 'Savings Account' ) { $wpdb->delete( $collector_tx_table, array( 'reference_payment_id' => $payment_id ), array( '%d' ) ); } else { $target_collector_id = $edit_collector_id > 0 ? $edit_collector_id : (int) $payment->collector_id; if ( $target_collector_id > 0 ) { $existing_collector_tx = $wpdb->get_row( $wpdb->prepare( "SELECT id FROM $collector_tx_table WHERE reference_payment_id=%d LIMIT 1", $payment_id ) ); $tx_note = $note !== '' ? $note : ( $payment->payment_type === 'Processing Fee' ? 'Processing Fee Collection' : 'EMI Collection' ); if ( $existing_collector_tx ) { $wpdb->update( $collector_tx_table, array( 'collector_id'=>$target_collector_id, 'amount'=>$amount, 'transaction_date'=>$payment_date, 'method'=>$new_method, 'note'=>$tx_note ), array( 'id'=>(int)$existing_collector_tx->id ), array( '%d','%f','%s','%s','%s' ), array( '%d' ) ); } else { lms_record_collector_payment_transaction( $payment_id, $target_collector_id, $payment->payment_type, $amount, $payment_date, $new_method, $note ); } } } if ( $payment->method === 'Savings Account' && $payment->savings_transaction_id ) { $wpdb->update( $savings, array( 'amount' => $amount, 'transaction_date' => $payment_date, 'note' => $note !== '' ? $note : 'EMI paid from Savings Bank Account', 'created_by' => $created_by > 0 ? $created_by : get_current_user_id(), ), array( 'id' => (int) $payment->savings_transaction_id, 'customer_id' => $customer_id ), array( '%f','%s','%s','%d' ), array( '%d','%d' ) ); } if ( $payment->payment_type === 'Processing Fee' ) { lms_recalculate_processing_fee_state( $payment->loan_id ); lms_maybe_close_loan( $payment->loan_id ); $message = '

Processing fee payment updated successfully.

'; } else { lms_recalculate_installment_payment_state( $payment->installment_id ); lms_maybe_close_loan( $payment->loan_id ); $message = '

EMI payment updated successfully.

'; } } } } } } /* ===================================================== * EMI PAYMENT HISTORY - BULK ACTIONS * Delete selected payments / mark selected payments unpaid. * Mark Unpaid intentionally removes the selected payment record * and recalculates the related installment and loan. * ===================================================== */ if ( isset( $_POST['lms_bulk_payment_action'] ) && check_admin_referer( 'lms_bulk_payment_action', 'lms_bulk_payment_nonce' ) ) { if ( ! current_user_can( 'lms_collect_emi' ) || ! lms_emi_history_bulk_enabled() ) { wp_die( 'EMI payment bulk actions are currently disabled.' ); } $bulk_action = sanitize_key( $_POST['lms_bulk_payment_action'] ); $payment_ids = isset( $_POST['payment_ids'] ) && is_array( $_POST['payment_ids'] ) ? array_values( array_unique( array_filter( array_map( 'absint', $_POST['payment_ids'] ) ) ) ) : array(); if ( empty( $payment_ids ) ) { $message = '

Please select at least one payment.

'; } elseif ( $bulk_action === 'delete' && ! lms_emi_history_delete_enabled() ) { $message = '

Payment deletion is currently disabled.

'; } elseif ( $bulk_action === 'unpaid' && ! lms_emi_history_mark_unpaid_enabled() ) { $message = '

Mark Unpaid is currently disabled.

'; } elseif ( ! in_array( $bulk_action, array( 'delete', 'unpaid' ), true ) ) { $message = '

Invalid bulk action.

'; } else { $placeholders = implode( ',', array_fill( 0, count( $payment_ids ), '%d' ) ); $query_args = array_merge( array( $customer_id ), $payment_ids ); $selected_payments = $wpdb->get_results( $wpdb->prepare( "SELECT p.id, p.installment_id, p.loan_id, p.payment_type, p.savings_transaction_id FROM $payments p INNER JOIN $loans l ON l.id=p.loan_id WHERE l.customer_id=%d AND p.id IN ($placeholders)", $query_args ) ); $affected_installments = array(); $affected_loans = array(); $deleted_count = 0; foreach ( $selected_payments as $selected_payment ) { $deleted = $wpdb->delete( $payments, array( 'id' => (int) $selected_payment->id ), array( '%d' ) ); if ( false !== $deleted && $deleted > 0 ) { $wpdb->delete( $wpdb->prefix . 'lms_emi_collector_transactions', array( 'reference_payment_id' => (int) $selected_payment->id ), array( '%d' ) ); if ( $selected_payment->savings_transaction_id ) { $wpdb->delete( $savings, array( 'id' => (int) $selected_payment->savings_transaction_id, 'customer_id' => $customer_id ), array( '%d','%d' ) ); } $deleted_count++; if ( $selected_payment->payment_type === 'EMI' && $selected_payment->installment_id ) { $affected_installments[] = (int) $selected_payment->installment_id; } $affected_loans[] = (int) $selected_payment->loan_id; } } $affected_installments = array_values( array_unique( $affected_installments ) ); $affected_loans = array_values( array_unique( $affected_loans ) ); foreach ( $affected_installments as $affected_installment_id ) { lms_recalculate_installment_payment_state( $affected_installment_id ); } foreach ( $affected_loans as $affected_loan_id ) { lms_maybe_close_loan( $affected_loan_id ); } if ( $deleted_count > 0 ) { $message_text = $bulk_action === 'unpaid' ? sprintf( '%d selected payment(s) marked as unpaid successfully.', $deleted_count ) : sprintf( '%d selected payment(s) deleted successfully.', $deleted_count ); $message = '

' . esc_html( $message_text ) . '

'; } else { $message = '

No valid payment records were found.

'; } } } /* Mark one EMI installment unpaid by removing all linked EMI payments. */ if ( isset( $_POST['lms_mark_installment_unpaid'] ) && check_admin_referer( 'lms_mark_installment_unpaid_action', 'lms_mark_installment_unpaid_nonce' ) ) { if ( ! current_user_can( 'lms_collect_emi' ) || ! lms_emi_history_mark_unpaid_enabled() ) { wp_die( 'Mark Unpaid is currently disabled.' ); } $installment_id = absint( $_POST['installment_id'] ?? 0 ); $installment = $wpdb->get_row( $wpdb->prepare( "SELECT i.*, l.customer_id FROM $installments i INNER JOIN $loans l ON l.id=i.loan_id WHERE i.id=%d AND l.customer_id=%d", $installment_id, $customer_id ) ); if ( ! $installment ) { $message = '

Installment not found.

'; } else { $linked_payments = $wpdb->get_results( $wpdb->prepare( "SELECT id, savings_transaction_id FROM $payments WHERE installment_id=%d AND payment_type='EMI'", $installment_id ) ); foreach ( $linked_payments as $lp ) { $wpdb->delete( $wpdb->prefix . 'lms_emi_collector_transactions', array( 'reference_payment_id' => (int) $lp->id ), array( '%d' ) ); if ( $lp->savings_transaction_id ) { $wpdb->delete( $savings, array( 'id' => (int) $lp->savings_transaction_id, 'customer_id' => $customer_id ), array( '%d','%d' ) ); } $wpdb->delete( $payments, array( 'id' => (int) $lp->id ), array( '%d' ) ); } lms_recalculate_installment_payment_state( $installment_id ); lms_maybe_close_loan( $installment->loan_id ); $message = '

EMI #' . esc_html( $installment->installment_no ) . ' marked as unpaid successfully.

'; } } /* Mark Processing Fee unpaid by removing all linked Processing Fee payments. */ if ( isset( $_POST['lms_mark_processing_fee_unpaid'] ) && check_admin_referer( 'lms_mark_processing_fee_unpaid_action', 'lms_mark_processing_fee_unpaid_nonce' ) ) { if ( ! current_user_can( 'lms_collect_emi' ) || ! lms_emi_history_mark_unpaid_enabled() ) { wp_die( 'Mark Unpaid is currently disabled.' ); } $processing_loan_id = absint( $_POST['processing_fee_loan_id'] ?? 0 ); $processing_loan = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $loans WHERE id=%d AND customer_id=%d", $processing_loan_id, $customer_id ) ); if ( ! $processing_loan ) { $message = '

Loan not found.

'; } else { $linked_processing_payments = $wpdb->get_results( $wpdb->prepare( "SELECT id, savings_transaction_id FROM $payments WHERE loan_id=%d AND payment_type='Processing Fee'", $processing_loan_id ) ); foreach ( $linked_processing_payments as $lp ) { $wpdb->delete( $wpdb->prefix . 'lms_emi_collector_transactions', array( 'reference_payment_id' => (int) $lp->id ), array( '%d' ) ); if ( $lp->savings_transaction_id ) { $wpdb->delete( $savings, array( 'id' => (int) $lp->savings_transaction_id, 'customer_id' => $customer_id ), array( '%d','%d' ) ); } $wpdb->delete( $payments, array( 'id' => (int) $lp->id ), array( '%d' ) ); } lms_recalculate_processing_fee_state( $processing_loan_id ); lms_maybe_close_loan( $processing_loan_id ); $message = '

Processing Fee for ' . esc_html( $processing_loan->loan_code ) . ' marked as unpaid successfully.

'; } } if ( isset( $_POST['lms_delete_payment'] ) && check_admin_referer( 'lms_delete_payment_action', 'lms_delete_payment_nonce' ) ) { if ( ! current_user_can( 'lms_collect_emi' ) || ! lms_emi_history_delete_enabled() ) { wp_die( 'EMI payment deletion is currently disabled.' ); } $payment_id = absint( $_POST['payment_id'] ?? 0 ); $payment = $wpdb->get_row( $wpdb->prepare( "SELECT p.* FROM $payments p INNER JOIN $loans l ON l.id=p.loan_id WHERE p.id=%d AND l.customer_id=%d", $payment_id, $customer_id ) ); if ( ! $payment ) { $message = '

Payment history record not found.

'; } else { $deleted = $wpdb->delete( $payments, array( 'id' => $payment_id ), array( '%d' ) ); if ( false === $deleted ) { $message = '

Payment could not be deleted.

'; } else { $wpdb->delete( $wpdb->prefix . 'lms_emi_collector_transactions', array( 'reference_payment_id' => $payment_id ), array( '%d' ) ); if ( $payment->savings_transaction_id ) { $wpdb->delete( $savings, array( 'id' => (int) $payment->savings_transaction_id, 'customer_id' => $customer_id ), array( '%d','%d' ) ); } if ( $payment->payment_type === 'Processing Fee' ) { lms_recalculate_processing_fee_state( $payment->loan_id ); lms_maybe_close_loan( $payment->loan_id ); $message = '

Processing fee payment deleted successfully.

'; } else { lms_recalculate_installment_payment_state( $payment->installment_id ); lms_maybe_close_loan( $payment->loan_id ); $message = '

EMI payment deleted successfully.

'; } } } } /* ===================================================== * SAVINGS TRANSACTION HISTORY - EDIT * Controlled from Loan Settings. * ===================================================== */ if ( isset( $_POST['lms_edit_savings_transaction'] ) && check_admin_referer( 'lms_edit_savings_transaction_action', 'lms_edit_savings_transaction_nonce' ) ) { if ( ! current_user_can( 'lms_collect_emi' ) || ! lms_savings_history_edit_delete_enabled() ) { wp_die( 'Saving account transaction editing is currently disabled.' ); } $tx_id = absint( $_POST['savings_transaction_id'] ?? 0 ); $new_amount = (float) ( $_POST['savings_edit_amount'] ?? 0 ); $new_date = sanitize_text_field( wp_unslash( $_POST['savings_edit_date'] ?? '' ) ); $new_method = sanitize_text_field( wp_unslash( $_POST['savings_edit_method'] ?? 'Cash' ) ); $new_note = sanitize_textarea_field( wp_unslash( $_POST['savings_edit_note'] ?? '' ) ); $new_collector = absint( $_POST['savings_edit_collector'] ?? get_current_user_id() ); $tx = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $savings WHERE id=%d AND customer_id=%d", $tx_id, $customer_id ) ); if ( ! $tx ) { $message = '

Saving account transaction not found.

'; } elseif ( $new_amount <= 0 || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $new_date ) ) { $message = '

Please enter a valid amount and date.

'; } else { $allowed_methods = array( 'Cash','UPI','Bank','Other','Savings Account' ); if ( ! in_array( $new_method, $allowed_methods, true ) ) { $new_method = 'Cash'; } $old_effect = $tx->transaction_type === 'deposit' ? (float) $tx->amount : -(float) $tx->amount; $balance_without = lms_get_savings_balance( $customer_id ) - $old_effect; $new_effect = $tx->transaction_type === 'deposit' ? $new_amount : -$new_amount; if ( $balance_without + $new_effect < -0.00001 ) { $message = '

This change would make the savings balance negative.

'; } else { $update_ok = true; if ( $tx->transaction_type === 'withdrawal' && in_array( $tx->withdrawal_type, array( 'emi', 'processing_fee' ), true ) && $tx->payment_id ) { $linked_payment = $wpdb->get_row( $wpdb->prepare( "SELECT p.*, i.amount AS installment_amount, l.processing_fee FROM $payments p LEFT JOIN $installments i ON i.id=p.installment_id INNER JOIN $loans l ON l.id=p.loan_id WHERE p.id=%d AND p.loan_id=%d", (int) $tx->payment_id, (int) $tx->loan_id ) ); if ( ! $linked_payment ) { $update_ok = false; $message = '

Linked EMI payment was not found.

'; } else { if ( $tx->withdrawal_type === 'processing_fee' ) { $other_paid = (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(amount),0) FROM $payments WHERE loan_id=%d AND payment_type='Processing Fee' AND id<>%d", (int) $linked_payment->loan_id, (int) $linked_payment->id ) ); $max_linked_amount = max( 0, (float) $linked_payment->processing_fee - $other_paid ); } else { $other_paid = (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(amount),0) FROM $payments WHERE installment_id=%d AND payment_type='EMI' AND id<>%d", (int) $linked_payment->installment_id, (int) $linked_payment->id ) ); $max_linked_amount = max( 0, (float) $linked_payment->installment_amount - $other_paid ); } if ( $new_amount > $max_linked_amount + 0.00001 ) { $update_ok = false; $message = $tx->withdrawal_type === 'processing_fee' ? '

Amount cannot be greater than the remaining Processing Fee amount.

' : '

Amount cannot be greater than the remaining amount of the selected EMI.

'; } else { $payment_updated = $wpdb->update( $payments, array( 'amount' => $new_amount, 'payment_date' => $new_date, 'note' => $new_note !== '' ? $new_note : 'EMI paid from Savings Bank Account', 'created_by' => $new_collector > 0 ? $new_collector : get_current_user_id(), ), array( 'id' => (int) $tx->payment_id ), array( '%f','%s','%s','%d' ), array( '%d' ) ); if ( false === $payment_updated ) { $update_ok = false; $message = '

Linked EMI payment could not be updated.

'; } } } } if ( $update_ok ) { $updated = $wpdb->update( $savings, array( 'amount' => $new_amount, 'transaction_date' => $new_date, 'method' => $new_method, 'note' => $new_note, 'created_by' => $new_collector > 0 ? $new_collector : get_current_user_id(), ), array( 'id' => $tx_id, 'customer_id' => $customer_id ), array( '%f','%s','%s','%s','%d' ), array( '%d','%d' ) ); if ( false === $updated ) { $message = '

Saving transaction could not be updated.

'; } else { if ( $tx->transaction_type === 'withdrawal' && $tx->withdrawal_type === 'emi' && $tx->installment_id ) { lms_recalculate_installment_payment_state( $tx->installment_id ); lms_maybe_close_loan( $tx->loan_id ); } elseif ( $tx->transaction_type === 'withdrawal' && $tx->withdrawal_type === 'processing_fee' && $tx->loan_id ) { lms_recalculate_processing_fee_state( $tx->loan_id ); lms_maybe_close_loan( $tx->loan_id ); } $message = '

Saving account transaction updated successfully.

'; } } } } } /* ===================================================== * SAVINGS TRANSACTION HISTORY - DELETE * Controlled from Loan Settings. * ===================================================== */ if ( isset( $_POST['lms_delete_savings_transaction'] ) && check_admin_referer( 'lms_delete_savings_transaction_action', 'lms_delete_savings_transaction_nonce' ) ) { if ( ! current_user_can( 'lms_collect_emi' ) || ! lms_savings_history_edit_delete_enabled() ) { wp_die( 'Saving account transaction deletion is currently disabled.' ); } $tx_id = absint( $_POST['savings_transaction_id'] ?? 0 ); $tx = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $savings WHERE id=%d AND customer_id=%d", $tx_id, $customer_id ) ); if ( ! $tx ) { $message = '

Saving account transaction not found.

'; } elseif ( $tx->transaction_type === 'deposit' && lms_get_savings_balance( $customer_id ) - (float) $tx->amount < -0.00001 ) { $message = '

This deposit cannot be deleted because the amount is already used from the savings balance.

'; } else { $deleted = $wpdb->delete( $savings, array( 'id' => $tx_id, 'customer_id' => $customer_id ), array( '%d','%d' ) ); if ( false === $deleted || $deleted < 1 ) { $message = '

Saving account transaction could not be deleted.

'; } else { if ( $tx->transaction_type === 'withdrawal' && in_array( $tx->withdrawal_type, array( 'emi', 'processing_fee' ), true ) && $tx->payment_id ) { $wpdb->delete( $wpdb->prefix . 'lms_emi_collector_transactions', array( 'reference_payment_id' => (int) $tx->payment_id ), array( '%d' ) ); $wpdb->delete( $payments, array( 'id' => (int) $tx->payment_id ), array( '%d' ) ); if ( $tx->withdrawal_type === 'emi' && $tx->installment_id ) { lms_recalculate_installment_payment_state( $tx->installment_id ); } if ( $tx->withdrawal_type === 'processing_fee' && $tx->loan_id ) { lms_recalculate_processing_fee_state( $tx->loan_id ); } if ( $tx->loan_id ) { lms_maybe_close_loan( $tx->loan_id ); } } $message = '

Saving account transaction deleted successfully.

'; } } } /* ===================================================== * SAVINGS BANK ACCOUNT * Deposit money, cash withdrawal, or use savings to pay * the customer's next unpaid EMI directly. * ===================================================== */ if ( isset( $_POST['lms_savings_action'] ) && check_admin_referer( 'lms_savings_action', 'lms_savings_nonce' ) ) { if ( ! current_user_can( 'lms_collect_emi' ) ) { wp_die( 'You do not have permission to manage the savings account.' ); } $savings_action = sanitize_key( $_POST['lms_savings_action'] ); $amount = (float) ( $_POST['savings_amount'] ?? 0 ); $date = sanitize_text_field( wp_unslash( $_POST['savings_date'] ?? current_time( 'Y-m-d' ) ) ); $method = sanitize_text_field( wp_unslash( $_POST['savings_method'] ?? 'Cash' ) ); $note = sanitize_textarea_field( wp_unslash( $_POST['savings_note'] ?? '' ) ); $collector = absint( $_POST['savings_collector'] ?? get_current_user_id() ); if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) { $date = current_time( 'Y-m-d' ); } if ( ! in_array( $method, array( 'Cash','UPI','Bank','Other','Savings Account' ), true ) ) { $method = 'Cash'; } if ( $savings_action === 'deposit' ) { if ( $amount <= 0 ) { $message = '

Deposit amount must be greater than 0.

'; } else { $transaction_id = lms_savings_add_transaction( array( 'customer_id' => $customer_id, 'transaction_type' => 'deposit', 'amount' => $amount, 'transaction_date' => $date, 'method' => $method, 'note' => $note, 'created_by' => $collector > 0 ? $collector : get_current_user_id(), ) ); $message = $transaction_id ? '

' . esc_html( lms_money( $amount ) . ' deposited into savings account.' ) . '

' : '

Deposit could not be saved.

'; } } elseif ( $savings_action === 'withdraw' ) { $withdrawal_type = sanitize_key( $_POST['withdrawal_type'] ?? 'cash' ); $balance = lms_get_savings_balance( $customer_id ); if ( $amount <= 0 || $amount > $balance + 0.00001 ) { $message = '

Insufficient savings balance or invalid withdrawal amount.

'; } elseif ( $withdrawal_type === 'cash' ) { $transaction_id = lms_savings_add_transaction( array( 'customer_id' => $customer_id, 'transaction_type' => 'withdrawal', 'withdrawal_type' => 'cash', 'amount' => $amount, 'transaction_date' => $date, 'method' => $method, 'note' => $note, 'created_by' => $collector > 0 ? $collector : get_current_user_id(), ) ); $message = $transaction_id ? '

' . esc_html( lms_money( $amount ) . ' cash withdrawn from savings account.' ) . '

' : '

Cash withdrawal could not be saved.

'; } elseif ( $withdrawal_type === 'emi' ) { $installment_id = absint( $_POST['savings_installment_id'] ?? 0 ); $installment = $wpdb->get_row( $wpdb->prepare( "SELECT i.*, l.loan_code FROM $installments i INNER JOIN $loans l ON l.id=i.loan_id WHERE i.id=%d AND l.customer_id=%d", $installment_id, $customer_id ) ); $remaining = $installment ? max( 0, (float) $installment->amount - (float) $installment->paid_amount ) : 0; if ( ! $installment || $remaining <= 0 || $amount > $remaining + 0.00001 ) { $message = '

Invalid EMI or amount. Amount cannot exceed the selected EMI remaining balance.

'; } else { $wpdb->query( 'START TRANSACTION' ); $payment_ok = $wpdb->insert( $payments, array( 'loan_id' => $installment->loan_id, 'installment_id' => $installment->id, 'payment_type' => 'EMI', 'amount' => $amount, 'payment_date' => $date, 'method' => 'Savings Account', 'note' => $note !== '' ? $note : 'EMI paid from Savings Bank Account', 'created_by' => $collector > 0 ? $collector : get_current_user_id(), 'savings_transaction_id' => null, 'created_at' => current_time( 'mysql' ), ), array( '%d','%d','%s','%f','%s','%s','%s','%d','%d','%s' ) ); if ( false === $payment_ok ) { $wpdb->query( 'ROLLBACK' ); $message = '

EMI payment could not be saved.

'; } else { $payment_id = (int) $wpdb->insert_id; $transaction_id = lms_savings_add_transaction( array( 'customer_id' => $customer_id, 'transaction_type' => 'withdrawal', 'withdrawal_type' => 'emi', 'amount' => $amount, 'transaction_date' => $date, 'loan_id' => $installment->loan_id, 'installment_id' => $installment->id, 'payment_id' => $payment_id, 'method' => 'Savings Account', 'note' => $note !== '' ? $note : 'EMI paid from Savings Bank Account', 'created_by' => $collector > 0 ? $collector : get_current_user_id(), ) ); if ( ! $transaction_id ) { $wpdb->query( 'ROLLBACK' ); $message = '

Savings withdrawal could not be recorded. EMI payment was not completed.

'; } else { $wpdb->update( $payments, array( 'savings_transaction_id' => $transaction_id ), array( 'id' => $payment_id ), array( '%d' ), array( '%d' ) ); $wpdb->query( 'COMMIT' ); lms_recalculate_installment_payment_state( $installment->id ); lms_maybe_close_loan( $installment->loan_id ); $message = '

' . esc_html( lms_money( $amount ) . ' EMI paid directly from Savings Bank Account.' ) . '

'; } } } } else { $message = '

Invalid withdrawal type.

'; } } } /* Customer update - ADMIN ONLY. */ if ( isset( $_POST['lms_update_customer'] ) && check_admin_referer( 'lms_update_customer_action', 'lms_update_customer_nonce' ) ) { lms_admin_only(); $name = sanitize_text_field( wp_unslash( $_POST['customer_name'] ?? '' ) ); $guardian = sanitize_text_field( wp_unslash( $_POST['guardian_name'] ?? '' ) ); $mobile = sanitize_text_field( wp_unslash( $_POST['mobile'] ?? '' ) ); $address = sanitize_textarea_field( wp_unslash( $_POST['address'] ?? '' ) ); $aadhaar = sanitize_text_field( wp_unslash( $_POST['aadhaar'] ?? '' ) ); $pan = sanitize_text_field( wp_unslash( $_POST['pan'] ?? '' ) ); $status = sanitize_text_field( wp_unslash( $_POST['status'] ?? 'Active' ) ); $new_customer_id = absint( $_POST['customer_id_number'] ?? $customer_id ); $new_customer_code = strtoupper( sanitize_text_field( wp_unslash( $_POST['customer_code'] ?? $customer->customer_code ) ) ); if ( $name === '' || $mobile === '' ) { $message = '

Name and mobile are required.

'; } else { if ( ! lms_customer_identity_controls_enabled() ) { $new_customer_id = $customer_id; $new_customer_code = $customer->customer_code; } $identity_error = ''; if ( $new_customer_id !== $customer_id ) { $has_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $customers WHERE id=%d AND id<>%d", $new_customer_id, $customer_id ) ); $loan_count_for_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $loans WHERE customer_id=%d", $customer_id ) ); if ( $has_id || $loan_count_for_id > 0 || $new_customer_id < 1 ) { $identity_error = $loan_count_for_id > 0 ? 'Customer ID cannot be changed after loan history exists.' : 'This Customer ID is already in use or invalid.'; } } if ( $new_customer_code !== $customer->customer_code ) { $has_code = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $customers WHERE customer_code=%s AND id<>%d", $new_customer_code, $customer_id ) ); if ( $has_code || ! preg_match( '/^CUS-[0-9]{1,10}$/', $new_customer_code ) ) { $identity_error = 'Customer Code is already in use or invalid. Use format CUS-00001.'; } } if ( $identity_error !== '' ) { $message = '

' . esc_html( $identity_error ) . '

'; } else { if ( $new_customer_id !== $customer_id ) { $savings_table = $wpdb->prefix . 'lms_savings_transactions'; $wpdb->update( $savings_table, array( 'customer_id' => $new_customer_id ), array( 'customer_id' => $customer_id ), array( '%d' ), array( '%d' ) ); } $wpdb->update( $customers, array( 'id' => $new_customer_id, 'customer_code' => $new_customer_code, 'name' => $name, 'guardian_name' => $guardian, 'mobile' => $mobile, 'address' => $address, 'aadhaar' => $aadhaar, 'pan' => $pan, 'status' => in_array( $status, array( 'Active', 'Inactive' ), true ) ? $status : 'Active', ), array( 'id' => $customer_id ), array( '%d','%s','%s','%s','%s','%s','%s','%s','%s' ), array( '%d' ) ); $customer = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $customers WHERE id=%d", $new_customer_id ) ); $customer_id = $new_customer_id; $message = '

Customer details updated successfully.

'; } } } /* Delete customer - ADMIN ONLY. Financial history is protected: a customer with loans/payments cannot be deleted. */ if ( isset( $_POST['lms_delete_customer'] ) && check_admin_referer( 'lms_delete_customer_action', 'lms_delete_customer_nonce' ) ) { lms_admin_only(); if ( ! lms_customer_identity_controls_enabled() ) { $message = '

Customer deletion is currently disabled in Settings.

'; } else { $loan_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $loans WHERE customer_id=%d", $customer_id ) ); if ( $loan_count > 0 ) { $message = '

This customer cannot be deleted because loan history exists.

'; } else { $wpdb->delete( $customers, array( 'id' => $customer_id ), array( '%d' ) ); echo '

Customer deleted successfully.

'; echo '

Back to Customers

'; return; } } } /* ===================================================== * CUSTOMER PAY EMI - SINGLE NEXT EMI COLLECTION * ===================================================== */ if ( isset( $_POST['lms_customer_collect_emi'] ) && check_admin_referer( 'lms_customer_collect_emi_action', 'lms_customer_collect_emi_nonce' ) ) { if ( ! current_user_can( 'lms_collect_emi' ) ) { wp_die( 'You do not have permission to collect EMI.' ); } $installment_id = absint( $_POST['customer_emi_installment_id'] ?? 0 ); $pay_amount = (float) ( $_POST['customer_emi_amount'] ?? 0 ); $collection_date = sanitize_text_field( wp_unslash( $_POST['customer_emi_date'] ?? current_time( 'Y-m-d' ) ) ); $collection_collector_id = absint( $_POST['customer_emi_collector'] ?? 0 ); $collection_collector = $collection_collector_id > 0 ? $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}lms_emi_collectors WHERE id=%d AND status='Active'", $collection_collector_id ) ) : null; $collection_person = $collection_collector && ! empty( $collection_collector->user_id ) ? (int) $collection_collector->user_id : get_current_user_id(); $method = sanitize_text_field( wp_unslash( $_POST['customer_emi_method'] ?? 'Cash' ) ); $note = sanitize_textarea_field( wp_unslash( $_POST['customer_emi_note'] ?? '' ) ); if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $collection_date ) ) { $collection_date = current_time( 'Y-m-d' ); } if ( ! in_array( $method, array( 'Cash', 'Savings Account' ), true ) ) { $method = 'Cash'; } if ( $method !== 'Savings Account' && ! $collection_collector ) { $message = '

Please select a valid EMI Collector.

'; } $inst = $wpdb->get_row( $wpdb->prepare( "SELECT i.*, l.customer_id, l.loan_code FROM $installments i INNER JOIN $loans l ON l.id=i.loan_id WHERE i.id=%d AND l.customer_id=%d", $installment_id, $customer_id ) ); if ( ! $inst ) { $message = '

EMI installment not found.

'; } elseif ( $message !== '' ) { // Collector validation failed; do not process the payment. } else { $remaining = max( 0, (float) $inst->amount - (float) $inst->paid_amount ); $pay_amount = min( max( 0, $pay_amount ), $remaining ); $balance = lms_get_savings_balance( $customer_id ); if ( $pay_amount <= 0 ) { $message = '

Invalid EMI payment amount.

'; } elseif ( $method === 'Savings Account' && $pay_amount > $balance + 0.00001 ) { $message = '

Insufficient Savings Account balance. Available: ' . esc_html( lms_money( $balance ) ) . '

'; } else { $wpdb->query( 'START TRANSACTION' ); $new_paid = (float) $inst->paid_amount + $pay_amount; $new_status = $new_paid >= (float) $inst->amount - 0.00001 ? 'Paid' : 'Partial'; $updated = $wpdb->update( $installments, array( 'paid_amount' => $new_paid, 'paid_date' => $collection_date, 'status' => $new_status ), array( 'id' => $installment_id ), array( '%f','%s','%s' ), array( '%d' ) ); if ( false === $updated ) { $wpdb->query( 'ROLLBACK' ); $message = '

EMI could not be updated.

'; } else { $payment_id = $wpdb->insert( $payments, array( 'loan_id' => $inst->loan_id, 'installment_id' => $installment_id, 'payment_type' => 'EMI', 'amount' => $pay_amount, 'payment_date' => $collection_date, 'method' => $method, 'note' => $note, 'created_by' => $collection_person > 0 ? $collection_person : get_current_user_id(), 'collector_id' => $method === 'Savings Account' ? null : ( $collection_collector_id > 0 ? $collection_collector_id : null ), 'created_at' => current_time( 'mysql' ), ), array( '%d','%d','%s','%f','%s','%s','%s','%d','%s' ) ); if ( false === $payment_id ) { $wpdb->query( 'ROLLBACK' ); lms_recalculate_installment_payment_state( $installment_id ); $message = '

EMI payment could not be saved.

'; } else { $payment_id = (int) $wpdb->insert_id; $collector_tx_ok = true; if ( $method !== 'Savings Account' && $collection_collector_id > 0 ) { $collector_tx_ok = (bool) lms_record_collector_payment_transaction( $payment_id, $collection_collector_id, 'EMI', $pay_amount, $collection_date, 'Cash', $note ); if ( ! $collector_tx_ok ) { $wpdb->delete( $payments, array( 'id' => $payment_id ), array( '%d' ) ); $wpdb->query( 'ROLLBACK' ); lms_recalculate_installment_payment_state( $installment_id ); $message = '

EMI payment could not be linked to the collector account.

'; } } $savings_ok = true; if ( $collector_tx_ok && $method === 'Savings Account' ) { $savings_id = lms_savings_add_transaction( array( 'customer_id' => $customer_id, 'transaction_type' => 'withdrawal', 'withdrawal_type' => 'emi', 'amount' => $pay_amount, 'transaction_date' => $collection_date, 'loan_id' => $inst->loan_id, 'installment_id' => $installment_id, 'payment_id' => $payment_id, 'method' => 'Savings Account', 'note' => $note !== '' ? $note : 'EMI paid from Savings Bank Account', 'created_by' => $collection_person > 0 ? $collection_person : get_current_user_id(), ) ); if ( false === $savings_id ) { $savings_ok = false; } } if ( ! $collector_tx_ok ) { // Collector link failure was already rolled back. } elseif ( ! $savings_ok ) { $wpdb->delete( $payments, array( 'id' => $payment_id ), array( '%d' ) ); $wpdb->query( 'ROLLBACK' ); lms_recalculate_installment_payment_state( $installment_id ); $message = '

Savings Account transaction could not be saved, so EMI payment was not completed.

'; } else { $wpdb->query( 'COMMIT' ); lms_maybe_close_loan( $inst->loan_id ); $collector_name = $collection_person > 0 ? get_userdata( $collection_person ) : null; $collector_label = $collector_name ? $collector_name->display_name : 'Current user'; $message = '

EMI #' . esc_html( $inst->installment_no ) . ' collected successfully — ' . esc_html( lms_money( $pay_amount ) ) . ' by ' . esc_html( $collector_label ) . '.

'; } } } } } } /* ===================================================== * CUSTOMER PAY EMI - PROCESSING FEE COLLECTION * ===================================================== */ if ( isset( $_POST['lms_customer_collect_processing_fee'] ) && check_admin_referer( 'lms_customer_collect_processing_fee_action', 'lms_customer_collect_processing_fee_nonce' ) ) { if ( ! current_user_can( 'lms_collect_emi' ) ) { wp_die( 'You do not have permission to collect Processing Fee.' ); } $loan_id = absint( $_POST['customer_processing_loan_id'] ?? 0 ); $pay_amount = (float) ( $_POST['customer_processing_amount'] ?? 0 ); $collection_date = sanitize_text_field( wp_unslash( $_POST['customer_processing_date'] ?? current_time( 'Y-m-d' ) ) ); $collector_id = absint( $_POST['customer_processing_collector'] ?? 0 ); $method = sanitize_text_field( wp_unslash( $_POST['customer_processing_method'] ?? 'Cash' ) ); $note = sanitize_textarea_field( wp_unslash( $_POST['customer_processing_note'] ?? '' ) ); if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $collection_date ) ) $collection_date = current_time( 'Y-m-d' ); if ( ! in_array( $method, array( 'Cash', 'Savings Account' ), true ) ) $method = 'Cash'; $loan = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $loans WHERE id=%d AND customer_id=%d", $loan_id, $customer_id ) ); $collector = $collector_id > 0 ? $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}lms_emi_collectors WHERE id=%d AND status='Active'", $collector_id ) ) : null; if ( ! $loan ) { $message = '

Loan not found.

'; } elseif ( $method !== 'Savings Account' && ! $collector ) { $message = '

Please select a valid EMI Collector.

'; } else { $already_paid = lms_get_processing_fee_paid( $loan->id ); $remaining = max( 0, (float) $loan->processing_fee - $already_paid ); $pay_amount = min( max( 0, $pay_amount ), $remaining ); $savings_balance = lms_get_savings_balance( $customer_id ); if ( $pay_amount <= 0 ) { $message = '

Processing Fee is already fully paid or the amount is invalid.

'; } elseif ( $method === 'Savings Account' && $pay_amount > $savings_balance + 0.00001 ) { $message = '

Insufficient Savings Account balance. Available: ' . esc_html( lms_money( $savings_balance ) ) . '

'; } else { $wpdb->query( 'START TRANSACTION' ); $created_by = $collector && ! empty( $collector->user_id ) ? (int) $collector->user_id : get_current_user_id(); $inserted = $wpdb->insert( $payments, array( 'loan_id' => $loan->id, 'installment_id' => null, 'payment_type' => 'Processing Fee', 'amount' => $pay_amount, 'payment_date' => $collection_date, 'method' => $method, 'note' => $note, 'created_by' => $created_by, 'collector_id' => $method === 'Savings Account' ? null : $collector_id, 'created_at' => current_time( 'mysql' ), ), array( '%d','%d','%s','%f','%s','%s','%s','%d','%d','%s' ) ); if ( false === $inserted ) { $wpdb->query( 'ROLLBACK' ); $message = '

Processing Fee payment could not be saved.

'; } else { $payment_id = (int) $wpdb->insert_id; $collector_ok = true; $savings_ok = true; $savings_id = 0; if ( $method !== 'Savings Account' && $collector_id > 0 ) { $collector_ok = (bool) lms_record_collector_payment_transaction( $payment_id, $collector_id, 'Processing Fee', $pay_amount, $collection_date, 'Cash', $note ); } if ( $method === 'Savings Account' ) { $savings_id = lms_savings_add_transaction( array( 'customer_id' => $customer_id, 'transaction_type' => 'withdrawal', 'withdrawal_type' => 'processing_fee', 'amount' => $pay_amount, 'transaction_date' => $collection_date, 'loan_id' => $loan->id, 'payment_id' => $payment_id, 'method' => 'Savings Account', 'note' => $note !== '' ? $note : 'Processing Fee paid from Savings Bank Account', 'created_by' => $created_by, ) ); $savings_ok = false !== $savings_id; if ( $savings_ok ) { $wpdb->update( $payments, array( 'savings_transaction_id' => (int) $savings_id ), array( 'id' => $payment_id ), array( '%d' ), array( '%d' ) ); } } if ( ! $collector_ok || ! $savings_ok ) { $wpdb->delete( $payments, array( 'id' => $payment_id ), array( '%d' ) ); if ( $savings_ok && $savings_id > 0 ) $wpdb->delete( $savings, array( 'id' => $savings_id, 'customer_id' => $customer_id ), array( '%d','%d' ) ); $wpdb->query( 'ROLLBACK' ); $message = '

Processing Fee collection could not be completed.

'; } else { $wpdb->query( 'COMMIT' ); lms_recalculate_processing_fee_state( $loan->id ); lms_maybe_close_loan( $loan->id ); $collector_label = $collector ? ( $collector->user_display_name ?: $collector->name ) : 'Savings Account'; $message = '

Processing Fee of ' . esc_html( lms_money( $pay_amount ) ) . ' collected successfully by ' . esc_html( $collector_label ) . '.

'; } } } } } /* ===================================================== * CUSTOMER PAY EMI - PAY SELECTED EMI * ===================================================== */ if ( isset( $_POST['lms_customer_pay_selected_emi'] ) && check_admin_referer( 'lms_customer_pay_selected_emi_action', 'lms_customer_pay_selected_emi_nonce' ) ) { if ( ! current_user_can( 'lms_collect_emi' ) ) { wp_die( 'You do not have permission to collect EMI.' ); } $selected_ids = isset( $_POST['pay_all_installment_ids'] ) && is_array( $_POST['pay_all_installment_ids'] ) ? array_values( array_unique( array_filter( array_map( 'absint', wp_unslash( $_POST['pay_all_installment_ids'] ) ) ) ) ) : array(); $collection_date = sanitize_text_field( wp_unslash( $_POST['pay_all_collection_date'] ?? current_time( 'Y-m-d' ) ) ); $pay_all_collector_id = absint( $_POST['pay_all_collection_collector'] ?? 0 ); $pay_all_collector = $pay_all_collector_id > 0 ? $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}lms_emi_collectors WHERE id=%d AND status='Active'", $pay_all_collector_id ) ) : null; $collection_person = $pay_all_collector && ! empty( $pay_all_collector->user_id ) ? (int) $pay_all_collector->user_id : get_current_user_id(); $method = sanitize_text_field( wp_unslash( $_POST['pay_all_payment_method'] ?? 'Cash' ) ); $note = sanitize_textarea_field( wp_unslash( $_POST['pay_all_payment_note'] ?? '' ) ); if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $collection_date ) ) { $collection_date = current_time( 'Y-m-d' ); } if ( ! in_array( $method, array( 'Cash', 'Savings Account' ), true ) ) { $method = 'Cash'; } if ( $method !== 'Savings Account' && ! $pay_all_collector ) { $message = '

Please select a valid EMI Collector.

'; } if ( empty( $selected_ids ) ) { $message = '

Please select at least one EMI.

'; } else { $placeholders = implode( ',', array_fill( 0, count( $selected_ids ), '%d' ) ); $query_args = array_merge( array( $customer_id ), $selected_ids ); $selected_installments = $wpdb->get_results( $wpdb->prepare( "SELECT i.* FROM $installments i INNER JOIN $loans l ON l.id=i.loan_id WHERE l.customer_id=%d AND i.id IN ($placeholders) AND i.paid_amount < i.amount ORDER BY i.due_date ASC, i.id ASC", $query_args ) ); $total_selected = 0; foreach ( $selected_installments as $si ) { $total_selected += max( 0, (float) $si->amount - (float) $si->paid_amount ); } $balance = lms_get_savings_balance( $customer_id ); if ( $message !== '' ) { // Collector validation failed; do not process the selected EMI payments. } elseif ( empty( $selected_installments ) || $total_selected <= 0 ) { $message = '

No unpaid EMI was selected.

'; } elseif ( $method === 'Savings Account' && $total_selected > $balance + 0.00001 ) { $message = '

Insufficient Savings Account balance. Required: ' . esc_html( lms_money( $total_selected ) ) . ' · Available: ' . esc_html( lms_money( $balance ) ) . '

'; } else { $wpdb->query( 'START TRANSACTION' ); $success_count = 0; $failed = false; foreach ( $selected_installments as $si ) { $remaining = max( 0, (float) $si->amount - (float) $si->paid_amount ); if ( $remaining <= 0 ) { continue; } $new_paid = (float) $si->paid_amount + $remaining; $updated = $wpdb->update( $installments, array( 'paid_amount' => $new_paid, 'paid_date' => $collection_date, 'status' => 'Paid' ), array( 'id' => (int) $si->id ), array( '%f','%s','%s' ), array( '%d' ) ); if ( false === $updated ) { $failed = true; break; } $payment_id = $wpdb->insert( $payments, array( 'loan_id' => $si->loan_id, 'installment_id' => $si->id, 'payment_type' => 'EMI', 'amount' => $remaining, 'payment_date' => $collection_date, 'method' => $method, 'note' => $note !== '' ? $note : 'Pay All EMI', 'created_by' => $collection_person > 0 ? $collection_person : get_current_user_id(), 'collector_id' => $method === 'Savings Account' ? null : $pay_all_collector_id, 'created_at' => current_time( 'mysql' ), ), array( '%d','%d','%s','%f','%s','%s','%s','%d','%s' ) ); if ( false === $payment_id ) { $failed = true; break; } if ( $method !== 'Savings Account' && $pay_all_collector_id > 0 ) { $collector_tx_id = lms_record_collector_payment_transaction( (int) $wpdb->insert_id, $pay_all_collector_id, 'EMI', $remaining, $collection_date, 'Cash', $note !== '' ? $note : 'Pay All EMI' ); if ( ! $collector_tx_id ) { $failed = true; break; } } if ( $method === 'Savings Account' ) { $savings_id = lms_savings_add_transaction( array( 'customer_id' => $customer_id, 'transaction_type' => 'withdrawal', 'withdrawal_type' => 'emi', 'amount' => $remaining, 'transaction_date' => $collection_date, 'loan_id' => $si->loan_id, 'installment_id' => $si->id, 'payment_id' => (int) $wpdb->insert_id, 'method' => 'Savings Account', 'note' => $note !== '' ? $note : 'Pay All EMI from Savings Bank Account', 'created_by' => $collection_person > 0 ? $collection_person : get_current_user_id(), ) ); if ( false === $savings_id ) { $failed = true; break; } } $success_count++; } if ( $failed ) { $wpdb->query( 'ROLLBACK' ); foreach ( $selected_installments as $si ) { lms_recalculate_installment_payment_state( $si->id ); } $message = '

Payment could not be completed. No selected EMI was changed.

'; } else { $wpdb->query( 'COMMIT' ); foreach ( $selected_installments as $si ) { lms_maybe_close_loan( $si->loan_id ); } $message = '

' . esc_html( $success_count . ' EMI(s) paid successfully — ' . lms_money( $total_selected ) . ' via ' . $method . '.' ) . '

'; } } } } /* ===================================================== * CUSTOMER PAY EMI - PAY ALL REMAINING EMI * ===================================================== */ /* Pay-all processing is handled by lms_customer_pay_selected_emi above. */ lms_refresh_installment_statuses(); $customer_loans = $wpdb->get_results( $wpdb->prepare( "SELECT l.*, COALESCE(SUM(i.amount),0) AS scheduled_emi, COALESCE(SUM(i.paid_amount),0) AS paid_emi, COALESCE(SUM(GREATEST(i.amount-i.paid_amount,0)),0) AS remaining_emi FROM $loans l LEFT JOIN $installments i ON i.loan_id=l.id WHERE l.customer_id=%d GROUP BY l.id ORDER BY l.id DESC", $customer_id ) ); $customer_payments = $wpdb->get_results( $wpdb->prepare( "SELECT p.*, p.savings_transaction_id, i.installment_no, i.due_date, i.amount AS installment_amount, i.paid_amount AS installment_paid_amount, i.status AS installment_status, l.loan_code, l.processing_fee, l.processing_paid_amount, u.display_name AS collector_name FROM $payments p LEFT JOIN $installments i ON i.id=p.installment_id INNER JOIN $loans l ON l.id=p.loan_id LEFT JOIN {$wpdb->users} u ON u.ID=p.created_by WHERE l.customer_id=%d ORDER BY p.payment_date DESC, p.id DESC", $customer_id ) ); $users = get_users( array( 'orderby' => 'display_name', 'order' => 'ASC' ) ); $customer_collectors = lms_get_emi_collectors(); $new_loan_url = admin_url( 'admin.php?page=lms-new-loan&customer_id=' . $customer_id ); $active_tab = sanitize_key( $_GET['tab'] ?? 'contact' ); if ( ! in_array( $active_tab, array( 'contact','loans','new-loan','emi','savings' ), true ) ) { $active_tab = 'contact'; } ?>

name ); ?>

'Contact Details', 'loans' => 'Loan Details', 'new-loan' => 'New Loan Apply', 'emi' => 'Pay EMI', 'savings' => 'Saving Bank Account', ); foreach ( $tabs as $key => $label ) : $url = $key === 'new-loan' ? $new_loan_url : admin_url( 'admin.php?page=lms-customer-profile&customer_id=' . $customer_id . '&tab=' . $key ); $active_style = $active_tab === $key ? 'background:#2271b1;color:#fff;' : 'background:#fff;color:#222;'; ?>

← Customers

Contact Details

Customer ID

New customers automatically get the next serial ID. Deleted/gap IDs are available here for manual reuse.

id ); ?>
Customer Code

New customers automatically get the next serial code. Deleted/gap codes can be assigned manually.

customer_code ); ?>
Name *
Father / Husband Name
Mobile *
Address
Aadhaar
PAN
Status

Customers with loan history cannot be deleted.

Customer Codecustomer_code ); ?>
Namename ); ?>
Father / Husbandguardian_name ); ?>
Mobilemobile ); ?>
Addressaddress ) ); ?>
Aadhaaraadhaar ); ?>
PANpan ); ?>
Statusstatus ); ?>

Loan Details

get_results( $wpdb->prepare( "SELECT i.*, l.loan_code FROM $installments i INNER JOIN $loans l ON l.id=i.loan_id WHERE l.customer_id=%d AND i.paid_amount < i.amount ORDER BY i.due_date ASC, i.id ASC", $customer_id ) ); $total_due_emi_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $installments i INNER JOIN $loans l ON l.id=i.loan_id WHERE l.customer_id=%d AND i.paid_amount < i.amount", $customer_id ) ); $total_due_emi_amount = (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(GREATEST(i.amount-i.paid_amount,0)),0) FROM $installments i INNER JOIN $loans l ON l.id=i.loan_id WHERE l.customer_id=%d AND i.paid_amount < i.amount", $customer_id ) ); $history_loan_id = absint( $_GET['loan_history'] ?? 0 ); $history_loan = null; $history_rows = array(); if ( $history_loan_id ) { $history_loan = $wpdb->get_row( $wpdb->prepare( "SELECT l.* FROM $loans l WHERE l.id=%d AND l.customer_id=%d", $history_loan_id, $customer_id ) ); if ( $history_loan ) { $history_installments = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $installments WHERE loan_id=%d ORDER BY installment_no ASC", $history_loan_id ) ); foreach ( $history_installments as $hi ) { $payment_rows = $wpdb->get_results( $wpdb->prepare( "SELECT p.method, p.payment_date, p.amount, u.display_name AS collector_name, p.note FROM $payments p LEFT JOIN {$wpdb->users} u ON u.ID=p.created_by WHERE p.installment_id=%d AND p.payment_type='EMI' ORDER BY p.id ASC", $hi->id ) ); $methods = array(); $paid_dates = array(); $collectors = array(); $notes = array(); $paid_total = 0; foreach ( $payment_rows as $pr ) { $methods[] = $pr->method; $paid_dates[] = $pr->payment_date; if ( $pr->collector_name ) { $collectors[] = $pr->collector_name; } if ( $pr->note ) { $notes[] = $pr->note; } $paid_total += (float) $pr->amount; } $history_rows[] = array( 'emi_no' => $hi->installment_no, 'due_date' => $hi->due_date, 'amount' => (float) $hi->amount, 'method' => ! empty( $methods ) ? implode( ', ', array_unique( $methods ) ) : '—', 'paid_date' => ! empty( $paid_dates ) ? implode( ', ', array_unique( $paid_dates ) ) : '—', 'type' => 'EMI', 'status' => (float) $hi->paid_amount >= (float) $hi->amount - 0.00001 ? 'Paid' : ( (float) $hi->paid_amount > 0 ? 'Partial' : 'Unpaid' ), 'installment_id' => (int) $hi->id, 'loan_id' => (int) $history_loan_id, 'payment_id' => ! empty( $payment_rows ) ? (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $payments WHERE installment_id=%d AND payment_type='EMI' ORDER BY id DESC LIMIT 1", $hi->id ) ) : 0, 'collector' => ! empty( $collectors ) ? implode( ', ', array_unique( $collectors ) ) : '—', 'note' => ! empty( $notes ) ? implode( ' | ', array_unique( $notes ) ) : '—', 'paid_total' => $paid_total, ); } $processing_total = (float) $history_loan->processing_fee; $processing_payments = $wpdb->get_results( $wpdb->prepare( "SELECT p.id, p.method, p.payment_date, p.amount, p.collector_id, u.display_name AS collector_name, p.note FROM $payments p LEFT JOIN {$wpdb->users} u ON u.ID=p.created_by WHERE p.loan_id=%d AND p.payment_type='Processing Fee' ORDER BY p.id ASC", $history_loan_id ) ); $processing_paid = 0; $pmethods = array(); $pdates = array(); $pcollectors = array(); $pnotes = array(); foreach ( $processing_payments as $pp ) { $processing_paid += (float) $pp->amount; $pmethods[] = $pp->method; $pdates[] = $pp->payment_date; if ( $pp->collector_name ) { $pcollectors[] = $pp->collector_name; } if ( $pp->note ) { $pnotes[] = $pp->note; } } if ( $processing_total > 0 || $processing_paid > 0 ) { $history_rows[] = array( 'emi_no' => '—', 'due_date' => '—', 'amount' => $processing_total, 'method' => ! empty( $pmethods ) ? implode( ', ', array_unique( $pmethods ) ) : '—', 'paid_date' => ! empty( $pdates ) ? implode( ', ', array_unique( $pdates ) ) : '—', 'type' => 'Processing Fee', 'status' => $processing_paid >= $processing_total - 0.00001 ? 'Paid' : 'Unpaid', 'installment_id' => 0, 'loan_id' => (int) $history_loan_id, 'payment_id' => ! empty( $processing_payments ) ? (int) end( $processing_payments )->id : 0, 'collector' => ! empty( $pcollectors ) ? implode( ', ', array_unique( $pcollectors ) ) : '—', 'note' => ! empty( $pnotes ) ? implode( ' | ', array_unique( $pnotes ) ) : '—', 'paid_total' => $processing_paid, ); } } } ?>
Total Due EMI
EMI
Due Amount:
0 ) : ?>
id ); $cpl_remaining = max( 0, (float) $cpl->processing_fee - $cpl_paid ); if ( (float) $cpl->processing_fee > 0 && $cpl_remaining > 0.00001 ) { $customer_processing_loans[] = array( 'id' => (int) $cpl->id, 'loan_code' => $cpl->loan_code, 'total' => (float) $cpl->processing_fee, 'paid' => $cpl_paid, 'remaining' => $cpl_remaining, ); } } ?>

Pay Processing Fee

Collector, collection date এবং Cash / Savings Account source select করে Processing Fee collect করুন।

No Processing Fee due.
Processing Fee · Paid
Remaining:

Pay EMI

প্রতিটি loan-এর শুধু next unpaid EMI দেখাবে। Paid হলে পরের EMI automatically আসবে।

Loan ID Amount Tenure EMI Start Loan End Weekly EMI Processing Remaining EMI Complete EMI Status
No loans found.
loan_code ); ?> loan_amount ) ); ?> tenure_weeks ); ?> Weeks emi_start_date ); ?> loan_end_date ); ?> weekly_emi ) ); ?> processing_fee ) ); ?> remaining_emi ) ); ?> paid_emi ) ); ?> status ); ?>
amount - (float) $next->paid_amount ); $status_class = strtolower( $next->status ); $history_url = admin_url( 'admin.php?page=lms-customer-profile&customer_id=' . $customer_id . '&tab=emi&loan_history=' . absint( $next->loan_id ) ); ?>
EMI DateCustomerMobileLoan idInstallmentRemainingStatusCollect
No unpaid EMI found.
due_date ); ?> name ); ?> mobile ); ?> loan_code ); ?> #installment_no ); ?> status ); ?> —

Loan History

Customer-এর সব loan আলাদা করে দেখুন। একটি Loan ID select করলে শুধু ওই loan-এর complete payment history খুলবে।

Loan Payment History

loan_code ); ?> · name ); ?>

Customer: name ); ?>
Mobile: mobile ); ?>
Loan Amount: loan_amount ) ); ?>
Tenure: tenure_weeks ); ?> Weeks
EMI Start: emi_start_date ); ?>
Loan End: loan_end_date ); ?>
Weekly EMI: weekly_emi ) ); ?>
Print Date:
Bulk Edit: 0 selected
EMI No.Due DateEMI AmountMethodPaid DateTypeStatus
0 ) : ?> 0 && $hr['status'] === 'Paid' && ( lms_emi_history_edit_enabled() || lms_emi_history_delete_enabled() ) ) || ( $hr['type'] === 'EMI' && $hr['installment_id'] > 0 && lms_emi_history_mark_unpaid_enabled() && $hr['status'] === 'Paid' ) || ( $hr['type'] === 'Processing Fee' && $hr['loan_id'] > 0 && lms_emi_history_mark_unpaid_enabled() && $hr['status'] === 'Paid' ) ); ?>
0 && $hr['status'] === 'Paid' && lms_emi_history_edit_enabled() ) : ?> 0 && lms_emi_history_mark_unpaid_enabled() && $hr['status'] === 'Paid' ) : ?>
0 && lms_emi_history_mark_unpaid_enabled() && $hr['status'] === 'Paid' ) : ?>
0 && $hr['status'] === 'Paid' && lms_emi_history_delete_enabled() ) : ?>
Payment details (collector / note) are recorded with every payment and are available when editing the payment record.
get_results( $wpdb->prepare( "SELECT s.*, l.loan_code, i.installment_no, u.display_name AS collector_name FROM $savings s LEFT JOIN $loans l ON l.id=s.loan_id LEFT JOIN $installments i ON i.id=s.installment_id LEFT JOIN {$wpdb->users} u ON u.ID=s.created_by WHERE s.customer_id=%d ORDER BY s.transaction_date DESC, s.id DESC", $customer_id ) ); $next_emis = lms_get_customer_next_emis( $customer_id ); ?>
Saving Bank Account
Available Balance
get_row( $wpdb->prepare( "SELECT * FROM $savings WHERE id=%d AND customer_id=%d", $savings_edit_id, $customer_id ) ); } ?>

Edit Saving Account Transaction

Cancel

Deposit Money

Withdraw


Saving Account Transaction History

transaction_type === 'deposit'; $details = $is_deposit ? 'Deposit' : ( $tx->withdrawal_type === 'emi' ? 'EMI Payment' : ( $tx->withdrawal_type === 'processing_fee' ? 'Processing Fee Payment' : 'Cash Withdrawal' ) ); $edit_url = admin_url( 'admin.php?page=lms-customer-profile&customer_id=' . $customer_id . '&tab=savings&edit_savings=' . absint( $tx->id ) ); ?>
DateTypeDetailsAmountMethodCollectorNoteAction
No savings transactions found.
transaction_date ); ?> loan_code ? ' — ' . $tx->loan_code : '' ) . ( $tx->installment_no ? ' — EMI #' . $tx->installment_no : '' ) ); ?> amount ) ); ?> method ); ?> collector_name ? $tx->collector_name : '—' ); ?> note ); ?> Edit
prefix . 'lms_loans'; $installments = $wpdb->prefix . 'lms_installments'; $payments = $wpdb->prefix . 'lms_payments'; $customers = $wpdb->prefix . 'lms_customers'; $loan = $wpdb->get_row( $wpdb->prepare( "SELECT l.*, c.name AS customer_name, c.id AS customer_id FROM $loans l INNER JOIN $customers c ON c.id=l.customer_id WHERE l.id=%d", $loan_id ) ); if ( ! $loan ) { wp_die( 'Loan not found.' ); } $message = ''; /* FORECLOSE: collect every remaining EMI at once and close the loan. */ if ( isset( $_POST['lms_foreclose_loan'] ) && check_admin_referer( 'lms_foreclose_loan_action', 'lms_foreclose_nonce' ) ) { lms_admin_only(); $collection_date = sanitize_text_field( wp_unslash( $_POST['collection_date'] ?? current_time( 'Y-m-d' ) ) ); $collection_person = absint( $_POST['collection_person'] ?? get_current_user_id() ); if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $collection_date ) ) { $collection_date = current_time( 'Y-m-d' ); } $due_rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $installments WHERE loan_id=%d AND paid_amount < amount ORDER BY installment_no ASC", $loan_id ) ); foreach ( $due_rows as $inst ) { $remaining = max( 0, (float) $inst->amount - (float) $inst->paid_amount ); if ( $remaining <= 0 ) { continue; } $wpdb->update( $installments, array( 'paid_amount' => (float) $inst->amount, 'paid_date' => $collection_date, 'status' => 'Paid', ), array( 'id' => $inst->id ), array( '%f','%s','%s' ), array( '%d' ) ); $wpdb->insert( $payments, array( 'loan_id' => $loan_id, 'installment_id' => $inst->id, 'amount' => $remaining, 'payment_date' => $collection_date, 'method' => 'Cash', 'note' => 'Foreclosure - all remaining EMI collected together.', 'created_by' => $collection_person, 'created_at' => current_time( 'mysql' ), ), array( '%d','%d','%f','%s','%s','%s','%d','%s' ) ); } lms_maybe_close_loan( $loan_id ); $loan = $wpdb->get_row( $wpdb->prepare( "SELECT l.*, c.name AS customer_name, c.id AS customer_id FROM $loans l INNER JOIN $customers c ON c.id=l.customer_id WHERE l.id=%d", $loan_id ) ); $message = '

Loan foreclosed successfully. All remaining EMI has been collected and the loan is closed.

'; } /* DELETE LOAN: ADMIN ONLY, and only when no payment history exists. */ if ( isset( $_POST['lms_delete_loan'] ) && check_admin_referer( 'lms_delete_loan_action', 'lms_delete_loan_nonce' ) ) { lms_admin_only(); $payment_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $payments WHERE loan_id=%d", $loan_id ) ); if ( $payment_count > 0 ) { $message = '

This loan cannot be deleted because payment history exists.

'; } else { lms_ensure_collector_schema(); if ( isset($loan->funding_source) && $loan->funding_source === 'organization_bank' ) { lms_record_organization_bank_loan_refund($loan_id, (float)$loan->loan_amount, current_time('Y-m-d')); } $wpdb->delete( $installments, array( 'loan_id' => $loan_id ), array( '%d' ) ); $wpdb->delete( $loans, array( 'id' => $loan_id ), array( '%d' ) ); echo '

Loan deleted successfully.

'; echo '

Back to Loan Details

'; return; } } /* EDIT: ADMIN ONLY. To protect financial history, schedule-changing edits are blocked once any payment has been recorded. */ if ( isset( $_POST['lms_update_loan'] ) && check_admin_referer( 'lms_update_loan_action', 'lms_update_loan_nonce' ) ) { lms_admin_only(); $payment_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $payments WHERE loan_id=%d", $loan_id ) ); $new_loan_code = strtoupper( sanitize_text_field( wp_unslash( $_POST['loan_code'] ?? $loan->loan_code ) ) ); $amount = (float) ( $_POST['loan_amount'] ?? 0 ); $tenure = absint( $_POST['tenure_weeks'] ?? 0 ); $weekly_emi = (float) ( $_POST['weekly_emi'] ?? 0 ); $processing_fee = (float) ( $_POST['processing_fee'] ?? 0 ); $loan_date = sanitize_text_field( wp_unslash( $_POST['loan_date'] ?? current_time( 'Y-m-d' ) ) ); $emi_start_date = sanitize_text_field( wp_unslash( $_POST['emi_start_date'] ?? '' ) ); // If EMI Start Date is not supplied, use the 2nd Tuesday after the loan date. if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $emi_start_date ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $loan_date ) ) { $emi_start_date = lms_default_emi_start_date( $loan_date ); } $status = sanitize_text_field( wp_unslash( $_POST['status'] ?? 'Active' ) ); $loan_code_valid = (bool) preg_match( '/^LN-\d{1,10}$/', $new_loan_code ); $loan_code_exists = false; if ( $loan_code_valid && $new_loan_code !== $loan->loan_code ) { $loan_code_exists = (bool) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $loans WHERE loan_code=%s AND id<>%d", $new_loan_code, $loan_id ) ); } if ( ! lms_loan_id_edit_enabled() && $new_loan_code !== $loan->loan_code ) { $message = '

Loan ID editing is currently disabled in Loan Settings.

'; } elseif ( ! $loan_code_valid || $loan_code_exists ) { $message = '

Loan ID must be unique and use the format LN-00001.

'; } elseif ( $amount < 5000 || $tenure < 1 || $weekly_emi <= 0 || $processing_fee < 0 || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $loan_date ) || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $emi_start_date ) || $emi_start_date < $loan_date ) { $message = '

Please enter valid loan details.

'; } elseif ( $payment_count > 0 && ( (float) $loan->loan_amount !== $amount || (int) $loan->tenure_weeks !== $tenure || (float) $loan->weekly_emi !== $weekly_emi || (float) $loan->processing_fee !== $processing_fee || $loan->loan_date !== $loan_date ) ) { $message = '

This loan already has payment history. Amount, tenure, EMI, processing fee and loan date cannot be changed after collection has started. EMI Start Date can still be adjusted.

'; } else { $total_emi = round( $weekly_emi * $tenure, 2 ); $total_collection = round( $total_emi + $processing_fee, 2 ); // Loan End Date is fixed after loan creation. Editing EMI Start Date // does not change the stored Loan End Date. $loan_end_date = $loan->loan_end_date; $wpdb->update( $loans, array( 'loan_code' => $new_loan_code, 'loan_amount' => $amount, 'tenure_weeks' => $tenure, 'weekly_emi' => $weekly_emi, 'processing_fee' => $processing_fee, 'processing_paid_amount' => 0, 'total_emi' => $total_emi, 'total_collection' => $total_collection, 'loan_date' => $loan_date, 'emi_start_date' => $emi_start_date, 'loan_end_date' => $loan_end_date, 'start_date' => $emi_start_date, 'status' => in_array( $status, array( 'Active','Closed' ), true ) ? $status : 'Active', ), array( 'id' => $loan_id ), array( '%s','%f','%d','%f','%f','%f','%f','%s','%s','%s','%s','%s' ), array( '%d' ) ); if ( $payment_count === 0 ) { $wpdb->delete( $installments, array( 'loan_id' => $loan_id ), array( '%d' ) ); $base = new DateTime( $emi_start_date ); for ( $i = 1; $i <= $tenure; $i++ ) { $due = clone $base; $due->modify( '+' . ( $i - 1 ) . ' weeks' ); $wpdb->insert( $installments, array( 'loan_id' => $loan_id, 'installment_no' => $i, 'due_date' => $due->format( 'Y-m-d' ), 'amount' => $weekly_emi, 'paid_amount' => 0, 'status' => 'Due', ), array( '%d','%d','%s','%f','%f','%s' ) ); } } $loan = $wpdb->get_row( $wpdb->prepare( "SELECT l.*, c.name AS customer_name, c.id AS customer_id FROM $loans l INNER JOIN $customers c ON c.id=l.customer_id WHERE l.id=%d", $loan_id ) ); $message = '

Loan updated successfully.

'; } } $users = get_users( array( 'orderby' => 'display_name', 'order' => 'ASC' ) ); $default_person = get_current_user_id(); ?>

Edit Loan — loan_code ); ?>

← Back to Loan Details

customer_name ); ?>

Loan ID

Format: LN-00001. Must be unique. This control is linked to Customer ID & Customer Code Controls.

loan_code ); ?>
Loan Amount
Tenure (Weeks)
Weekly EMI
Processing Fee
Loan Date
EMI Start Date
Loan End Date
Status
status === 'Active' ) : ?>

Foreclose Loan

Foreclose করলে customer-এর সব remaining EMI একসাথে Paid হবে এবং loan automatically Closed হবে.

Delete Loan

Delete is allowed only when this loan has no payment history.

prefix . 'lms_tenure_settings'; $rows = $wpdb->get_results( "SELECT id, loan_amount, processing_fee, processing_multiplier FROM $table WHERE processing_multiplier=0 AND loan_amount>0 AND processing_fee>0" ); foreach ( $rows as $row ) { // v1.8.6 stored Custom Processing Fee as a percentage of Loan Amount. // v1.8.7 stores it as a rupee amount per ₹10,000 of Loan Amount. // Example: 5% becomes ₹500 per ₹10,000. $per_10000 = round( ( (float) $row->processing_fee / 100 ) * 10000, 2 ); $wpdb->update( $table, array( 'processing_fee' => $per_10000 ), array( 'id' => (int) $row->id ), array( '%f' ), array( '%d' ) ); } update_option( 'lms_custom_processing_per_10000_migrated_v186', 1 ); } function lms_settings_page() { lms_admin_only(); global $wpdb; lms_migrate_custom_processing_fee_to_per_10000(); $table = $wpdb->prefix . 'lms_tenure_settings'; $message = ''; $edit_id = absint( $_GET['edit_id'] ?? 0 ); /* Saving Account transaction history Edit/Delete control. */ if ( isset( $_POST['lms_save_savings_controls'] ) && check_admin_referer( 'lms_save_savings_controls_action', 'lms_savings_controls_nonce' ) ) { $enabled = isset( $_POST['savings_history_edit_delete_enabled'] ) ? 1 : 0; update_option( 'lms_savings_history_edit_delete_enabled', $enabled ); $message = $enabled ? '

Saving Account Transaction History Edit/Delete is now ON.

' : '

Saving Account Transaction History Edit/Delete is now OFF.

'; } /* Founder / Investor transaction history Edit/Delete control. */ if ( isset( $_POST['lms_save_investor_controls'] ) && check_admin_referer( 'lms_save_investor_controls_action', 'lms_investor_controls_nonce' ) ) { $enabled = isset( $_POST['investor_history_edit_delete_enabled'] ) ? 1 : 0; update_option( 'lms_investor_history_edit_delete_enabled', $enabled ); $message = $enabled ? '

Founder/Investor Transaction Edit/Delete is now ON.

' : '

Founder/Investor Transaction Edit/Delete is now OFF.

'; } /* Customer ID / Customer Code edit-delete control. */ if ( isset( $_POST['lms_save_customer_identity_controls'] ) && check_admin_referer( 'lms_save_customer_identity_controls_action', 'lms_customer_identity_controls_nonce' ) ) { $enabled = isset( $_POST['customer_identity_edit_delete_enabled'] ) ? 1 : 0; update_option( 'lms_customer_identity_edit_delete_enabled', $enabled ); $message = $enabled ? '

Customer ID, Customer Code & Loan ID controls are now ON.

' : '

Customer ID, Customer Code & Loan ID controls are now OFF.

'; } /* Loan ID edit control (shares the Customer ID & Customer Code Controls setting). */ if ( isset( $_POST['lms_save_customer_identity_controls'] ) && check_admin_referer( 'lms_save_customer_identity_controls_action', 'lms_customer_identity_controls_nonce' ) ) { update_option( 'lms_loan_id_edit_enabled', isset( $_POST['loan_id_edit_enabled'] ) ? 1 : 0 ); } /* EMI Collector account transaction Edit/Delete controls. */ if ( isset($_POST['lms_save_collector_account_controls']) && check_admin_referer('lms_save_collector_account_controls_action','lms_collector_account_controls_nonce') ) { update_option('lms_collector_account_edit_delete_enabled', isset($_POST['collector_account_edit_delete_enabled']) ? 1 : 0); $message='

EMI Collector Account Edit/Delete control saved successfully.

'; } /* EMI / Organization Bank controls. */ if ( isset($_POST['lms_save_emi_controls']) && check_admin_referer('lms_save_emi_controls_action','lms_emi_controls_nonce') ) { $map=array( 'lms_emi_show_collector_count'=>'emi_show_collector_count', 'lms_emi_show_collector_manage'=>'emi_show_collector_manage', 'lms_emi_show_collector_actions'=>'emi_show_collector_actions', 'lms_emi_show_collector_history'=>'emi_show_collector_history', 'lms_emi_show_processing_fee'=>'emi_show_processing_fee', 'lms_emi_show_collection'=>'emi_show_collection', 'lms_emi_show_bulk'=>'emi_show_bulk', 'lms_emi_show_org_bank'=>'emi_show_org_bank', 'lms_emi_show_org_bank_actions'=>'emi_show_org_bank_actions', 'lms_emi_org_bank_edit_delete_enabled'=>'emi_org_bank_edit_delete_enabled', 'lms_emi_org_bank_funding_enabled'=>'emi_org_bank_funding_enabled' ); foreach($map as $okey=>$pkey) update_option($okey,isset($_POST[$pkey])?1:0); $org_name=sanitize_text_field(wp_unslash($_POST['organization_name']??'My Organization')); update_option('lms_organization_name',$org_name!==''?$org_name:'My Organization'); $message='

EMI & Organization Bank controls saved successfully.

'; } /* EMI payment history controls. */ if ( isset( $_POST['lms_save_emi_history_controls'] ) && check_admin_referer( 'lms_save_emi_history_controls_action', 'lms_emi_history_controls_nonce' ) ) { update_option( 'lms_emi_history_edit_enabled', isset( $_POST['emi_history_edit_enabled'] ) ? 1 : 0 ); update_option( 'lms_emi_history_delete_enabled', isset( $_POST['emi_history_delete_enabled'] ) ? 1 : 0 ); update_option( 'lms_emi_history_mark_unpaid_enabled', isset( $_POST['emi_history_mark_unpaid_enabled'] ) ? 1 : 0 ); update_option( 'lms_emi_history_bulk_enabled', isset( $_POST['emi_history_bulk_enabled'] ) ? 1 : 0 ); $message = '

EMI Payment History controls saved successfully.

'; } /* Save / Add / Update setting. */ if ( isset( $_POST['lms_save_settings'] ) && check_admin_referer( 'lms_save_settings_action', 'lms_settings_nonce' ) ) { $setting_id = absint( $_POST['setting_id'] ?? 0 ); $loan_amount = (float) ( $_POST['loan_amount'] ?? 0 ); $tenure = absint( $_POST['tenure_weeks'] ?? 0 ); $frequency = sanitize_key( $_POST['emi_frequency'] ?? 'weekly' ); $weekly_emi = (float) ( $_POST['weekly_emi'] ?? 0 ); $monthly_emi = (float) ( $_POST['monthly_emi'] ?? 0 ); $rate = (float) ( $_POST['emi_rate'] ?? 0 ); $processing = (float) ( $_POST['processing_fee'] ?? 0 ); $calc_mode = sanitize_key( $_POST['calc_mode'] ?? 'rate' ); if ( ! in_array( $frequency, array( 'weekly', 'monthly' ), true ) ) { $frequency = 'weekly'; } /* * Two-way calculation: * 1) Loan Amount + Interest % => EMI * 2) Loan Amount + EMI => Interest % * Tenure controls the number of instalments, not the per-period EMI formula. */ if ( $loan_amount > 0 ) { if ( $frequency === 'weekly' ) { if ( $calc_mode === 'emi' && $weekly_emi > 0 ) { $rate = round( ( $weekly_emi / $loan_amount ) * 100, 6 ); } elseif ( $rate > 0 ) { $weekly_emi = round( $loan_amount * $rate / 100, 2 ); } $monthly_emi = 0; } else { if ( $calc_mode === 'emi' && $monthly_emi > 0 ) { $rate = round( ( $monthly_emi / $loan_amount ) * 100, 6 ); } elseif ( $rate > 0 ) { $monthly_emi = round( $loan_amount * $rate / 100, 2 ); } $weekly_emi = 0; } } if ( $loan_amount < 0 || $tenure < 1 || $rate <= 0 ) { $message = '

Please enter a valid Loan Amount, Tenure and Interest Percentage.

'; } elseif ( $frequency === 'weekly' && $weekly_emi <= 0 ) { $message = '

Weekly EMI must be greater than 0.

'; } elseif ( $frequency === 'monthly' && $monthly_emi <= 0 ) { $message = '

Monthly EMI must be greater than 0.

'; } else { /* Processing Fee modes: * - auto_46: 46 Weeks = 1× selected EMI * - auto_92: 92 Weeks = 2× selected EMI * - custom: Custom Processing Fee per ₹10,000 of Loan Amount */ $processing_mode = sanitize_key( $_POST['processing_mode'] ?? '' ); if ( $processing_mode === 'custom' ) { // Custom Processing Fee is stored as a rupee amount per ₹10,000 of Loan Amount. // Example: ₹500 per ₹10,000 => ₹1,000 on ₹20,000 and ₹2,000 on ₹40,000. $multiplier = 0; $processing = max( 0, round( $processing, 6 ) ); } else { if ( $tenure === 46 ) { $multiplier = 1; } elseif ( $tenure === 92 ) { $multiplier = 2; } else { $message = '

Processing Fee rule is available for 46 Weeks (1× EMI) and 92 Weeks (2× EMI), or choose Custom Processing Fee.

'; $multiplier = 0; } $selected_emi = $frequency === 'monthly' ? $monthly_emi : $weekly_emi; $processing = round( $selected_emi * $multiplier, 2 ); } if ( $message === '' ) { $data = array( 'loan_amount' => $loan_amount, 'tenure_weeks' => $tenure, 'emi_rate' => $rate, 'emi_frequency' => $frequency, 'weekly_emi' => $weekly_emi, 'monthly_emi' => $monthly_emi, 'processing_fee' => $processing, 'processing_multiplier' => $multiplier, 'active' => 1, ); $formats = array( '%f','%d','%f','%s','%f','%f','%f','%f','%d' ); if ( $setting_id ) { $updated = $wpdb->update( $table, $data, array( 'id' => $setting_id ), $formats, array( '%d' ) ); $message = $updated !== false ? '

Loan setting updated successfully.

' : '

Could not update the loan setting.

'; $edit_id = 0; } else { /* Tenure is kept unique for compatibility with the existing New Loan selector. */ $exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $table WHERE tenure_weeks=%d", $tenure ) ); if ( $exists ) { $message = '

This tenure already exists. Please use Edit for the existing setting.

'; } else { $wpdb->insert( $table, $data, $formats ); $message = '

Loan setting added successfully.

'; } } } } } $edit_row = null; if ( $edit_id ) { $edit_row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table WHERE id=%d", $edit_id ) ); } $rows = $wpdb->get_results( "SELECT * FROM $table ORDER BY tenure_weeks ASC" ); $form = $edit_row ?: (object) array( 'id' => 0, 'loan_amount' => '', 'tenure_weeks' => '', 'emi_rate' => '', 'emi_frequency' => 'weekly', 'weekly_emi' => '', 'monthly_emi' => '', 'processing_fee' => '', 'processing_multiplier' => 1, ); ?>

Loan Settings

Loan Amount + Interest % থেকে EMI automatic হবে, আবার Loan Amount + EMI দিলে Interest % automatic calculate হবে.

emi_frequency === 'monthly' ? 'Months' : 'Weeks'; ?>
%

Interest % change করলে selected EMI automatic change হবে.

EMI Frequency *

Loan Amount + Weekly EMI দিলে Interest % automatic বের হবে.

46 Weeks = selected EMI × 1। 92 Weeks = selected EMI × 2। Custom Processing Fee হলে এখানে ₹ per ₹10,000 দিন। যেমন ₹500 দিলে ₹10,000 loan-এ ₹500, ₹20,000-এ ₹1,000 এবং ₹40,000-এ ₹2,000 হবে।

Calculation: Loan Amount + Interest % দিলে EMI automatic হবে। Loan Amount + EMI manually দিলে Interest % automatic হবে। Weekly ও Monthly একই সময়ে editable থাকবে না।

Cancel Edit

EMI & Organization Bank Controls

Admin can control which EMI page blocks, buttons and account actions are visible. OFF means the option is hidden from the UI; protected server-side actions remain permission checked.

This name is shown on the Organization Bank card, loan funding records and bank audit history. You can change it anytime.

EMI Payment History Controls

Customer → Pay EMI → Loan History-এর payment action control করুন। OFF করলে UI এবং server-side action দুটোই বন্ধ থাকবে।

Mark Unpaid বর্তমান payment record remove করে installment balance পুনরায় হিসাব করবে।

EMI Collector Account Controls

Collector account-এর Manual Deposit, Withdrawal এবং transaction history Edit/Delete control করুন। Transfer history নিরাপত্তার জন্য editable/deletable নয়।

OFF করলে collector account-এর Manual Deposit/Withdrawal transaction-এর Edit/Delete button এবং server-side action বন্ধ থাকবে।

Saving Bank Account Controls

Admin can control whether Saving Account Transaction History can be edited or deleted.

OFF করলে Customer → Saving Bank Account-এর Transaction History থেকে Edit/Delete button এবং server-side edit/delete action দুটোই বন্ধ থাকবে।

Founder & Investor Controls

Admin can hide Edit/Delete for Founder and Investor transaction history when it is not needed.

Customer ID & Customer Code Controls

Admin can control whether Customer ID and Customer Code can be edited, and whether a customer can be deleted. Deleted customer codes and IDs become available for reuse.

OFF করলে Customer ID/Code এবং Loan ID edit controls বন্ধ থাকবে। ON করলে Contact Details থেকে Customer ID/Code edit করা যাবে এবং Edit Loan থেকে Loan ID edit করা যাবে। Customer delete option-ও এই setting অনুযায়ী থাকবে।

Current Settings

Loan Amount Tenure Interest % EMI Type Weekly EMI Monthly EMI Processing Fee Rule Processing Fee Action
No loan settings found.
loan_amount ) ); ?> tenure_weeks ); ?> emi_frequency === 'monthly' ? 'Months' : 'Weeks'; ?> emi_rate, 6, '.', '' ), '0' ), '.' ) ); ?>% emi_frequency ) ); ?> weekly_emi > 0 ? lms_money( $row->weekly_emi ) : '—' ); ?> monthly_emi > 0 ? lms_money( $row->monthly_emi ) : '—' ); ?> processing_multiplier === 0.0 ) ? 'Custom Processing Fee' : ( ( (int) $row->tenure_weeks === 46 ) ? '1× EMI' : ( ( (int) $row->tenure_weeks === 92 ) ? '2× EMI' : number_format_i18n( (float) $row->processing_multiplier, 4 ) . '× EMI' ) ) ); ?> processing_multiplier === 0.0 ) ? lms_money( (float) $row->processing_fee ) . ' / ₹10,000' : lms_money( $row->processing_fee ) ); ?> Edit
format( 'N' ) + 7 ) % 7; if ( $days_until_next_tuesday === 0 ) { $days_until_next_tuesday = 7; } $date->modify( '+' . ( $days_until_next_tuesday + 7 ) . ' days' ); return $date->format( 'Y-m-d' ); } function lms_calculate_loan_end_date( $emi_start_date, $tenure ) { try { $date = new DateTime( $emi_start_date ); } catch ( Exception $e ) { return ''; } $date->modify( '+' . max( 0, (int) $tenure - 1 ) . ' weeks' ); return $date->format( 'Y-m-d' ); } /* ========================================================= * NEW LOAN * ========================================================= */ function lms_new_loan_page() { if ( ! current_user_can( 'lms_manage_loans' ) ) { wp_die( 'You do not have permission to create loans.' ); } global $wpdb; $customers_table = $wpdb->prefix . 'lms_customers'; $settings_table = $wpdb->prefix . 'lms_tenure_settings'; $loans_table = $wpdb->prefix . 'lms_loans'; $message = ''; $prefill_customer_id = absint( $_GET['customer_id'] ?? $_POST['customer_id'] ?? 0 ); $customers = $wpdb->get_results( "SELECT id, customer_code, name, mobile FROM $customers_table WHERE status='Active' ORDER BY name ASC" ); $settings = $wpdb->get_results( "SELECT * FROM $settings_table WHERE active=1 ORDER BY tenure_weeks ASC" ); if ( isset( $_POST['lms_create_loan'] ) && check_admin_referer( 'lms_create_loan_action', 'lms_loan_nonce' ) ) { lms_ensure_collector_schema(); $customer_id = absint( $_POST['customer_id'] ?? 0 ); $loan_amount = (float) ( $_POST['loan_amount'] ?? 0 ); $tenure = absint( $_POST['tenure_weeks'] ?? 0 ); $funding_source = sanitize_key( $_POST['loan_funding_source'] ?? 'system' ); if(!in_array($funding_source,array('system','organization_bank'),true)) $funding_source='system'; $weekly_emi_input = (float) ( $_POST['weekly_emi'] ?? 0 ); $loan_date = sanitize_text_field( wp_unslash( $_POST['loan_date'] ?? '' ) ); $emi_start_date = sanitize_text_field( wp_unslash( $_POST['emi_start_date'] ?? '' ) ); $rule = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $settings_table WHERE tenure_weeks=%d AND active=1", $tenure ) ); $customer_exists = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $customers_table WHERE id=%d", $customer_id ) ); if ( ! $customer_exists || $loan_amount < 5000 || ! $rule || ( $funding_source === 'organization_bank' && ! lms_emi_org_bank_funding_enabled() ) || ( $funding_source === 'organization_bank' && lms_get_organization_bank_balance() + 0.00001 < $loan_amount ) || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $loan_date ) || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $emi_start_date ) || $emi_start_date < $loan_date ) { $message = '

Please enter valid loan details.

'; } else { $weekly_emi = $weekly_emi_input > 0 ? round( $weekly_emi_input, 2 ) : round( $loan_amount * ( (float) $rule->emi_rate / 100 ), 2 ); $processing_fee = ( (float) $rule->processing_multiplier === 0.0 ) ? round( ( $loan_amount / 10000 ) * (float) $rule->processing_fee, 2 ) : round( $weekly_emi * (float) $rule->processing_multiplier, 2 ); $total_emi = round( $weekly_emi * $tenure, 2 ); $total_collection = round( $total_emi + $processing_fee, 2 ); $loan_end_date = lms_calculate_loan_end_date( $emi_start_date, $tenure ); $wpdb->insert( $loans_table, array( 'loan_code' => lms_next_loan_code(), 'customer_id' => $customer_id, 'loan_amount' => $loan_amount, 'funding_source' => $funding_source, 'tenure_weeks' => $tenure, 'weekly_emi' => $weekly_emi, 'processing_fee' => $processing_fee, 'total_emi' => $total_emi, 'total_collection' => $total_collection, 'loan_date' => $loan_date, 'emi_start_date' => $emi_start_date, 'loan_end_date' => $loan_end_date, 'start_date' => $emi_start_date, 'status' => 'Active', 'created_at' => current_time( 'mysql' ), ), array( '%s','%d','%f','%s','%d','%f','%f','%f','%f','%s','%s','%s','%s','%s','%s' ) ); $loan_id = $wpdb->insert_id; if ( $loan_id ) { $installments_table = $wpdb->prefix . 'lms_installments'; $base = new DateTime( $emi_start_date ); for ( $i = 1; $i <= $tenure; $i++ ) { $due = clone $base; $due->modify( '+' . ( $i - 1 ) . ' weeks' ); $wpdb->insert( $installments_table, array( 'loan_id' => $loan_id, 'installment_no' => $i, 'due_date' => $due->format( 'Y-m-d' ), 'amount' => $weekly_emi, 'paid_amount' => 0, 'status' => 'Due', ), array( '%d','%d','%s','%f','%f','%s' ) ); } if($funding_source==='organization_bank'){ $bank_tx_id=lms_record_organization_bank_loan_withdrawal($loan_id,$loan_amount,$loan_date); if(!$bank_tx_id){ $wpdb->delete($installments_table,array('loan_id'=>$loan_id),array('%d')); $wpdb->delete($loans_table,array('id'=>$loan_id),array('%d')); $message='

Loan creation failed because the Organization Bank disbursement could not be recorded.

'; }else{ $message='

Loan created successfully and '.esc_html(lms_money($loan_amount)).' was allocated from '.esc_html(lms_organization_name()).' Bank.

'; } }else{ $message='

Loan created successfully. Installment schedule generated.

'; } } } } $default_loan_date = current_time( 'Y-m-d' ); $default_emi_start_date = lms_default_emi_start_date( $default_loan_date ); ?>

New Loan

Create a new loan, set the EMI schedule and review the collection summary before saving.

Loan Application

Enter the customer and loan details below. EMI and collection values are calculated automatically.

Customer & Loan Details

Bank Balance:

EMI & Collection Summary

Tenure select করলে automatic EMI বসবে; প্রয়োজন হলে manually পরিবর্তন করতে পারবেন।

Processing Fee—
Total EMI—
Total Collection—

Loan Schedule

Default: Loan Date-এর পরের Tuesday বাদ দিয়ে 2nd Tuesday।

EMI Start Date ও Tenure অনুযায়ী automatic calculate হবে।

prefix . 'lms_loans'; $installments_table = $wpdb->prefix . 'lms_installments'; $payments_table = $wpdb->prefix . 'lms_payments'; $customers_table = $wpdb->prefix . 'lms_customers'; $message = ''; if ( isset( $_POST['lms_delete_loan_from_list'] ) && check_admin_referer( 'lms_delete_loan_from_list_action', 'lms_delete_loan_from_list_nonce' ) ) { lms_admin_only(); $loan_id = absint( $_POST['loan_id'] ?? 0 ); $loan = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $loans_table WHERE id=%d", $loan_id ) ); if ( ! $loan ) { $message = '

Loan not found.

'; } else { $payment_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $payments_table WHERE loan_id=%d", $loan_id ) ); if ( $payment_count > 0 ) { $message = '

This loan cannot be deleted because payment history exists.

'; } else { lms_ensure_collector_schema(); $wpdb->query( 'START TRANSACTION' ); $bank_refund_ok = true; if ( isset($loan->funding_source) && $loan->funding_source === 'organization_bank' ) { $bank_refund_ok = (bool) lms_record_organization_bank_loan_refund($loan_id, (float)$loan->loan_amount, current_time('Y-m-d')); } $a = $wpdb->delete( $installments_table, array( 'loan_id' => $loan_id ), array( '%d' ) ); $b = $bank_refund_ok ? $wpdb->delete( $loans_table, array( 'id' => $loan_id ), array( '%d' ) ) : false; if ( false === $a || false === $b ) { $wpdb->query( 'ROLLBACK' ); $message = '

Loan could not be deleted.

'; } else { $wpdb->query( 'COMMIT' ); $message = '

Loan deleted successfully. Its Loan ID is now available for reuse.

'; } } } } $rows = $wpdb->get_results( "SELECT l.*, c.name AS customer_name, c.mobile, c.customer_code, c.id AS customer_record_id FROM $loans_table l INNER JOIN $customers_table c ON c.id=l.customer_id ORDER BY l.id DESC" ); ?>

Loans

Loan RegisterSerial Loan ID · Click Loan ID to edit · Click Customer to open profile
+ New Loan
id ) . '&customer_id=' . absint( $row->customer_record_id ) ); $customer_url = admin_url( 'admin.php?page=lms-customer-profile&customer_id=' . absint( $row->customer_record_id ) ); $status_class = strtolower( sanitize_key( $row->status ) ); ?>
#Loan IDCustomerCustomer CodeAmountTenure Loan DateEMI StartLoan EndWeekly EMIProcessingProcessing PaidTotalStatusAction
No loans found.
loan_code ); ?> customer_name ); ?> customer_code ); ?> loan_amount ) ); ?> tenure_weeks ); ?> Weeks loan_date ); ?> emi_start_date ); ?> loan_end_date ); ?> weekly_emi ) ); ?> processing_fee ) ); ?> processing_paid_amount ) ); ?> total_collection ) ); ?> status ); ?> Edit
prefix . 'lms_installments'; $loans = $wpdb->prefix . 'lms_loans'; $customers = $wpdb->prefix . 'lms_customers'; $payments = $wpdb->prefix . 'lms_payments'; $message = ''; $collectors_table = $wpdb->prefix . 'lms_emi_collectors'; $collector_tx_table = $wpdb->prefix . 'lms_emi_collector_transactions'; lms_sync_collector_payment_transactions(); /* COLLECTOR PROFILE: WordPress users can be assigned as EMI Collectors. */ if ( isset($_POST['lms_add_emi_collector']) && check_admin_referer('lms_add_emi_collector_action','lms_add_emi_collector_nonce') ) { if(!lms_emi_show_collector_manage()) wp_die('Collector management is disabled from Settings.'); lms_admin_only(); $user_id=absint($_POST['collector_user_id']??0); $user=get_userdata($user_id); if(!$user) $message='

Please select a valid WordPress user.

'; else { $exists=(int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $collectors_table WHERE user_id=%d AND status='Active'",$user_id)); if($exists) $message='

This user is already an EMI Collector.

'; else { $wpdb->insert($collectors_table,array('user_id'=>$user_id,'name'=>$user->display_name,'mobile'=>get_user_meta($user_id,'billing_phone',true),'address'=>'','status'=>'Active','created_at'=>current_time('mysql')),array('%d','%s','%s','%s','%s','%s')); $message='

User added as EMI Collector successfully.

'; } } } /* * COLLECTOR PROFILE DELETE * Admin may permanently delete a collector profile even when it has history. * Collector transaction records are removed as part of the delete. Existing * EMI payment records are preserved, but their collector_id is cleared so * customer payment history is not destroyed and no orphan collector link remains. * Transfer records involving this collector are removed from both sides. */ if ( isset($_POST['lms_delete_emi_collector']) && check_admin_referer('lms_delete_emi_collector_action','lms_delete_emi_collector_nonce') ) { if(!lms_emi_show_collector_actions()) wp_die('Collector actions are disabled from Settings.'); lms_admin_only(); $cid = absint($_POST['collector_id'] ?? 0); $collector_row = $wpdb->get_row($wpdb->prepare("SELECT * FROM $collectors_table WHERE id=%d", $cid)); if ( ! $collector_row ) { $message = '

Collector profile not found.

'; } else { $wpdb->query('START TRANSACTION'); /* Preserve customer/loan payment history, but remove the deleted collector link. */ $payments_unlinked = $wpdb->query( $wpdb->prepare("UPDATE $payments SET collector_id=NULL WHERE collector_id=%d", $cid) ); /* Permanently remove every collector transaction involving this collector. * This also removes the opposite side of collector-to-collector transfers, * so no transfer record can remain pointing to a deleted collector. */ $tx_deleted = $wpdb->query( $wpdb->prepare( "DELETE FROM $collector_tx_table WHERE collector_id=%d OR transfer_to_collector_id=%d", $cid, $cid ) ); $profile_deleted = $wpdb->delete($collectors_table, array('id'=>$cid), array('%d')); if ( false === $payments_unlinked || false === $tx_deleted || false === $profile_deleted ) { $wpdb->query('ROLLBACK'); $message = '

Collector deletion failed. No collector data was changed.

'; } else { $wpdb->query('COMMIT'); $message = '

Collector profile and all collector account data were permanently deleted. Existing EMI payment records were preserved.

'; } } } if ( isset($_POST['lms_collector_manual_deposit']) && check_admin_referer('lms_collector_manual_deposit_action','lms_collector_manual_deposit_nonce') ) { if ( ! current_user_can('lms_collect_emi') || ! lms_emi_show_collector_actions() ) wp_die('You do not have permission.'); $cid=absint($_POST['collector_id']??0); $amount=(float)($_POST['collector_amount']??0); $date=sanitize_text_field(wp_unslash($_POST['collector_date']??current_time('Y-m-d'))); $method=sanitize_text_field(wp_unslash($_POST['collector_method']??'Cash')); $note=sanitize_textarea_field(wp_unslash($_POST['collector_note']??'')); if($amount<=0 || !preg_match('/^\d{4}-\d{2}-\d{2}$/',$date)) $message='

Enter a valid amount and date.

'; elseif(!in_array($method,array('Cash','UPI','Bank','Other'),true)) $message='

Invalid method.

'; elseif(!lms_collector_name($cid) || lms_collector_name($cid)==='—') $message='

Collector not found.

'; else { lms_add_collector_transaction(array('collector_id'=>$cid,'transaction_type'=>'manual_deposit','amount'=>$amount,'transaction_date'=>$date,'method'=>$method,'note'=>$note!==''?$note:'Manual deposit','created_by'=>get_current_user_id())); $message='

Money added to collector account successfully.

'; } } if ( isset($_POST['lms_transfer_collector']) && check_admin_referer('lms_transfer_collector_action','lms_transfer_collector_nonce') ) { if ( ! current_user_can('lms_collect_emi') || ! lms_emi_show_collector_actions() ) wp_die('You do not have permission.'); $from=absint($_POST['transfer_from']??0); $to=absint($_POST['transfer_to']??0); $amount=(float)($_POST['transfer_amount']??0); $date=sanitize_text_field(wp_unslash($_POST['transfer_date']??current_time('Y-m-d'))); $note=sanitize_textarea_field(wp_unslash($_POST['transfer_note']??'')); $balance=lms_get_collector_balance($from); if($from<=0 || $to<=0 || $from===$to || $amount<=0 || $amount>$balance+0.00001) $message='

Invalid transfer or insufficient collector balance.

'; elseif(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$date)) $message='

Invalid transfer date.

'; else { $group_key = 'TR-' . wp_generate_uuid4(); $wpdb->query('START TRANSACTION'); $a=lms_add_collector_transaction(array('collector_id'=>$from,'transaction_type'=>'transfer_out','amount'=>$amount,'transaction_date'=>$date,'method'=>'Transfer','transfer_to_collector_id'=>$to,'transfer_group_key'=>$group_key,'note'=>$note,'created_by'=>get_current_user_id())); $b=$a ? lms_add_collector_transaction(array('collector_id'=>$to,'transaction_type'=>'transfer_in','amount'=>$amount,'transaction_date'=>$date,'method'=>'Transfer','transfer_to_collector_id'=>$from,'transfer_group_key'=>$group_key,'note'=>$note,'created_by'=>get_current_user_id())) : false; if(!$b){$wpdb->query('ROLLBACK');$message='

Transfer failed.

';}else{$wpdb->query('COMMIT');$message='

Money transferred successfully.

';}} } /* COLLECTOR -> ORGANIZATION BANK */ if(isset($_POST['lms_collector_bank_deposit']) && check_admin_referer('lms_collector_bank_deposit_action','lms_collector_bank_deposit_nonce')){ if(!current_user_can('lms_collect_emi') || !lms_emi_show_org_bank_actions()) wp_die('Organization Bank actions are disabled from Settings.'); $cid=absint($_POST['collector_id']??0); $amount=(float)($_POST['collector_amount']??0); $date=sanitize_text_field(wp_unslash($_POST['collector_date']??current_time('Y-m-d'))); $note=sanitize_textarea_field(wp_unslash($_POST['collector_note']??'')); $balance=lms_get_collector_balance($cid); if($amount<=0 || !preg_match('/^\d{4}-\d{2}-\d{2}$/',$date)) $message='

Enter a valid amount and date.

'; elseif($amount>$balance+0.00001) $message='

Deposit amount cannot be greater than collector balance.

'; elseif(lms_collector_name($cid)==='—') $message='

Collector not found.

'; else{ $wpdb->query('START TRANSACTION'); $tx_id=lms_add_collector_transaction(array('collector_id'=>$cid,'transaction_type'=>'bank_deposit','amount'=>$amount,'transaction_date'=>$date,'method'=>'Organization Bank','note'=>$note!==''?$note:'Deposited to '.lms_organization_name().' Bank')); $bank_id=$tx_id?lms_add_organization_bank_transaction(array('transaction_type'=>'deposit','amount'=>$amount,'transaction_date'=>$date,'method'=>'Collector Deposit','reference_collector_tx_id'=>$tx_id,'note'=>$note!==''?$note:'Deposit from '.lms_collector_name($cid).' to '.lms_organization_name().' Bank')):false; if(!$bank_id){$wpdb->query('ROLLBACK');$message='

Organization Bank deposit failed.

';} else{$wpdb->query('COMMIT');$message='

'.esc_html(lms_money($amount)).' deposited to '.esc_html(lms_organization_name()).' Bank successfully.

';} } } /* COLLECTOR ACCOUNT WITHDRAWAL */ if ( isset($_POST['lms_collector_withdraw']) && check_admin_referer('lms_collector_withdraw_action','lms_collector_withdraw_nonce') ) { if ( ! current_user_can('lms_collect_emi') || ! lms_emi_show_collector_actions() ) wp_die('You do not have permission.'); $cid=absint($_POST['collector_id']??0); $amount=(float)($_POST['collector_amount']??0); $date=sanitize_text_field(wp_unslash($_POST['collector_date']??current_time('Y-m-d'))); $method=sanitize_text_field(wp_unslash($_POST['collector_method']??'Cash')); $note=sanitize_textarea_field(wp_unslash($_POST['collector_note']??'')); $balance=lms_get_collector_balance($cid); if($amount<=0 || !preg_match('/^\d{4}-\d{2}-\d{2}$/',$date)) $message='

Enter a valid amount and date.

'; elseif($amount>$balance+0.00001) $message='

Withdrawal amount cannot be greater than available balance.

'; elseif($note==='') $message='

Withdrawal reason is required.

'; elseif(!in_array($method,array('Cash','UPI','Bank','Other'),true)) $message='

Invalid method.

'; elseif(lms_collector_name($cid)==='—') $message='

Collector not found.

'; else { lms_add_collector_transaction(array('collector_id'=>$cid,'transaction_type'=>'withdrawal','amount'=>$amount,'transaction_date'=>$date,'method'=>$method,'note'=>$note!==''?$note:'Manual withdrawal','created_by'=>get_current_user_id())); $message='

Money withdrawn from collector account successfully.

'; } } /* COLLECTOR ACCOUNT TRANSACTION EDIT - transfers are edited as a paired transaction. */ if ( isset($_POST['lms_edit_collector_transaction']) && check_admin_referer('lms_edit_collector_transaction_action','lms_edit_collector_transaction_nonce') ) { if ( ! current_user_can('lms_collect_emi') || ! lms_collector_account_edit_delete_enabled() ) wp_die('Collector transaction editing is disabled from Settings.'); $txid=absint($_POST['collector_tx_id']??0); $txrow=$wpdb->get_row($wpdb->prepare("SELECT * FROM $collector_tx_table WHERE id=%d",$txid)); $amount=(float)($_POST['collector_amount']??0); $date=sanitize_text_field(wp_unslash($_POST['collector_date']??current_time('Y-m-d'))); $method=sanitize_text_field(wp_unslash($_POST['collector_method']??'Cash')); $note=sanitize_textarea_field(wp_unslash($_POST['collector_note']??'')); if(!$txrow || $amount<=0 || !preg_match('/^\d{4}-\d{2}-\d{2}$/',$date) || !in_array($method,array('Cash','UPI','Bank','Other','Transfer','Organization Bank'),true)) $message='

Invalid collector transaction details.

'; elseif($txrow->transaction_type==='collection') { $payment_id=absint($txrow->reference_payment_id); $payment=$payment_id ? $wpdb->get_row($wpdb->prepare("SELECT * FROM $payments WHERE id=%d AND collector_id=%d",$payment_id,(int)$txrow->collector_id)) : null; $amount_error = ''; if ( $payment && $payment->payment_type === 'EMI' && !empty($payment->installment_id) ) { $inst_total = (float) $wpdb->get_var($wpdb->prepare("SELECT amount FROM $installments WHERE id=%d",(int)$payment->installment_id)); $other_paid = (float) $wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(amount),0) FROM $payments WHERE installment_id=%d AND id<>%d",(int)$payment->installment_id,$payment_id)); if ( $amount > max(0,$inst_total-$other_paid)+0.00001 ) $amount_error = 'EMI amount cannot be greater than the remaining installment amount.'; } elseif ( $payment && $payment->payment_type === 'Processing Fee' ) { $fee_total = (float) $wpdb->get_var($wpdb->prepare("SELECT processing_fee FROM $loans WHERE id=%d",(int)$payment->loan_id)); $other_paid = (float) $wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(amount),0) FROM $payments WHERE loan_id=%d AND payment_type='Processing Fee' AND id<>%d",(int)$payment->loan_id,$payment_id)); if ( $amount > max(0,$fee_total-$other_paid)+0.00001 ) $amount_error = 'Processing fee amount cannot be greater than the remaining processing fee.'; } if(!$payment || $amount<=0 || !preg_match('/^\d{4}-\d{2}-\d{2}$/',$date) || !in_array($method,array('Cash','UPI','Bank','Other'),true) || $amount_error !== '') { $message='

'.esc_html($amount_error!==''?$amount_error:'Invalid collection transaction details.').'

'; } else { $wpdb->query('START TRANSACTION'); $ok=$wpdb->update($payments,array('amount'=>$amount,'payment_date'=>$date,'method'=>$method,'note'=>$note),array('id'=>$payment_id),array('%f','%s','%s','%s'),array('%d')); $ok2=$wpdb->update($collector_tx_table,array('amount'=>$amount,'transaction_date'=>$date,'method'=>$method,'note'=>$note!==''?$note:($payment->payment_type==='Processing Fee'?'Processing Fee Collection':'EMI Collection')),array('id'=>$txid),array('%f','%s','%s','%s'),array('%d')); if(false===$ok || false===$ok2) { $wpdb->query('ROLLBACK'); $message='

Collection transaction update failed.

'; } else { if($payment->payment_type==='EMI' && !empty($payment->installment_id)) lms_recalculate_installment_payment_state((int)$payment->installment_id); if($payment->payment_type==='Processing Fee') lms_recalculate_processing_fee_state((int)$payment->loan_id); lms_maybe_close_loan((int)$payment->loan_id); $wpdb->query('COMMIT'); $message='

Collector collection updated successfully.

'; } } } elseif($txrow->transaction_type==='bank_deposit') { $old=(float)$txrow->amount; $bal=lms_get_collector_balance((int)$txrow->collector_id); $new_balance=$bal+$old-$amount; $bank_tx=lms_organization_bank_tx_for_collector_tx($txid); $bank_bal=lms_get_organization_bank_balance(); $new_bank_balance=$bank_bal-$old+$amount; if($new_balance< -0.00001) $message='

Insufficient collector balance for this edit.

'; elseif(!$bank_tx || $new_bank_balance< -0.00001) $message='

Organization Bank balance does not allow this edit.

'; else{ $wpdb->query('START TRANSACTION'); $ok1=$wpdb->update($collector_tx_table,array('amount'=>$amount,'transaction_date'=>$date,'method'=>'Organization Bank','note'=>$note!==''?$note:'Deposited to '.lms_organization_name().' Bank'),array('id'=>$txid),array('%f','%s','%s','%s'),array('%d')); $ok2=$wpdb->update($wpdb->prefix.'lms_organization_bank_transactions',array('amount'=>$amount,'transaction_date'=>$date,'note'=>$note!==''?$note:'Deposit to '.lms_organization_name().' Bank'),array('id'=>(int)$bank_tx->id),array('%f','%s','%s'),array('%d')); if(false===$ok1 || false===$ok2){$wpdb->query('ROLLBACK');$message='

Organization Bank deposit update failed.

';} else{$wpdb->query('COMMIT');$message='

Organization Bank deposit updated successfully.

';} } } elseif(in_array($txrow->transaction_type,array('transfer_out','transfer_in'),true)) { $old=(float)$txrow->amount; $counterpart=null; if(!empty($txrow->transfer_group_key)) $counterpart=$wpdb->get_row($wpdb->prepare("SELECT * FROM $collector_tx_table WHERE transfer_group_key=%s AND id<>%d LIMIT 1",$txrow->transfer_group_key,$txid)); if(!$counterpart) $counterpart=$wpdb->get_row($wpdb->prepare("SELECT * FROM $collector_tx_table WHERE transaction_type=%s AND collector_id=%d AND transfer_to_collector_id=%d AND ABS(amount-%f)<0.00001 AND id<>%d ORDER BY id DESC LIMIT 1",$txrow->transaction_type==='transfer_out'?'transfer_in':'transfer_out',(int)$txrow->transfer_to_collector_id,(int)$txrow->collector_id,$old,$txid)); if(!$counterpart) $message='

Matching transfer transaction could not be found.

'; else { $out=$txrow->transaction_type==='transfer_out'?$txrow:$counterpart; $in=$txrow->transaction_type==='transfer_in'?$txrow:$counterpart; $out_bal=lms_get_collector_balance((int)$out->collector_id); $in_bal=lms_get_collector_balance((int)$in->collector_id); $new_out_bal=$out_bal+$old-$amount; $new_in_bal=$in_bal-$old+$amount; if($new_out_bal < -0.00001 || $new_in_bal < -0.00001) $message='

Transfer cannot be edited because the resulting collector balance would be insufficient.

'; else { $group_key=$txrow->transfer_group_key?:'TR-'.wp_generate_uuid4(); $wpdb->query('START TRANSACTION'); $ok1=$wpdb->update($collector_tx_table,array('amount'=>$amount,'transaction_date'=>$date,'note'=>$note,'transfer_group_key'=>$group_key),array('id'=>(int)$out->id),array('%f','%s','%s','%s'),array('%d')); $ok2=$wpdb->update($collector_tx_table,array('amount'=>$amount,'transaction_date'=>$date,'note'=>$note,'transfer_group_key'=>$group_key),array('id'=>(int)$in->id),array('%f','%s','%s','%s'),array('%d')); if(false===$ok1 || false===$ok2){$wpdb->query('ROLLBACK');$message='

Transfer update failed.

';}else{$wpdb->query('COMMIT');$message='

Transfer updated successfully for both collector accounts.

';} } } } else { $old=(float)$txrow->amount; $bal=lms_get_collector_balance((int)$txrow->collector_id); $new_balance=$bal; if($txrow->transaction_type==='manual_deposit') $new_balance=$bal-$old+$amount; if($txrow->transaction_type==='withdrawal') $new_balance=$bal+$old-$amount; if($new_balance < -0.00001) $message='

Insufficient collector balance for this edit.

'; else { $wpdb->update($collector_tx_table,array('amount'=>$amount,'transaction_date'=>$date,'method'=>$method,'note'=>$note),array('id'=>$txid),array('%f','%s','%s','%s'),array('%d')); $message='

Collector transaction updated successfully.

'; } } } /* COLLECTOR ACCOUNT TRANSACTION DELETE - transfers are deleted as a paired transaction. */ if ( isset($_POST['lms_delete_collector_transaction']) && check_admin_referer('lms_delete_collector_transaction_action','lms_delete_collector_transaction_nonce') ) { if ( ! current_user_can('lms_collect_emi') || ! lms_collector_account_edit_delete_enabled() ) wp_die('Collector transaction deletion is disabled from Settings.'); $txid=absint($_POST['collector_tx_id']??0); $txrow=$wpdb->get_row($wpdb->prepare("SELECT * FROM $collector_tx_table WHERE id=%d",$txid)); if(!$txrow) $message='

Transaction not found.

'; elseif($txrow->transaction_type==='collection' && !empty($txrow->reference_payment_id)) { $payment_id=absint($txrow->reference_payment_id); $payment=$wpdb->get_row($wpdb->prepare("SELECT * FROM $payments WHERE id=%d AND collector_id=%d",$payment_id,(int)$txrow->collector_id)); if(!$payment) { $wpdb->delete($collector_tx_table,array('id'=>$txid),array('%d')); $message='

Collector history entry deleted.

'; } else { $wpdb->query('START TRANSACTION'); $d1=$wpdb->delete($payments,array('id'=>$payment_id),array('%d')); $d2=$wpdb->delete($collector_tx_table,array('id'=>$txid),array('%d')); if(false===$d1 || false===$d2) { $wpdb->query('ROLLBACK'); $message='

Collection deletion failed.

'; } else { if($payment->payment_type==='EMI' && !empty($payment->installment_id)) lms_recalculate_installment_payment_state((int)$payment->installment_id); if($payment->payment_type==='Processing Fee') lms_recalculate_processing_fee_state((int)$payment->loan_id); lms_maybe_close_loan((int)$payment->loan_id); $wpdb->query('COMMIT'); $message='

Collector collection and payment history deleted successfully.

'; } } } elseif($txrow->transaction_type==='bank_deposit') { $bank_tx=lms_organization_bank_tx_for_collector_tx($txid); if(!$bank_tx){$wpdb->delete($collector_tx_table,array('id'=>$txid),array('%d'));$message='

Collector history entry deleted.

';} elseif(lms_get_organization_bank_balance()+0.00001<(float)$bank_tx->amount) $message='

This bank deposit cannot be deleted because the organization bank balance has already been used.

'; else{ $wpdb->query('START TRANSACTION'); $d1=$wpdb->delete($wpdb->prefix.'lms_organization_bank_transactions',array('id'=>(int)$bank_tx->id),array('%d')); $d2=$wpdb->delete($collector_tx_table,array('id'=>$txid),array('%d')); if(false===$d1 || false===$d2){$wpdb->query('ROLLBACK');$message='

Organization Bank deposit deletion failed.

';} else{$wpdb->query('COMMIT');$message='

Organization Bank deposit deleted successfully.

';} } } elseif(in_array($txrow->transaction_type,array('transfer_out','transfer_in'),true)) { $counterpart=null; if(!empty($txrow->transfer_group_key)) $counterpart=$wpdb->get_row($wpdb->prepare("SELECT * FROM $collector_tx_table WHERE transfer_group_key=%s AND id<>%d LIMIT 1",$txrow->transfer_group_key,$txid)); if(!$counterpart) $counterpart=$wpdb->get_row($wpdb->prepare("SELECT * FROM $collector_tx_table WHERE transaction_type=%s AND collector_id=%d AND transfer_to_collector_id=%d AND ABS(amount-%f)<0.00001 AND id<>%d ORDER BY id DESC LIMIT 1",$txrow->transaction_type==='transfer_out'?'transfer_in':'transfer_out',(int)$txrow->transfer_to_collector_id,(int)$txrow->collector_id,(float)$txrow->amount,$txid)); $in_leg=$txrow->transaction_type==='transfer_in'?$txrow:$counterpart; if(!$counterpart || !$in_leg) $message='

Matching transfer transaction could not be found.

'; elseif(lms_get_collector_balance((int)$in_leg->collector_id)+0.00001 < (float)$txrow->amount) $message='

This transfer cannot be deleted because the receiving collector has already used that balance.

'; else { $wpdb->query('START TRANSACTION'); $d1=$wpdb->delete($collector_tx_table,array('id'=>$txid),array('%d')); $d2=$wpdb->delete($collector_tx_table,array('id'=>(int)$counterpart->id),array('%d')); if(false===$d1 || false===$d2){$wpdb->query('ROLLBACK');$message='

Transfer deletion failed.

';}else{$wpdb->query('COMMIT');$message='

Transfer deleted successfully from both collector accounts.

'; } } } elseif($txrow->transaction_type==='manual_deposit' && lms_get_collector_balance((int)$txrow->collector_id)+0.00001 < (float)$txrow->amount) $message='

This deposit cannot be deleted because the balance has already been used.

'; else { $wpdb->delete($collector_tx_table,array('id'=>$txid),array('%d')); $message='

Collector transaction deleted successfully.

'; } } /* BULK EMI COLLECTION */ if ( isset( $_POST['lms_bulk_collect_emi'] ) && check_admin_referer( 'lms_bulk_collect_emi_action', 'lms_bulk_emi_nonce' ) ) { $selected_ids = isset( $_POST['selected_installments'] ) && is_array( $_POST['selected_installments'] ) ? array_map( 'absint', wp_unslash( $_POST['selected_installments'] ) ) : array(); $bulk_date = sanitize_text_field( wp_unslash( $_POST['bulk_collection_date'] ?? current_time( 'Y-m-d' ) ) ); $bulk_collector_id = absint( $_POST['bulk_collector_id'] ?? 0 ); $bulk_person = get_current_user_id(); $bulk_method = sanitize_text_field( wp_unslash( $_POST['bulk_payment_method'] ?? 'Cash' ) ); $bulk_note = sanitize_textarea_field( wp_unslash( $_POST['bulk_payment_note'] ?? '' ) ); if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $bulk_date ) ) { $bulk_date = current_time( 'Y-m-d' ); } if ( ! in_array( $bulk_method, array( 'Cash','UPI','Bank','Other' ), true ) ) { $bulk_method = 'Cash'; } if ( $bulk_method !== 'Savings Account' && ( $bulk_collector_id <= 0 || ! lms_collector_name( $bulk_collector_id ) || lms_collector_name( $bulk_collector_id ) === '—' ) ) { $message = '

Please select a valid EMI Collector before collecting.

'; $selected_ids = array(); } $success_count = 0; $skip_count = 0; foreach ( array_unique( array_filter( $selected_ids ) ) as $bulk_installment_id ) { $inst = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $installments WHERE id=%d", $bulk_installment_id ) ); if ( ! $inst ) { $skip_count++; continue; } $remaining = max( 0, (float) $inst->amount - (float) $inst->paid_amount ); if ( $remaining <= 0 ) { $skip_count++; continue; } $new_paid = (float) $inst->paid_amount + $remaining; $updated = $wpdb->update( $installments, array( 'paid_amount' => $new_paid, 'paid_date' => $bulk_date, 'status' => 'Paid', ), array( 'id' => $bulk_installment_id ), array( '%f','%s','%s' ), array( '%d' ) ); if ( false === $updated ) { $skip_count++; continue; } $inserted = $wpdb->insert( $payments, array( 'loan_id' => $inst->loan_id, 'installment_id' => $bulk_installment_id, 'payment_type' => 'EMI', 'amount' => $remaining, 'payment_date' => $bulk_date, 'method' => $bulk_method, 'note' => $bulk_note, 'created_by' => $bulk_person, 'collector_id' => ( $bulk_method === 'Savings Account' ) ? null : ( $bulk_collector_id > 0 ? $bulk_collector_id : null ), 'created_at' => current_time( 'mysql' ), ), array( '%d','%d','%s','%f','%s','%s','%s','%d','%d','%s' ) ); if ( false === $inserted ) { lms_recalculate_installment_payment_state( $bulk_installment_id ); $skip_count++; continue; } $bulk_payment_id = (int) $wpdb->insert_id; if ( $bulk_method !== 'Savings Account' && $bulk_collector_id > 0 ) { $bulk_tx_id = lms_record_collector_payment_transaction( $bulk_payment_id, $bulk_collector_id, 'EMI', $remaining, $bulk_date, $bulk_method, $bulk_note ); if ( ! $bulk_tx_id ) { $wpdb->delete( $payments, array( 'id' => $bulk_payment_id ), array( '%d' ) ); lms_recalculate_installment_payment_state( $bulk_installment_id ); $skip_count++; continue; } } lms_maybe_close_loan( $inst->loan_id ); $success_count++; } if ( $success_count > 0 ) { $message = '

' . esc_html( $success_count . ' EMI payment(s) collected successfully.' . ( $skip_count ? ' ' . $skip_count . ' skipped.' : '' ) ) . '

'; } else { $message = '

No EMI payment was collected. Please select at least one unpaid EMI.

'; } } if ( isset( $_POST['lms_collect_payment'] ) && check_admin_referer( 'lms_collect_payment_action', 'lms_payment_nonce' ) ) { $payment_type = sanitize_text_field( wp_unslash( $_POST['payment_type'] ?? 'EMI' ) ); $installment_id = absint( $_POST['installment_id'] ?? 0 ); $loan_id_post = absint( $_POST['loan_id'] ?? 0 ); $pay_amount = (float) ( $_POST['payment_amount'] ?? 0 ); $collection_date = sanitize_text_field( wp_unslash( $_POST['collection_date'] ?? current_time( 'Y-m-d' ) ) ); $collection_collector_id = absint( $_POST['collection_collector_id'] ?? 0 ); $collection_person = get_current_user_id(); $method = sanitize_text_field( wp_unslash( $_POST['payment_method'] ?? 'Cash' ) ); $note = sanitize_textarea_field( wp_unslash( $_POST['payment_note'] ?? '' ) ); if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $collection_date ) ) { $collection_date = current_time( 'Y-m-d' ); } if ( $method !== 'Savings Account' && ( $collection_collector_id <= 0 || ! lms_collector_name( $collection_collector_id ) || lms_collector_name( $collection_collector_id ) === '—' ) ) { $message = '

Please select a valid EMI Collector before collecting.

'; } if ( $payment_type === 'Processing Fee' && $message === '' ) { $loan = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $loans WHERE id=%d", $loan_id_post ) ); if ( ! $loan || $pay_amount <= 0 ) { $message = '

Invalid processing fee payment.

'; } else { $already_paid = lms_get_processing_fee_paid( $loan->id ); $remaining = max( 0, (float) $loan->processing_fee - $already_paid ); $pay_amount = min( $pay_amount, $remaining ); if ( $pay_amount <= 0 ) { $message = '

Processing fee is already fully paid.

'; } else { $inserted = $wpdb->insert( $payments, array( 'loan_id' => $loan->id, 'installment_id' => null, 'payment_type' => 'Processing Fee', 'amount' => $pay_amount, 'payment_date' => $collection_date, 'method' => in_array( $method, array( 'Cash','UPI','Bank','Other' ), true ) ? $method : 'Cash', 'note' => $note, 'created_by' => $collection_person, 'collector_id' => ( $method === 'Savings Account' ) ? null : ( $collection_collector_id > 0 ? $collection_collector_id : null ), 'created_at' => current_time( 'mysql' ), ), array( '%d','%d','%s','%f','%s','%s','%s','%d','%d','%s' ) ); if ( false === $inserted ) { $message = '

Processing fee payment could not be saved.

'; } else { $payment_id = (int) $wpdb->insert_id; if ( $method !== 'Savings Account' && $collection_collector_id > 0 ) { $tx_id = lms_record_collector_payment_transaction( $payment_id, $collection_collector_id, 'Processing Fee', $pay_amount, $collection_date, in_array( $method, array( 'Cash','UPI','Bank','Other' ), true ) ? $method : 'Cash', $note ); if ( ! $tx_id ) { $wpdb->delete( $payments, array( 'id' => $payment_id ), array( '%d' ) ); $message = '

Processing fee payment could not be linked to the collector account.

'; } } if ( $message === '' ) { lms_recalculate_processing_fee_state( $loan->id ); lms_maybe_close_loan( $loan->id ); $message = '

Processing fee payment saved successfully and added to the collector account.

'; } } } } } else { $inst = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $installments WHERE id=%d", $installment_id ) ); if ( ! $inst || $pay_amount <= 0 ) { $message = '

Invalid payment.

'; } else { $remaining = max( 0, (float) $inst->amount - (float) $inst->paid_amount ); $pay_amount = min( $pay_amount, $remaining ); if ( $method === 'Savings Account' ) { $loan_customer_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT customer_id FROM $loans WHERE id=%d", $inst->loan_id ) ); $savings_balance = lms_get_savings_balance( $loan_customer_id ); if ( $pay_amount > $savings_balance + 0.00001 ) { $message = '

Savings Account balance is not enough for this EMI.

'; } elseif ( $pay_amount > 0 ) { $wpdb->query( 'START TRANSACTION' ); $new_paid = (float) $inst->paid_amount + $pay_amount; $new_status = $new_paid >= (float) $inst->amount ? 'Paid' : 'Partial'; $updated_inst = $wpdb->update( $installments, array('paid_amount'=>$new_paid,'paid_date'=>$collection_date,'status'=>$new_status), array('id'=>$installment_id), array('%f','%s','%s'), array('%d') ); $payment_ok = false; if ( false !== $updated_inst ) { $payment_ok = $wpdb->insert( $payments, array('loan_id'=>$inst->loan_id,'installment_id'=>$installment_id,'payment_type'=>'EMI','amount'=>$pay_amount,'payment_date'=>$collection_date,'method'=>'Savings Account','note'=>$note,'created_by'=>$collection_person,'collector_id'=>null,'created_at'=>current_time('mysql')), array('%d','%d','%s','%f','%s','%s','%s','%d','%d','%s') ); } if ( false === $payment_ok ) { $wpdb->query('ROLLBACK'); $message = '

EMI payment could not be saved.

'; } else { $payment_id=(int)$wpdb->insert_id; $savings_tx=lms_savings_add_transaction(array('customer_id'=>$loan_customer_id,'transaction_type'=>'withdrawal','withdrawal_type'=>'emi','amount'=>$pay_amount,'transaction_date'=>$collection_date,'loan_id'=>$inst->loan_id,'installment_id'=>$installment_id,'payment_id'=>$payment_id,'method'=>'Savings Account','note'=>$note!==''?$note:'EMI paid from Savings Bank Account','created_by'=>$collection_person)); if(!$savings_tx){ $wpdb->query('ROLLBACK'); $message='

Savings withdrawal could not be recorded. EMI payment was not completed.

'; } else { $wpdb->update($payments,array('savings_transaction_id'=>$savings_tx),array('id'=>$payment_id),array('%d'),array('%d')); $wpdb->query('COMMIT'); lms_maybe_close_loan($inst->loan_id); $message='

EMI paid from Savings Account. No collector balance was added.

'; } } } } if ( $message !== '' ) { // Savings Account payment has already been handled above. } else { $new_paid = (float) $inst->paid_amount + $pay_amount; $new_status = $new_paid >= (float) $inst->amount ? 'Paid' : 'Partial'; $wpdb->update( $installments, array( 'paid_amount' => $new_paid, 'paid_date' => $collection_date, 'status' => $new_status, ), array( 'id' => $installment_id ), array( '%f','%s','%s' ), array( '%d' ) ); $inserted = $wpdb->insert( $payments, array( 'loan_id' => $inst->loan_id, 'installment_id' => $installment_id, 'payment_type' => 'EMI', 'amount' => $pay_amount, 'payment_date' => $collection_date, 'method' => in_array( $method, array( 'Cash','UPI','Bank','Other' ), true ) ? $method : 'Cash', 'note' => $note, 'created_by' => $collection_person, 'collector_id' => ( $method === 'Savings Account' ) ? null : ( $collection_collector_id > 0 ? $collection_collector_id : null ), 'created_at' => current_time( 'mysql' ), ), array( '%d','%d','%s','%f','%s','%s','%s','%d','%d','%s' ) ); if ( false === $inserted ) { lms_recalculate_installment_payment_state( $installment_id ); $message = '

EMI payment could not be saved.

'; } else { $payment_id = (int) $wpdb->insert_id; if ( $method !== 'Savings Account' && $collection_collector_id > 0 ) { $tx_id = lms_record_collector_payment_transaction( $payment_id, $collection_collector_id, 'EMI', $pay_amount, $collection_date, in_array( $method, array( 'Cash','UPI','Bank','Other' ), true ) ? $method : 'Cash', $note ); if ( ! $tx_id ) { $wpdb->delete( $payments, array( 'id' => $payment_id ), array( '%d' ) ); lms_recalculate_installment_payment_state( $installment_id ); $message = '

EMI payment could not be linked to the collector account.

'; } } if ( $message === '' ) { lms_maybe_close_loan( $inst->loan_id ); $message = '

EMI payment saved successfully and added to the collector account.

'; } } } } } } $rows = $wpdb->get_results( "SELECT i.*, l.loan_code, c.id AS customer_record_id, c.name AS customer_name, c.mobile FROM $installments i INNER JOIN $loans l ON l.id=i.loan_id INNER JOIN $customers c ON c.id=l.customer_id WHERE i.paid_amount < i.amount AND ( i.due_date < CURDATE() OR i.id = ( SELECT MIN(i2.id) FROM $installments i2 WHERE i2.loan_id=i.loan_id AND i2.paid_amount < i2.amount ) ) ORDER BY i.due_date ASC, i.id ASC" ); $processing_rows = $wpdb->get_results( "SELECT l.*, c.name AS customer_name, c.mobile, COALESCE(SUM(CASE WHEN p.payment_type='Processing Fee' THEN p.amount ELSE 0 END),0) AS processing_paid FROM $loans l INNER JOIN $customers c ON c.id=l.customer_id LEFT JOIN $payments p ON p.loan_id=l.id WHERE l.processing_fee > COALESCE(l.processing_paid_amount,0) GROUP BY l.id ORDER BY l.loan_date ASC, l.id ASC" ); ?>

EMI

EMI Collection: প্রতিটি collection একজন EMI Collector-এর account-এ যাবে। Collector balance, manual deposit ও collector-to-collector transfer আলাদা account হিসেবে maintain হবে; transfer/manual deposit Super Admin cash-এ যোগ হবে না। EMI payment নিজস্ব financial collection হিসাব অনুযায়ী Super Admin balance-এ আগের নিয়মেই গণনা হবে।

Organization Bank Balance
· Collector deposits can be reused for new loans.
id); ?>
get_results("SELECT * FROM {$wpdb->prefix}lms_organization_bank_transactions ORDER BY transaction_date DESC,id DESC LIMIT 20"); ?>
Organization Bank Audit History
DateTypeAmountMethodReferenceNote
No bank transactions yet.
transaction_date); ?>transaction_type==='deposit'?'Deposit':'Withdrawal'); ?>transaction_type==='deposit'?'+':'-').lms_money($obr->amount)); ?>method); ?>reference_loan_id ? esc_html('Loan #'.$obr->reference_loan_id) : ($obr->reference_collector_tx_id ? esc_html('Collector Deposit #'.$obr->reference_collector_tx_id) : '—'); ?>note); ?>
Collectors
Collector Cash
id);} echo esc_html(lms_money($all_collector_balance)); ?>
Separate from Super Admin cash
Manage
id); $collected_total=(float)$wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(amount),0) FROM $payments WHERE collector_id=%d",$cc->id)); $display_name=$cc->user_display_name ? $cc->user_display_name : $cc->name; ?>
user_login ? '@'.$cc->user_login : 'Collector #'.$cc->id); ?>
Collector
Available balance
Total EMI collected:
id===$history_collector_id){$history_selected=$hc;break;}} $history_txs=$history_selected?$wpdb->get_results($wpdb->prepare("SELECT t.*, p.payment_type FROM $collector_tx_table t LEFT JOIN $payments p ON p.id=t.reference_payment_id WHERE t.collector_id=%d ORDER BY t.transaction_date DESC,t.id DESC",$history_collector_id)):array(); ?>

Collector History

Account: user_display_name?:$history_selected->name); ?> · Balance

Close
transaction_type==='manual_deposit'?'Manual Deposit':($htx->transaction_type==='withdrawal'?'Withdrawal':($htx->transaction_type==='bank_deposit'?'Organization Bank Deposit':($htx->transaction_type==='transfer_in'?'Transfer In':($htx->transaction_type==='transfer_out'?'Transfer Out':(($htx->payment_type??'')==='Processing Fee'?'Processing Fee Collection':'EMI Collection'))))); ?>
DateTypeAmountMethodNoteAction
No collector transactions found.
transaction_date); ?>amount)); ?>method); ?>note); ?>

Processing Fee Collection

processing_paid; $p_remaining = max( 0, (float) $prow->processing_fee - $p_paid ); ?>
LoanCustomerMobileProcessing FeePaidRemainingCollect
No processing fee due.
loan_code ); ?> customer_name ); ?> mobile ); ?> processing_fee ) ); ?>

EMI Collection

Bulk Action: 0 selected
amount - (float) $row->paid_amount ); ?>
EMI DateCustomerMobileLoan id InstallmentRemainingStatusCollect
No due EMI found.
due_date ); ?> customer_name ); ?> mobile ); ?> loan_code ); ?> installment_no ); ?> status ); ?>
prefix . 'lms_investors'; $tx_table = $wpdb->prefix . 'lms_investor_transactions'; $message = ''; $investor_id = absint( $_GET['investor_id'] ?? $_POST['investor_id'] ?? 0 ); $add_type = sanitize_key( $_GET['add_type'] ?? '' ); $edit_tx_id = absint( $_GET['edit_tx'] ?? 0 ); $edit_profile_id = absint( $_GET['edit_profile'] ?? 0 ); /* Delete Founder / Investor profile and its transaction history. */ if ( isset( $_POST['lms_delete_investor_profile'] ) && check_admin_referer( 'lms_delete_investor_profile_action', 'lms_delete_investor_profile_nonce' ) ) { $delete_id = absint( $_POST['delete_investor_id'] ?? 0 ); $deleted_person = $delete_id ? $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table WHERE id=%d", $delete_id ) ) : null; if ( $deleted_person ) { $wpdb->delete( $tx_table, array( 'investor_id' => $delete_id ), array( '%d' ) ); $deleted = $wpdb->delete( $table, array( 'id' => $delete_id ), array( '%d' ) ); $message = $deleted ? '

' . esc_html( $deleted_person->investor_type . ' profile and its transaction history were deleted.' ) . '

' : '

Profile could not be deleted.

'; if ( $investor_id === $delete_id ) { $investor_id = 0; } } } /* Add or edit profile. Founder has no interest or term fields. */ if ( isset( $_POST['lms_save_investor'] ) && check_admin_referer( 'lms_save_investor_action', 'lms_investor_nonce' ) ) { $profile_id = absint( $_POST['profile_id'] ?? 0 ); $type = sanitize_text_field( wp_unslash( $_POST['investor_type'] ?? 'Investor' ) ); $type = in_array( $type, array( 'Founder','Investor' ), true ) ? $type : 'Investor'; $name = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) ); $email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) ); $phone = sanitize_text_field( wp_unslash( $_POST['phone'] ?? '' ) ); $facebook = esc_url_raw( wp_unslash( $_POST['facebook'] ?? '' ) ); $instagram = esc_url_raw( wp_unslash( $_POST['instagram'] ?? '' ) ); $x_url = esc_url_raw( wp_unslash( $_POST['x_url'] ?? '' ) ); $initial_amount = (float) ( $_POST['initial_amount'] ?? 0 ); $rate = (float) ( $_POST['interest_rate_monthly'] ?? 0 ); $start_date = sanitize_text_field( wp_unslash( $_POST['start_date'] ?? current_time( 'Y-m-d' ) ) ); $notes = sanitize_textarea_field( wp_unslash( $_POST['notes'] ?? '' ) ); $profile_image_url = ''; if ( ! empty( $_FILES['profile_image']['name'] ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; $uploaded = wp_handle_upload( $_FILES['profile_image'], array( 'test_form' => false, 'mimes' => array( 'jpg'=>'image/jpeg', 'jpeg'=>'image/jpeg', 'png'=>'image/png', 'webp'=>'image/webp' ), ) ); if ( ! empty( $uploaded['url'] ) ) { $profile_image_url = esc_url_raw( $uploaded['url'] ); } } if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $start_date ) ) { $start_date = current_time( 'Y-m-d' ); } if ( $name === '' || $initial_amount < 0 ) { $message = '

Please enter a valid name and amount.

'; } elseif ( $type === 'Investor' && $rate <= 0 ) { $message = '

Investor-এর জন্য Interest % দিন.

'; } else { $data = array( 'investor_type'=>$type, 'name'=>$name, 'email'=>$email, 'phone'=>$phone, 'facebook'=>$facebook, 'instagram'=>$instagram, 'x_url'=>$x_url, 'interest_rate_monthly'=>$type === 'Investor' ? $rate : 0, 'start_date'=>$start_date, 'notes'=>$notes, ); if ( $profile_image_url !== '' ) { $data['image_url'] = $profile_image_url; } if ( $profile_id ) { $data_formats = array('%s','%s','%s','%s','%s','%s','%s','%f','%s','%s'); if ( $profile_image_url !== '' ) { $data_formats[] = '%s'; } $updated = $wpdb->update( $table, $data, array('id'=>$profile_id), $data_formats, array('%d') ); $investor_id = $profile_id; $message = $updated === false ? '

Profile could not be updated.

' : '

Profile updated successfully.

'; } else { $data['amount'] = $initial_amount; $data['term_months'] = 0; $data['end_date'] = null; $data['created_at'] = current_time('mysql'); $insert_formats = array('%s','%s','%s','%s','%s','%s','%s','%f','%s','%s','%f','%d','%s','%s'); if ( $profile_image_url !== '' ) { $insert_formats[] = '%s'; } $ok = $wpdb->insert( $table, $data, $insert_formats ); if ( false === $ok ) { $message = '

Could not create the profile.

'; } else { $new_id = (int) $wpdb->insert_id; if ( $initial_amount > 0 ) { $wpdb->insert( $tx_table, array('investor_id'=>$new_id,'transaction_type'=>'deposit','amount'=>$initial_amount,'transaction_date'=>$start_date,'method'=>'Cash','note'=>'Initial investment','created_by'=>get_current_user_id(),'created_at'=>current_time('mysql')), array('%d','%s','%f','%s','%s','%s','%d','%s') ); } $investor_id = $new_id; $add_type = ''; $message = '

' . esc_html($type . ' added successfully.') . '

'; } } } } /* Add deposit, capital withdrawal or interest payout. */ if ( isset( $_POST['lms_save_investor_tx'] ) && check_admin_referer( 'lms_investor_tx_action', 'lms_investor_tx_nonce' ) ) { $investor_id = absint( $_POST['investor_id'] ?? 0 ); $tx_type = sanitize_key( $_POST['transaction_type'] ?? 'deposit' ); $amount = (float) ( $_POST['transaction_amount'] ?? 0 ); $date = sanitize_text_field( wp_unslash( $_POST['transaction_date'] ?? current_time('Y-m-d') ) ); $method = sanitize_text_field( wp_unslash( $_POST['transaction_method'] ?? 'Cash' ) ); $note = sanitize_textarea_field( wp_unslash( $_POST['transaction_note'] ?? '' ) ); if ( ! in_array($method,array('Cash','UPI','Bank','Other','Savings Account'),true) ) { $method='Cash'; } $person = $wpdb->get_row( $wpdb->prepare("SELECT * FROM $table WHERE id=%d",$investor_id) ); $profile = $person ? lms_investor_financials($investor_id,$date) : null; $valid_date = preg_match('/^\d{4}-\d{2}-\d{2}$/',$date); if ( ! $person || $amount <= 0 || ! $valid_date ) { $message='

Invalid transaction.

'; } elseif ( $tx_type === 'capital_withdrawal' && $amount > $profile['balance'] + 0.00001 ) { $message='

Capital withdrawal cannot exceed available capital.

'; } elseif ( $tx_type === 'interest_payout' && ($person->investor_type !== 'Investor' || $amount > $profile['interest'] + 0.00001) ) { $message='

Interest payout cannot exceed currently accrued interest.

'; } elseif ( ! in_array($tx_type,array('deposit','capital_withdrawal','interest_payout'),true) ) { $message='

Invalid transaction type.

'; } else { $ok=$wpdb->insert($tx_table,array('investor_id'=>$investor_id,'transaction_type'=>$tx_type,'amount'=>$amount,'transaction_date'=>$date,'method'=>$method,'note'=>$note,'created_by'=>get_current_user_id(),'created_at'=>current_time('mysql')),array('%d','%s','%f','%s','%s','%s','%d','%s')); $message=false===$ok?'

Transaction could not be saved.

':'

Transaction saved successfully.

'; } } /* Edit transaction. */ if ( isset($_POST['lms_update_investor_tx']) && check_admin_referer('lms_update_investor_tx_action','lms_update_investor_tx_nonce') ) { if ( ! lms_investor_history_edit_delete_enabled() ) { $message='

Transaction Edit/Delete is disabled from Settings.

'; } else { $tx_id=absint($_POST['tx_id']??0); $investor_id=absint($_POST['investor_id']??0); $tx_type=sanitize_key($_POST['transaction_type']??'deposit'); $amount=(float)($_POST['transaction_amount']??0); $date=sanitize_text_field(wp_unslash($_POST['transaction_date']??current_time('Y-m-d'))); $method=sanitize_text_field(wp_unslash($_POST['transaction_method']??'Cash')); $note=sanitize_textarea_field(wp_unslash($_POST['transaction_note']??'')); $old=$wpdb->get_row($wpdb->prepare("SELECT * FROM $tx_table WHERE id=%d AND investor_id=%d",$tx_id,$investor_id)); if(!$old || $amount<=0 || !preg_match('/^\d{4}-\d{2}-\d{2}$/',$date) || !in_array($tx_type,array('deposit','capital_withdrawal','interest_payout'),true)) { $message='

Invalid transaction update.

'; } else { $wpdb->update($tx_table,array('transaction_type'=>'__editing__'),array('id'=>$tx_id),array('%s'),array('%d')); $check=lms_investor_financials($investor_id,$date); $wpdb->update($tx_table,array('transaction_type'=>$old->transaction_type),array('id'=>$tx_id),array('%s'),array('%d')); $balance_after_old = $check['balance']; $interest_after_old = $check['interest']; if($tx_type==='capital_withdrawal' && $amount>$balance_after_old+$old->amount+0.00001) $message='

Capital withdrawal exceeds available capital.

'; elseif($tx_type==='interest_payout' && $amount>$interest_after_old+$old->amount+0.00001) $message='

Interest payout exceeds accrued interest.

'; else { $wpdb->update($tx_table,array('transaction_type'=>$tx_type,'amount'=>$amount,'transaction_date'=>$date,'method'=>$method,'note'=>$note),array('id'=>$tx_id),array('%s','%f','%s','%s','%s'),array('%d')); $message='

Transaction updated successfully.

'; } } } } /* Delete transaction. */ if ( isset($_POST['lms_delete_investor_tx']) && check_admin_referer('lms_delete_investor_tx_action','lms_delete_investor_tx_nonce') ) { if ( ! lms_investor_history_edit_delete_enabled() ) $message='

Transaction Edit/Delete is disabled from Settings.

'; else { $tx_id=absint($_POST['tx_id']??0); $investor_id=absint($_POST['investor_id']??0); $deleted=$wpdb->delete($tx_table,array('id'=>$tx_id,'investor_id'=>$investor_id),array('%d','%d')); $message=$deleted?'

Transaction deleted.

':'

Transaction could not be deleted.

'; } } $founders=$wpdb->get_results("SELECT * FROM $table WHERE investor_type='Founder' ORDER BY name ASC,id ASC"); $other_investors=$wpdb->get_results("SELECT * FROM $table WHERE investor_type='Investor' ORDER BY name ASC,id ASC"); $selected=$investor_id?$wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id=%d",$investor_id)):null; if(!$selected && empty($add_type) && !empty($founders)){ $selected=$founders[0]; $investor_id=(int)$selected->id; } $profile=$selected?lms_investor_financials($selected->id):null; $transactions=$selected?$wpdb->get_results($wpdb->prepare("SELECT t.*,u.display_name FROM $tx_table t LEFT JOIN {$wpdb->users} u ON u.ID=t.created_by WHERE t.investor_id=%d ORDER BY t.transaction_date DESC,t.id DESC",$selected->id)):array(); $all_current_capital=(float)$wpdb->get_var("SELECT COALESCE(SUM(CASE WHEN transaction_type='deposit' THEN amount WHEN transaction_type IN ('capital_withdrawal','withdrawal') THEN -amount ELSE 0 END),0) FROM $tx_table"); $closed_profit=(float)$wpdb->get_var("SELECT COALESCE(SUM(total_collection-loan_amount),0) FROM {$wpdb->prefix}lms_loans WHERE status='Closed'"); $founder_profit_share=($selected&&$selected->investor_type==='Founder'&&$all_current_capital>0)?max(0,$closed_profit)*$profile['balance']/$all_current_capital:0; $add_url=admin_url('admin.php?page=lms-investors'); $form_type=$add_type==='founder'?'Founder':'Investor'; $profile_form=$edit_profile_id?$wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id=%d",$edit_profile_id)):null; $tx_type_form=sanitize_key($_GET['tx_type']??''); $edit_tx=$edit_tx_id&&$selected&&lms_investor_history_edit_delete_enabled()?$wpdb->get_row($wpdb->prepare("SELECT * FROM $tx_table WHERE id=%d AND investor_id=%d",$edit_tx_id,$selected->id)):null; ?>

Founders & Investors

Manage capital, daily investor interest, payouts and withdrawals.

investor_type:$form_type; ?>
image_url)): ?>

Current image is already set. Upload a new one to replace it.

Interest is counted from each deposit date by calendar days.

Select a Founder or Investor

Use the buttons above to add a new person.

image_url)): ?>

name); ?>

investor_type); ?>investor_type==='Investor'): ?> · interest_rate_monthly,2)); ?>% / month
Edit Profile
Total Invested
Current Capital:
investor_type==='Investor'?'Accrued Interest':'Profit'; ?>
investor_type==='Investor'?$profile['interest']:$founder_profit_share)); ?>
Total Amount
investor_type==='Investor'?$profile['total']:$profile['balance']+$founder_profit_share)); ?>
Total Withdrawn / Paid
investor_type==='Investor'): ?>
Interest is calculated from each deposit date using calendar-day count. Interest payout reduces only the interest balance; capital withdrawal reduces only capital.
Founder has no monthly interest or term. Profit is shown as an estimated share of closed-loan profit based on current founder capital.

investor_type==='Investor'): ?>

Edit Transaction

Cancel

Transaction History

Every deposit, capital withdrawal and interest payout is recorded separately.

transaction_type==='deposit'?'Deposit':($tx->transaction_type==='interest_payout'?'Interest Paid':'Capital Withdrawn'); ?>
DateTypeAmountMethodCollectorNoteAction
No transactions found.
transaction_date); ?>amount)); ?>method); ?>display_name?:'—'); ?>note); ?>Edit
investor_type==='Investor'): ?>

Permissions

Admin: full access to dashboard, customer editing, founders/investors and settings.

Staff: can be assigned only the operational capabilities you choose.

Customer details editing is protected by the lms_edit_customer capability and is not given to staff by this plugin.

Loan settings and investment records are administrator-only.

Post a Comment

Previous Post Next Post