Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e8da3cc2c | |||
| 384ae8b719 | |||
| 654453222f | |||
| a18e8b92a6 | |||
| 6444b61416 | |||
| 090cb88573 | |||
| 95bcefc3a9 | |||
| f24f4311e8 | |||
| 89913d7473 | |||
| 079c869477 | |||
| 9ee75fe4cc | |||
| fcfa56b3f5 | |||
| 39ab0d113e | |||
| ce56c96cdd | |||
| b4a06903e2 | |||
| 0654668530 | |||
| 3737b3522d | |||
| c294d7bed7 | |||
| 5ed65a867c | |||
| 22d6f960ea | |||
| cc0b83c1b9 | |||
| 31976752da | |||
| 0bf8d13777 | |||
| 4ae62e37a3 | |||
| d8fdbeb4c3 | |||
| 239d35f8ff | |||
| 3333e71c94 | |||
| af75475214 | |||
| a8ef665239 | |||
| 7c97a60795 | |||
| acca87c77b | |||
| 9aacbe8972 | |||
| 76866ffe1e | |||
| b01a4496e6 | |||
| 1f51be91e4 | |||
| 92016f90d0 | |||
| 45d7895182 | |||
| 596d368b1d | |||
| 35988e9ce1 | |||
| 5fc4d8be30 | |||
| dcfd1f9542 | |||
| b99f816261 | |||
| 931e30b6f1 | |||
| 189bb1a628 | |||
| f04934039e | |||
| c13dcb07d1 | |||
| dd1d492c30 | |||
| a448288878 | |||
| 821e9cdc81 | |||
| 4e2d12e524 | |||
| 7bc999643a | |||
| 89e95b7851 | |||
| 2543df3d33 | |||
| 112d073235 | |||
| 2611730ec6 | |||
| 05dad52e10 | |||
| d158650be9 | |||
| 61facee902 | |||
| ed67836701 | |||
| d2abbc1458 | |||
| b52475ff0b | |||
| b2026812d5 | |||
| 58445b2a48 | |||
| 0f8a1fa0b1 | |||
| fee07bcceb | |||
| 70a6e2c104 | |||
| 11c93d3e82 | |||
| 7c5028a76d | |||
| aa1260afd6 | |||
| 35b9cba882 | |||
| a18198d547 | |||
| 97c3b03c39 | |||
| 7770b60658 | |||
| 4f5925e10a | |||
| 3cc5546733 | |||
| fe7b99581d | |||
| a6880ea14f |
@@ -1,180 +0,0 @@
|
|||||||
#--------------------------------------------------------------------
|
|
||||||
# Example Environment Configuration file
|
|
||||||
#
|
|
||||||
# This file can be used as a starting point for your own
|
|
||||||
# custom .env files, and contains most of the possible settings
|
|
||||||
# available in a default install.
|
|
||||||
#
|
|
||||||
# By default, all of the settings are commented out. If you want
|
|
||||||
# to override the setting, you must un-comment it by removing the '#'
|
|
||||||
# at the beginning of the line.
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
# ENVIRONMENT
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
|
|
||||||
CI_ENVIRONMENT = development
|
|
||||||
|
|
||||||
# Which profile to use if none is supplied to sendEmail()
|
|
||||||
# Options: default, communication, payment, ...
|
|
||||||
MAIL_PROFILE_DEFAULT=default
|
|
||||||
|
|
||||||
# ===== DEFAULT (system) =====
|
|
||||||
# ---- Legacy fallbacks (kept for compatibility) ----
|
|
||||||
SMTP_HOST = smtp.gmail.com
|
|
||||||
SMTP_USER = alrahma.sunday.school@gmail.com
|
|
||||||
SMTP_PASS = "psnp emdq dykw ypul"
|
|
||||||
SMTP_PORT = 587
|
|
||||||
SMTP_ENCRYPTION = tls # was SSL; set to ssl to match 587
|
|
||||||
|
|
||||||
# Reply-To for all mail profiles
|
|
||||||
MAIL_DEFAULT_REPLY_TO=alrahma.isgl@gmail.com
|
|
||||||
MAIL_DEFAULT_REPLY_TO_NAME="Al Rahma Sunday School"
|
|
||||||
MAIL_COMMUNICATION_REPLY_TO=alrahma.isgl@gmail.com
|
|
||||||
MAIL_COMMUNICATION_REPLY_TO_NAME="School Communications"
|
|
||||||
MAIL_PAYMENT_REPLY_TO=alrahma.isgl@gmail.com
|
|
||||||
MAIL_PAYMENT_REPLY_TO_NAME="School Payments"
|
|
||||||
MAIL_DEFAULT_REPLY_TO="alrahma.isgl@gmail.com"
|
|
||||||
|
|
||||||
# Optional sender directory you already use elsewhere
|
|
||||||
MAIL_SENDERS="{\"general\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma No-Reply\"},\
|
|
||||||
\"registration\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Register Office\"},\
|
|
||||||
\"notifications\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Notifications\"},\
|
|
||||||
\"finance\":{\"email\":\"alrahma.isgl@gmail.com\",\"name\":\"Al Rahma Finance Office\"}}"
|
|
||||||
|
|
||||||
# Principal notification recipient for TimeOff requests (comma-separated allowed)
|
|
||||||
PRINCIPAL_EMAIL="principal@alrahmaisgl.org"
|
|
||||||
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
# APP
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
|
|
||||||
# If you have trouble with `.`, you could also use `_`.
|
|
||||||
app_baseURL = 'http://localhost:8080/'
|
|
||||||
# app.forceGlobalSecureRequests = true
|
|
||||||
# app.CSPEnabled = false
|
|
||||||
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
# DATABASE
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
database.default.hostname = 127.0.0.1
|
|
||||||
database.default.database = school
|
|
||||||
database.default.username = root
|
|
||||||
database.default.password = root
|
|
||||||
database.default.DBDriver = MySQLi
|
|
||||||
database.default.DBPrefix =
|
|
||||||
database.default.port = 3306
|
|
||||||
|
|
||||||
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
# CONTENT SECURITY POLICY
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
|
|
||||||
# contentsecuritypolicy.reportOnly = false
|
|
||||||
# contentsecuritypolicy.defaultSrc = 'none'
|
|
||||||
# contentsecuritypolicy.scriptSrc = 'self'
|
|
||||||
# contentsecuritypolicy.styleSrc = 'self'
|
|
||||||
# contentsecuritypolicy.imageSrc = 'self'
|
|
||||||
# contentsecuritypolicy.baseURI = null
|
|
||||||
# contentsecuritypolicy.childSrc = null
|
|
||||||
# contentsecuritypolicy.connectSrc = 'self'
|
|
||||||
# contentsecuritypolicy.fontSrc = null
|
|
||||||
# contentsecuritypolicy.formAction = null
|
|
||||||
# contentsecuritypolicy.frameAncestors = null
|
|
||||||
# contentsecuritypolicy.frameSrc = null
|
|
||||||
# contentsecuritypolicy.mediaSrc = null
|
|
||||||
# contentsecuritypolicy.objectSrc = null
|
|
||||||
# contentsecuritypolicy.pluginTypes = null
|
|
||||||
# contentsecuritypolicy.reportURI = null
|
|
||||||
# contentsecuritypolicy.sandbox = false
|
|
||||||
# contentsecuritypolicy.upgradeInsecureRequests = false
|
|
||||||
# contentsecuritypolicy.styleNonceTag = '{csp-style-nonce}'
|
|
||||||
# contentsecuritypolicy.scriptNonceTag = '{csp-script-nonce}'
|
|
||||||
# contentsecuritypolicy.autoNonce = true
|
|
||||||
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
# COOKIE
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
|
|
||||||
# cookie.prefix = ''
|
|
||||||
# cookie.expires = 0
|
|
||||||
# cookie.path = '/'
|
|
||||||
# cookie.domain = ''
|
|
||||||
# cookie.secure = false
|
|
||||||
# cookie.httponly = false
|
|
||||||
# cookie.samesite = 'Lax'
|
|
||||||
# cookie.raw = false
|
|
||||||
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
# ENCRYPTION
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
|
|
||||||
# encryption.key =
|
|
||||||
# encryption.driver = OpenSSL
|
|
||||||
# encryption.blockSize = 16
|
|
||||||
# encryption.digest = SHA512
|
|
||||||
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
# HONEYPOT
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
|
|
||||||
# honeypot.hidden = 'true'
|
|
||||||
# honeypot.label = 'Fill This Field'
|
|
||||||
# honeypot.name = 'honeypot'
|
|
||||||
# honeypot.template = '<label>{label}</label><input type="text" name="{name}" value=""/>'
|
|
||||||
# honeypot.container = '<div style="display:none">{template}</div>'
|
|
||||||
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
# SECURITY
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
|
|
||||||
# security.csrfProtection = 'cookie'
|
|
||||||
# security.tokenRandomize = false
|
|
||||||
# security.tokenName = 'csrf_token_name'
|
|
||||||
# security.headerName = 'X-CSRF-TOKEN'
|
|
||||||
# security.cookieName = 'csrf_cookie_name'
|
|
||||||
# security.expires = 7200
|
|
||||||
# security.regenerate = true
|
|
||||||
# security.redirect = false
|
|
||||||
# security.samesite = 'Lax'
|
|
||||||
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
# SESSION
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
|
|
||||||
# session.driver = 'CodeIgniter\Session\Handlers\FileHandler'
|
|
||||||
# session.cookieName = 'ci_session'
|
|
||||||
# session.expiration = 7200
|
|
||||||
# session.savePath = null
|
|
||||||
# session.matchIP = false
|
|
||||||
# session.timeToUpdate = 300
|
|
||||||
# session.regenerateDestroy = false
|
|
||||||
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
# LOGGER
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
|
|
||||||
# logger.threshold = 4
|
|
||||||
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
# CURLRequest
|
|
||||||
#--------------------------------------------------------------------
|
|
||||||
|
|
||||||
# curlrequest.shareOptions = false
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
mail.protocol = smtp
|
|
||||||
mail.SMTPHost = ${SMTP_HOST}
|
|
||||||
mail.SMTPUser = ${SMTP_USER}
|
|
||||||
mail.SMTPPass = ${SMTP_PASS}
|
|
||||||
mail.SMTPPort = ${SMTP_PORT}
|
|
||||||
mail.SMTPCrypto = tls
|
|
||||||
|
|
||||||
mail.fromEmail = ${SMTP_USER}
|
|
||||||
mail.fromName = "Al Rahma Sunday School"
|
|
||||||
mail.mailType = text
|
|
||||||
mail.charset = UTF-8
|
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
/vendor/
|
||||||
|
/node_modules/
|
||||||
|
|
||||||
|
/writable/cache/*
|
||||||
|
/writable/debugbar/*
|
||||||
|
/writable/logs/*
|
||||||
|
/writable/session/*
|
||||||
|
/writable/uploads/*
|
||||||
|
/writable/tmp/*
|
||||||
|
/writable/*.log
|
||||||
|
|
||||||
|
!/writable/.gitkeep
|
||||||
|
!/writable/cache/.gitkeep
|
||||||
|
!/writable/debugbar/.gitkeep
|
||||||
|
!/writable/logs/.gitkeep
|
||||||
|
!/writable/session/.gitkeep
|
||||||
|
!/writable/uploads/.gitkeep
|
||||||
|
!/writable/tmp/.gitkeep
|
||||||
|
|
||||||
|
/public/build/
|
||||||
|
/public/hot/
|
||||||
|
/public/uploads/**
|
||||||
|
!/public/uploads/**/index.html
|
||||||
|
!/public/uploads/.gitkeep
|
||||||
|
|
||||||
|
/uploads/**
|
||||||
|
!/uploads/**/index.html
|
||||||
|
!/uploads/.gitkeep
|
||||||
|
|
||||||
|
docker-compose.override.yml
|
||||||
|
docker-compose.local.yml
|
||||||
|
|
||||||
|
*.log
|
||||||
|
.swp
|
||||||
|
.swo
|
||||||
|
*~
|
||||||
|
.tmp
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.codex/
|
||||||
|
|
||||||
|
/coverage/
|
||||||
|
/build/
|
||||||
|
/builds
|
||||||
|
/phpunit.xml.cache
|
||||||
|
/.phpunit.result.cache
|
||||||
|
/writable/reports/*
|
||||||
|
/writable/test.txt
|
||||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Commands;
|
||||||
|
|
||||||
|
use App\Libraries\InvoiceLedgerService;
|
||||||
|
use App\Models\InvoiceModel;
|
||||||
|
use CodeIgniter\CLI\BaseCommand;
|
||||||
|
use CodeIgniter\CLI\CLI;
|
||||||
|
|
||||||
|
class RecalculateInvoices extends BaseCommand
|
||||||
|
{
|
||||||
|
protected $group = 'Financial';
|
||||||
|
protected $name = 'financial:recalculate-invoices';
|
||||||
|
protected $description = 'Audits invoice totals against the centralized ledger and optionally writes the corrected values.';
|
||||||
|
protected $usage = 'financial:recalculate-invoices [--commit] [--invoice-id=123] [--parent-id=456] [--school-year=2025-2026] [--semester=Fall]';
|
||||||
|
protected $options = [
|
||||||
|
'--commit' => 'Persist recalculated totals. Without this flag the command runs in dry-run mode.',
|
||||||
|
'--invoice-id' => 'Only process a single invoice ID.',
|
||||||
|
'--parent-id' => 'Only process invoices for one parent.',
|
||||||
|
'--school-year' => 'Only process invoices for the specified school year.',
|
||||||
|
'--semester' => 'Only process invoices for the specified semester.',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function run(array $params)
|
||||||
|
{
|
||||||
|
$options = $this->parseOptions($params);
|
||||||
|
$invoiceModel = new InvoiceModel();
|
||||||
|
$ledgerService = new InvoiceLedgerService();
|
||||||
|
$invoiceIds = $this->loadInvoiceIds($invoiceModel, $options);
|
||||||
|
|
||||||
|
if ($invoiceIds === []) {
|
||||||
|
CLI::write('No invoices matched the provided filters.', 'yellow');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$commit = !empty($options['commit']);
|
||||||
|
CLI::write(($commit ? 'Applying' : 'Dry-run auditing') . ' ' . count($invoiceIds) . ' invoice(s)...', 'yellow');
|
||||||
|
|
||||||
|
$changed = 0;
|
||||||
|
$unchanged = 0;
|
||||||
|
$errors = 0;
|
||||||
|
|
||||||
|
foreach ($invoiceIds as $invoiceId) {
|
||||||
|
$invoice = $invoiceModel->find($invoiceId);
|
||||||
|
if (!$invoice) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$calculation = $ledgerService->calculateInvoice($invoiceId);
|
||||||
|
$diffs = $this->diffInvoice($invoice, $calculation);
|
||||||
|
|
||||||
|
if ($diffs === []) {
|
||||||
|
$unchanged++;
|
||||||
|
CLI::write('Invoice #' . $invoiceId . ' unchanged.', 'green');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$changed++;
|
||||||
|
CLI::write('Invoice #' . $invoiceId . ' requires recalculation:', 'light_yellow');
|
||||||
|
foreach ($diffs as $label => $values) {
|
||||||
|
CLI::write(sprintf(
|
||||||
|
' %s: %s -> %s',
|
||||||
|
$label,
|
||||||
|
(string) $values['from'],
|
||||||
|
(string) $values['to']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($commit) {
|
||||||
|
$ledgerService->recalculateInvoice($invoiceId);
|
||||||
|
CLI::write(' saved', 'blue');
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$errors++;
|
||||||
|
CLI::error('Invoice #' . $invoiceId . ' failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CLI::newLine();
|
||||||
|
CLI::write('Processed: ' . count($invoiceIds), 'white');
|
||||||
|
CLI::write('Changed: ' . $changed, $changed > 0 ? 'yellow' : 'white');
|
||||||
|
CLI::write('Unchanged: ' . $unchanged, 'green');
|
||||||
|
CLI::write('Errors: ' . $errors, $errors > 0 ? 'red' : 'white');
|
||||||
|
|
||||||
|
if (!$commit) {
|
||||||
|
CLI::write('Dry-run complete. Re-run with --commit to persist corrections.', 'light_blue');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function parseOptions(array $params): array
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'commit' => false,
|
||||||
|
'invoice-id' => null,
|
||||||
|
'parent-id' => null,
|
||||||
|
'school-year' => null,
|
||||||
|
'semester' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($params as $param) {
|
||||||
|
$value = trim((string) $param);
|
||||||
|
if ($value === '--commit') {
|
||||||
|
$options['commit'] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!str_starts_with($value, '--') || !str_contains($value, '=')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$key, $raw] = explode('=', substr($value, 2), 2);
|
||||||
|
if (array_key_exists($key, $options)) {
|
||||||
|
$options[$key] = $raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function loadInvoiceIds(InvoiceModel $invoiceModel, array $options): array
|
||||||
|
{
|
||||||
|
$builder = $invoiceModel->select('id')->orderBy('id', 'ASC');
|
||||||
|
|
||||||
|
if (!empty($options['invoice-id'])) {
|
||||||
|
$builder->where('id', (int) $options['invoice-id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($options['parent-id'])) {
|
||||||
|
$builder->where('parent_id', (int) $options['parent-id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($options['school-year'])) {
|
||||||
|
$builder->where('school_year', (string) $options['school-year']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($options['semester'])) {
|
||||||
|
$builder->where('semester', (string) $options['semester']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_map(
|
||||||
|
static fn (array $row): int => (int) ($row['id'] ?? 0),
|
||||||
|
$builder->findAll()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function diffInvoice(array $invoice, array $calculation): array
|
||||||
|
{
|
||||||
|
$fields = [
|
||||||
|
'total_amount' => 'total_amount',
|
||||||
|
'paid_amount' => 'paid_amount',
|
||||||
|
'balance' => 'balance',
|
||||||
|
'status' => 'status',
|
||||||
|
'has_discount' => 'has_discount',
|
||||||
|
];
|
||||||
|
$diffs = [];
|
||||||
|
|
||||||
|
foreach ($fields as $invoiceKey => $calcKey) {
|
||||||
|
$current = (string) ($invoice[$invoiceKey] ?? '');
|
||||||
|
$recalculated = (string) ($calculation[$calcKey] ?? '');
|
||||||
|
if ($current !== $recalculated) {
|
||||||
|
$diffs[$invoiceKey] = [
|
||||||
|
'from' => $current,
|
||||||
|
'to' => $recalculated,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs;
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
@@ -1,232 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Commands;
|
|
||||||
|
|
||||||
use CodeIgniter\CLI\BaseCommand;
|
|
||||||
use CodeIgniter\CLI\CLI;
|
|
||||||
use App\Models\PayPalPaymentModel;
|
|
||||||
use App\Models\PaymentModel;
|
|
||||||
use App\Models\UserModel;
|
|
||||||
use App\Models\ConfigurationModel;
|
|
||||||
use App\Models\InvoiceModel;
|
|
||||||
use App\Models\StudentModel;
|
|
||||||
use App\Models\EnrollmentModel;
|
|
||||||
|
|
||||||
class SyncPaypalPayments extends BaseCommand
|
|
||||||
{
|
|
||||||
protected $group = 'Payments';
|
|
||||||
protected $name = 'payments:sync-paypal';
|
|
||||||
protected $description = 'Sync PayPal payments to internal payments table from paypal_payments';
|
|
||||||
protected $configModel;
|
|
||||||
protected $semester;
|
|
||||||
protected $schoolYear;
|
|
||||||
protected $paypalModel;
|
|
||||||
protected $paymentModel;
|
|
||||||
protected $userModel;
|
|
||||||
protected $invoiceModel;
|
|
||||||
protected $studentModel;
|
|
||||||
protected $enrollmentModel;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->configModel = new ConfigurationModel();
|
|
||||||
$this->paypalModel = new PayPalPaymentModel();
|
|
||||||
$this->paymentModel = new PaymentModel();
|
|
||||||
$this->userModel = new UserModel();
|
|
||||||
$this->invoiceModel = new InvoiceModel();
|
|
||||||
$this->studentModel = new StudentModel();
|
|
||||||
$this->enrollmentModel = new EnrollmentModel();
|
|
||||||
|
|
||||||
$this->semester = $this->configModel->getConfig('semester');
|
|
||||||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function run(array $params)
|
|
||||||
{
|
|
||||||
$dryRun = CLI::getOption('dry-run');
|
|
||||||
$reportOnly = CLI::getOption('report-only');
|
|
||||||
$mode = $reportOnly ? 'REPORT-ONLY' : ($dryRun ? 'DRY-RUN' : 'LIVE');
|
|
||||||
|
|
||||||
$paypalEntries = $this->paypalModel
|
|
||||||
->where('status', 'COMPLETED')
|
|
||||||
->where('synced', 0)
|
|
||||||
->where('sync_attempts <', 3)
|
|
||||||
->where('transaction_id IS NOT NULL')
|
|
||||||
->findAll();
|
|
||||||
|
|
||||||
$syncedCount = 0;
|
|
||||||
$failed = [];
|
|
||||||
|
|
||||||
foreach ($paypalEntries as $entry) {
|
|
||||||
$parentId = null;
|
|
||||||
$invoiceId = 0;
|
|
||||||
|
|
||||||
$users = $this->userModel->getUsersBySchoolId($entry['parent_school_id']);
|
|
||||||
$user = $users[0] ?? null;
|
|
||||||
|
|
||||||
// Always increment sync_attempts unless report-only
|
|
||||||
if (!$reportOnly) {
|
|
||||||
$this->paypalModel->update($entry['id'], [
|
|
||||||
'sync_attempts' => $entry['sync_attempts'] + 1
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($user) {
|
|
||||||
$parentId = $user['id'];
|
|
||||||
|
|
||||||
$invoice = $this->invoiceModel->getInvoicesByParentId($parentId, $this->schoolYear);
|
|
||||||
|
|
||||||
if (!$reportOnly && !$dryRun) {
|
|
||||||
if ($invoice) {
|
|
||||||
$invoiceId = $invoice['id'];
|
|
||||||
|
|
||||||
$success = $this->processPayment(
|
|
||||||
$invoiceId,
|
|
||||||
$entry['amount'],
|
|
||||||
'PayPal',
|
|
||||||
null,
|
|
||||||
$entry['transaction_id'],
|
|
||||||
date('Y-m-d', strtotime($entry['created_at'])),
|
|
||||||
$this->schoolYear,
|
|
||||||
$this->semester
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!$success) {
|
|
||||||
$failed[] = $entry['transaction_id'];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$this->paymentModel->insert([
|
|
||||||
'parent_id' => $parentId,
|
|
||||||
'invoice_id' => 0,
|
|
||||||
'total_amount' => $entry['amount'],
|
|
||||||
'paid_amount' => $entry['amount'],
|
|
||||||
'balance' => 0.00,
|
|
||||||
'number_of_installments' => 1,
|
|
||||||
'transaction_id' => $entry['transaction_id'],
|
|
||||||
'payment_method' => 'PayPal',
|
|
||||||
'payment_date' => date('Y-m-d', strtotime($entry['created_at'])),
|
|
||||||
'school_year' => $this->schoolYear,
|
|
||||||
'semester' => $this->semester,
|
|
||||||
'status' => 'Completed',
|
|
||||||
'updated_by' => null,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as synced only in LIVE mode
|
|
||||||
$this->paypalModel->update($entry['id'], ['synced' => 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$syncedCount++;
|
|
||||||
} else {
|
|
||||||
log_message('error', "[PAYPAL SYNC FAILED] No user found for parent_school_id: {$entry['parent_school_id']}");
|
|
||||||
$failed[] = $entry['transaction_id'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Logging ===
|
|
||||||
log_message('info', "[$mode] PAYPAL SYNC: $syncedCount processed.");
|
|
||||||
if (!empty($failed)) {
|
|
||||||
log_message('error', "[$mode] PAYPAL SYNC Failed: " . implode(', ', $failed));
|
|
||||||
}
|
|
||||||
|
|
||||||
// === CLI Output ===
|
|
||||||
CLI::write("[$mode] $syncedCount PayPal payments processed.", 'green');
|
|
||||||
if (!empty($failed)) {
|
|
||||||
CLI::error("[$mode] Failed transactions: " . implode(', ', $failed));
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Email Report: Only if there's any update ===
|
|
||||||
if ($syncedCount > 0 || !empty($failed)) {
|
|
||||||
helper('email');
|
|
||||||
$email = \Config\Services::email();
|
|
||||||
$email->setTo('support@alrahmaisgl.org');
|
|
||||||
$email->setFrom('no-parentsreply@alrahmaisgl.org', 'PayPal Sync Report');
|
|
||||||
$email->setSubject("[$mode] PayPal Sync Report - " . date('Y-m-d H:i'));
|
|
||||||
|
|
||||||
$body = "PayPal Sync Mode: $mode\n\n";
|
|
||||||
$body .= "$syncedCount PayPal payments processed.\n\n";
|
|
||||||
|
|
||||||
if (!empty($failed)) {
|
|
||||||
$body .= count($failed) . " failed transactions:\n";
|
|
||||||
$body .= implode("\n", $failed);
|
|
||||||
} else {
|
|
||||||
$body .= "No failed transactions.\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$email->setMessage(nl2br($body));
|
|
||||||
|
|
||||||
if ($email->send()) {
|
|
||||||
CLI::write("[$mode] Email report sent successfully.", 'yellow');
|
|
||||||
} else {
|
|
||||||
CLI::error("[$mode] Failed to send email report.");
|
|
||||||
log_message('error', 'Email send error: ' . $email->printDebugger(['headers']));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log_message('info', "[$mode] No PayPal sync updates. Email not sent.");
|
|
||||||
CLI::write("[$mode] No changes to report. Email not sent.", 'blue');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function processPayment($invoiceId, $amount, $paymentMethod, $checkFile = null, $transactionId = null, $paymentDate = null, $schoolYear = null, $semester = null)
|
|
||||||
{
|
|
||||||
$invoice = $this->invoiceModel->find($invoiceId);
|
|
||||||
if (!$invoice) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$transactionId = $transactionId ?? 'INV-' . $invoiceId . '-' . time();
|
|
||||||
$paymentDate = $paymentDate ?? date('Y-m-d');
|
|
||||||
|
|
||||||
$newPaid = $invoice['paid_amount'] + $amount;
|
|
||||||
$newBalance = $invoice['balance'] - $amount;
|
|
||||||
|
|
||||||
$invoiceUpdateData = [
|
|
||||||
'paid_amount' => $newPaid,
|
|
||||||
'balance' => $newBalance,
|
|
||||||
'status' => ($newBalance <= 0) ? 'Paid' : $invoice['status'],
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!$this->invoiceModel->update($invoiceId, $invoiceUpdateData)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->paymentModel->insert([
|
|
||||||
'parent_id' => $invoice['parent_id'],
|
|
||||||
'invoice_id' => $invoiceId,
|
|
||||||
'total_amount' => $invoice['total_amount'],
|
|
||||||
'paid_amount' => $amount,
|
|
||||||
'balance' => $newBalance,
|
|
||||||
'number_of_installments' => 1,
|
|
||||||
'transaction_id' => $transactionId,
|
|
||||||
'payment_method' => $paymentMethod,
|
|
||||||
'payment_date' => $paymentDate,
|
|
||||||
'status' => ($newBalance <= 0) ? 'Full' : 'Partial',
|
|
||||||
'check_file' => $checkFile,
|
|
||||||
'updated_by' => null, // Avoid using session()->get() in CLI
|
|
||||||
'school_year' => $schoolYear,
|
|
||||||
'semester' => $semester
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->updateEnrollmentStatusIfPaid($invoiceId, $schoolYear);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function updateEnrollmentStatusIfPaid($invoiceId, $schoolYear)
|
|
||||||
{
|
|
||||||
$invoice = $this->invoiceModel->find($invoiceId);
|
|
||||||
if (!$invoice || $invoice['balance'] > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$students = $this->studentModel->where('parent_id', $invoice['parent_id'])
|
|
||||||
->where('school_year', $schoolYear)
|
|
||||||
->findAll();
|
|
||||||
|
|
||||||
foreach ($students as $student) {
|
|
||||||
$this->enrollmentModel->set(['enrollment_status' => 'enrolled'])
|
|
||||||
->where('student_id', $student['id'])
|
|
||||||
->update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+2
-2
@@ -16,11 +16,11 @@ class Commands extends BaseService
|
|||||||
\App\Commands\CleanupPasswordResets::class,
|
\App\Commands\CleanupPasswordResets::class,
|
||||||
\App\Commands\ConfigUpdate::class,
|
\App\Commands\ConfigUpdate::class,
|
||||||
\App\Commands\DeleteInactiveUsers::class,
|
\App\Commands\DeleteInactiveUsers::class,
|
||||||
|
\App\Commands\RecalculateInvoices::class,
|
||||||
\App\Commands\SendAbsenteesSummary::class,
|
\App\Commands\SendAbsenteesSummary::class,
|
||||||
\App\Commands\SendLatesSummary::class,
|
\App\Commands\SendLatesSummary::class,
|
||||||
\App\Commands\SendMonthlyPaymentNotifications::class,
|
\App\Commands\SendMonthlyPaymentNotifications::class,
|
||||||
\App\Commands\SendTestPaymentNotification::class,
|
\App\Commands\SendTestPaymentNotification::class,
|
||||||
\App\Commands\SyncPaypalPayments::class,
|
|
||||||
\App\Commands\RecalculateAttendance::class,
|
\App\Commands\RecalculateAttendance::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -30,4 +30,4 @@ class Commands extends BaseService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+2
@@ -9,6 +9,7 @@ use App\Listeners\SchoolEventListener;
|
|||||||
use App\Listeners\AttendanceConsequenceListener;
|
use App\Listeners\AttendanceConsequenceListener;
|
||||||
use App\Listeners\WhatsappInviteListener;
|
use App\Listeners\WhatsappInviteListener;
|
||||||
use App\Listeners\BelowSixtyEmailListener;
|
use App\Listeners\BelowSixtyEmailListener;
|
||||||
|
use App\Listeners\DecisionEmailListener;
|
||||||
|
|
||||||
// Create an instance so we can use $this->emailService like your other handlers
|
// Create an instance so we can use $this->emailService like your other handlers
|
||||||
$waListener = new WhatsappInviteListener(service('emailService'));
|
$waListener = new WhatsappInviteListener(service('emailService'));
|
||||||
@@ -118,6 +119,7 @@ Events::on('attendance.follow_up', [AttendanceConsequenceListener::class, 'fol
|
|||||||
Events::on('attendance.final_warning', [AttendanceConsequenceListener::class, 'finalWarning']);
|
Events::on('attendance.final_warning', [AttendanceConsequenceListener::class, 'finalWarning']);
|
||||||
Events::on('attendance.dismissal', [AttendanceConsequenceListener::class, 'dismissal']);
|
Events::on('attendance.dismissal', [AttendanceConsequenceListener::class, 'dismissal']);
|
||||||
Events::on('below60.email', [BelowSixtyEmailListener::class, 'handle']);
|
Events::on('below60.email', [BelowSixtyEmailListener::class, 'handle']);
|
||||||
|
Events::on('below60.decision_email', [DecisionEmailListener::class, 'handle']);
|
||||||
|
|
||||||
//Whatsapp Event listener
|
//Whatsapp Event listener
|
||||||
Events::on('whatsapp_invites.send', [$waListener, 'handle']);
|
Events::on('whatsapp_invites.send', [$waListener, 'handle']);
|
||||||
|
|||||||
Executable → Regular
Executable → Regular
Executable → Regular
+6
-5
@@ -24,6 +24,8 @@ class Filters extends BaseConfig
|
|||||||
'honeypot' => Honeypot::class,
|
'honeypot' => Honeypot::class,
|
||||||
'invalidchars' => InvalidChars::class,
|
'invalidchars' => InvalidChars::class,
|
||||||
'secureheaders' => SecureHeaders::class,
|
'secureheaders' => SecureHeaders::class,
|
||||||
|
'sanitizeinput' => \App\Filters\SanitizeInputFilter::class,
|
||||||
|
'apiratelimit' => \App\Filters\ApiRateLimitFilter::class,
|
||||||
'auth' => \App\Filters\AuthFilter::class, // Define the alias for your auth filter
|
'auth' => \App\Filters\AuthFilter::class, // Define the alias for your auth filter
|
||||||
'apiAuth' => \App\Filters\ApiAuthFilter::class, // JWT-based API authentication
|
'apiAuth' => \App\Filters\ApiAuthFilter::class, // JWT-based API authentication
|
||||||
'cleanupScheduler' => \App\Filters\CleanupScheduler::class,
|
'cleanupScheduler' => \App\Filters\CleanupScheduler::class,
|
||||||
@@ -42,11 +44,9 @@ class Filters extends BaseConfig
|
|||||||
public array $globals = [
|
public array $globals = [
|
||||||
'before' => [
|
'before' => [
|
||||||
'timezone',
|
'timezone',
|
||||||
|
'sanitizeinput',
|
||||||
|
'invalidchars',
|
||||||
'csrf' => ['except' => [
|
'csrf' => ['except' => [
|
||||||
// Webhooks / integrations
|
|
||||||
'api/paypal-webhook',
|
|
||||||
'index.php/api/paypal-webhook',
|
|
||||||
|
|
||||||
// WhatsApp membership management (legacy allowances retained)
|
// WhatsApp membership management (legacy allowances retained)
|
||||||
'whatsapp/update-membership',
|
'whatsapp/update-membership',
|
||||||
'index.php/whatsapp/update-membership',
|
'index.php/whatsapp/update-membership',
|
||||||
@@ -80,6 +80,7 @@ class Filters extends BaseConfig
|
|||||||
]],
|
]],
|
||||||
],
|
],
|
||||||
'after' => [
|
'after' => [
|
||||||
|
'secureheaders',
|
||||||
'toolbar',
|
'toolbar',
|
||||||
'cleanupScheduler' => ['except' => ['cleanup/*']],
|
'cleanupScheduler' => ['except' => ['cleanup/*']],
|
||||||
],
|
],
|
||||||
@@ -112,7 +113,7 @@ class Filters extends BaseConfig
|
|||||||
* @var array<string, array<string, list<string>>>
|
* @var array<string, array<string, list<string>>>
|
||||||
*/
|
*/
|
||||||
public array $filters = [
|
public array $filters = [
|
||||||
|
'apiratelimit' => ['before' => ['api/*', 'index.php/api/*']],
|
||||||
];
|
];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Executable → Regular
Executable → Regular
+1
@@ -9,6 +9,7 @@ use CodeIgniter\Format\XMLFormatter;
|
|||||||
|
|
||||||
class Format extends BaseConfig
|
class Format extends BaseConfig
|
||||||
{
|
{
|
||||||
|
public int $jsonEncodeDepth = 512;
|
||||||
/**
|
/**
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
* Available Response Formats
|
* Available Response Formats
|
||||||
|
|||||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
@@ -1,13 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Config;
|
|
||||||
|
|
||||||
use CodeIgniter\Config\BaseConfig;
|
|
||||||
|
|
||||||
class PaypalConfig extends BaseConfig
|
|
||||||
{
|
|
||||||
// PayPal API Credentials
|
|
||||||
public $paypalClientId = 'YOUR_PAYPAL_CLIENT_ID';
|
|
||||||
public $paypalSecret = 'YOUR_PAYPAL_SECRET';
|
|
||||||
public $paypalMode = 'sandbox'; // Change to 'live' when moving to production
|
|
||||||
}
|
|
||||||
Executable → Regular
Executable → Regular
Executable → Regular
+90
-72
@@ -174,6 +174,18 @@ $routes->group('administrator/subject-curriculum', ['filter' => 'auth:update_cur
|
|||||||
$routes->post('delete/(:num)', 'View\SubjectCurriculumController::delete/$1');
|
$routes->post('delete/(:num)', 'View\SubjectCurriculumController::delete/$1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$routes->get('administrator/trophy', 'View\TrophyController::index', ['filter' => 'auth:admin']);
|
||||||
|
$routes->get('administrator/trophy/winners', 'View\TrophyController::winners', ['filter' => 'auth:admin']);
|
||||||
|
$routes->get('administrator/trophy/final', 'View\TrophyController::final', ['filter' => 'auth:admin']);
|
||||||
|
|
||||||
|
// Certificates
|
||||||
|
$routes->get('administrator/certificates', 'View\CertificateController::index', ['filter' => 'auth:admin']);
|
||||||
|
$routes->get('administrator/certificates/csrf-token', 'View\CertificateController::csrfToken', ['filter' => 'auth:admin']);
|
||||||
|
$routes->post('administrator/certificates/generate', 'View\CertificateController::generate', ['filter' => 'auth:admin']);
|
||||||
|
$routes->get('administrator/certificates/log', 'View\CertificateController::auditLog', ['filter' => 'auth:admin']);
|
||||||
|
$routes->get('administrator/certificates/reprint/(:any)', 'View\CertificateController::reprint/$1', ['filter' => 'auth:read']);
|
||||||
|
$routes->get('verify/(:segment)', 'View\CertificateController::verify/$1');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -245,15 +257,17 @@ $routes->post('administrator/sections/auto-distribute', 'View\StudentController:
|
|||||||
$routes->get('administrator/sections/promotion-totals', 'View\StudentController::promotionTotalsApi');
|
$routes->get('administrator/sections/promotion-totals', 'View\StudentController::promotionTotalsApi');
|
||||||
|
|
||||||
|
|
||||||
$routes->get('refunds/list', 'View\RefundController::listRefunds');
|
$routes->get('refunds/list', 'View\RefundController::listRefunds', ['filter' => 'auth:view_invoice|view_financial_reports|administrator|administrative staff|principal']);
|
||||||
$routes->post('refunds/request', 'View\RefundController::requestRefund');
|
$routes->post('refunds/request', 'View\RefundController::requestRefund', ['filter' => 'auth:create_invoice|view_financial_reports|administrator|administrative staff|principal,create']);
|
||||||
$routes->post('refunds/approve/(:num)', 'View\RefundController::approveRefund/$1');
|
$routes->post('refunds/approve/(:num)', 'View\RefundController::approveRefund/$1', ['filter' => 'auth:update_invoice|view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
$routes->post('refunds/pay/(:num)', 'View\RefundController::payRefund/$1');
|
$routes->post('refunds/pay/(:num)', 'View\RefundController::payRefund/$1', ['filter' => 'auth:update_invoice|view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
$routes->post('refunds/updateStatus/(:num)', 'View\RefundController::updateStatus/$1');
|
$routes->post('refunds/updateStatus/(:num)', 'View\RefundController::updateStatus/$1', ['filter' => 'auth:update_invoice|view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
$routes->post('refunds/processRefunds', 'View\RefundController::processRefunds');
|
$routes->post('refunds/processRefunds', 'View\RefundController::processRefunds', ['filter' => 'auth:update_invoice|view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
$routes->post('refunds/updateStatus', 'View\RefundController::updateStatus');
|
$routes->post('refunds/updateStatus', 'View\RefundController::updateStatus', ['filter' => 'auth:update_invoice|view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
$routes->post('refunds/updatePayment', 'View\RefundController::updatePayment');
|
$routes->post('refunds/updatePayment', 'View\RefundController::updatePayment', ['filter' => 'auth:update_invoice|view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
$routes->post('refunds/recalculateOverpayments', 'View\RefundController::recalculateOverpayments');
|
$routes->post('refunds/recalculateOverpayments', 'View\RefundController::recalculateOverpayments', ['filter' => 'auth:update_invoice|view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
|
$routes->get('refunds/file/(:num)', 'View\RefundController::serveRefundFile/$1', ['filter' => 'auth:view_invoice|view_financial_reports|administrator|administrative staff|principal']);
|
||||||
|
$routes->get('refunds/file/(:num)/(:segment)', 'View\RefundController::serveRefundFile/$1/$2', ['filter' => 'auth:view_invoice|view_financial_reports|administrator|administrative staff|principal']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -511,13 +525,31 @@ $routes->post('grading/below-60/email', 'View\GradingController::sendBelowSixtyE
|
|||||||
$routes->post('grading/below-60/status', 'View\GradingController::updateBelowSixtyStatus', ['filter' => 'auth:read']);
|
$routes->post('grading/below-60/status', 'View\GradingController::updateBelowSixtyStatus', ['filter' => 'auth:read']);
|
||||||
$routes->get('grading/below-60/schedule', 'View\GradingController::scheduleBelowSixty', ['filter' => 'auth:read']);
|
$routes->get('grading/below-60/schedule', 'View\GradingController::scheduleBelowSixty', ['filter' => 'auth:read']);
|
||||||
$routes->post('grading/below-60/schedule', 'View\GradingController::saveBelowSixtyMeeting', ['filter' => 'auth:read']);
|
$routes->post('grading/below-60/schedule', 'View\GradingController::saveBelowSixtyMeeting', ['filter' => 'auth:read']);
|
||||||
|
$routes->get('grading/decisions', 'View\GradingController::allDecisions', ['filter' => 'auth:read']);
|
||||||
|
$routes->post('grading/decisions/generate', 'View\GradingController::generateAllDecisions', ['filter' => 'auth:read']);
|
||||||
|
$routes->get('grading/below-60/decisions', 'View\GradingController::belowSixtyDecisions', ['filter' => 'auth:read']);
|
||||||
|
$routes->post('grading/below-60/decisions/save', 'View\GradingController::saveBelowSixtyDecision', ['filter' => 'auth:read']);
|
||||||
|
$routes->get('grading/below-60/decisions/student-details', 'View\GradingController::studentDecisionDetails', ['filter' => 'auth:read']);
|
||||||
|
$routes->get(
|
||||||
|
'grading/below-60/decisions/email/preview',
|
||||||
|
'View\GradingController::previewBelowSixtyDecisionEmail',
|
||||||
|
['filter' => 'auth:read']
|
||||||
|
);
|
||||||
|
|
||||||
|
$routes->post(
|
||||||
|
'grading/below-60/decisions/email',
|
||||||
|
'View\GradingController::sendBelowSixtyDecisionEmail',
|
||||||
|
['filter' => 'auth:read']
|
||||||
|
);
|
||||||
|
|
||||||
// Final part
|
// Final part
|
||||||
$routes->get('grading/(:segment)/(:num)/(:num)', 'View\GradingController::show/$1/$2/$3');
|
$routes->get('grading/(:segment)/(:num)/(:num)', 'View\GradingController::show/$1/$2/$3');
|
||||||
$routes->post('grading/update', 'View\GradingController::update');
|
$routes->post('grading/update', 'View\GradingController::update');
|
||||||
$routes->get('grading', 'View\GradingController::grading');
|
$routes->get('grading', 'View\GradingController::grading');
|
||||||
$routes->post('grading/toggle-score-lock', 'View\GradingController::toggleScoreLock', ['filter' => 'auth:read']);
|
$routes->post('grading/toggle-score-lock', 'View\GradingController::toggleScoreLock', ['filter' => 'auth:read']);
|
||||||
|
$routes->get('grading/toggle-score-lock', static function () {
|
||||||
|
return 'GET route hit. Something is calling this route incorrectly.';
|
||||||
|
});
|
||||||
$routes->post('grading/lock-all-scores', 'View\GradingController::lockAllScores', ['filter' => 'auth:read']);
|
$routes->post('grading/lock-all-scores', 'View\GradingController::lockAllScores', ['filter' => 'auth:read']);
|
||||||
$routes->post('grading/release-scores', 'View\GradingController::toggleParentScoresRelease', ['filter' => 'auth:read']);
|
$routes->post('grading/release-scores', 'View\GradingController::toggleParentScoresRelease', ['filter' => 'auth:read']);
|
||||||
$routes->post('grading/refresh-semester-scores', 'View\GradingController::refreshSemesterScores', ['filter' => 'auth:read']);
|
$routes->post('grading/refresh-semester-scores', 'View\GradingController::refreshSemesterScores', ['filter' => 'auth:read']);
|
||||||
@@ -535,6 +567,7 @@ $routes->post('payment/event_charges', 'View\EventController::eventUpdate');
|
|||||||
|
|
||||||
// Parent event participation
|
// Parent event participation
|
||||||
$routes->get('administrator/event-charges', 'View\EventController::eventShow');
|
$routes->get('administrator/event-charges', 'View\EventController::eventShow');
|
||||||
|
$routes->get('administrator/event-charges/pdf', 'View\EventController::eventChargesPdf');
|
||||||
$routes->post('administrator/event-charges/remove/(:num)', 'View\EventController::removeCharge/$1');
|
$routes->post('administrator/event-charges/remove/(:num)', 'View\EventController::removeCharge/$1');
|
||||||
$routes->post('administrator/event-charges/payment/(:num)', 'View\EventController::toggleEventPayment/$1');
|
$routes->post('administrator/event-charges/payment/(:num)', 'View\EventController::toggleEventPayment/$1');
|
||||||
$routes->post('administrator/event-charges/waiver/(:num)', 'View\EventController::toggleWaiverStatus/$1');
|
$routes->post('administrator/event-charges/waiver/(:num)', 'View\EventController::toggleWaiverStatus/$1');
|
||||||
@@ -555,19 +588,19 @@ $routes->post('invoices/updateStatus/(:num)', 'View\InvoiceController::updateSta
|
|||||||
// app/Config/Routes.php
|
// app/Config/Routes.php
|
||||||
$routes->group('payment', ['filter' => 'auth'], static function ($routes) {
|
$routes->group('payment', ['filter' => 'auth'], static function ($routes) {
|
||||||
// Read
|
// Read
|
||||||
$routes->get('manual_pay', 'View\PaymentController::manualPaySearch', ['filter' => 'auth:read']);
|
$routes->get('manual_pay', 'View\PaymentController::manualPaySearch', ['filter' => 'auth:view_invoice|view_payment|view_financial_reports|administrator|administrative staff|principal']);
|
||||||
$routes->get('manual_pay_suggest', 'View\PaymentController::manualPaySuggest', ['filter' => 'auth:read']);
|
$routes->get('manual_pay_suggest', 'View\PaymentController::manualPaySuggest', ['filter' => 'auth:view_invoice|view_payment|view_financial_reports|administrator|administrative staff|principal']);
|
||||||
|
|
||||||
// Create
|
// Create
|
||||||
$routes->post('manual_pay', 'View\PaymentController::manualPayUpdate', ['filter' => 'auth:create']);
|
$routes->post('manual_pay', 'View\PaymentController::manualPayUpdate', ['filter' => 'auth:create_payment|create_invoice|view_financial_reports|administrator|administrative staff|principal,create']);
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
$routes->post('manual_pay_edit', 'View\PaymentController::manualPayEdit', ['filter' => 'auth:update']);
|
$routes->post('manual_pay_edit', 'View\PaymentController::manualPayEdit', ['filter' => 'auth:update_payment|update_invoice|view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
$routes->post('manual_pay_update', 'View\PaymentController::manualPayUpdate', ['filter' => 'auth:update']);
|
$routes->post('manual_pay_update', 'View\PaymentController::manualPayUpdate', ['filter' => 'auth:create_payment|create_invoice|view_financial_reports|administrator|administrative staff|principal,create']);
|
||||||
|
|
||||||
// Read (serve files)
|
// Read (serve files)
|
||||||
$routes->get('serveCheckFile/(:any)/(:any)', 'View\PaymentController::serveCheckFile/$1/$2', ['filter' => 'auth:read']);
|
$routes->get('file/(:num)/(:segment)', 'View\PaymentController::servePaymentFile/$1/$2', ['filter' => 'auth:view_invoice|view_payment|view_financial_reports|administrator|administrative staff|principal']);
|
||||||
$routes->get('serveCheckFile/(:any)', 'View\PaymentController::serveCheckFile/$1', ['filter' => 'auth:read']);
|
$routes->get('file/(:num)', 'View\PaymentController::servePaymentFile/$1', ['filter' => 'auth:view_invoice|view_payment|view_financial_reports|administrator|administrative staff|principal']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -610,9 +643,14 @@ $routes->group('admin', ['filter' => 'auth'], static function ($routes) {
|
|||||||
$routes->post('admin/broadcast-email/upload-image', 'View\BroadcastEmailController::uploadImage');
|
$routes->post('admin/broadcast-email/upload-image', 'View\BroadcastEmailController::uploadImage');
|
||||||
|
|
||||||
|
|
||||||
$routes->get('payment/financial_report', 'View\FinancialController::financialReport');
|
$routes->get('payment/financial_report', 'View\FinancialController::financialReport', ['filter' => 'auth:view_financial_reports|view_invoice|administrator|administrative staff|principal']);
|
||||||
$routes->get('financial-report/financialReportSummary', 'View\FinancialController::financialReportSummary');
|
$routes->get('financial-report/financialReportSummary', 'View\FinancialController::financialReportSummary', ['filter' => 'auth:view_financial_reports|view_invoice|administrator|administrative staff|principal']);
|
||||||
$routes->get('payment/download_csv', 'View\FinancialController::downloadCsv');
|
$routes->get('financial-report/downloadSummaryCsv', 'View\FinancialController::downloadSummaryCsv', ['filter' => 'auth:view_financial_reports|view_invoice|administrator|administrative staff|principal']);
|
||||||
|
$routes->get('financial-report/downloadSummaryAllDetailsCsv', 'View\FinancialController::downloadSummaryAllDetailsCsv', ['filter' => 'auth:view_financial_reports|view_invoice|administrator|administrative staff|principal']);
|
||||||
|
$routes->get('payment/download_csv', 'View\FinancialController::downloadCsv', ['filter' => 'auth:view_financial_reports|view_invoice|administrator|administrative staff|principal']);
|
||||||
|
$routes->get('administrator/tuition-forecast', 'View\TuitionForecastController::index', ['filter' => 'auth:view_financial_reports|view_invoice|administrator|administrative staff|principal']);
|
||||||
|
$routes->post('administrator/tuition-forecast/calculate', 'View\TuitionForecastController::calculate', ['filter' => 'auth:view_financial_reports|view_invoice|administrator|administrative staff|principal']);
|
||||||
|
$routes->get('administrator/tuition-forecast/export', 'View\TuitionForecastController::exportCsv', ['filter' => 'auth:view_financial_reports|view_invoice|administrator|administrative staff|principal']);
|
||||||
$routes->get('reports/downloadFinancialReport', 'View\FinancialController::downloadFinancialReport');
|
$routes->get('reports/downloadFinancialReport', 'View\FinancialController::downloadFinancialReport');
|
||||||
// Financial APIs (JSON)
|
// Financial APIs (JSON)
|
||||||
$routes->get('api/financial/report', 'View\FinancialController::financialReportData', ['filter' => 'auth']);
|
$routes->get('api/financial/report', 'View\FinancialController::financialReportData', ['filter' => 'auth']);
|
||||||
@@ -623,27 +661,27 @@ $routes->get('payment/unpaid-parents', 'View\FinancialController::unpaidParents'
|
|||||||
$routes->get('api/financial/unpaid-parents', 'View\FinancialController::unpaidParents', ['filter' => 'auth:view_invoice']);
|
$routes->get('api/financial/unpaid-parents', 'View\FinancialController::unpaidParents', ['filter' => 'auth:view_invoice']);
|
||||||
|
|
||||||
|
|
||||||
$routes->get('expenses/index', 'View\ExpenseController::index');
|
$routes->get('expenses/index', 'View\ExpenseController::index', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal']);
|
||||||
$routes->get('expenses/create', 'View\ExpenseController::create');
|
$routes->get('expenses/create', 'View\ExpenseController::create', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal']);
|
||||||
$routes->post('expenses/store', 'View\ExpenseController::store');
|
$routes->post('expenses/store', 'View\ExpenseController::store', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal,create']);
|
||||||
$routes->post('expenses/updateStatus', 'View\ExpenseController::updateStatus');
|
$routes->post('expenses/updateStatus', 'View\ExpenseController::updateStatus', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
|
|
||||||
$routes->get('reimbursements/index', 'View\ReimbursementController::index');
|
$routes->get('reimbursements/index', 'View\ReimbursementController::index', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal']);
|
||||||
$routes->get('reimbursements/create', 'View\ReimbursementController::create');
|
$routes->get('reimbursements/create', 'View\ReimbursementController::create', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal']);
|
||||||
$routes->post('reimbursements/store', 'View\ReimbursementController::store');
|
$routes->post('reimbursements/store', 'View\ReimbursementController::store', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal,create']);
|
||||||
$routes->get('reimbursements/under-processing', 'View\ReimbursementController::underProcessing');
|
$routes->get('reimbursements/under-processing', 'View\ReimbursementController::underProcessing', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal']);
|
||||||
$routes->post('reimbursements/mark-donation', 'View\ReimbursementController::markDonation');
|
$routes->post('reimbursements/mark-donation', 'View\ReimbursementController::markDonation', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
$routes->post('reimbursements/batch/create', 'View\ReimbursementController::createBatch');
|
$routes->post('reimbursements/batch/create', 'View\ReimbursementController::createBatch', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal,create']);
|
||||||
$routes->post('reimbursements/batch/update', 'View\ReimbursementController::updateBatchAssignment');
|
$routes->post('reimbursements/batch/update', 'View\ReimbursementController::updateBatchAssignment', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
$routes->post('reimbursements/batch/lock', 'View\ReimbursementController::lockBatch');
|
$routes->post('reimbursements/batch/lock', 'View\ReimbursementController::lockBatch', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
$routes->post('reimbursements/batch/admin-file/upload', 'View\ReimbursementController::uploadBatchAdminFile');
|
$routes->post('reimbursements/batch/admin-file/upload', 'View\ReimbursementController::uploadBatchAdminFile', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
$routes->get('reimbursements/batch/admin-file/(:segment)', 'View\ReimbursementController::serveAdminCheckFile/$1');
|
$routes->get('reimbursements/batch/admin-file/(:segment)', 'View\ReimbursementController::serveAdminCheckFile/$1', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal']);
|
||||||
$routes->get('reimbursements/batch/admin-file/(:segment)/(:segment)', 'View\ReimbursementController::serveAdminCheckFile/$1/$2');
|
$routes->get('reimbursements/batch/admin-file/(:segment)/(:segment)', 'View\ReimbursementController::serveAdminCheckFile/$1/$2', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal']);
|
||||||
$routes->post('reimbursements/batch/send', 'View\ReimbursementController::sendBatchEmail');
|
$routes->post('reimbursements/batch/send', 'View\ReimbursementController::sendBatchEmail', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
$routes->get('reimbursements/batch/export', 'View\ReimbursementController::exportBatch');
|
$routes->get('reimbursements/batch/export', 'View\ReimbursementController::exportBatch', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal']);
|
||||||
$routes->post('reimbursements/process', 'View\ReimbursementController::process');
|
$routes->post('reimbursements/process', 'View\ReimbursementController::process', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
$routes->get('reimbursements/export', 'View\ReimbursementController::export');
|
$routes->get('reimbursements/export', 'View\ReimbursementController::export', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal']);
|
||||||
$routes->get('reimbursements', 'View\ReimbursementController::index');
|
$routes->get('reimbursements', 'View\ReimbursementController::index', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal']);
|
||||||
|
|
||||||
// Health check (upload dirs + DB timezone columns)
|
// Health check (upload dirs + DB timezone columns)
|
||||||
$routes->get('admin/health', 'View\HealthController::index');
|
$routes->get('admin/health', 'View\HealthController::index');
|
||||||
@@ -710,14 +748,9 @@ $routes->get('payment_transactions/getByPayment/(:num)', 'View\PaymentTransactio
|
|||||||
$routes->get('payment_transactions/create', 'View\PaymentTransactionController::create');
|
$routes->get('payment_transactions/create', 'View\PaymentTransactionController::create');
|
||||||
$routes->post('payment_transactions/updateStatus/(:num)', 'View\PaymentTransactionController::updateStatus/$1');
|
$routes->post('payment_transactions/updateStatus/(:num)', 'View\PaymentTransactionController::updateStatus/$1');
|
||||||
|
|
||||||
// Routes for PayPal integration
|
// Routes for payment pages
|
||||||
$routes->get('payments/createPaypalPayment/(:num)', 'View\PaymentController::createPaypalPayment/$1');
|
|
||||||
$routes->get('payments/executePaypalPayment', 'View\PaymentController::executePaypalPayment');
|
|
||||||
$routes->get('payments/cancelPaypalPayment', 'View\PaymentController::cancelPaypalPayment');
|
|
||||||
|
|
||||||
$routes->get('payments/view/(:num)', 'View\PaymentController::viewPayment/$1');
|
$routes->get('payments/view/(:num)', 'View\PaymentController::viewPayment/$1');
|
||||||
$routes->post('payments/createPaypalPayment/(:num)', 'View\PaymentController::createPaypalPayment/$1');
|
$routes->get('payment/get_enrolled_students/(:num)', 'View\PaymentController::getEnrolledStudents/$1', ['filter' => 'auth:view_invoice|view_payment|view_financial_reports|administrator|administrative staff|principal']);
|
||||||
$routes->get('payment/get_enrolled_students/(:num)', 'View\PaymentController::getEnrolledStudents/$1');
|
|
||||||
|
|
||||||
// Payment Notification Management
|
// Payment Notification Management
|
||||||
$routes->get('payment/notification_management', 'View\PaymentNotificationController::index', ['filter' => 'auth:view_invoice']);
|
$routes->get('payment/notification_management', 'View\PaymentNotificationController::index', ['filter' => 'auth:view_invoice']);
|
||||||
@@ -937,15 +970,6 @@ $routes->group('inventory', ['filter' => 'csrf'], static function ($routes) {
|
|||||||
$routes->get('admin/enrollment/new-students', 'View\AdministratorController::showNewStudents', ['filter' => 'auth:view_new_students']);
|
$routes->get('admin/enrollment/new-students', 'View\AdministratorController::showNewStudents', ['filter' => 'auth:view_new_students']);
|
||||||
|
|
||||||
|
|
||||||
//Paypal transactions
|
|
||||||
$routes->post('api/paypal-webhook', 'Api\PaypalWebhook::handle');
|
|
||||||
$routes->get('administrator/paypal_transactions', 'View\PaypalTransactionsController::index');
|
|
||||||
$routes->get('administrator/paypal_transactions/export', 'View\PaypalTransactionsController::exportCsv');
|
|
||||||
$routes->get('admin/paypal-transactions', 'View\PaypalTransactionsController::index');
|
|
||||||
$routes->get('admin/paypal-transactions/export', 'View\PaypalTransactionsController::exportCsv');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//Emergency Contact
|
//Emergency Contact
|
||||||
$routes->get('administrator/emergency_contact', 'View\EmergencyContactController::index');
|
$routes->get('administrator/emergency_contact', 'View\EmergencyContactController::index');
|
||||||
$routes->get('administrator/emergency_contact/edit/(:num)', 'View\EmergencyContactController::edit/$1');
|
$routes->get('administrator/emergency_contact/edit/(:num)', 'View\EmergencyContactController::edit/$1');
|
||||||
@@ -1024,16 +1048,16 @@ $routes->get('family', 'View\FamilyAdminController::index');
|
|||||||
//////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////
|
||||||
|
|
||||||
//upload files
|
//upload files
|
||||||
$routes->get('receipts/(:any)', 'View\FilesController::receipt/$1');
|
$routes->get('receipts/(:any)', 'View\FilesController::receipt/$1', ['filter' => 'auth']);
|
||||||
$routes->get('reimbreceipts/(:any)', 'View\FilesController::reimb/$1'); // serves from writable/uploads/reimbursements
|
$routes->get('reimbreceipts/(:any)', 'View\FilesController::reimb/$1', ['filter' => 'auth']); // serves from writable/uploads/reimbursements
|
||||||
$routes->get('early-dismissal-signatures/(:any)', 'View\FilesController::earlyDismissalSignature/$1');
|
$routes->get('early-dismissal-signatures/(:any)', 'View\FilesController::earlyDismissalSignature/$1');
|
||||||
// Expenses
|
// Expenses
|
||||||
$routes->get('expenses/edit/(:num)', 'View\ExpenseController::edit/$1');
|
$routes->get('expenses/edit/(:num)', 'View\ExpenseController::edit/$1', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal']);
|
||||||
$routes->post('expenses/update/(:num)', 'View\ExpenseController::update/$1');
|
$routes->post('expenses/update/(:num)', 'View\ExpenseController::update/$1', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
|
|
||||||
// Reimbursements
|
// Reimbursements
|
||||||
$routes->get('reimbursements/edit/(:num)', 'View\ReimbursementController::edit/$1');
|
$routes->get('reimbursements/edit/(:num)', 'View\ReimbursementController::edit/$1', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal']);
|
||||||
$routes->post('reimbursements/update/(:num)', 'View\ReimbursementController::update/$1');
|
$routes->post('reimbursements/update/(:num)', 'View\ReimbursementController::update/$1', ['filter' => 'auth:view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1178,16 +1202,15 @@ $routes->get('/terms_of_service', 'View\PageController::termsOfService');
|
|||||||
$routes->get('/help_center', 'View\PageController::helpCenter');
|
$routes->get('/help_center', 'View\PageController::helpCenter');
|
||||||
|
|
||||||
//payment
|
//payment
|
||||||
$routes->get('/payment', 'View\PaymentController::redirectPage');
|
$routes->get('/payment', 'View\PaymentController::redirectPage', ['filter' => 'auth:parent']);
|
||||||
$routes->get('/payment/paypal', 'View\PaymentController::paypal');
|
|
||||||
$routes->get('/payment/manual', 'View\PaymentController::manual');
|
$routes->get('/payment/manual', 'View\PaymentController::manual');
|
||||||
$routes->post('/payment/manual', 'View\PaymentController::manual');
|
$routes->post('/payment/manual', 'View\PaymentController::manual');
|
||||||
|
|
||||||
// Voucher management
|
// Voucher management
|
||||||
$routes->get('discounts/list', 'View\DiscountController::listVouchers');
|
$routes->get('discounts/list', 'View\DiscountController::listVouchers', ['filter' => 'auth:view_invoice|view_financial_reports|administrator|administrative staff|principal']);
|
||||||
$routes->match(['get', 'post'], 'discount/create', 'View\DiscountController::createVoucher');
|
$routes->match(['get', 'post'], 'discount/create', 'View\DiscountController::createVoucher', ['filter' => 'auth:create_invoice|view_financial_reports|administrator|administrative staff|principal,create']);
|
||||||
$routes->match(['get', 'post'], 'discount/editVoucher/(:num)', 'View\DiscountController::editVoucher/$1');
|
$routes->match(['get', 'post'], 'discount/editVoucher/(:num)', 'View\DiscountController::editVoucher/$1', ['filter' => 'auth:update_invoice|view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
$routes->match(['get', 'post'], 'discount/apply', 'View\DiscountController::applyVoucher');
|
$routes->match(['get', 'post'], 'discount/apply', 'View\DiscountController::applyVoucher', ['filter' => 'auth:update_invoice|view_financial_reports|administrator|administrative staff|principal,update']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1531,11 +1554,6 @@ $routes->group('api/v1', ['filter' => 'apiAuth', 'namespace' => 'App\Controllers
|
|||||||
$routes->get('class-preparation/(:num)', 'View\SupportController::show/$1');
|
$routes->get('class-preparation/(:num)', 'View\SupportController::show/$1');
|
||||||
$routes->post('class-preparation/(:num)/mark-printed', 'View\SupportController::markPrinted/$1');
|
$routes->post('class-preparation/(:num)/mark-printed', 'View\SupportController::markPrinted/$1');
|
||||||
|
|
||||||
// PayPal Transactions
|
|
||||||
$routes->get('paypal-transactions', 'View\UserController::index');
|
|
||||||
$routes->get('paypal-transactions/(:num)', 'View\SupportController::show/$1');
|
|
||||||
$routes->get('paypal-transactions/transaction/(:segment)', 'View\SupportController::getByTransactionId/$1');
|
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
$routes->get('stats', 'View\UserController::index');
|
$routes->get('stats', 'View\UserController::index');
|
||||||
|
|
||||||
|
|||||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+1
-1
@@ -36,7 +36,7 @@ class Services extends BaseService
|
|||||||
/**
|
/**
|
||||||
* Override CI Email service to enforce a global Reply-To.
|
* Override CI Email service to enforce a global Reply-To.
|
||||||
*/
|
*/
|
||||||
public static function email(array $config = null, bool $getShared = true)
|
public static function email(?array $config = null, bool $getShared = true)
|
||||||
{
|
{
|
||||||
if ($getShared) {
|
if ($getShared) {
|
||||||
return static::getSharedInstance('email', $config);
|
return static::getSharedInstance('email', $config);
|
||||||
|
|||||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+49
-29
@@ -5,7 +5,6 @@ namespace App\Controllers;
|
|||||||
use App\Models\LoginActivityModel;
|
use App\Models\LoginActivityModel;
|
||||||
use App\Models\UserModel;
|
use App\Models\UserModel;
|
||||||
use App\Models\UserRoleModel;
|
use App\Models\UserRoleModel;
|
||||||
use CodeIgniter\Controller;
|
|
||||||
use CodeIgniter\Events\Events;
|
use CodeIgniter\Events\Events;
|
||||||
use App\Models\IpAttemptModel;
|
use App\Models\IpAttemptModel;
|
||||||
use App\Models\PasswordResetModel;
|
use App\Models\PasswordResetModel;
|
||||||
@@ -19,7 +18,7 @@ require_once APPPATH . 'Helpers/pbkdf2_helper.php';
|
|||||||
require_once APPPATH . 'Helpers/jwt_helper.php';
|
require_once APPPATH . 'Helpers/jwt_helper.php';
|
||||||
|
|
||||||
|
|
||||||
class AuthController extends Controller
|
class AuthController extends BaseController
|
||||||
{
|
{
|
||||||
protected $configModel;
|
protected $configModel;
|
||||||
protected $userModel;
|
protected $userModel;
|
||||||
@@ -83,9 +82,23 @@ class AuthController extends Controller
|
|||||||
log_message('info', 'Processing login form submission.');
|
log_message('info', 'Processing login form submission.');
|
||||||
$redirectTo = $this->sanitizeRedirectTarget((string) ($this->request->getPost('redirect_to') ?? $this->request->getGet('redirect_to') ?? ''));
|
$redirectTo = $this->sanitizeRedirectTarget((string) ($this->request->getPost('redirect_to') ?? $this->request->getGet('redirect_to') ?? ''));
|
||||||
|
|
||||||
|
$validator = \Config\Services::validation();
|
||||||
|
$requestData = sanitize_request_value($this->request->getPost(['email', 'password']));
|
||||||
|
$validator->setRules([
|
||||||
|
'email' => 'required|valid_email|max_length[254]',
|
||||||
|
'password' => 'required|max_length[255]',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! $validator->run($requestData)) {
|
||||||
|
return redirect()
|
||||||
|
->back()
|
||||||
|
->with('error', 'Please enter a valid email and password.')
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
// Step 1: Get email, password, and IP from the request
|
// Step 1: Get email, password, and IP from the request
|
||||||
$email = $this->request->getPost('email');
|
$email = strtolower((string) $requestData['email']);
|
||||||
$password = $this->request->getPost('password');
|
$password = (string) $requestData['password'];
|
||||||
$ip = $this->request->getIPAddress();
|
$ip = $this->request->getIPAddress();
|
||||||
|
|
||||||
log_message('info', 'Login attempt from IP: ' . $ip . ' for email: ' . $email);
|
log_message('info', 'Login attempt from IP: ' . $ip . ' for email: ' . $email);
|
||||||
@@ -139,26 +152,29 @@ class AuthController extends Controller
|
|||||||
// JSON API: POST /api/login
|
// JSON API: POST /api/login
|
||||||
public function apiLogin()
|
public function apiLogin()
|
||||||
{
|
{
|
||||||
$requestData = $this->request->getJSON(true);
|
$requestData = sanitize_request_value($this->request->getJSON(true) ?: [
|
||||||
if (!$requestData) {
|
'email' => $this->request->getPost('email'),
|
||||||
// fallback to form vars
|
'password' => $this->request->getPost('password'),
|
||||||
$requestData = [
|
]);
|
||||||
'email' => $this->request->getPost('email'),
|
|
||||||
'password' => $this->request->getPost('password'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$email = $requestData['email'] ?? '';
|
$validation = \Config\Services::validation();
|
||||||
$password = $requestData['password'] ?? '';
|
$validation->setRules([
|
||||||
$ip = $this->request->getIPAddress();
|
'email' => 'required|valid_email|max_length[254]',
|
||||||
|
'password' => 'required|max_length[255]',
|
||||||
|
]);
|
||||||
|
|
||||||
if (!$email || !$password) {
|
if (! $validation->run($requestData)) {
|
||||||
return $this->response->setStatusCode(400)->setJSON([
|
return $this->response->setStatusCode(422)->setJSON([
|
||||||
'status' => false,
|
'status' => false,
|
||||||
'message' => 'Email and password are required.'
|
'message' => 'Validation failed.',
|
||||||
|
'errors' => $validation->getErrors(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$email = strtolower((string) ($requestData['email'] ?? ''));
|
||||||
|
$password = (string) ($requestData['password'] ?? '');
|
||||||
|
$ip = $this->request->getIPAddress();
|
||||||
|
|
||||||
if ($this->isIpBlocked($ip)) {
|
if ($this->isIpBlocked($ip)) {
|
||||||
return $this->response->setStatusCode(429)->setJSON([
|
return $this->response->setStatusCode(429)->setJSON([
|
||||||
'status' => false,
|
'status' => false,
|
||||||
@@ -220,7 +236,15 @@ class AuthController extends Controller
|
|||||||
'exp' => $exp,
|
'exp' => $exp,
|
||||||
];
|
];
|
||||||
|
|
||||||
$secret = env('JWT_SECRET', 'change-me-in-env');
|
try {
|
||||||
|
$secret = require_env('JWT_SECRET');
|
||||||
|
} catch (\RuntimeException $e) {
|
||||||
|
return $this->response->setStatusCode(500)->setJSON([
|
||||||
|
'status' => false,
|
||||||
|
'message' => 'JWT configuration is missing.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$token = jwt_encode($payload, $secret, 'HS256');
|
$token = jwt_encode($payload, $secret, 'HS256');
|
||||||
|
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
@@ -240,18 +264,14 @@ class AuthController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function apiRegister()
|
public function apiRegister()
|
||||||
{
|
{
|
||||||
$requestData = $this->request->getJSON(true);
|
$requestData = sanitize_request_value($this->request->getJSON(true) ?: $this->request->getPost());
|
||||||
if (!$requestData) {
|
|
||||||
// fallback to form vars
|
|
||||||
$requestData = $this->request->getPost();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic validation
|
// Basic validation
|
||||||
$rules = [
|
$rules = [
|
||||||
'firstname' => 'required|min_length[2]|max_length[30]',
|
'firstname' => "required|regex_match[/^[\\p{L}\\s'\\-]+$/u]|min_length[2]|max_length[30]",
|
||||||
'lastname' => 'required|min_length[2]|max_length[30]',
|
'lastname' => "required|regex_match[/^[\\p{L}\\s'\\-]+$/u]|min_length[2]|max_length[30]",
|
||||||
'email' => 'required|valid_email|is_unique[users.email]',
|
'email' => 'required|valid_email|max_length[254]|is_unique[users.email]',
|
||||||
'password' => 'required|min_length[8]',
|
'password' => 'required|min_length[8]|max_length[255]',
|
||||||
'cellphone' => 'required|min_length[10]|max_length[20]',
|
'cellphone' => 'required|min_length[10]|max_length[20]',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -332,7 +352,7 @@ class AuthController extends Controller
|
|||||||
'exp' => $exp,
|
'exp' => $exp,
|
||||||
];
|
];
|
||||||
|
|
||||||
$secret = env('JWT_SECRET', 'change-me-in-env');
|
$secret = require_env('JWT_SECRET');
|
||||||
$token = jwt_encode($payload, $secret, 'HS256');
|
$token = jwt_encode($payload, $secret, 'HS256');
|
||||||
|
|
||||||
return $this->response->setStatusCode(201)->setJSON([
|
return $this->response->setStatusCode(201)->setJSON([
|
||||||
|
|||||||
Executable → Regular
+17
-2
@@ -36,7 +36,7 @@ abstract class BaseController extends Controller
|
|||||||
*
|
*
|
||||||
* @var list<string>
|
* @var list<string>
|
||||||
*/
|
*/
|
||||||
protected $helpers = [];
|
protected $helpers = ['security'];
|
||||||
|
|
||||||
/** @var ApiClient */
|
/** @var ApiClient */
|
||||||
protected ApiClient $api;
|
protected ApiClient $api;
|
||||||
@@ -73,6 +73,21 @@ abstract class BaseController extends Controller
|
|||||||
// Assuming the user role is stored in the session
|
// Assuming the user role is stored in the session
|
||||||
return session()->get('role');
|
return session()->get('role');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function validated(array $rules, ?array $data = null): array
|
||||||
|
{
|
||||||
|
$validation = service('validation');
|
||||||
|
$payload = $data ?? ($this->request->getJSON(true) ?: $this->request->getPost());
|
||||||
|
$payload = sanitize_request_value($payload);
|
||||||
|
|
||||||
|
$validation->setRules($rules);
|
||||||
|
|
||||||
|
if (! $validation->run($payload)) {
|
||||||
|
throw new \InvalidArgumentException(json_encode($validation->getErrors(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: 'Validation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|||||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+36
-9
@@ -10,8 +10,13 @@ class ProofreadController extends ResourceController
|
|||||||
{
|
{
|
||||||
// Basic per-IP throttling: 10 requests per minute
|
// Basic per-IP throttling: 10 requests per minute
|
||||||
$throttler = service('throttler');
|
$throttler = service('throttler');
|
||||||
$key = 'proofread-' . $this->request->getIPAddress();
|
|
||||||
if (!$throttler->check($key, 10, MINUTE)) {
|
// Cache/throttler keys cannot contain reserved characters.
|
||||||
|
// Raw IPv6 addresses contain ":", so hash the IP first.
|
||||||
|
$ip = $this->request->getIPAddress();
|
||||||
|
$key = 'proofread-' . hash('sha256', $ip);
|
||||||
|
|
||||||
|
if (! $throttler->check($key, 10, MINUTE)) {
|
||||||
return $this->respond([
|
return $this->respond([
|
||||||
'ok' => false,
|
'ok' => false,
|
||||||
'error' => 'Too many requests. Try again in a minute.',
|
'error' => 'Too many requests. Try again in a minute.',
|
||||||
@@ -20,29 +25,51 @@ class ProofreadController extends ResourceController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Accept form-urlencoded payload to play nicely with CSRF protection
|
// Accept form-urlencoded payload to play nicely with CSRF protection
|
||||||
$text = (string) ($this->request->getPost('text') ?? '');
|
$payload = sanitize_request_value($this->request->getPost(['text']));
|
||||||
if ($text === '' || mb_strlen($text) > 20000) {
|
|
||||||
|
$validation = service('validation');
|
||||||
|
$validation->setRules([
|
||||||
|
'text' => 'required|min_length[1]|max_length[20000]',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! $validation->run($payload)) {
|
||||||
return $this->respond([
|
return $this->respond([
|
||||||
'ok' => false,
|
'ok' => false,
|
||||||
'error' => 'Invalid text (empty or too long).',
|
'error' => 'Invalid text payload.',
|
||||||
'csrfHash' => csrf_hash(),
|
'csrfHash' => csrf_hash(),
|
||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
$client = \Config\Services::curlrequest(['timeout' => 10]);
|
$text = (string) $payload['text'];
|
||||||
|
|
||||||
|
$client = \Config\Services::curlrequest([
|
||||||
|
'timeout' => 10,
|
||||||
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$resp = $client->post('https://api.languagetool.org/v2/check', [
|
$resp = $client->post('https://api.languagetool.org/v2/check', [
|
||||||
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/x-www-form-urlencoded',
|
||||||
|
],
|
||||||
'form_params' => [
|
'form_params' => [
|
||||||
'text' => $text,
|
'text' => $text,
|
||||||
'language' => 'en-US',
|
'language' => 'en-US',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$result = json_decode($resp->getBody(), true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
return $this->respond([
|
||||||
|
'ok' => false,
|
||||||
|
'error' => 'Invalid response from proofread service.',
|
||||||
|
'csrfHash' => csrf_hash(),
|
||||||
|
], 502);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->respond([
|
return $this->respond([
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'result' => json_decode($resp->getBody(), true),
|
'result' => $result,
|
||||||
'csrfHash' => csrf_hash(),
|
'csrfHash' => csrf_hash(),
|
||||||
]);
|
]);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
@@ -53,4 +80,4 @@ class ProofreadController extends ResourceController
|
|||||||
], 502);
|
], 502);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
@@ -0,0 +1,664 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers\View;
|
||||||
|
|
||||||
|
use App\Controllers\BaseController;
|
||||||
|
use App\Models\ClassSectionModel;
|
||||||
|
use App\Models\ConfigurationModel;
|
||||||
|
use App\Models\CertificateRecordModel;
|
||||||
|
|
||||||
|
class CertificateController extends BaseController
|
||||||
|
{
|
||||||
|
protected $classSectionModel;
|
||||||
|
protected $configModel;
|
||||||
|
protected $certRecordModel;
|
||||||
|
protected $schoolYear;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->classSectionModel = new ClassSectionModel();
|
||||||
|
$this->configModel = new ConfigurationModel();
|
||||||
|
$this->certRecordModel = new CertificateRecordModel();
|
||||||
|
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Admin UI ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$selectedCsid = $this->request->getGet('class_section_id');
|
||||||
|
$schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear;
|
||||||
|
|
||||||
|
// ── All enrolled students across every class section ───────────────────
|
||||||
|
$allEnrolled = $db->table('student_class sc')
|
||||||
|
->select('s.id AS student_id, s.firstname, s.lastname, sc.class_section_id, cs.class_section_name')
|
||||||
|
->join('students s', 's.id = sc.student_id')
|
||||||
|
->join('classSection cs', 'cs.class_section_id = sc.class_section_id')
|
||||||
|
->where('s.is_active', 1)
|
||||||
|
->where('sc.school_year', $schoolYear)
|
||||||
|
->orderBy('s.firstname', 'ASC')
|
||||||
|
->orderBy('s.lastname', 'ASC')
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
|
$allIds = array_values(array_unique(array_map('intval', array_column($allEnrolled, 'student_id'))));
|
||||||
|
|
||||||
|
// ── Saved generated YEAR decisions from student_decisions ──────────────
|
||||||
|
//
|
||||||
|
// Source of truth:
|
||||||
|
// student_decisions.year_score
|
||||||
|
// student_decisions.decision
|
||||||
|
//
|
||||||
|
// Do NOT recalculate certificate decisions here from semester_scores.
|
||||||
|
$decisionsByStudent = [];
|
||||||
|
|
||||||
|
if (!empty($allIds)) {
|
||||||
|
$decisionRows = $db->table('student_decisions')
|
||||||
|
->whereIn('student_id', $allIds)
|
||||||
|
->where('school_year', $schoolYear)
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
|
foreach ($decisionRows as $d) {
|
||||||
|
$sid = (int)($d['student_id'] ?? 0);
|
||||||
|
|
||||||
|
if ($sid <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decision = trim((string)($d['decision'] ?? ''));
|
||||||
|
$source = trim((string)($d['source'] ?? ''));
|
||||||
|
|
||||||
|
if ($source === '') {
|
||||||
|
$source = $decision === '' ? 'pending' : 'manual';
|
||||||
|
}
|
||||||
|
|
||||||
|
$decisionsByStudent[$sid]['Year'] = [
|
||||||
|
'decision' => $decision,
|
||||||
|
'source' => $source,
|
||||||
|
'notes' => (string)($d['notes'] ?? ''),
|
||||||
|
'year_score' => $d['year_score'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Certificate records: most recent per student ───────────────────────
|
||||||
|
$certsByStudent = [];
|
||||||
|
|
||||||
|
if (!empty($allIds)) {
|
||||||
|
foreach ($db->table('certificate_records')
|
||||||
|
->select('student_id, certificate_number, issued_at')
|
||||||
|
->whereIn('student_id', $allIds)
|
||||||
|
->where('school_year', $schoolYear)
|
||||||
|
->orderBy('issued_at', 'DESC')
|
||||||
|
->get()
|
||||||
|
->getResultArray() as $c) {
|
||||||
|
$sid = (int)$c['student_id'];
|
||||||
|
|
||||||
|
if (!isset($certsByStudent[$sid])) {
|
||||||
|
$certsByStudent[$sid] = $c['certificate_number'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build per-class buckets and stats ──────────────────────────────────
|
||||||
|
$studentsByClass = [];
|
||||||
|
$statsPerClass = [];
|
||||||
|
|
||||||
|
foreach ($allEnrolled as $row) {
|
||||||
|
$sid = (int)$row['student_id'];
|
||||||
|
$csid = (int)$row['class_section_id'];
|
||||||
|
|
||||||
|
if (!isset($statsPerClass[$csid])) {
|
||||||
|
$statsPerClass[$csid] = [
|
||||||
|
'name' => $row['class_section_name'],
|
||||||
|
'total' => 0,
|
||||||
|
'pass' => 0,
|
||||||
|
'cert' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$statsPerClass[$csid]['total']++;
|
||||||
|
|
||||||
|
// If no generated decision exists yet, explicitly mark pending.
|
||||||
|
if (!isset($decisionsByStudent[$sid])) {
|
||||||
|
$decisionsByStudent[$sid]['Decision'] = [
|
||||||
|
'decision' => '',
|
||||||
|
'source' => 'pending',
|
||||||
|
'notes' => '',
|
||||||
|
'year_score' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Certificate eligibility:
|
||||||
|
// A student is eligible only if the saved final generated decision is Pass.
|
||||||
|
$studentDecisions = $decisionsByStudent[$sid] ?? [];
|
||||||
|
$isPass = !empty($studentDecisions);
|
||||||
|
|
||||||
|
foreach ($studentDecisions as $decisionRow) {
|
||||||
|
$decision = trim((string)($decisionRow['decision'] ?? ''));
|
||||||
|
|
||||||
|
if (strcasecmp($decision, 'Pass') !== 0) {
|
||||||
|
$isPass = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isPass) {
|
||||||
|
$statsPerClass[$csid]['pass']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($certsByStudent[$sid])) {
|
||||||
|
$statsPerClass[$csid]['cert']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$studentsByClass[$csid][] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Determine default active tab ───────────────────────────────────────
|
||||||
|
$firstCsid = !empty($allEnrolled) ? (int)$allEnrolled[0]['class_section_id'] : null;
|
||||||
|
|
||||||
|
if ($selectedCsid === null && $firstCsid !== null) {
|
||||||
|
$selectedCsid = (string)$firstCsid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('admin/certificates/index', [
|
||||||
|
'studentsByClass' => $studentsByClass,
|
||||||
|
'selectedClassId' => $selectedCsid,
|
||||||
|
'schoolYear' => $schoolYear,
|
||||||
|
'certDate' => date('m/d/Y'),
|
||||||
|
'decisionsByStudent' => $decisionsByStudent,
|
||||||
|
'certsByStudent' => $certsByStudent,
|
||||||
|
'statsPerClass' => $statsPerClass,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function auditLog()
|
||||||
|
{
|
||||||
|
$schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear;
|
||||||
|
$records = $this->certRecordModel->getAuditLog($schoolYear);
|
||||||
|
$yearSummary = $this->certRecordModel->yearSummary();
|
||||||
|
|
||||||
|
return view('admin/certificates/audit_log', [
|
||||||
|
'records' => $records,
|
||||||
|
'schoolYear' => $schoolYear,
|
||||||
|
'yearSummary' => $yearSummary,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function csrfToken()
|
||||||
|
{
|
||||||
|
return $this->response
|
||||||
|
->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||||
|
->setHeader('Pragma', 'no-cache')
|
||||||
|
->setJSON([
|
||||||
|
'csrf_token' => csrf_token(),
|
||||||
|
'csrf_hash' => csrf_hash(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Public verification page ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function verify(string $certNumber)
|
||||||
|
{
|
||||||
|
$record = \Config\Database::connect()
|
||||||
|
->table('certificate_records cr')
|
||||||
|
->select('cr.*, u.firstname AS admin_firstname, u.lastname AS admin_lastname')
|
||||||
|
->join('users u', 'u.id = cr.issued_by', 'left')
|
||||||
|
->groupStart()
|
||||||
|
->where('cr.verification_token', $certNumber)
|
||||||
|
->orWhere('cr.certificate_number', strtoupper($certNumber))
|
||||||
|
->groupEnd()
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
return view('certificates/verify', [
|
||||||
|
'record' => $record ?: null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Reprint an existing certificate ──────────────────────────────────────
|
||||||
|
|
||||||
|
public function reprint(string $certNumber)
|
||||||
|
{
|
||||||
|
$record = \Config\Database::connect()
|
||||||
|
->table('certificate_records')
|
||||||
|
->where('certificate_number', strtoupper($certNumber))
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
if (!$record) {
|
||||||
|
return redirect()->to('administrator/certificates/log')
|
||||||
|
->with('error', 'Certificate not found: ' . $certNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
$certDateFormatted = '';
|
||||||
|
|
||||||
|
if (!empty($record['cert_date'])) {
|
||||||
|
$ts = strtotime((string)$record['cert_date']);
|
||||||
|
|
||||||
|
if ($ts) {
|
||||||
|
$certDateFormatted = date('m/d/Y', $ts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$student = [
|
||||||
|
'firstname' => (string)($record['student_name'] ?? ''),
|
||||||
|
'lastname' => '',
|
||||||
|
'grade' => (string)($record['grade'] ?? ''),
|
||||||
|
'cert_number' => (string)($record['certificate_number'] ?? ''),
|
||||||
|
'verify_token' => $this->ensureVerificationTokenForRecord($record),
|
||||||
|
];
|
||||||
|
|
||||||
|
// student_name is stored as "Firstname Lastname" — split for the PDF builder.
|
||||||
|
$parts = explode(' ', trim($student['firstname']), 2);
|
||||||
|
|
||||||
|
if (count($parts) === 2) {
|
||||||
|
$student['firstname'] = $parts[0];
|
||||||
|
$student['lastname'] = $parts[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdfData = $this->buildPdf([$student], $certDateFormatted ?: date('m/d/Y'));
|
||||||
|
$filename = 'Certificate_' . strtoupper($certNumber) . '.pdf';
|
||||||
|
|
||||||
|
return $this->response
|
||||||
|
->setHeader('Content-Type', 'application/pdf')
|
||||||
|
->setHeader('Content-Disposition', 'inline; filename="' . $filename . '"')
|
||||||
|
->setBody($pdfData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PDF generation ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function generate()
|
||||||
|
{
|
||||||
|
$studentIds = $this->request->getPost('student_ids') ?? [];
|
||||||
|
$certDate = trim($this->request->getPost('cert_date') ?? date('m/d/Y'));
|
||||||
|
$classSectionId = $this->request->getPost('class_section_id');
|
||||||
|
$schoolYear = $this->request->getPost('school_year') ?? $this->schoolYear;
|
||||||
|
|
||||||
|
if (empty($studentIds)) {
|
||||||
|
if ($this->request->isAJAX()) {
|
||||||
|
return $this->response
|
||||||
|
->setStatusCode(422)
|
||||||
|
->setJSON([
|
||||||
|
'ok' => false,
|
||||||
|
'error' => 'Please select at least one student.',
|
||||||
|
'csrf_token' => csrf_token(),
|
||||||
|
'csrf_hash' => csrf_hash(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->to('administrator/certificates')
|
||||||
|
->with('error', 'Please select at least one student.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$studentIds = array_filter(array_map('intval', $studentIds));
|
||||||
|
$certDate = preg_replace('/[^0-9\/\-]/', '', $certDate);
|
||||||
|
|
||||||
|
if (empty($studentIds)) {
|
||||||
|
if ($this->request->isAJAX()) {
|
||||||
|
return $this->response
|
||||||
|
->setStatusCode(422)
|
||||||
|
->setJSON([
|
||||||
|
'ok' => false,
|
||||||
|
'error' => 'Invalid student selection.',
|
||||||
|
'csrf_token' => csrf_token(),
|
||||||
|
'csrf_hash' => csrf_hash(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->to('administrator/certificates')
|
||||||
|
->with('error', 'Invalid student selection.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$students = [];
|
||||||
|
|
||||||
|
foreach ($studentIds as $id) {
|
||||||
|
$row = null;
|
||||||
|
|
||||||
|
if ($classSectionId) {
|
||||||
|
$row = $db->table('student_class sc')
|
||||||
|
->select('s.id, s.firstname, s.lastname, cs.class_section_name AS grade')
|
||||||
|
->join('students s', 's.id = sc.student_id')
|
||||||
|
->join('classSection cs', 'cs.class_section_id = sc.class_section_id')
|
||||||
|
->where('sc.class_section_id', (int)$classSectionId)
|
||||||
|
->where('sc.school_year', $schoolYear)
|
||||||
|
->where('s.id', $id)
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$row) {
|
||||||
|
$s = $db->table('students')
|
||||||
|
->select('id, firstname, lastname, registration_grade AS grade')
|
||||||
|
->where('id', $id)
|
||||||
|
->get()
|
||||||
|
->getRowArray();
|
||||||
|
|
||||||
|
$row = $s ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($row) {
|
||||||
|
$students[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($students)) {
|
||||||
|
if ($this->request->isAJAX()) {
|
||||||
|
return $this->response
|
||||||
|
->setStatusCode(422)
|
||||||
|
->setJSON([
|
||||||
|
'ok' => false,
|
||||||
|
'error' => 'No valid students found.',
|
||||||
|
'csrf_token' => csrf_token(),
|
||||||
|
'csrf_hash' => csrf_hash(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->to('administrator/certificates')
|
||||||
|
->with('error', 'No valid students found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$issuedAt = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
// Load existing certificates for these students this school year.
|
||||||
|
$existingCerts = $db->table('certificate_records')
|
||||||
|
->select('id, student_id, certificate_number, verification_token')
|
||||||
|
->whereIn('student_id', array_column($students, 'id'))
|
||||||
|
->where('school_year', $schoolYear)
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
|
$existingCertMap = [];
|
||||||
|
|
||||||
|
foreach ($existingCerts as $ec) {
|
||||||
|
$existingCertMap[(int)$ec['student_id']] = $ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($students as &$student) {
|
||||||
|
$sid = (int)$student['id'];
|
||||||
|
|
||||||
|
if (isset($existingCertMap[$sid])) {
|
||||||
|
// Reuse existing certificate number — do not create a new record.
|
||||||
|
$existing = $existingCertMap[$sid];
|
||||||
|
|
||||||
|
$student['cert_number'] = (string)($existing['certificate_number'] ?? '');
|
||||||
|
$student['verify_token'] = $this->ensureVerificationTokenForRecord($existing);
|
||||||
|
} else {
|
||||||
|
$certNumber = $this->certRecordModel->nextNumber($schoolYear);
|
||||||
|
$verifyToken = $this->certRecordModel->generateVerificationToken();
|
||||||
|
|
||||||
|
$this->certRecordModel->insert([
|
||||||
|
'certificate_number' => $certNumber,
|
||||||
|
'verification_token' => $verifyToken,
|
||||||
|
'student_id' => $sid,
|
||||||
|
'student_name' => $student['firstname'] . ' ' . $student['lastname'],
|
||||||
|
'grade' => $this->formatGrade($student['grade'] ?? ''),
|
||||||
|
'school_year' => $schoolYear,
|
||||||
|
'class_section_id' => $classSectionId ?: null,
|
||||||
|
'issued_at' => $issuedAt,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$student['cert_number'] = $certNumber;
|
||||||
|
$student['verify_token'] = $verifyToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($student);
|
||||||
|
|
||||||
|
$pdfData = $this->buildPdf($students, $certDate);
|
||||||
|
$filename = 'Certificates_' . date('Ymd_His') . '.pdf';
|
||||||
|
|
||||||
|
return $this->response
|
||||||
|
->setHeader('Content-Type', 'application/pdf')
|
||||||
|
->setHeader('Content-Disposition', 'inline; filename="' . $filename . '"')
|
||||||
|
->setHeader('X-CSRF-TOKEN-NAME', csrf_token())
|
||||||
|
->setHeader('X-CSRF-TOKEN', csrf_hash())
|
||||||
|
->setBody($pdfData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function parseCertDate(string $certDate): ?string
|
||||||
|
{
|
||||||
|
if (preg_match('#^(\d{2})/(\d{2})/(\d{4})$#', $certDate, $m)) {
|
||||||
|
return $m[3] . '-' . $m[1] . '-' . $m[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('#^\d{4}-\d{2}-\d{2}$#', $certDate)) {
|
||||||
|
return $certDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatGrade(string $raw): string
|
||||||
|
{
|
||||||
|
$clean = trim($raw);
|
||||||
|
$lower = strtolower($clean);
|
||||||
|
|
||||||
|
// Strip section suffix: "Grade 1-A" → "Grade 1", "Grade 2-B" → "Grade 2".
|
||||||
|
if (preg_match('/^grade\s*(\d+)/i', $clean, $m)) {
|
||||||
|
return 'Grade ' . (int)$m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($lower === 'youth') {
|
||||||
|
return 'Youth';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($lower === 'kg' || $lower === 'kindergarten') {
|
||||||
|
return 'Kindergarten';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw number or number-section: "1", "1-A", "2-B" → keep number only.
|
||||||
|
if (preg_match('/^(\d+)([- ][A-Za-z0-9]+)?$/', $clean, $m)) {
|
||||||
|
return 'Grade ' . (int)$m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureVerificationTokenForRecord(array $record): string
|
||||||
|
{
|
||||||
|
$token = trim((string)($record['verification_token'] ?? ''));
|
||||||
|
|
||||||
|
if ($token !== '') {
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
$recordId = (int)($record['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($recordId <= 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = $this->certRecordModel->generateVerificationToken();
|
||||||
|
|
||||||
|
$this->certRecordModel->update($recordId, [
|
||||||
|
'verification_token' => $token,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PDF rendering ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private function buildPdf(array $students, string $certDate): string
|
||||||
|
{
|
||||||
|
$fontDir = FCPATH . 'assets' . DIRECTORY_SEPARATOR . 'certificates' . DIRECTORY_SEPARATOR . 'fonts' . DIRECTORY_SEPARATOR;
|
||||||
|
$imgDir = FCPATH . 'assets' . DIRECTORY_SEPARATOR . 'certificates' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR;
|
||||||
|
|
||||||
|
$edwardianFont = \TCPDF_FONTS::addTTFfont($fontDir . 'Edwardian Script ITC Regular.ttf', 'TrueTypeUnicode', '', 32);
|
||||||
|
$garamondBold = \TCPDF_FONTS::addTTFfont($fontDir . 'Garamond Bold.ttf', 'TrueTypeUnicode', '', 32);
|
||||||
|
$ebGaramond = \TCPDF_FONTS::addTTFfont($fontDir . 'EBGaramond-Regular.ttf', 'TrueTypeUnicode', '', 32);
|
||||||
|
|
||||||
|
$pdf = new \TCPDF('L', 'pt', 'A4', true, 'UTF-8', false);
|
||||||
|
|
||||||
|
$pdf->SetCreator('Al Rahma Sunday School');
|
||||||
|
$pdf->SetTitle('Student Certificates');
|
||||||
|
$pdf->SetMargins(0, 0, 0, true);
|
||||||
|
$pdf->SetAutoPageBreak(false, 0);
|
||||||
|
$pdf->setPrintHeader(false);
|
||||||
|
$pdf->setPrintFooter(false);
|
||||||
|
$pdf->SetHeaderMargin(0);
|
||||||
|
$pdf->SetFooterMargin(0);
|
||||||
|
|
||||||
|
$W = $pdf->getPageWidth();
|
||||||
|
$H = $pdf->getPageHeight();
|
||||||
|
|
||||||
|
foreach ($students as $student) {
|
||||||
|
$pdf->AddPage();
|
||||||
|
|
||||||
|
$name = trim((string)($student['firstname'] ?? '') . ' ' . (string)($student['lastname'] ?? ''));
|
||||||
|
$grade = $this->formatGrade($student['grade'] ?? '');
|
||||||
|
$certNumber = $student['cert_number'] ?? '';
|
||||||
|
$verifyToken = $student['verify_token'] ?? '';
|
||||||
|
|
||||||
|
$verifyUrl = $verifyToken !== ''
|
||||||
|
? site_url('verify/' . rawurlencode($verifyToken))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
$this->drawCertificate(
|
||||||
|
$pdf,
|
||||||
|
$W,
|
||||||
|
$H,
|
||||||
|
$name,
|
||||||
|
$grade,
|
||||||
|
$certDate,
|
||||||
|
$certNumber,
|
||||||
|
$verifyUrl,
|
||||||
|
$imgDir,
|
||||||
|
$edwardianFont,
|
||||||
|
$garamondBold,
|
||||||
|
$ebGaramond
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pdf->Output('', 'S');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coordinate mapping: iTextSharp origin bottom-left → TCPDF origin top-left.
|
||||||
|
* y_tcpdf ≈ H − y_iTextSharp
|
||||||
|
*/
|
||||||
|
private function drawCertificate(
|
||||||
|
\TCPDF $pdf,
|
||||||
|
float $W,
|
||||||
|
float $H,
|
||||||
|
string $name,
|
||||||
|
string $grade,
|
||||||
|
string $certDate,
|
||||||
|
string $certNumber,
|
||||||
|
?string $verifyUrl,
|
||||||
|
string $imgDir,
|
||||||
|
string $edwardianFont,
|
||||||
|
string $garamondBold,
|
||||||
|
string $ebGaramond
|
||||||
|
): void {
|
||||||
|
// ── Images
|
||||||
|
$pdf->Image($imgDir . 'title.png', 126, 0, 600);
|
||||||
|
$pdf->Image($imgDir . 'background.png', 280, 176, 291, 340);
|
||||||
|
$pdf->Image($imgDir . 'signature.png', 140, 410, 90, 80);
|
||||||
|
|
||||||
|
// ── "Presented to:"
|
||||||
|
$pdf->SetFont('times', 'B', 24);
|
||||||
|
$pdf->SetTextColor(0, 0, 0);
|
||||||
|
$pdf->SetXY(0, 151);
|
||||||
|
$pdf->Cell($W, 24, 'Presented to:', 0, 0, 'C');
|
||||||
|
|
||||||
|
// ── QR code — left-aligned with "Presented to:", vertically centred on that line.
|
||||||
|
$qrSize = 42;
|
||||||
|
|
||||||
|
if (!empty($verifyUrl)) {
|
||||||
|
$qrX = 120;
|
||||||
|
$qrY = 171 + (24 - $qrSize) / 2;
|
||||||
|
|
||||||
|
$style = [
|
||||||
|
'border' => false,
|
||||||
|
'padding' => 0,
|
||||||
|
'fgcolor' => [0, 0, 0],
|
||||||
|
'bgcolor' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
$pdf->write2DBarcode($verifyUrl, 'QRCODE,L', $qrX, $qrY, $qrSize, $qrSize, $style, 'N');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Student name — center based on actual string width.
|
||||||
|
$pdf->SetFont($edwardianFont, '', 38);
|
||||||
|
$nameX = ($W - $pdf->GetStringWidth($name)) / 2;
|
||||||
|
$this->drawGradientText($pdf, $edwardianFont, 38, $name, $nameX, 221.5);
|
||||||
|
|
||||||
|
// ── Line under name
|
||||||
|
$pdf->SetFont('times', '', 20);
|
||||||
|
$pdf->SetXY(0, 236);
|
||||||
|
$pdf->Cell(600, 20, '___________________________________', 0, 0, 'R');
|
||||||
|
|
||||||
|
// ── Description
|
||||||
|
$pdf->SetFont($ebGaramond, '', 20);
|
||||||
|
$pdf->SetXY(0, 273);
|
||||||
|
$pdf->Cell($W, 20, 'for successfully completing the requirements of', 0, 0, 'C');
|
||||||
|
|
||||||
|
// ── Grade
|
||||||
|
$pdf->SetFont($garamondBold, '', 20);
|
||||||
|
$pdf->SetXY(0, 310);
|
||||||
|
$pdf->Cell($W, 20, $grade, 0, 0, 'C');
|
||||||
|
|
||||||
|
// ── "at"
|
||||||
|
$pdf->SetFont('times', '', 20);
|
||||||
|
$pdf->SetXY(0, 343);
|
||||||
|
$pdf->Cell($W, 20, 'at', 0, 0, 'C');
|
||||||
|
|
||||||
|
// ── School name
|
||||||
|
$pdf->SetFont($garamondBold, '', 20);
|
||||||
|
$pdf->SetXY(0, 375);
|
||||||
|
$pdf->Cell($W, 20, 'Al Rahma Sunday School', 0, 0, 'C');
|
||||||
|
|
||||||
|
// ── Date
|
||||||
|
$this->drawGradientText($pdf, $edwardianFont, 26, $certDate, 586, 456);
|
||||||
|
|
||||||
|
// ── Date underline + label
|
||||||
|
$pdf->SetFont('times', '', 20);
|
||||||
|
$pdf->SetXY(0, 463);
|
||||||
|
$pdf->Cell(742, 20, '_________________', 0, 0, 'R');
|
||||||
|
$pdf->SetXY(650, 492);
|
||||||
|
$pdf->Cell(80, 20, 'Date', 0, 0, 'C');
|
||||||
|
|
||||||
|
// ── Signature underline + label
|
||||||
|
$pdf->SetXY(106, 463);
|
||||||
|
$pdf->Cell(200, 20, '_________________', 0, 0, 'L');
|
||||||
|
$pdf->SetXY(106, 492);
|
||||||
|
$pdf->Cell(168, 20, 'Signature', 0, 0, 'C');
|
||||||
|
|
||||||
|
// ── Certificate number — 1.4 cm from bottom, 2.5 cm from left.
|
||||||
|
if ($certNumber !== '') {
|
||||||
|
$pdf->SetFont('helvetica', '', 8);
|
||||||
|
$pdf->SetTextColor(150, 150, 150);
|
||||||
|
$pdf->SetXY(70.86, $H - 39.68);
|
||||||
|
$pdf->Cell(200, 12, 'Certificate No. ' . $certNumber, 0, 0, 'L');
|
||||||
|
$pdf->SetTextColor(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function drawGradientText(
|
||||||
|
\TCPDF $pdf,
|
||||||
|
string $fontName,
|
||||||
|
float $fontSize,
|
||||||
|
string $text,
|
||||||
|
float $x,
|
||||||
|
float $y
|
||||||
|
): void {
|
||||||
|
$pdf->SetFont($fontName, '', $fontSize);
|
||||||
|
|
||||||
|
for ($i = 0; $i < 6; $i++) {
|
||||||
|
$pdf->setAlpha(($i + 1) * 25 / 255);
|
||||||
|
$pdf->SetTextColor(0, 0, 0);
|
||||||
|
$pdf->Text($x + $i / 4, $y + $i / 4, $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdf->setAlpha(1);
|
||||||
|
$pdf->SetTextColor(0, 0, 0);
|
||||||
|
$pdf->Text($x, $y, $text);
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user