Skip to content

feat: atualização e ajustes gráficos - #16

Merged
p3drobitencourt merged 2 commits into
mainfrom
develop
Jun 2, 2026
Merged

feat: atualização e ajustes gráficos#16
p3drobitencourt merged 2 commits into
mainfrom
develop

Conversation

@p3drobitencourt

@p3drobitencourt p3drobitencourt commented May 22, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Client dashboard with real-time wallet, place/update/cancel bets, wallet top-up page, and full bet history.
    • Admin liquidation UI and paginated games management; seeded admin account support for first-time setup.
  • Refactor
    • Stronger transactional wallet handling and role-based route/view gating for more consistent behavior.
  • Documentation
    • README: added admin seeder step and a “Adicionar saldo (teste)” client workflow.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Migrates the app to CodeIgniter 4: adds domain entities, CI4 models with validation and transactions, services and controllers wired to models, explicit routing and filters, views and layouts, a DB migration and seeder, Docker/README updates, and removes legacy PDO-based files.

Changes

CodeIgniter 4 Architecture Migration

Layer / File(s) Summary
Domain entities
app/Entities/*
Adds Aposta, Cliente, Usuario, Jogo, Campeonato, Time entities with type casting and small helpers (formatters, label mapping, password hashing).
Core models
app/Models/*
Adds ApostaModel (transactional wallet and bet lifecycle), ResolucaoModel (pending games + result processing), ClienteModel (balance credit/debit), UsuarioModel, JogoModel (datetime normalization), TimeModel, CampeonatoModel with validation and query helpers.
Services & controllers
app/Services/*, app/Controllers/*
Adds AuthService, ApostaService; refactors controllers (Auth, Cliente, Jogo, Time, Usuario, Admin, Liquidacao, BaseController) to delegate to services/models and handle real persistence and redirects.
Routing / Filters / Config
app/Config/Routes.php, app/Filters/*, app/Config/Database.php
Disables auto-routing, adds admin/cliente route groups with filters, normalizes session profile handling in filters, and sources DB config from env variables.
Views & layout updates
app/Views/*
Adds/updates client dashboard, add-saldo, admin liquidation/debug views; gates admin actions in admin lists; adds method spoofing hidden inputs in edit forms and CSRF-protected delete forms.
DB migration & seed, schema, Docker, README
app/Database/*, database/schema.sql, Dockerfile, README.md
Adds migration to add jogo.status/resultado_final, AdminSeeder to create bootstrap admin, updates schema enums, adds mysqli to Dockerfile, and documents seeder usage in README.
Legacy removal
backup_old_system/*
Removes legacy procedural public entrypoints, controllers, services, repositories, and view templates (legacy PDO architecture).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • EnrLobo
  • kkjaokk

Poem

🐰 I hopped from old PDO lands,

to entities and tidy plans.
Transactions snug the wallet tight,
Controllers call, views shine bright.
A little seed grows admin light.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (11)
app/Entities/Aposta.php (4)

21-24: ⚡ Quick win

Remove redundant criado_em from $dates array.

When a field is already cast as 'datetime' in $casts (line 21), it's automatically handled as a date field. Including it again in $dates is redundant.

♻️ Proposed cleanup
         'criado_em'      => 'datetime',
     ];
-
-    protected $dates = ['criado_em'];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Entities/Aposta.php` around lines 21 - 24, Remove the redundant date
entry by deleting 'criado_em' from the $dates array in the Aposta entity: since
'criado_em' is already defined as 'datetime' in the $casts property, remove it
from protected $dates to avoid duplication and rely on the existing $casts
handling.

39-47: ⚡ Quick win

Use property accessor instead of direct attribute access.

Access $this->tipo_escolhido rather than $this->attributes['tipo_escolhido'] for consistency with entity accessor patterns.

♻️ Proposed fix
     public function getTipoLabel(): string
     {
-        return match ($this->attributes['tipo_escolhido'] ?? '') {
+        return match ($this->tipo_escolhido ?? '') {
             'vitoria_casa' => 'Vitória Casa',
             'empate'       => 'Empate',
             'vitoria_fora' => 'Vitória Fora',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Entities/Aposta.php` around lines 39 - 47, The method getTipoLabel uses
direct attribute array access ($this->attributes['tipo_escolhido']) instead of
the entity property accessor pattern; update getTipoLabel to read the value via
the property accessor ($this->tipo_escolhido) and keep the same match cases
('vitoria_casa','empate','vitoria_fora',default) so the behaviour is unchanged
while using the tipo_escolhido property rather than $this->attributes.

13-13: 💤 Low value

Remove unused empty $datamap array.

The empty $datamap property serves no purpose and can be removed to reduce clutter.

♻️ Proposed cleanup
-    protected $datamap = [];
-
     protected $casts = [
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Entities/Aposta.php` at line 13, Remove the unused empty datamap property
from the Aposta entity class: delete the protected $datamap = []; declaration in
class Aposta to eliminate dead code and reduce clutter, ensuring no other code
references $datamap before removing it.

52-59: ⚡ Quick win

Use property accessor instead of direct attribute access.

Access $this->status rather than $this->attributes['status'] for consistency.

♻️ Proposed fix
     public function getStatusBadgeClass(): string
     {
-        return match ($this->attributes['status'] ?? '') {
+        return match ($this->status ?? '') {
             'vencida' => 'bg-emerald-950/50 text-emerald-400 border-emerald-500/30',
             'perdida' => 'bg-red-950/50 text-red-400 border-red-500/30',
             default   => 'bg-amber-950/50 text-amber-400 border-amber-500/30',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Entities/Aposta.php` around lines 52 - 59, Replace the direct attribute
access in getStatusBadgeClass with the entity property accessor: use
$this->status (or $this->status ?? '') inside the match instead of
$this->attributes['status'] ?? ''; update the match expression in the
Aposta::getStatusBadgeClass method to refer to $this->status to ensure
consistent property access across the entity and avoid direct attribute array
reads.
app/Models/ApostaModel.php (2)

56-67: ⚡ Quick win

Consider using CodeIgniter's Time class for date comparisons.

Using PHP's date() function (line 63) works but is inconsistent with CodeIgniter 4 best practices. Consider using Time::now() for better consistency and testability.

♻️ Proposed enhancement

Add at the top of the file:

 use App\Entities\Aposta;
 use CodeIgniter\Model;
+use CodeIgniter\I18n\Time;

Then update the query:

             ->join('time tc', 'tc.id = j.time_casa_id')
             ->join('time tf', 'tf.id = j.time_fora_id')
-            ->where('j.data_horario >', date('Y-m-d H:i:s'))
+            ->where('j.data_horario >', Time::now()->toDateTimeString())
             ->orderBy('j.data_horario', 'ASC')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/ApostaModel.php` around lines 56 - 67, In getMercadoAtivo()
replace the direct PHP date() call with CodeIgniter's Time class: add a use
import for CodeIgniter\I18n\Time at the top of the file and change the where
clause in getMercadoAtivo() to use Time::now()->toDateTimeString() (or another
Time instance method) instead of date('Y-m-d H:i:s') so the query uses CI4 Time
for consistency and testability.

138-146: ⚡ Quick win

Consider using database timestamp functions or Time::now().

Line 145 uses date('Y-m-d H:i:s') for the timestamp. For consistency with CodeIgniter 4 best practices, consider using Time::now()->toDateTimeString() or letting the database handle the timestamp with NOW() if the field has a default value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/ApostaModel.php` around lines 138 - 146, Replace the manual PHP
date() call used when inserting a record in ApostaModel (the 'criado_em' value
in the insert() call) with a CodeIgniter Time instance or rely on the DB
default: either use Time::now()->toDateTimeString() (remember to import
CodeIgniter\I18n\Time at the top and call Time::now() when building the data
array for insert) or remove 'criado_em' from the insert payload and ensure the
database column has a NOW() default so the DB sets the timestamp automatically;
update the insert call in the method that calls $this->insert([... 'criado_em'
=> ... ]) accordingly.
app/Entities/Usuario.php (2)

17-20: ⚡ Quick win

Use property accessor instead of direct attribute access.

Access $this->perfil rather than $this->attributes['perfil'] for consistency with entity accessor patterns.

♻️ Proposed fix
     public function isAdmin(): bool
     {
-        return ($this->attributes['perfil'] ?? '') === 'admin';
+        return ($this->perfil ?? '') === 'admin';
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Entities/Usuario.php` around lines 17 - 20, The isAdmin method currently
reads the perfil value from the internal attributes array; change it to use the
entity property accessor by referencing $this->perfil (i.e., update
Usuario::isAdmin to check $this->perfil instead of $this->attributes['perfil']
and keep the null-coalescing fallback to an empty string if needed) so it
follows the entity accessor pattern and remains consistent with other property
reads.

25-28: ⚡ Quick win

Consider using PASSWORD_DEFAULT for future-proof password hashing.

While PASSWORD_BCRYPT works correctly, PASSWORD_DEFAULT is recommended as it automatically uses the strongest algorithm available in the current PHP version and will evolve with future PHP releases.

♻️ Proposed change
     public function setSenha(string $senha): self
     {
-        $this->attributes['senha'] = password_hash($senha, PASSWORD_BCRYPT);
+        $this->attributes['senha'] = password_hash($senha, PASSWORD_DEFAULT);
         return $this;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Entities/Usuario.php` around lines 25 - 28, In Usuario::setSenha replace
the hard-coded PASSWORD_BCRYPT usage with PASSWORD_DEFAULT to future-proof
hashing; update the password_hash call inside setSenha to use PASSWORD_DEFAULT
so PHP can select the best algorithm going forward while leaving the rest of the
method (returning $this and storing in $this->attributes['senha']) unchanged.
app/Entities/Cliente.php (1)

25-28: ⚡ Quick win

Use cast property instead of direct attribute access.

Access $this->saldo_carteira rather than $this->attributes['saldo_carteira'] for consistency.

♻️ Proposed fix
     public function getSaldoFormatado(): string
     {
-        return 'R$ ' . number_format($this->attributes['saldo_carteira'], 2, ',', '.');
+        return 'R$ ' . number_format($this->saldo_carteira, 2, ',', '.');
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Entities/Cliente.php` around lines 25 - 28, The getSaldoFormatado method
is directly reading $this->attributes['saldo_carteira']; update it to use the
model cast/property access ($this->saldo_carteira) instead to respect casts and
accessors—modify the getSaldoFormatado() implementation to concatenate 'R$ '
with number_format($this->saldo_carteira, 2, ',', '.') so it uses the property
accessor.
app/Models/JogoModel.php (1)

51-62: ⚖️ Poor tradeoff

Consider validating datetime format for robustness.

The normalizarDataHorario method assumes well-formed input from HTML datetime-local format. It only checks length and performs string replacement without validating the result. If the input is malformed or already contains seconds, the behavior is undefined.

Consider adding format validation using DateTime::createFromFormat() to ensure the input is valid before normalization, or handle edge cases where the input might not match the expected format.

♻️ More robust datetime normalization
 protected function normalizarDataHorario(array $data): array
 {
     if (isset($data['data']['data_horario'])) {
         $dt = str_replace('T', ' ', $data['data']['data_horario']);
         if (strlen($dt) === 16) {
             $dt .= ':00';
         }
+        
+        // Validate the resulting datetime format
+        $parsed = \DateTime::createFromFormat('Y-m-d H:i:s', $dt);
+        if (!$parsed || $parsed->format('Y-m-d H:i:s') !== $dt) {
+            throw new \InvalidArgumentException('Formato de data/hora inválido.');
+        }
+        
         $data['data']['data_horario'] = $dt;
     }
 
     return $data;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/JogoModel.php` around lines 51 - 62, The normalizarDataHorario
method assumes a correct datetime-local string and only checks length; validate
and normalize using DateTime parsing: try to parse $data['data']['data_horario']
with DateTime::createFromFormat for both 'Y-m-d\TH:i' and 'Y-m-d\TH:i:s' (or
their space equivalents after str_replace), and only replace
$data['data']['data_horario'] when parsing succeeds, formatting it to a
consistent 'Y-m-d H:i:s' output; if parsing fails, leave the value unchanged or
handle the error (e.g., set to null or add validation error) to avoid undefined
behavior in normalizarDataHorario.
app/Models/ResolucaoModel.php (1)

27-35: ⚡ Quick win

Consider LEFT JOIN instead of correlated subquery for better performance.

The correlated subquery at line 29 executes once per game row, which can be expensive on large datasets. A LEFT JOIN with GROUP BY would typically perform better.

⚡ Proposed refactor using LEFT JOIN
 public function getJogosPendentes(): array
 {
-    return $this->db->table('jogo j')
-        ->select('j.*, c.nome AS campeonato, tc.nome AS casa, tf.nome AS fora')
-        ->select('(SELECT COUNT(*) FROM aposta WHERE jogo_id = j.id AND status = "aberta") AS apostas_abertas')
+    return $this->db->table('jogo j')
+        ->select('j.*, c.nome AS campeonato, tc.nome AS casa, tf.nome AS fora')
+        ->select('COALESCE(COUNT(a.id), 0) AS apostas_abertas')
         ->join('campeonato c', 'c.id = j.campeonato_id')
         ->join('time tc', 'tc.id = j.time_casa_id')
         ->join('time tf', 'tf.id = j.time_fora_id')
+        ->join('aposta a', 'a.jogo_id = j.id AND a.status = "aberta"', 'left')
+        ->groupBy('j.id, c.nome, tc.nome, tf.nome')
         ->orderBy('j.data_horario', 'DESC')
         ->get()
         ->getResultArray();
 }

Note: Ensure all non-aggregated columns in SELECT are included in GROUP BY, or adjust based on your SQL mode settings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/ResolucaoModel.php` around lines 27 - 35, The correlated subquery
in the query built in ResolucaoModel (the $this->db->table('jogo j') chain)
should be replaced by a LEFT JOIN to the aposta table and an aggregate count to
avoid executing a subquery per row; modify the query to LEFT JOIN 'aposta a' ON
a.jogo_id = j.id, SELECT COUNT(a.id) AS apostas_abertas, and add a GROUP BY that
includes the non-aggregated jogo columns (or adapt SELECT to only
aggregated/allowed columns) so the query returns one row per jogo with a single
aggregated apostas_abertas value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Entities/Aposta.php`:
- Around line 31-34: In getRetornoPotencial() of class Aposta, stop reading
$this->attributes['valor'] and $this->attributes['odd_escolhida'] directly
(which bypasses $casts) and use the casted properties $this->valor and
$this->odd_escolhida instead; update the return expression in
getRetornoPotencial() to compute and round using those property accesses so type
casting is respected.
- Around line 64-68: The setter Aposta::setValor currently uses abs() which
silently converts negative inputs; change setValor to validate the incoming
float instead: if $valor is negative throw a domain/invalid argument exception
(e.g., InvalidArgumentException or a custom DomainException) with a clear
message, otherwise assign the value to $this->attributes['valor'] and return
$this; update any callers/tests to expect the exception behavior where
appropriate.

In `@app/Entities/Cliente.php`:
- Around line 17-20: In Cliente::temSaldoSuficiente replace direct attributes
array access with the casted property to ensure float casting is applied; use
$this->saldo_carteira instead of $this->attributes['saldo_carteira'] inside the
temSaldoSuficiente(float $valor): bool method so the value benefits from the
$casts definition.

In `@app/Models/ApostaModel.php`:
- Around line 166-182: The depositarSaldo method currently uses abs($valor)
which silently converts negative inputs to positive; instead validate $valor at
the start of depositarSaldo (e.g., ensure $valor > 0) and return ['success' =>
false, 'message' => 'Valor de depósito inválido.'] for non-positive values, and
then proceed to start the transaction and update saldo_carteira using the
original $valor (without abs) to avoid masking input errors; ensure the check
happens before $this->db->transStart() and reference depositarSaldo and the
saldo_carteira update call when making the change.
- Around line 115-121: The manual call to transRollback() inside the error
branch after fetching $row should be removed; locate the block around the query
($this->db->query($cliente . ' FOR UPDATE') → $result/$row) and delete the
$this->db->transRollback() statement so the method simply returns ['success' =>
false, 'message' => 'Cliente não encontrado.'] and lets
transComplete()/transStart() handle transaction rollback automatically.

In `@app/Models/JogoModel.php`:
- Around line 35-42: The $validationMessages array in JogoModel only provides a
Portuguese custom message for 'odd_casa' but not for 'odd_empate' and
'odd_fora', so add entries for those keys mapping the 'greater_than' rule to the
same Portuguese message ("Todas as odds devem ser maiores que 1.00.") to ensure
consistent localization; update the protected $validationMessages property in
the JogoModel class to include 'odd_empate' => ['greater_than' => 'Todas as odds
devem ser maiores que 1.00.'] and 'odd_fora' => ['greater_than' => 'Todas as
odds devem ser maiores que 1.00.'] alongside the existing 'odd_casa' entry.

In `@app/Models/ResolucaoModel.php`:
- Around line 72-75: In ResolucaoModel replace the direct string concatenation
that builds the SQL expression (the ->set('saldo_carteira', 'saldo_carteira + '
. $premio, false) update) with a parameter-bound update to avoid injection: use
the database query method with placeholders and pass [$premio,
$aposta['cliente_id']] as bindings (for example, run an UPDATE cliente SET
saldo_carteira = saldo_carteira + ? WHERE id = ? via $this->db->query) so the
increment value is passed as a bound parameter instead of concatenated.
- Around line 69-76: The loop updating cliente balances ($apostasVencedoras ->
foreach) is vulnerable to lost updates because only aposta rows were locked;
before performing the per-client updates you must lock the affected cliente rows
in the same transaction (e.g. collect cliente IDs from $apostasVencedoras and
issue a SELECT ... FOR UPDATE on the cliente table for those IDs using $this->db
within the same transaction) so concurrent resolucao processes cannot
interleave; after that perform the UPDATEs (or the atomic "saldo_carteira =
saldo_carteira + ..." updates already used via
$this->db->table('cliente')->set(..., false)->update()) and then commit.

---

Nitpick comments:
In `@app/Entities/Aposta.php`:
- Around line 21-24: Remove the redundant date entry by deleting 'criado_em'
from the $dates array in the Aposta entity: since 'criado_em' is already defined
as 'datetime' in the $casts property, remove it from protected $dates to avoid
duplication and rely on the existing $casts handling.
- Around line 39-47: The method getTipoLabel uses direct attribute array access
($this->attributes['tipo_escolhido']) instead of the entity property accessor
pattern; update getTipoLabel to read the value via the property accessor
($this->tipo_escolhido) and keep the same match cases
('vitoria_casa','empate','vitoria_fora',default) so the behaviour is unchanged
while using the tipo_escolhido property rather than $this->attributes.
- Line 13: Remove the unused empty datamap property from the Aposta entity
class: delete the protected $datamap = []; declaration in class Aposta to
eliminate dead code and reduce clutter, ensuring no other code references
$datamap before removing it.
- Around line 52-59: Replace the direct attribute access in getStatusBadgeClass
with the entity property accessor: use $this->status (or $this->status ?? '')
inside the match instead of $this->attributes['status'] ?? ''; update the match
expression in the Aposta::getStatusBadgeClass method to refer to $this->status
to ensure consistent property access across the entity and avoid direct
attribute array reads.

In `@app/Entities/Cliente.php`:
- Around line 25-28: The getSaldoFormatado method is directly reading
$this->attributes['saldo_carteira']; update it to use the model cast/property
access ($this->saldo_carteira) instead to respect casts and accessors—modify the
getSaldoFormatado() implementation to concatenate 'R$ ' with
number_format($this->saldo_carteira, 2, ',', '.') so it uses the property
accessor.

In `@app/Entities/Usuario.php`:
- Around line 17-20: The isAdmin method currently reads the perfil value from
the internal attributes array; change it to use the entity property accessor by
referencing $this->perfil (i.e., update Usuario::isAdmin to check $this->perfil
instead of $this->attributes['perfil'] and keep the null-coalescing fallback to
an empty string if needed) so it follows the entity accessor pattern and remains
consistent with other property reads.
- Around line 25-28: In Usuario::setSenha replace the hard-coded PASSWORD_BCRYPT
usage with PASSWORD_DEFAULT to future-proof hashing; update the password_hash
call inside setSenha to use PASSWORD_DEFAULT so PHP can select the best
algorithm going forward while leaving the rest of the method (returning $this
and storing in $this->attributes['senha']) unchanged.

In `@app/Models/ApostaModel.php`:
- Around line 56-67: In getMercadoAtivo() replace the direct PHP date() call
with CodeIgniter's Time class: add a use import for CodeIgniter\I18n\Time at the
top of the file and change the where clause in getMercadoAtivo() to use
Time::now()->toDateTimeString() (or another Time instance method) instead of
date('Y-m-d H:i:s') so the query uses CI4 Time for consistency and testability.
- Around line 138-146: Replace the manual PHP date() call used when inserting a
record in ApostaModel (the 'criado_em' value in the insert() call) with a
CodeIgniter Time instance or rely on the DB default: either use
Time::now()->toDateTimeString() (remember to import CodeIgniter\I18n\Time at the
top and call Time::now() when building the data array for insert) or remove
'criado_em' from the insert payload and ensure the database column has a NOW()
default so the DB sets the timestamp automatically; update the insert call in
the method that calls $this->insert([... 'criado_em' => ... ]) accordingly.

In `@app/Models/JogoModel.php`:
- Around line 51-62: The normalizarDataHorario method assumes a correct
datetime-local string and only checks length; validate and normalize using
DateTime parsing: try to parse $data['data']['data_horario'] with
DateTime::createFromFormat for both 'Y-m-d\TH:i' and 'Y-m-d\TH:i:s' (or their
space equivalents after str_replace), and only replace
$data['data']['data_horario'] when parsing succeeds, formatting it to a
consistent 'Y-m-d H:i:s' output; if parsing fails, leave the value unchanged or
handle the error (e.g., set to null or add validation error) to avoid undefined
behavior in normalizarDataHorario.

In `@app/Models/ResolucaoModel.php`:
- Around line 27-35: The correlated subquery in the query built in
ResolucaoModel (the $this->db->table('jogo j') chain) should be replaced by a
LEFT JOIN to the aposta table and an aggregate count to avoid executing a
subquery per row; modify the query to LEFT JOIN 'aposta a' ON a.jogo_id = j.id,
SELECT COUNT(a.id) AS apostas_abertas, and add a GROUP BY that includes the
non-aggregated jogo columns (or adapt SELECT to only aggregated/allowed columns)
so the query returns one row per jogo with a single aggregated apostas_abertas
value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9aca6330-3cf2-46a4-a330-3695e3951ea4

📥 Commits

Reviewing files that changed from the base of the PR and between df3941a and 32f9f9f.

📒 Files selected for processing (51)
  • app/Entities/Aposta.php
  • app/Entities/Campeonato.php
  • app/Entities/Cliente.php
  • app/Entities/Jogo.php
  • app/Entities/Time.php
  • app/Entities/Usuario.php
  • app/Models/ApostaModel.php
  • app/Models/CampeonatoModel.php
  • app/Models/JogoModel.php
  • app/Models/ResolucaoModel.php
  • app/Models/TimeModel.php
  • backup_old_system/public/apostar.php
  • backup_old_system/public/cadastro.php
  • backup_old_system/public/campeonatos.php
  • backup_old_system/public/index_old.php
  • backup_old_system/public/jogos.php
  • backup_old_system/public/login.php
  • backup_old_system/public/resolver.php
  • backup_old_system/public/times.php
  • backup_old_system/public/usuarios.php
  • backup_old_system/src/Application/Controllers/ApostaController.php
  • backup_old_system/src/Application/Controllers/AuthController.php
  • backup_old_system/src/Application/Controllers/CampeonatoController.php
  • backup_old_system/src/Application/Controllers/ClienteController.php
  • backup_old_system/src/Application/Controllers/JogoController.php
  • backup_old_system/src/Application/Controllers/ResolucaoController.php
  • backup_old_system/src/Application/Controllers/TimeController.php
  • backup_old_system/src/Application/Controllers/UsuarioController.php
  • backup_old_system/src/Application/Services/CampeonatoService.php
  • backup_old_system/src/Application/Services/DashboardService.php
  • backup_old_system/src/Application/Services/JogoService.php
  • backup_old_system/src/Application/Services/TimeService.php
  • backup_old_system/src/Application/Services/UsuarioService.php
  • backup_old_system/src/Infrastructure/Database/DatabaseConnector.php
  • backup_old_system/src/Infrastructure/Repositories/ApostaRepository.php
  • backup_old_system/src/Infrastructure/Repositories/CampeonatoRepository.php
  • backup_old_system/src/Infrastructure/Repositories/ClienteApostaRepository.php
  • backup_old_system/src/Infrastructure/Repositories/JogoRepository.php
  • backup_old_system/src/Infrastructure/Repositories/ResolucaoRepository.php
  • backup_old_system/src/Infrastructure/Repositories/TimeRepository.php
  • backup_old_system/src/Infrastructure/Repositories/UsuarioRepository.php
  • backup_old_system/views/admin/campeonatos/form.phtml
  • backup_old_system/views/admin/campeonatos/index.phtml
  • backup_old_system/views/admin/jogos/form.phtml
  • backup_old_system/views/admin/jogos/index.phtml
  • backup_old_system/views/admin/resolucao.phtml
  • backup_old_system/views/admin/times/form.phtml
  • backup_old_system/views/admin/times/index.phtml
  • backup_old_system/views/admin/usuarios/form.phtml
  • backup_old_system/views/admin/usuarios/index.phtml
  • backup_old_system/views/cliente/sportsbook.phtml
💤 Files with no reviewable changes (40)
  • backup_old_system/views/admin/times/index.phtml
  • backup_old_system/src/Application/Services/DashboardService.php
  • backup_old_system/views/admin/usuarios/form.phtml
  • backup_old_system/src/Infrastructure/Repositories/JogoRepository.php
  • backup_old_system/public/apostar.php
  • backup_old_system/public/usuarios.php
  • backup_old_system/src/Application/Controllers/ClienteController.php
  • backup_old_system/src/Infrastructure/Repositories/TimeRepository.php
  • backup_old_system/src/Application/Controllers/ResolucaoController.php
  • backup_old_system/src/Application/Controllers/ApostaController.php
  • backup_old_system/views/admin/campeonatos/index.phtml
  • backup_old_system/src/Application/Services/CampeonatoService.php
  • backup_old_system/src/Infrastructure/Repositories/ResolucaoRepository.php
  • backup_old_system/public/jogos.php
  • backup_old_system/public/cadastro.php
  • backup_old_system/src/Application/Controllers/TimeController.php
  • backup_old_system/public/campeonatos.php
  • backup_old_system/views/admin/jogos/index.phtml
  • backup_old_system/views/cliente/sportsbook.phtml
  • backup_old_system/src/Infrastructure/Repositories/CampeonatoRepository.php
  • backup_old_system/views/admin/resolucao.phtml
  • backup_old_system/src/Application/Services/JogoService.php
  • backup_old_system/public/resolver.php
  • backup_old_system/src/Infrastructure/Repositories/ApostaRepository.php
  • backup_old_system/views/admin/usuarios/index.phtml
  • backup_old_system/public/login.php
  • backup_old_system/views/admin/campeonatos/form.phtml
  • backup_old_system/views/admin/jogos/form.phtml
  • backup_old_system/src/Application/Controllers/AuthController.php
  • backup_old_system/public/index_old.php
  • backup_old_system/src/Application/Controllers/CampeonatoController.php
  • backup_old_system/src/Infrastructure/Repositories/ClienteApostaRepository.php
  • backup_old_system/src/Infrastructure/Repositories/UsuarioRepository.php
  • backup_old_system/src/Infrastructure/Database/DatabaseConnector.php
  • backup_old_system/src/Application/Services/TimeService.php
  • backup_old_system/src/Application/Controllers/UsuarioController.php
  • backup_old_system/src/Application/Services/UsuarioService.php
  • backup_old_system/public/times.php
  • backup_old_system/views/admin/times/form.phtml
  • backup_old_system/src/Application/Controllers/JogoController.php

Comment thread app/Entities/Aposta.php
Comment on lines +31 to +34
public function getRetornoPotencial(): float
{
return round($this->attributes['valor'] * $this->attributes['odd_escolhida'], 2);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use cast properties instead of direct attribute access.

Accessing $this->attributes['valor'] and $this->attributes['odd_escolhida'] directly bypasses the type casting defined in $casts. Use $this->valor and $this->odd_escolhida instead to ensure type safety.

🛡️ Proposed fix
     public function getRetornoPotencial(): float
     {
-        return round($this->attributes['valor'] * $this->attributes['odd_escolhida'], 2);
+        return round($this->valor * $this->odd_escolhida, 2);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function getRetornoPotencial(): float
{
return round($this->attributes['valor'] * $this->attributes['odd_escolhida'], 2);
}
public function getRetornoPotencial(): float
{
return round($this->valor * $this->odd_escolhida, 2);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Entities/Aposta.php` around lines 31 - 34, In getRetornoPotencial() of
class Aposta, stop reading $this->attributes['valor'] and
$this->attributes['odd_escolhida'] directly (which bypasses $casts) and use the
casted properties $this->valor and $this->odd_escolhida instead; update the
return expression in getRetornoPotencial() to compute and round using those
property accesses so type casting is respected.

Comment thread app/Entities/Aposta.php
Comment on lines +64 to +68
public function setValor(float $valor): self
{
$this->attributes['valor'] = abs($valor);
return $this;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate instead of silently converting negative values.

Using abs() silently converts negative bet amounts to positive values, masking potential data errors or malicious input. Consider validating and throwing an exception for negative values instead.

🛡️ Proposed fix with validation
     public function setValor(float $valor): self
     {
+        if ($valor < 0) {
+            throw new \InvalidArgumentException('O valor da aposta não pode ser negativo.');
+        }
         $this->attributes['valor'] = $valor;
         return $this;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Entities/Aposta.php` around lines 64 - 68, The setter Aposta::setValor
currently uses abs() which silently converts negative inputs; change setValor to
validate the incoming float instead: if $valor is negative throw a
domain/invalid argument exception (e.g., InvalidArgumentException or a custom
DomainException) with a clear message, otherwise assign the value to
$this->attributes['valor'] and return $this; update any callers/tests to expect
the exception behavior where appropriate.

Comment thread app/Entities/Cliente.php
Comment on lines +17 to +20
public function temSaldoSuficiente(float $valor): bool
{
return $this->attributes['saldo_carteira'] >= $valor;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use cast property instead of direct attribute access.

Access $this->saldo_carteira rather than $this->attributes['saldo_carteira'] to leverage the float casting defined in $casts.

🛡️ Proposed fix
     public function temSaldoSuficiente(float $valor): bool
     {
-        return $this->attributes['saldo_carteira'] >= $valor;
+        return $this->saldo_carteira >= $valor;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Entities/Cliente.php` around lines 17 - 20, In
Cliente::temSaldoSuficiente replace direct attributes array access with the
casted property to ensure float casting is applied; use $this->saldo_carteira
instead of $this->attributes['saldo_carteira'] inside the
temSaldoSuficiente(float $valor): bool method so the value benefits from the
$casts definition.

Comment on lines +115 to +121
$result = $this->db->query($cliente . ' FOR UPDATE');
$row = $result->getRowArray();

if (!$row) {
$this->db->transRollback();
return ['success' => false, 'message' => 'Cliente não encontrado.'];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Remove manual transRollback() call.

When using transStart() and transComplete(), CodeIgniter automatically rolls back the transaction if any errors occur. The manual transRollback() call on line 119 is unnecessary and deviates from the standard CI4 transaction pattern used elsewhere in this method (lines 127, 153-154).

Note: The static analysis warning about SQL injection on line 115 is a false positive—the query is safely built using CI4's query builder with proper parameter binding.

♻️ Proposed fix
         if (!$row) {
-            $this->db->transRollback();
-            return ['success' => false, 'message' => 'Cliente não encontrado.'];
+            $this->db->transComplete();
+            return ['success' => false, 'message' => 'Cliente não encontrado.'];
         }
 
         $saldoAtual = (float) $row['saldo_carteira'];
 
         // 2. Valida saldo suficiente
         if ($saldoAtual < $dados['valor']) {
-            $this->db->transRollback();
+            $this->db->transComplete();
             return ['success' => false, 'message' => 'Saldo insuficiente. Saldo atual: R$ ' . number_format($saldoAtual, 2, ',', '.')];
         }
🧰 Tools
🪛 OpenGrep (1.21.0)

[ERROR] 115-115: SQL query built via string concatenation passed to a database method. Use prepared statements with bound parameters instead.

(coderabbit.sql-injection.php-query-concat)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/ApostaModel.php` around lines 115 - 121, The manual call to
transRollback() inside the error branch after fetching $row should be removed;
locate the block around the query ($this->db->query($cliente . ' FOR UPDATE') →
$result/$row) and delete the $this->db->transRollback() statement so the method
simply returns ['success' => false, 'message' => 'Cliente não encontrado.'] and
lets transComplete()/transStart() handle transaction rollback automatically.

Comment on lines +166 to +182
public function depositarSaldo(int $clienteId, float $valor): array
{
$this->db->transStart();

$this->db->table('cliente')
->where('id', $clienteId)
->set('saldo_carteira', 'saldo_carteira + ' . abs($valor), false)
->update();

$this->db->transComplete();

if ($this->db->transStatus() === false) {
return ['success' => false, 'message' => 'Falha ao processar depósito.'];
}

return ['success' => true, 'message' => 'Depósito realizado com sucesso!'];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate instead of silently converting negative deposits.

Line 172 uses abs($valor) to ensure positive values, which silently converts negative deposits to positive. This masks input errors. Consider validating the input and returning an error for negative values instead.

🛡️ Proposed fix with validation
     public function depositarSaldo(int $clienteId, float $valor): array
     {
+        if ($valor <= 0) {
+            return ['success' => false, 'message' => 'O valor do depósito deve ser positivo.'];
+        }
+
         $this->db->transStart();
 
         $this->db->table('cliente')
             ->where('id', $clienteId)
-            ->set('saldo_carteira', 'saldo_carteira + ' . abs($valor), false)
+            ->set('saldo_carteira', 'saldo_carteira + ' . $valor, false)
             ->update();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/ApostaModel.php` around lines 166 - 182, The depositarSaldo method
currently uses abs($valor) which silently converts negative inputs to positive;
instead validate $valor at the start of depositarSaldo (e.g., ensure $valor > 0)
and return ['success' => false, 'message' => 'Valor de depósito inválido.'] for
non-positive values, and then proceed to start the transaction and update
saldo_carteira using the original $valor (without abs) to avoid masking input
errors; ensure the check happens before $this->db->transStart() and reference
depositarSaldo and the saldo_carteira update call when making the change.

Comment thread app/Models/JogoModel.php
Comment on lines +35 to +42
protected $validationMessages = [
'time_fora_id' => [
'differs' => 'O time visitante deve ser diferente do time da casa.',
],
'odd_casa' => [
'greater_than' => 'Todas as odds devem ser maiores que 1.00.',
],
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Complete validation messages for all odds fields.

The validation messages only provide a custom greater_than message for odd_casa (line 40), but odd_empate and odd_fora also use the greater_than[1] rule. Users will receive generic English error messages instead of the Portuguese message for these fields.

💬 Proposed fix to add missing validation messages
     protected $validationMessages = [
         'time_fora_id' => [
             'differs' => 'O time visitante deve ser diferente do time da casa.',
         ],
         'odd_casa' => [
             'greater_than' => 'Todas as odds devem ser maiores que 1.00.',
         ],
+        'odd_empate' => [
+            'greater_than' => 'Todas as odds devem ser maiores que 1.00.',
+        ],
+        'odd_fora' => [
+            'greater_than' => 'Todas as odds devem ser maiores que 1.00.',
+        ],
     ];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/JogoModel.php` around lines 35 - 42, The $validationMessages array
in JogoModel only provides a Portuguese custom message for 'odd_casa' but not
for 'odd_empate' and 'odd_fora', so add entries for those keys mapping the
'greater_than' rule to the same Portuguese message ("Todas as odds devem ser
maiores que 1.00.") to ensure consistent localization; update the protected
$validationMessages property in the JogoModel class to include 'odd_empate' =>
['greater_than' => 'Todas as odds devem ser maiores que 1.00.'] and 'odd_fora'
=> ['greater_than' => 'Todas as odds devem ser maiores que 1.00.'] alongside the
existing 'odd_casa' entry.

Comment on lines +69 to +76
foreach ($apostasVencedoras as $aposta) {
$premio = round((float) $aposta['valor'] * (float) $aposta['odd_escolhida'], 2);

$this->db->table('cliente')
->where('id', $aposta['cliente_id'])
->set('saldo_carteira', 'saldo_carteira + ' . $premio, false)
->update();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

CRITICAL: Race condition on client wallet balance updates.

The SELECT FOR UPDATE at line 63 only locks the aposta rows, but does not lock the cliente rows being updated. If two game resolutions process simultaneously with bets from the same client, concurrent updates to saldo_carteira can cause a lost update:

  1. Transaction A reads saldo_carteira = 1000, calculates new balance 1100
  2. Transaction B reads saldo_carteira = 1000, calculates new balance 1050
  3. Transaction A commits with 1100
  4. Transaction B commits with 1050 ← overwrites A's update, losing 50 in credits

This can result in financial data corruption where winning clients don't receive their full prize money.

🔒 Proposed fix: Lock client rows before updating
     $this->db->transStart();
 
     // 1. Lock nas apostas vencedoras
     $apostasVencedoras = $this->db->query(
         'SELECT id, cliente_id, valor, odd_escolhida FROM aposta WHERE jogo_id = ? AND tipo_escolhido = ? AND status = "aberta" FOR UPDATE',
         [$jogoId, $resultadoVencedor]
     )->getResultArray();
+    
+    // 1b. Lock client rows to prevent concurrent balance updates
+    if (!empty($apostasVencedoras)) {
+        $clienteIds = array_unique(array_column($apostasVencedoras, 'cliente_id'));
+        $placeholders = implode(',', array_fill(0, count($clienteIds), '?'));
+        $this->db->query(
+            "SELECT id FROM cliente WHERE id IN ($placeholders) FOR UPDATE",
+            $clienteIds
+        );
+    }
 
     // 2. Credita prêmio para cada vencedora
     foreach ($apostasVencedoras as $aposta) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/ResolucaoModel.php` around lines 69 - 76, The loop updating
cliente balances ($apostasVencedoras -> foreach) is vulnerable to lost updates
because only aposta rows were locked; before performing the per-client updates
you must lock the affected cliente rows in the same transaction (e.g. collect
cliente IDs from $apostasVencedoras and issue a SELECT ... FOR UPDATE on the
cliente table for those IDs using $this->db within the same transaction) so
concurrent resolucao processes cannot interleave; after that perform the UPDATEs
(or the atomic "saldo_carteira = saldo_carteira + ..." updates already used via
$this->db->table('cliente')->set(..., false)->update()) and then commit.

Comment on lines +72 to +75
$this->db->table('cliente')
->where('id', $aposta['cliente_id'])
->set('saldo_carteira', 'saldo_carteira + ' . $premio, false)
->update();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use parameter binding for SQL expressions to prevent injection risks.

Line 74 concatenates $premio directly into the SQL string. While $premio is currently computed from database values and rounded to 2 decimals, this pattern bypasses CodeIgniter's parameter binding and could introduce SQL injection if the calculation logic changes or has bugs.

CodeIgniter's Query Builder supports expressions while maintaining parameter safety.

🛡️ Proposed fix using safer Query Builder patterns
         // 2. Credita prêmio para cada vencedora
         foreach ($apostasVencedoras as $aposta) {
             $premio = round((float) $aposta['valor'] * (float) $aposta['odd_escolhida'], 2);
 
             $this->db->table('cliente')
                 ->where('id', $aposta['cliente_id'])
-                ->set('saldo_carteira', 'saldo_carteira + ' . $premio, false)
+                ->set('saldo_carteira', 'saldo_carteira + ?', false)
+                ->setQueryAsData(false)
-                ->update();
+                ->update(null, null, [$premio]);
         }

Alternatively, use a raw query with proper binding:

$this->db->query(
    'UPDATE cliente SET saldo_carteira = saldo_carteira + ? WHERE id = ?',
    [$premio, $aposta['cliente_id']]
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/ResolucaoModel.php` around lines 72 - 75, In ResolucaoModel
replace the direct string concatenation that builds the SQL expression (the
->set('saldo_carteira', 'saldo_carteira + ' . $premio, false) update) with a
parameter-bound update to avoid injection: use the database query method with
placeholders and pass [$premio, $aposta['cliente_id']] as bindings (for example,
run an UPDATE cliente SET saldo_carteira = saldo_carteira + ? WHERE id = ? via
$this->db->query) so the increment value is passed as a bound parameter instead
of concatenated.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (19)
app/Views/auth/cadastro.php-113-113 (1)

113-113: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Não envie perfil no payload de cadastro público.

Campo oculto de perfil é manipulável no cliente. Se qualquer camada confiar nesse valor, abre risco de elevação de privilégio. Defina perfil fixo no backend e ignore/remova o campo do formulário.

🔒 Suggested fix (view)
-                    <input type="hidden" name="perfil" value="cliente">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Views/auth/cadastro.php` at line 113, Remova o campo oculto <input
name="perfil"> do formulário em cadastro.php e, no código do backend que
processa o cadastro (o handler/controller/método responsável pelo endpoint de
registro), passe a ignorar qualquer campo "perfil" vindo do cliente e atribuir
explicitamente perfil = 'cliente' antes de persistir; além disso adicione
validação/whitelisting do payload para garantir que valores de perfil vindos do
cliente sejam descartados.
app/Models/ClienteModel.php-46-49 (1)

46-49: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

creditarSaldo() can return success when cliente_id does not exist.

The success flag is based only on transaction status; an update affecting zero rows still returns success. Include an affected-rows check (or prior existence check) before confirming success.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/ClienteModel.php` around lines 46 - 49, The current return in
creditarSaldo() only uses $this->db->transStatus() so an update that affects 0
rows (nonexistent cliente_id) can report success; after performing the update,
check the number of affected rows (e.g. via $this->db->affectedRows() or the
model's affectedRows()) and combine that with transStatus() to set 'success' and
an appropriate 'message' (e.g. "Nenhum cliente encontrado" when affected rows ==
0), replacing the existing return that only references transStatus().
database/schema.sql-46-46 (1)

46-46: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

aposta.status enum change needs a matching migration.

Line 46 updates schema.sql, but there is no corresponding migration in the reviewed changes to alter aposta.status. Environments upgraded through migrations can drift and reject cancelada writes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@database/schema.sql` at line 46, The schema change added 'cancelada' to the
aposta.status enum but no migration was provided; create a new SQL migration
that updates the existing enum type used by aposta.status to include 'cancelada'
(or create a new type and swap it), and ensure existing column default and
constraints are adjusted accordingly so upgrades don't fail; reference the
aposta.status column/enum in your migration and include both an up (add
'cancelada') and down (remove 'cancelada') step so environments can migrate
forward and backward safely.
app/Models/ClienteModel.php-41-42 (1)

41-42: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not coerce wallet amounts with abs(); validate input instead.

Using abs($valor) silently changes operation intent (e.g., negative amount becomes positive). For financial operations, reject non-positive values explicitly ($valor <= 0) and keep arithmetic direction intentional.

Also applies to: 58-60

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/ClienteModel.php` around lines 41 - 42, The current code uses
abs($valor) when updating 'saldo_carteira', which silently flips sign and hides
invalid input; remove abs(), and instead validate the input beforehand (e.g., if
($valor <= 0) throw/return an error) so only positive amounts are accepted for
credit operations; keep the DB arithmetic explicit by concatenating $valor (not
abs($valor)) in the set expression and apply the same validation/fix to the
other occurrence that updates 'saldo_carteira' in this file (the second block at
lines ~58-60).
app/Controllers/CampeonatoController.php-56-61 (1)

56-61: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Check persistence results before flashing “success” in CampeonatoController

In app/Controllers/CampeonatoController.php, store()/update()/delete() redirect with a success message without checking the return value of $campeonatoModel->insert(), $campeonatoModel->update(), or $campeonatoModel->delete(). In CI4, these calls can return false for non-exception failures (e.g., model-level validation like max_length), so users can still get a success flash even when nothing was persisted. store()/update() catch thrown exceptions, but not false returns; delete() has no exception handling at all.

Also applies to: 86-92, 106-108.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Controllers/CampeonatoController.php` around lines 56 - 61, store(),
update(), and delete() call $campeonatoModel->insert()/update()/delete() and
always flash a success message without checking their return values; change each
method (store, update, delete in CampeonatoController) to verify the model call
returned a truthy value and only flash success on true, otherwise collect model
errors ($campeonatoModel->errors() or $campeonatoModel->errors() +
$campeonatoModel->validation->getErrors()) and redirect back with an error
flash; also wrap delete() in try/catch like store()/update() to handle
exceptions and treat a false return as a failure to avoid false success
messages.
app/Database/Migrations/2026-05-22-000001_AddJogoLiquidacaoFields.php-12-16 (1)

12-16: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make this migration SQL-dialect aware (ENUM/AFTER are MySQL-only)

This migration executes raw MySQL DDL (ENUM(...) and AFTER status) via ALTER TABLE ... ADD COLUMN on lines 12–16, which will fail on non-MySQL drivers. The project’s test DB configuration uses SQLite3 (app/Config/Database.php tests group: DBDriver = SQLite3, database = :memory:), so these statements would break when migrations run under the tests connection. Use Forge/portable column definitions and avoid AFTER, or add a driver-guarded MySQL branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Database/Migrations/2026-05-22-000001_AddJogoLiquidacaoFields.php` around
lines 12 - 16, The migration AddJogoLiquidacaoFields currently uses raw MySQL
DDL via $this->db->query and fieldExists (ALTER TABLE ... ADD COLUMN with
ENUM(...) and AFTER status) which is MySQL-only; change it to be DB-driver aware
and/or use Forge portable column operations: detect the driver via
$this->db->DBDriver (or equivalent) and if it's a MySQL driver run the existing
ENUM + AFTER SQL, otherwise add compatible columns using $this->forge->addColumn
or $this->db->query with portable types (e.g., VARCHAR or SMALLINT +
nullable/default) and omit the AFTER clause; keep the existing fieldExists
checks around both branches so you only add columns when missing.
app/Services/ApostaService.php-31-56 (1)

31-56: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate required fields and bet value before indexing $dados.

jogo_id/valor are accessed directly; missing keys can emit warnings, and non-positive values are not blocked before transactional calls.

Suggested fix
     public function registrar(int $clienteId, array $dados): array
     {
+        if (!isset($dados['jogo_id'], $dados['tipo'], $dados['valor']) || !is_numeric($dados['valor']) || (float) $dados['valor'] <= 0) {
+            return ['success' => false, 'message' => 'Dados da aposta inválidos.'];
+        }
+
         $jogo = $this->jogoModel->find((int) $dados['jogo_id']);
@@
     public function atualizar(int $apostaId, int $clienteId, array $dados): array
     {
+        if (!isset($dados['jogo_id'], $dados['tipo'], $dados['valor']) || !is_numeric($dados['valor']) || (float) $dados['valor'] <= 0) {
+            return ['success' => false, 'message' => 'Dados da aposta inválidos.'];
+        }
+
         $jogo = $this->jogoModel->find((int) $dados['jogo_id']);

Also applies to: 61-84

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/ApostaService.php` around lines 31 - 56, In registrar, validate
required keys and values in $dados before indexing: ensure 'jogo_id' and 'valor'
(and 'tipo') exist, are numeric, and that 'valor' > 0 (and 'jogo_id' is an
integer) before calling $this->jogoModel->find or accessing $jogo[...] or
calling apostaModel->registrarApostaTransacional; return a clear
['success'=>false,'message'=>...] on validation failure. Apply the same
pre-checks to the other block referenced (lines 61-84) so neither registrar nor
the later transaction attempts to read undefined array keys or accept
non-positive bet amounts. Ensure you reference oddMap/$tipo validation after
these checks so you only access $oddMap[$tipo]['campo'] when $tipo is present
and valid.
app/Database/Seeds/AdminSeeder.php-12-13 (1)

12-13: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid default admin credentials in the seeder.

Using hardcoded fallback admin credentials can silently create a predictable privileged account when env vars are missing.

Suggested fix
-        $email = trim((string) (env('ADMIN_EMAIL') ?: 'admin@pimbastic.local'));
-        $senha = (string) (env('ADMIN_PASSWORD') ?: 'admin123');
+        $email = trim((string) env('ADMIN_EMAIL'));
+        $senha = (string) env('ADMIN_PASSWORD');
+
+        if ($email === '' || $senha === '') {
+            throw new \RuntimeException('ADMIN_EMAIL e ADMIN_PASSWORD devem ser definidos para executar o AdminSeeder.');
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Database/Seeds/AdminSeeder.php` around lines 12 - 13, The seeder
currently falls back to predictable defaults for $email and $senha; update
AdminSeeder (run method) to not use hardcoded fallbacks—read env('ADMIN_EMAIL')
and env('ADMIN_PASSWORD') without defaulting, validate that both are present and
non-empty, and if missing throw a clear exception (or abort seeding) so a
privileged account is never silently created; alternatively, generate a secure
random password and surface it explicitly if automatic credentials are required.
Ensure you update references to $email and $senha accordingly and add a short
error message that includes which env var is missing.
app/Models/UsuarioModel.php-47-53 (1)

47-53: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Block user creation when password is empty.

Current flow allows insert without a password hash when senha is empty, creating an invalid/insecure auth record.

Suggested fix
     public function criarUsuario(array $dados): array
     {
+        if (empty($dados['senha'])) {
+            return ['success' => false, 'id' => 0];
+        }
+
         if (!empty($dados['senha'])) {
             $dados['senha'] = password_hash($dados['senha'], PASSWORD_DEFAULT);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/UsuarioModel.php` around lines 47 - 53, criarUsuario currently
proceeds to $this->insert even when $dados['senha'] is empty; add a validation
at the start of criarUsuario to block creation when senha is missing/empty (do
not call $this->insert in that case) — either throw a clear exception (e.g.,
InvalidArgumentException) or return a standard error array consistent with the
method's return type, and only hash the password and call $this->insert($dados)
when senha is present and non-empty so no account is created with a
blank/unhashed password.
app/Services/ApostaService.php-25-27 (1)

25-27: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid duplicate identical query in dashboard().

The same bets query is executed twice for historico and apostas, doubling DB work on every dashboard load.

Suggested fix
     public function dashboard(int $clienteId): array
     {
+        $apostas = $this->apostaModel->getMinhasApostasComDetalhes($clienteId);
+
         return [
             'cliente' => [
                 'saldo_carteira' => $this->clienteModel->getSaldoAtual($clienteId),
             ],
             'jogos' => $this->jogoModel->getJogosAtivos(),
-            'historico' => $this->apostaModel->getMinhasApostasComDetalhes($clienteId),
-            'apostas' => $this->apostaModel->getMinhasApostasComDetalhes($clienteId),
+            'historico' => $apostas,
+            'apostas' => $apostas,
             'resumo' => $this->apostaModel->getResumoCarteira($clienteId),
         ];
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/ApostaService.php` around lines 25 - 27, In dashboard(), avoid
calling getMinhasApostasComDetalhes($clienteId) twice: call
$this->apostaModel->getMinhasApostasComDetalhes($clienteId) once, store the
result in a local variable (e.g. $minhasApostas) and use that variable for both
the 'historico' and 'apostas' array entries while leaving the 'resumo' call
unchanged; update references in the method so only one DB query is executed.
app/Models/UsuarioModel.php-69-73 (1)

69-73: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

atualizarUsuario() can report success even on failure.

Using $this->db->affectedRows() >= 0 makes success effectively always true for no-op/error-like outcomes. Use the return value of update() as the success source.

Suggested fix
-        $this->update($id, $dados);
+        $ok = $this->update($id, $dados);
 
         return [
-            'success' => $this->db->affectedRows() >= 0,
+            'success' => $ok,
         ];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/UsuarioModel.php` around lines 69 - 73, The success flag currently
uses $this->db->affectedRows() >= 0 which is always true; instead capture and
use the return value of update() as the authoritative success indicator (e.g.,
call $result = $this->update($id, $dados) and return ['success' => (bool)
$result]) — update the atualizarUsuario (or the method containing update()) to
stop relying on $this->db->affectedRows() and use $result from update().
app/Controllers/TimeController.php-57-63 (1)

57-63: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle insert()/update() false returns before success redirect.

Line 57 and Line 89 may return false (validation/DB failure) without throwing, but the code still emits success flash messages.

Suggested patch
-            $timeModel->insert([
+            $ok = $timeModel->insert([
                 'nome' => trim((string) $this->request->getPost('nome')),
                 'tecnico' => trim((string) $this->request->getPost('tecnico')),
                 'sigla' => trim((string) $this->request->getPost('sigla')) ?: null,
             ]);
+            if ($ok === false) {
+                return redirect()->back()->withInput()->with('error', $timeModel->errors() ?: ['database' => 'Não foi possível salvar o time.']);
+            }
@@
-            $timeModel->update((int) $id, [
+            $ok = $timeModel->update((int) $id, [
                 'nome' => trim((string) $this->request->getPost('nome')),
                 'tecnico' => trim((string) $this->request->getPost('tecnico')),
                 'sigla' => trim((string) $this->request->getPost('sigla')) ?: null,
             ]);
+            if ($ok === false) {
+                return redirect()->back()->withInput()->with('error', $timeModel->errors() ?: ['database' => 'Não foi possível atualizar o time.']);
+            }

Also applies to: 89-96

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Controllers/TimeController.php` around lines 57 - 63, The insert/update
calls in TimeController may return false on validation or DB failure but the
code always redirects with a success message; change the logic in the methods
that call TimeModel->insert(...) and TimeModel->update(...) so you capture the
return value (result = $timeModel->insert(...) / $timeModel->update(...)), check
if it's === false, and on failure redirect back with an error flash and include
model errors (e.g., $timeModel->errors()) or DB error info; only emit the
success flash and redirect to /admin/times when the insert/update returned a
truthy result.
app/Services/AuthService.php-49-50 (1)

49-50: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Check insert results before using insertID() and proceeding.

If the cliente insert fails, Line 50 still reads insertID() and the user insert is attempted. Handle insert failures immediately to keep transaction outcomes deterministic.

Suggested patch
-            $this->db->table('cliente')->insert($clienteData);
+            $okCliente = $this->db->table('cliente')->insert($clienteData);
+            if ($okCliente === false) {
+                $this->db->transRollback();
+                return [
+                    'success' => false,
+                    'message' => 'Falha ao registrar cliente.',
+                ];
+            }
             $clienteId = (int) $this->db->insertID();
         }
 
-        $this->db->table('usuario')->insert([
+        $okUsuario = $this->db->table('usuario')->insert([
             'nome' => $dados['nome'],
             'email' => $dados['email'],
             'senha' => password_hash($dados['senha'], PASSWORD_DEFAULT),
             'perfil' => $dados['perfil'],
             'cliente_id' => $clienteId,
         ]);
+        if ($okUsuario === false) {
+            $this->db->transRollback();
+            return [
+                'success' => false,
+                'message' => 'Falha ao registrar usuário.',
+            ];
+        }

Also applies to: 53-59

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/AuthService.php` around lines 49 - 50, The cliente insert result
is not being checked before calling $this->db->insertID() and proceeding to the
usuario insert; update the logic around the
$this->db->table('cliente')->insert($clienteData) call to capture its return
value and if it indicates failure (false / no affected rows) immediately handle
it (throw/return error and rollback any transaction) and do not call
$this->db->insertID() or attempt the usuario insert. Do the same check before
calling $this->db->table('usuario')->insert(...) so both insert operations
validate their results and maintain deterministic transaction handling.
app/Config/Routes.php-40-40 (1)

40-40: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid exposing debug liquidation route in production.

Line 40 publishes /admin/liquidacao/debug permanently. Even admin-only debug pages widen sensitive-data exposure and should be non-production only.

Suggested patch
-    $routes->get('liquidacao/debug', 'LiquidacaoController::debug');
+    if (ENVIRONMENT !== 'production') {
+        $routes->get('liquidacao/debug', 'LiquidacaoController::debug');
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Config/Routes.php` at line 40, The route registering the debug endpoint
($routes->get('liquidacao/debug', 'LiquidacaoController::debug')) exposes
sensitive admin debug functionality; wrap or remove this registration so it is
not available in production—e.g., only register the route when ENVIRONMENT !==
'production' or when running in a dev/test environment, or move it behind a
strict admin-only feature flag/auth check; locate the entry in Routes.php and
guard the LiquidacaoController::debug route accordingly so it is never published
in production.
app/Controllers/TimeController.php-110-112 (1)

110-112: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate delete result before returning success.

Line 110 can fail (e.g., DB constraint) and still returns success on Line 112. Check the return value and show an error when delete is not applied.

Suggested patch
-        $timeModel->delete((int) $id);
-
-        return redirect()->to('/admin/times')->with('success', 'Time removido com sucesso.');
+        $ok = $timeModel->delete((int) $id);
+        if ($ok === false) {
+            return redirect()->to('/admin/times')->with('error', 'Não foi possível remover o time.');
+        }
+        return redirect()->to('/admin/times')->with('success', 'Time removido com sucesso.');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Controllers/TimeController.php` around lines 110 - 112, The current
TimeController code calls $timeModel->delete((int) $id) and immediately returns
a success redirect; update this to capture the delete result (the return value
of TimeModel::delete), check if it indicates success (truthy/affected rows), and
only redirect()->to('/admin/times')->with('success', ...) when true; otherwise
redirect back or to the same route with with('error', 'Falha ao remover o
horário.') or log the failure and show an error. Ensure you reference the delete
call in TimeController and handle exceptions thrown by the model (try/catch) to
return an error response if an exception occurs.
app/Services/AuthService.php-43-58 (1)

43-58: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not trust perfil input in public registration flow.

Line 57 persists perfil from $dados as-is, which can allow privilege escalation if request payload is manipulated, and it can also trigger undefined-index behavior when perfil is absent. Set an explicit safe default in this service (cliente) for this flow.

Suggested patch
-        if (($dados['perfil'] ?? 'cliente') === 'cliente') {
+        $perfil = 'cliente'; // cadastro público
+
+        if ($perfil === 'cliente') {
@@
-            'perfil' => $dados['perfil'],
+            'perfil' => $perfil,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/AuthService.php` around lines 43 - 58, The code currently trusts
$dados['perfil'] and passes it into the usuario insert, enabling privilege
escalation and risking undefined-index errors; instead force the public
registration profile to 'cliente' and ensure $clienteId is initialized. Update
the method in AuthService (the block that inserts into 'cliente' and then into
'usuario') to: initialize $clienteId = null before the conditional, use a safe
$perfil = 'cliente' (do not read $dados['perfil']) for this flow, create the
cliente row when $perfil === 'cliente' and then insert into 'usuario' with
'perfil' => $perfil and 'cliente_id' => $clienteId. Ensure you still hash the
password with password_hash as before.
app/Views/cliente/dashboard.php-88-95 (1)

88-95: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bet type controls are submitting the form prematurely.

On Line 88–90, the type buttons default to type="submit", so clicking a selection submits immediately (often before amount input on Line 93). This conflicts with the dedicated submit button on Line 94.

Suggested patch
-                        <div class="grid grid-cols-3 gap-2">
-                            <button name="tipo" value="casa" class="rounded-xl bg-black/40 border border-white/5 px-3 py-2 text-xs uppercase tracking-wider text-emerald-300">Casa</button>
-                            <button name="tipo" value="empate" class="rounded-xl bg-black/40 border border-white/5 px-3 py-2 text-xs uppercase tracking-wider text-cyan-300">Empate</button>
-                            <button name="tipo" value="fora" class="rounded-xl bg-black/40 border border-white/5 px-3 py-2 text-xs uppercase tracking-wider text-emerald-300">Fora</button>
-                        </div>
+                        <div class="grid grid-cols-3 gap-2 text-xs">
+                            <label class="rounded-xl bg-black/40 border border-white/5 px-3 py-2 text-emerald-300">
+                                <input type="radio" name="tipo" value="casa" class="mr-1" required> Casa
+                            </label>
+                            <label class="rounded-xl bg-black/40 border border-white/5 px-3 py-2 text-cyan-300">
+                                <input type="radio" name="tipo" value="empate" class="mr-1" required> Empate
+                            </label>
+                            <label class="rounded-xl bg-black/40 border border-white/5 px-3 py-2 text-emerald-300">
+                                <input type="radio" name="tipo" value="fora" class="mr-1" required> Fora
+                            </label>
+                        </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Views/cliente/dashboard.php` around lines 88 - 95, The three bet-type
controls (buttons with name="tipo" and values "casa", "empate", "fora") are
submitting the form when clicked; change their HTML to non-submitting controls
(e.g., set type="button" or convert to radio inputs) so selection does not
trigger form submission, and ensure the dedicated submit button (the button with
type="submit" next to the input named "valor") remains the only way to submit
the form.
app/Controllers/UsuarioController.php-63-84 (1)

63-84: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wrap the cliente + usuario writes in one transaction.

Both paths create or link a cliente record before the corresponding usuario write completes. If the second write fails, you leave orphan wallet rows behind and the two tables drift out of sync. These multi-table mutations should commit or roll back together.

Also applies to: 105-136

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Controllers/UsuarioController.php` around lines 63 - 84, Wrap the
multi-table write in a DB transaction: in UsuarioController around the blocks
that call ClienteModel->insert(), ClienteModel->getInsertID() and
UsuarioModel->insert() (the block using UsuarioModel and ClienteModel in the
lines shown and the similar block at 105-136), begin a transaction before the
first insert (use the model's DB connection or
\Config\Database::connect()->transBegin()), perform the cliente insert and
capture clienteId, perform the usuario insert, then transCommit() on success; on
any exception or failure call transRollback() and surface the error. Ensure you
use the same DB connection for both inserts (model->db or the shared $db) so
commit/rollback covers both operations.
app/Controllers/JogoController.php-68-85 (1)

68-85: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Check model write return values before redirecting with success

In app/Controllers/JogoController.php (store() around lines 68-85), the code redirects success after $jogoModel->save(...) and only inspects $jogoModel->errors(), so a failed persistence (e.g., DB/constraint failure returning false without validation errors) can still look successful to the admin.

  • Guard on the return value of save()/update()/delete() and redirect back with an error when the write fails.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Controllers/JogoController.php` around lines 68 - 85, The current store()
flow calls $jogoModel->save(...) but only checks $jogoModel->errors(), so a
false return from save (DB/constraint failure) can still trigger the success
redirect; change this by capturing the save() return value (e.g., $saved =
$jogoModel->save(...)), and if $saved === false then log the failure and return
redirect()->back()->withInput()->with('error', 'Não foi possível salvar o
jogo.') (and include $jogoModel->errors() if present); keep the existing
Throwable catch as-is but ensure the success redirect only runs when $saved is
truthy. Ensure you reference $jogoModel and its save() and errors() methods in
the fix.
🟡 Minor comments (5)
app/Views/cliente/adicionar_saldo.php-20-20 (1)

20-20: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve valor after validation errors.

O campo perde o valor submetido quando há erro e redirecionamento com old(), piorando o fluxo de recarga.

💡 Suggested fix
-                <input id="valor" name="valor" type="number" step="0.01" min="1" required class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-white" placeholder="Ex: 50.00">
+                <input id="valor" name="valor" type="number" step="0.01" min="1" value="<?= old('valor') ?>" required class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-white" placeholder="Ex: 50.00">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Views/cliente/adicionar_saldo.php` at line 20, The input field with
id="valor" and name="valor" loses its submitted value after validation errors;
update the <input id="valor" name="valor" ...> to set its value from the
previous request (e.g., use old('valor') or the framework's set_value('valor'))
so the field repopulates on redirect after validation failure, ensuring you
escape/format the value consistently (keep step/min/placeholder as-is).
app/Controllers/BaseController.php-78-81 (1)

78-81: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Apply the same fallback pattern for cliente_id.

getClienteSaldo() only checks cliente_id, while other user fields already support fallback keys. This can incorrectly return 0.0 when session data uses the alternate convention.

Suggested fix
-        $clienteId = $this->session->get('cliente_id');
+        $clienteId = $this->session->get('cliente_id') ?? $this->session->get('usuario_cliente_id');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Controllers/BaseController.php` around lines 78 - 81, getClienteSaldo()
currently returns 0.0 if $this->session->get('cliente_id') is missing; mirror
the existing fallback pattern used for other user fields by checking the
alternate session keys before returning 0.0. Update getClienteSaldo() to attempt
$this->session->get('cliente_id'), then the alternate key(s) used elsewhere (for
example 'clienteId' or the nested user key used in other methods), and only
return 0.0 if none of those keys yield a value; ensure you reference the
getClienteSaldo() method and the 'cliente_id' session key when making the
changes.
app/Views/admin/usuarios/index.php-45-50 (1)

45-50: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Cast/escape the user ID in action URLs.

$u['id'] is written directly into href/action. Cast to int (or escape) before interpolating to avoid malformed attribute output.

Suggested fix
+                                <?php $userId = (int) $u['id']; ?>
-                                <a href="/admin/usuarios/edit/<?= $u['id'] ?>" class="bg-slate-800 hover:bg-slate-700 text-gray-200 border border-white/10 px-3 py-1 rounded-lg text-xs font-semibold uppercase tracking-wider transition-colors inline-block">
+                                <a href="/admin/usuarios/edit/<?= $userId ?>" class="bg-slate-800 hover:bg-slate-700 text-gray-200 border border-white/10 px-3 py-1 rounded-lg text-xs font-semibold uppercase tracking-wider transition-colors inline-block">
                                     Editar
                                 </a>
-                                <form action="/admin/usuarios/delete/<?= $u['id'] ?>" method="POST" class="inline-block" onsubmit="return confirm('Excluir este usuário?')">
+                                <form action="/admin/usuarios/delete/<?= $userId ?>" method="POST" class="inline-block" onsubmit="return confirm('Excluir este usuário?')">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Views/admin/usuarios/index.php` around lines 45 - 50, The user ID is
interpolated directly into the link and form action via $u['id'], which can
produce malformed attributes or XSS; update the href "/admin/usuarios/edit/<?=
$u['id'] ?>" and the form action "/admin/usuarios/delete/<?= $u['id'] ?>" to
output a sanitized/cast value (e.g., cast to int or use the framework esc()
helper) so the ID is safely rendered; change both the anchor and form action
usages that reference $u['id'] accordingly.
app/Views/admin/times/index.php-43-47 (1)

43-47: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Escape IDs in action URLs.

Line 43 and Line 46 print <?= $t['id'] ?> in attribute context without escaping. Prefer escaped output for consistency and safer rendering.

Suggested patch
-<a href="/admin/times/edit/<?= $t['id'] ?>" class="bg-slate-800 hover:bg-slate-700 text-gray-200 border border-white/10 px-3 py-1 rounded-lg text-xs font-semibold uppercase tracking-wider transition-colors inline-block">
+<a href="/admin/times/edit/<?= esc($t['id']) ?>" class="bg-slate-800 hover:bg-slate-700 text-gray-200 border border-white/10 px-3 py-1 rounded-lg text-xs font-semibold uppercase tracking-wider transition-colors inline-block">
@@
-<form action="/admin/times/delete/<?= $t['id'] ?>" method="POST" class="inline-block" onsubmit="return confirm('Excluir este time?')">
+<form action="/admin/times/delete/<?= esc($t['id']) ?>" method="POST" class="inline-block" onsubmit="return confirm('Excluir este time?')">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Views/admin/times/index.php` around lines 43 - 47, The anchor href and
form action currently inject $t['id'] directly into HTML attributes (the
occurrences of <?= $t['id'] ?>) and need to be escaped; update both locations to
output an escaped/URL-safe id (e.g., use your framework helper like
esc($t['id'], 'attr') or htmlspecialchars(rawurlencode($t['id']), ENT_QUOTES,
'UTF-8')) so the values are safe in attribute context and in the URL.
app/Views/admin/jogos/index.php-51-55 (1)

51-55: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Escape IDs in action URLs.

Line 51 and Line 54 interpolate <?= $j['id'] ?> directly into attributes. Use escaped output for consistency and attribute-safety hardening.

Suggested patch
-<a href="/admin/jogos/edit/<?= $j['id'] ?>" class="bg-slate-800 hover:bg-slate-700 text-gray-200 border border-white/10 px-3 py-1 rounded-lg text-xs font-semibold uppercase tracking-wider transition-colors inline-block">
+<a href="/admin/jogos/edit/<?= esc($j['id']) ?>" class="bg-slate-800 hover:bg-slate-700 text-gray-200 border border-white/10 px-3 py-1 rounded-lg text-xs font-semibold uppercase tracking-wider transition-colors inline-block">
@@
-<form action="/admin/jogos/delete/<?= $j['id'] ?>" method="POST" class="inline-block" onsubmit="return confirm('Excluir este jogo?')">
+<form action="/admin/jogos/delete/<?= esc($j['id']) ?>" method="POST" class="inline-block" onsubmit="return confirm('Excluir este jogo?')">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Views/admin/jogos/index.php` around lines 51 - 55, The template injects
raw IDs into HTML attributes via <?= $j['id'] ?> which risks unsafe attribute
content; update both occurrences (the Edit link href and the form action) to
output an escaped attribute-safe value using the framework's esc helper (e.g.,
esc($j['id'], 'attr')) so the ID is HTML/attribute-escaped before interpolation;
locate the two spots referencing $j['id'] in the jogos index view and replace
them accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Controllers/AuthController.php`:
- Line 16: The code currently creates a public fallback admin via
ensureBootstrapAdmin() (called from AuthController::login), which inserts a
predictable admin account; remove the automatic bootstrap from the login flow
and alter ensureBootstrapAdmin() so it no longer inserts a default account —
instead it should either (a) error/throw when no admin exists with a clear
message instructing operators to create the first admin via seeder/CLI, or (b)
require explicit credentials from environment variables (e.g., read
ADMIN_EMAIL/ADMIN_PASSWORD) and only create an admin if those env vars are set
and non-default. Update AuthController (remove the call to
ensureBootstrapAdmin() near the login path) and adjust the
ensureBootstrapAdmin() implementation (also referenced in the block around lines
102-123) accordingly so no predictable credentials are ever created
automatically.

In `@app/Controllers/ClienteController.php`:
- Around line 124-149: The adicionarSaldo() action currently allows any
authenticated cliente to directly credit saldo_carteira by calling
ClienteModel::creditarSaldo with the posted valor, which permits creating
arbitrary wallet funds; change this by disabling direct crediting and wiring in
a verified funding flow: remove or block direct calls to
ClienteModel::creditarSaldo in adicionarSaldo(), require an external payment
confirmation token or transaction reference from a trusted payment gateway, add
an idempotency key and server-side reconciliation step that verifies payment
status before calling creditarSaldo, and enforce admin or automated approval for
balance changes along with audit logging of transaction IDs and requester IDs;
alternatively gate the route behind a feature flag until the full
funding/reconciliation workflow is implemented.

In `@app/Models/ApostaModel.php`:
- Around line 181-205: Lock the aposta row in the same DB transaction before
re-checking status and performing any wallet debit/refund: replace the plain
read via getApostaComDetalhes (or add a new getApostaComDetalhesForUpdate) with
a SELECT ... FOR UPDATE (or your framework's forUpdate()/lockForUpdate() call)
inside the existing transaction, then re-check $aposta['status'] and
$aposta['data_horario'] and only afterwards perform the saldo_carteira update
and other changes; apply the same for the other flow referenced around lines
234-249 so both paths acquire the row lock first and avoid concurrent
double-debits/credits, and ensure proper transCommit/transRollback handling
after the locked operations.
- Around line 156-170: registrarApostaTransacional and
atualizarApostaTransacional currently mutate cliente.saldo_carteira before
calling Model::insert()/update() and only check the transaction after
transComplete(), risking committed balance changes when insert/update returned
false; fix by capturing the return value of $this->insert(...) and
$this->update(...) (and/or checking $this->errors()), and if either returns
false explicitly call $this->db->transRollback() (or
$this->db->transStatus(false) semantics) and return/throw an error before
calling $this->db->transComplete(); ensure you still roll back when
getInsertID() is invalid and only commit via transComplete() after successful
insert/update.

---

Major comments:
In `@app/Config/Routes.php`:
- Line 40: The route registering the debug endpoint
($routes->get('liquidacao/debug', 'LiquidacaoController::debug')) exposes
sensitive admin debug functionality; wrap or remove this registration so it is
not available in production—e.g., only register the route when ENVIRONMENT !==
'production' or when running in a dev/test environment, or move it behind a
strict admin-only feature flag/auth check; locate the entry in Routes.php and
guard the LiquidacaoController::debug route accordingly so it is never published
in production.

In `@app/Controllers/CampeonatoController.php`:
- Around line 56-61: store(), update(), and delete() call
$campeonatoModel->insert()/update()/delete() and always flash a success message
without checking their return values; change each method (store, update, delete
in CampeonatoController) to verify the model call returned a truthy value and
only flash success on true, otherwise collect model errors
($campeonatoModel->errors() or $campeonatoModel->errors() +
$campeonatoModel->validation->getErrors()) and redirect back with an error
flash; also wrap delete() in try/catch like store()/update() to handle
exceptions and treat a false return as a failure to avoid false success
messages.

In `@app/Controllers/JogoController.php`:
- Around line 68-85: The current store() flow calls $jogoModel->save(...) but
only checks $jogoModel->errors(), so a false return from save (DB/constraint
failure) can still trigger the success redirect; change this by capturing the
save() return value (e.g., $saved = $jogoModel->save(...)), and if $saved ===
false then log the failure and return
redirect()->back()->withInput()->with('error', 'Não foi possível salvar o
jogo.') (and include $jogoModel->errors() if present); keep the existing
Throwable catch as-is but ensure the success redirect only runs when $saved is
truthy. Ensure you reference $jogoModel and its save() and errors() methods in
the fix.

In `@app/Controllers/TimeController.php`:
- Around line 57-63: The insert/update calls in TimeController may return false
on validation or DB failure but the code always redirects with a success
message; change the logic in the methods that call TimeModel->insert(...) and
TimeModel->update(...) so you capture the return value (result =
$timeModel->insert(...) / $timeModel->update(...)), check if it's === false, and
on failure redirect back with an error flash and include model errors (e.g.,
$timeModel->errors()) or DB error info; only emit the success flash and redirect
to /admin/times when the insert/update returned a truthy result.
- Around line 110-112: The current TimeController code calls
$timeModel->delete((int) $id) and immediately returns a success redirect; update
this to capture the delete result (the return value of TimeModel::delete), check
if it indicates success (truthy/affected rows), and only
redirect()->to('/admin/times')->with('success', ...) when true; otherwise
redirect back or to the same route with with('error', 'Falha ao remover o
horário.') or log the failure and show an error. Ensure you reference the delete
call in TimeController and handle exceptions thrown by the model (try/catch) to
return an error response if an exception occurs.

In `@app/Controllers/UsuarioController.php`:
- Around line 63-84: Wrap the multi-table write in a DB transaction: in
UsuarioController around the blocks that call ClienteModel->insert(),
ClienteModel->getInsertID() and UsuarioModel->insert() (the block using
UsuarioModel and ClienteModel in the lines shown and the similar block at
105-136), begin a transaction before the first insert (use the model's DB
connection or \Config\Database::connect()->transBegin()), perform the cliente
insert and capture clienteId, perform the usuario insert, then transCommit() on
success; on any exception or failure call transRollback() and surface the error.
Ensure you use the same DB connection for both inserts (model->db or the shared
$db) so commit/rollback covers both operations.

In `@app/Database/Migrations/2026-05-22-000001_AddJogoLiquidacaoFields.php`:
- Around line 12-16: The migration AddJogoLiquidacaoFields currently uses raw
MySQL DDL via $this->db->query and fieldExists (ALTER TABLE ... ADD COLUMN with
ENUM(...) and AFTER status) which is MySQL-only; change it to be DB-driver aware
and/or use Forge portable column operations: detect the driver via
$this->db->DBDriver (or equivalent) and if it's a MySQL driver run the existing
ENUM + AFTER SQL, otherwise add compatible columns using $this->forge->addColumn
or $this->db->query with portable types (e.g., VARCHAR or SMALLINT +
nullable/default) and omit the AFTER clause; keep the existing fieldExists
checks around both branches so you only add columns when missing.

In `@app/Database/Seeds/AdminSeeder.php`:
- Around line 12-13: The seeder currently falls back to predictable defaults for
$email and $senha; update AdminSeeder (run method) to not use hardcoded
fallbacks—read env('ADMIN_EMAIL') and env('ADMIN_PASSWORD') without defaulting,
validate that both are present and non-empty, and if missing throw a clear
exception (or abort seeding) so a privileged account is never silently created;
alternatively, generate a secure random password and surface it explicitly if
automatic credentials are required. Ensure you update references to $email and
$senha accordingly and add a short error message that includes which env var is
missing.

In `@app/Models/ClienteModel.php`:
- Around line 46-49: The current return in creditarSaldo() only uses
$this->db->transStatus() so an update that affects 0 rows (nonexistent
cliente_id) can report success; after performing the update, check the number of
affected rows (e.g. via $this->db->affectedRows() or the model's affectedRows())
and combine that with transStatus() to set 'success' and an appropriate
'message' (e.g. "Nenhum cliente encontrado" when affected rows == 0), replacing
the existing return that only references transStatus().
- Around line 41-42: The current code uses abs($valor) when updating
'saldo_carteira', which silently flips sign and hides invalid input; remove
abs(), and instead validate the input beforehand (e.g., if ($valor <= 0)
throw/return an error) so only positive amounts are accepted for credit
operations; keep the DB arithmetic explicit by concatenating $valor (not
abs($valor)) in the set expression and apply the same validation/fix to the
other occurrence that updates 'saldo_carteira' in this file (the second block at
lines ~58-60).

In `@app/Models/UsuarioModel.php`:
- Around line 47-53: criarUsuario currently proceeds to $this->insert even when
$dados['senha'] is empty; add a validation at the start of criarUsuario to block
creation when senha is missing/empty (do not call $this->insert in that case) —
either throw a clear exception (e.g., InvalidArgumentException) or return a
standard error array consistent with the method's return type, and only hash the
password and call $this->insert($dados) when senha is present and non-empty so
no account is created with a blank/unhashed password.
- Around line 69-73: The success flag currently uses $this->db->affectedRows()
>= 0 which is always true; instead capture and use the return value of update()
as the authoritative success indicator (e.g., call $result = $this->update($id,
$dados) and return ['success' => (bool) $result]) — update the atualizarUsuario
(or the method containing update()) to stop relying on $this->db->affectedRows()
and use $result from update().

In `@app/Services/ApostaService.php`:
- Around line 31-56: In registrar, validate required keys and values in $dados
before indexing: ensure 'jogo_id' and 'valor' (and 'tipo') exist, are numeric,
and that 'valor' > 0 (and 'jogo_id' is an integer) before calling
$this->jogoModel->find or accessing $jogo[...] or calling
apostaModel->registrarApostaTransacional; return a clear
['success'=>false,'message'=>...] on validation failure. Apply the same
pre-checks to the other block referenced (lines 61-84) so neither registrar nor
the later transaction attempts to read undefined array keys or accept
non-positive bet amounts. Ensure you reference oddMap/$tipo validation after
these checks so you only access $oddMap[$tipo]['campo'] when $tipo is present
and valid.
- Around line 25-27: In dashboard(), avoid calling
getMinhasApostasComDetalhes($clienteId) twice: call
$this->apostaModel->getMinhasApostasComDetalhes($clienteId) once, store the
result in a local variable (e.g. $minhasApostas) and use that variable for both
the 'historico' and 'apostas' array entries while leaving the 'resumo' call
unchanged; update references in the method so only one DB query is executed.

In `@app/Services/AuthService.php`:
- Around line 49-50: The cliente insert result is not being checked before
calling $this->db->insertID() and proceeding to the usuario insert; update the
logic around the $this->db->table('cliente')->insert($clienteData) call to
capture its return value and if it indicates failure (false / no affected rows)
immediately handle it (throw/return error and rollback any transaction) and do
not call $this->db->insertID() or attempt the usuario insert. Do the same check
before calling $this->db->table('usuario')->insert(...) so both insert
operations validate their results and maintain deterministic transaction
handling.
- Around line 43-58: The code currently trusts $dados['perfil'] and passes it
into the usuario insert, enabling privilege escalation and risking
undefined-index errors; instead force the public registration profile to
'cliente' and ensure $clienteId is initialized. Update the method in AuthService
(the block that inserts into 'cliente' and then into 'usuario') to: initialize
$clienteId = null before the conditional, use a safe $perfil = 'cliente' (do not
read $dados['perfil']) for this flow, create the cliente row when $perfil ===
'cliente' and then insert into 'usuario' with 'perfil' => $perfil and
'cliente_id' => $clienteId. Ensure you still hash the password with
password_hash as before.

In `@app/Views/auth/cadastro.php`:
- Line 113: Remova o campo oculto <input name="perfil"> do formulário em
cadastro.php e, no código do backend que processa o cadastro (o
handler/controller/método responsável pelo endpoint de registro), passe a
ignorar qualquer campo "perfil" vindo do cliente e atribuir explicitamente
perfil = 'cliente' antes de persistir; além disso adicione
validação/whitelisting do payload para garantir que valores de perfil vindos do
cliente sejam descartados.

In `@app/Views/cliente/dashboard.php`:
- Around line 88-95: The three bet-type controls (buttons with name="tipo" and
values "casa", "empate", "fora") are submitting the form when clicked; change
their HTML to non-submitting controls (e.g., set type="button" or convert to
radio inputs) so selection does not trigger form submission, and ensure the
dedicated submit button (the button with type="submit" next to the input named
"valor") remains the only way to submit the form.

In `@database/schema.sql`:
- Line 46: The schema change added 'cancelada' to the aposta.status enum but no
migration was provided; create a new SQL migration that updates the existing
enum type used by aposta.status to include 'cancelada' (or create a new type and
swap it), and ensure existing column default and constraints are adjusted
accordingly so upgrades don't fail; reference the aposta.status column/enum in
your migration and include both an up (add 'cancelada') and down (remove
'cancelada') step so environments can migrate forward and backward safely.

---

Minor comments:
In `@app/Controllers/BaseController.php`:
- Around line 78-81: getClienteSaldo() currently returns 0.0 if
$this->session->get('cliente_id') is missing; mirror the existing fallback
pattern used for other user fields by checking the alternate session keys before
returning 0.0. Update getClienteSaldo() to attempt
$this->session->get('cliente_id'), then the alternate key(s) used elsewhere (for
example 'clienteId' or the nested user key used in other methods), and only
return 0.0 if none of those keys yield a value; ensure you reference the
getClienteSaldo() method and the 'cliente_id' session key when making the
changes.

In `@app/Views/admin/jogos/index.php`:
- Around line 51-55: The template injects raw IDs into HTML attributes via <?=
$j['id'] ?> which risks unsafe attribute content; update both occurrences (the
Edit link href and the form action) to output an escaped attribute-safe value
using the framework's esc helper (e.g., esc($j['id'], 'attr')) so the ID is
HTML/attribute-escaped before interpolation; locate the two spots referencing
$j['id'] in the jogos index view and replace them accordingly.

In `@app/Views/admin/times/index.php`:
- Around line 43-47: The anchor href and form action currently inject $t['id']
directly into HTML attributes (the occurrences of <?= $t['id'] ?>) and need to
be escaped; update both locations to output an escaped/URL-safe id (e.g., use
your framework helper like esc($t['id'], 'attr') or
htmlspecialchars(rawurlencode($t['id']), ENT_QUOTES, 'UTF-8')) so the values are
safe in attribute context and in the URL.

In `@app/Views/admin/usuarios/index.php`:
- Around line 45-50: The user ID is interpolated directly into the link and form
action via $u['id'], which can produce malformed attributes or XSS; update the
href "/admin/usuarios/edit/<?= $u['id'] ?>" and the form action
"/admin/usuarios/delete/<?= $u['id'] ?>" to output a sanitized/cast value (e.g.,
cast to int or use the framework esc() helper) so the ID is safely rendered;
change both the anchor and form action usages that reference $u['id']
accordingly.

In `@app/Views/cliente/adicionar_saldo.php`:
- Line 20: The input field with id="valor" and name="valor" loses its submitted
value after validation errors; update the <input id="valor" name="valor" ...> to
set its value from the previous request (e.g., use old('valor') or the
framework's set_value('valor')) so the field repopulates on redirect after
validation failure, ensuring you escape/format the value consistently (keep
step/min/placeholder as-is).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 293e53eb-eac7-4c76-9f6f-2523a6f5580f

📥 Commits

Reviewing files that changed from the base of the PR and between 32f9f9f and 26a6d25.

⛔ Files ignored due to path filters (1)
  • writable/image.png is excluded by !**/*.png
📒 Files selected for processing (45)
  • Dockerfile
  • README.md
  • app/Config/Database.php
  • app/Config/Routes.php
  • app/Controllers/AdminController.php
  • app/Controllers/AuthController.php
  • app/Controllers/BaseController.php
  • app/Controllers/CampeonatoController.php
  • app/Controllers/ClienteController.php
  • app/Controllers/JogoController.php
  • app/Controllers/LiquidacaoController.php
  • app/Controllers/TimeController.php
  • app/Controllers/UsuarioController.php
  • app/Database/Migrations/2026-05-22-000001_AddJogoLiquidacaoFields.php
  • app/Database/Seeds/AdminSeeder.php
  • app/Filters/AdminFilter.php
  • app/Filters/AuthFilter.php
  • app/Filters/ClienteFilter.php
  • app/Filters/GuestFilter.php
  • app/Models/ApostaModel.php
  • app/Models/CampeonatoModel.php
  • app/Models/ClienteModel.php
  • app/Models/JogoModel.php
  • app/Models/ResolucaoModel.php
  • app/Models/TimeModel.php
  • app/Models/UsuarioModel.php
  • app/Services/ApostaService.php
  • app/Services/AuthService.php
  • app/Views/admin/campeonatos/form.php
  • app/Views/admin/campeonatos/index.php
  • app/Views/admin/dashboard.php
  • app/Views/admin/jogos/form.php
  • app/Views/admin/jogos/index.php
  • app/Views/admin/liquidacao/debug.php
  • app/Views/admin/liquidacao/index.php
  • app/Views/admin/times/form.php
  • app/Views/admin/times/index.php
  • app/Views/admin/usuarios/form.php
  • app/Views/admin/usuarios/index.php
  • app/Views/auth/cadastro.php
  • app/Views/auth/login.php
  • app/Views/cliente/adicionar_saldo.php
  • app/Views/cliente/dashboard.php
  • app/Views/layouts/master.php
  • database/schema.sql
✅ Files skipped from review due to trivial changes (3)
  • Dockerfile
  • README.md
  • app/Views/auth/login.php


public function login()
{
$this->ensureBootstrapAdmin();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Do not create a public fallback admin with known credentials.

/login now triggers ensureBootstrapAdmin(), and when no admin exists it inserts admin@pimbastic.local / admin123. On a fresh deploy or after data repair, that gives anyone a predictable privileged account. Bootstrap the first admin through seeding/CLI only, and require explicit env-provided credentials instead of shipping defaults.

Suggested direction
 public function login()
 {
-    $this->ensureBootstrapAdmin();
-
     return view('auth/login', ['title' => 'Login - Pimbastic Esports']);
 }
 
 private function ensureBootstrapAdmin(): void
 {
+    if (!is_cli() || ENVIRONMENT === 'production') {
+        return;
+    }
+
     $usuarioModel = new UsuarioModel();
@@
-    $email = strtolower(trim((string) (env('ADMIN_EMAIL') ?: 'admin@pimbastic.local')));
-    $senha = (string) (env('ADMIN_PASSWORD') ?: 'admin123');
+    $email = strtolower(trim((string) env('ADMIN_EMAIL')));
+    $senha = (string) env('ADMIN_PASSWORD');
+
+    if ($email === '' || $senha === '') {
+        return;
+    }

Also applies to: 102-123

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Controllers/AuthController.php` at line 16, The code currently creates a
public fallback admin via ensureBootstrapAdmin() (called from
AuthController::login), which inserts a predictable admin account; remove the
automatic bootstrap from the login flow and alter ensureBootstrapAdmin() so it
no longer inserts a default account — instead it should either (a) error/throw
when no admin exists with a clear message instructing operators to create the
first admin via seeder/CLI, or (b) require explicit credentials from environment
variables (e.g., read ADMIN_EMAIL/ADMIN_PASSWORD) and only create an admin if
those env vars are set and non-default. Update AuthController (remove the call
to ensureBootstrapAdmin() near the login path) and adjust the
ensureBootstrapAdmin() implementation (also referenced in the block around lines
102-123) accordingly so no predictable credentials are ever created
automatically.

Comment on lines +124 to +149
public function adicionarSaldo()
{
$clienteId = (int) ($this->session->get('cliente_id') ?? 0);

if ($clienteId <= 0) {
return redirect()->to('/login')->with('error', 'Cliente não autenticado.');
}

$regras = [
'valor' => 'required|numeric|greater_than[0]'
];

if (!$this->validate($regras)) {
return redirect()->back()->withInput()->with('error', $this->validator->getErrors());
}

$valor = (float) $this->request->getPost('valor');

$clienteModel = new \App\Models\ClienteModel();
$resultado = $clienteModel->creditarSaldo($clienteId, $valor);

if (!$resultado['success']) {
return redirect()->back()->with('error', $resultado['message']);
}

return redirect()->to('/cliente/sportsbook')->with('success', "Aposta de R$ " . number_format($valor, 2, ',', '.') . " no resultado ($tipoAposta) registrada com sucesso! (Mock)");
return redirect()->to('/cliente/dashboard')->with('success', $resultado['message']);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

This endpoint lets any cliente mint wallet balance by posting a number.

After authentication, adicionarSaldo() credits saldo_carteira directly from the submitted valor, with no payment confirmation, reconciliation step, idempotency key, or admin approval. For a betting wallet, this is effectively free money and will corrupt financial state. This route should stay disabled until it is backed by a verified funding flow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Controllers/ClienteController.php` around lines 124 - 149, The
adicionarSaldo() action currently allows any authenticated cliente to directly
credit saldo_carteira by calling ClienteModel::creditarSaldo with the posted
valor, which permits creating arbitrary wallet funds; change this by disabling
direct crediting and wiring in a verified funding flow: remove or block direct
calls to ClienteModel::creditarSaldo in adicionarSaldo(), require an external
payment confirmation token or transaction reference from a trusted payment
gateway, add an idempotency key and server-side reconciliation step that
verifies payment status before calling creditarSaldo, and enforce admin or
automated approval for balance changes along with audit logging of transaction
IDs and requester IDs; alternatively gate the route behind a feature flag until
the full funding/reconciliation workflow is implemented.

Comment on lines +156 to +170
$this->insert([
'cliente_id' => (int) $dados['cliente_id'],
'jogo_id' => (int) $dados['jogo_id'],
'valor' => $valor,
'tipo_escolhido' => $dados['tipo_escolhido'],
'odd_escolhida' => round((float) $dados['odd_escolhida'], 2),
'status' => 'aberta',
'criado_em' => date('Y-m-d H:i:s'),
]);

$apostaId = (int) $this->getInsertID();

$this->db->transComplete();

if ($this->db->transStatus() === false) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print relevant portions of the model around the mentioned line ranges
FILE="app/Models/ApostaModel.php"

echo "== File info =="
wc -l "$FILE"
echo

echo "== registrarApostaTransacional (best-effort context) =="
# Print around function definition if present
rg -n "function\s+registrarApostaTransacional" "$FILE" || true
LINE=$(rg -n "function\s+registrarApostaTransacional" "$FILE" | head -n1 | cut -d: -f1 || true)
if [[ -n "${LINE:-}" ]]; then
  START=$((LINE-40))
  END=$((LINE+220))
  sed -n "${START},${END}p" "$FILE" | nl -ba | sed 's/^/  /'
else
  # Fall back: print around 120-280
  sed -n '120,280p' "$FILE" | nl -ba | sed 's/^/  /'
fi
echo

echo "== atualizarApostaTransacional (best-effort context) =="
rg -n "function\s+atualizarApostaTransacional" "$FILE" || true
LINE=$(rg -n "function\s+atualizarApostaTransacional" "$FILE" | head -n1 | cut -d: -f1 || true)
if [[ -n "${LINE:-}" ]]; then
  START=$((LINE-40))
  END=$((LINE+220))
  sed -n "${START},${END}p" "$FILE" | nl -ba | sed 's/^/  /'
else
  sed -n '180,340p' "$FILE" | nl -ba | sed 's/^/  /'
fi
echo

echo "== cancelarApostaTransacional (best-effort context) =="
rg -n "function\s+cancelarApostaTransacional" "$FILE" || true
LINE=$(rg -n "function\s+cancelarApostaTransacional" "$FILE" | head -n1 | cut -d: -f1 || true)
if [[ -n "${LINE:-}" ]]; then
  START=$((LINE-40))
  END=$((LINE+220))
  sed -n "${START},${END}p" "$FILE" | nl -ba | sed 's/^/  /'
else
  true
fi

echo
echo "== Around lines 140-260 for direct review pointer =="
sed -n '130,260p' "$FILE" | nl -ba | sed 's/^/  /'

Repository: p3drobitencourt/pimbastic_esports

Length of output: 316


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="app/Models/ApostaModel.php"

echo "== File info =="
wc -l "$FILE"
echo

print_with_numbers () {
  local start=$1 end=$2
  awk -v s="$start" -v e="$end" 'NR>=s && NR<=e { printf "%6d  %s\n", NR, $0 }' "$FILE"
}

echo "== Direct pointer: lines 150-230 =="
print_with_numbers 140 240
echo

echo "== registrarApostaTransacional (function body) =="
LINE=$(rg -n "function[[:space:]]+registrarApostaTransacional" "$FILE" | head -n1 | cut -d: -f1 || true)
if [[ -n "${LINE:-}" ]]; then
  START=$((LINE-10))
  END=$((LINE+170))
  print_with_numbers "$START" "$END"
fi
echo

echo "== atualizarApostaTransacional (function body) =="
LINE=$(rg -n "function[[:space:]]+atualizarApostaTransacional" "$FILE" | head -n1 | cut -d: -f1 || true)
if [[ -n "${LINE:-}" ]]; then
  START=$((LINE-10))
  END=$((LINE+200))
  print_with_numbers "$START" "$END"
fi
echo

echo "== cancelarApostaTransacional (function body) =="
LINE=$(rg -n "function[[:space:]]+cancelarApostaTransacional" "$FILE" | head -n1 | cut -d: -f1 || true)
if [[ -n "${LINE:-}" ]]; then
  START=$((LINE-10))
  END=$((LINE+200))
  print_with_numbers "$START" "$END"
fi

Repository: p3drobitencourt/pimbastic_esports

Length of output: 29412


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="app/Models/ApostaModel.php"

echo "== Search for CI4 model validation / hooks =="
rg -n "(validationRules|skipValidation|beforeInsert|afterInsert|beforeUpdate|afterUpdate|protectFields|allowedFields)" "$FILE" || true

echo
echo "== Top of file (first ~140 lines) =="
sed -n '1,140p' "$FILE" | nl -ba 2>/dev/null || sed -n '1,140p' "$FILE"

Repository: p3drobitencourt/pimbastic_esports

Length of output: 5342


🌐 Web query:

CodeIgniter 4 Model insert returns false on validation failure transaction transStatus behavior

💡 Result:

In CodeIgniter 4, the insert() method in the Model class performs data validation automatically before executing any database queries [1]. If validation fails, the insert() method returns false and the database operation is never performed [2][1]. Regarding transaction behavior, this creates a common pitfall: if you start a database transaction and then call insert(), a validation failure will prevent the insert, but it will not automatically trigger a transaction rollback [2][3]. Because the model’s validation logic runs at the PHP level before the database is ever touched, the transaction remains "open" or "pending" [2]. If you continue your execution logic or perform other database operations within that same transaction block, those subsequent operations may proceed, leading to partial data commits [2]. To ensure data integrity, you must explicitly check both the success of the model's operation and the transaction status [2][4]. Recommended approaches include: 1. Manual Verification: Always check if the insert() result is false and use $db->transStatus() to check if the transaction has failed [2][5]. If either is false, you should manually call $db->transRollback() [2]. if ($db->transStatus === FALSE || $model->insert($data) === FALSE) { $db->transRollback; // Handle error } else { $db->transCommit; } 2. Controller-Level Validation: Validate your data at the controller level before even starting the transaction [2][4]. This prevents the transaction from opening if the input data is invalid, avoiding the "partial commit" issue entirely [2]. $isValid = $this->validate($validationRules); if ($isValid) { $db->transStart; $model->insert($data); $db->transComplete; } Using the second approach is generally preferred as it keeps your database transaction blocks clean and focused only on actual database operations [2].

Citations:


🌐 Web query:

CodeIgniter 4 Model update validationRules partial data required fields missing behavior

💡 Result:

In CodeIgniter 4, when you call Model->update (or updateBatch/replace), validation typically behaves like this: only the fields that are actually present in the data being updated are validated; required rules for fields not present in the update payload will not fire. This is intentional to support partial updates, and can lead to “missing required fields still passing validation” if you expect full-row validation on update. [1] To control this behavior there is a model setting/option called $cleanValidationRules / cleanRules. When set, CodeIgniter removes validation rules for fields that are not present in the passed-in data (“used between inserts/updates”). [2] Also, the Model documentation for in-model validation explicitly states that “When you update data, by default, the validation in the model class only validates provided fields … incomplete data may pass the validation,” and that you can change the behavior by configuration, referring to $cleanValidationRules. [1] Important related detail: 1) During updates, using $validationRules with required rules means “required” is only evaluated for fields included in the update data, because CI is removing rules for missing fields. [1][2] 2) Historically, $cleanValidationRules had issues/edge cases for inserts/updates; CI4 maintainers added/fixed behavior so update uses the setting and insert forces it off (so inserts validate the full rules set). [3] (So if you’re seeing unexpected behavior around cleanValidationRules, make sure you’re on a version where this fix is present and that you’re testing update specifically.) 3) If you pass an empty dataset to model validation, validation may not run at all in some situations (there’s a known bug discussion: Model->validate returns TRUE if $data is empty). This can also look like “required fields missing” if the entity/array ends up empty. [4] Practical implications for “partial data required fields missing behavior” - If you want partial updates to succeed even when some “required” fields are omitted, then CI’s default behavior (validate only provided fields) is correct. [1] - If you want required rules to be enforced on update even when the payload is partial, you need to change the $cleanValidationRules behavior (or perform validation yourself in the controller with Form Validation / a separate rule set that ensures all required fields are present). CI’s docs indicate you can adjust behavior via $cleanValidationRules. [1] Grounding in official docs - “When you update data, by default, the validation in the model class only validates provided fields… incomplete data may pass the validation.” [1] - “Whether rules should be removed that do not exist in the passed in data… Used between inserts/updates.” [2] If you tell me your CI4 version and show your model snippet (especially $validationRules and whether you set/override $cleanValidationRules or call cleanRules), I can translate this into the exact expected behavior for your specific case and suggest the correct configuration.

Citations:


Rollback when Model::insert()/Model::update() fails to avoid committing saldo without persisting apuesta

app/Models/ApostaModel.php debits/credits cliente.saldo_carteira before persisting the bet (registrarApostaTransacional: saldo debit lines 145-149 → insert() lines 156-164; atualizarApostaTransacional: saldo adjust lines 197-213 → update() lines 215-219). The insert()/update() return values are ignored, and you only check transStatus() after transComplete(). In CI4, model validation failures can make insert()/update() return false without automatically failing the transaction, so the balance mutation can commit while the aposta row is not created/updated.

💡 Minimal guard pattern
-        $this->insert([
+        if (!$this->insert([
             'cliente_id' => (int) $dados['cliente_id'],
             'jogo_id' => (int) $dados['jogo_id'],
             'valor' => $valor,
             'tipo_escolhido' => $dados['tipo_escolhido'],
             'odd_escolhida' => round((float) $dados['odd_escolhida'], 2),
             'status' => 'aberta',
             'criado_em' => date('Y-m-d H:i:s'),
-        ]);
+        ])) {
+            $this->db->transRollback();
+            return ['success' => false, 'message' => implode(' ', $this->errors() ?: ['Falha ao registrar aposta.'])];
+        }
@@
-        $this->update($apostaId, [
+        if (!$this->update($apostaId, [
             'valor' => $novoValor,
             'tipo_escolhido' => $dados['tipo_escolhido'],
             'odd_escolhida' => $novoOdd,
-        ]);
+        ])) {
+            $this->db->transRollback();
+            return ['success' => false, 'message' => implode(' ', $this->errors() ?: ['Falha ao atualizar aposta.'])];
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/ApostaModel.php` around lines 156 - 170,
registrarApostaTransacional and atualizarApostaTransacional currently mutate
cliente.saldo_carteira before calling Model::insert()/update() and only check
the transaction after transComplete(), risking committed balance changes when
insert/update returned false; fix by capturing the return value of
$this->insert(...) and $this->update(...) (and/or checking $this->errors()), and
if either returns false explicitly call $this->db->transRollback() (or
$this->db->transStatus(false) semantics) and return/throw an error before
calling $this->db->transComplete(); ensure you still roll back when
getInsertID() is invalid and only commit via transComplete() after successful
insert/update.

Comment on lines +181 to +205
$aposta = $this->getApostaComDetalhes($apostaId, $clienteId);

if (!$aposta || $aposta['status'] !== 'aberta') {
$this->db->transRollback();
return ['success' => false, 'message' => 'Aposta não encontrada ou não pode ser atualizada.'];
}

if (strtotime($aposta['data_horario']) <= time()) {
$this->db->transRollback();
return ['success' => false, 'message' => 'Não é possível atualizar apostas de jogos iniciados.'];
}

$novoValor = round((float) $dados['valor'], 2);
$novoOdd = round((float) $dados['odd_escolhida'], 2);
$diferenca = round($novoValor - (float) $aposta['valor'], 2);

if ($diferenca > 0) {
$debitado = $this->db->table('cliente')
->where('id', $clienteId)
->where('saldo_carteira >=', $diferenca)
->set('saldo_carteira', 'saldo_carteira - ' . $diferenca, false)
->update();

if (!$debitado || $this->db->affectedRows() === 0) {
$this->db->transRollback();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Lock the bet row before refunding or re-pricing it.

Both flows read the aposta with a plain SELECT and only then move saldo_carteira. Two concurrent requests can both pass the status === 'aberta' check and apply refund/debit logic before either update lands, which can double-credit the wallet or clobber the final status/value.

Also applies to: 234-249

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/ApostaModel.php` around lines 181 - 205, Lock the aposta row in
the same DB transaction before re-checking status and performing any wallet
debit/refund: replace the plain read via getApostaComDetalhes (or add a new
getApostaComDetalhesForUpdate) with a SELECT ... FOR UPDATE (or your framework's
forUpdate()/lockForUpdate() call) inside the existing transaction, then re-check
$aposta['status'] and $aposta['data_horario'] and only afterwards perform the
saldo_carteira update and other changes; apply the same for the other flow
referenced around lines 234-249 so both paths acquire the row lock first and
avoid concurrent double-debits/credits, and ensure proper
transCommit/transRollback handling after the locked operations.

@p3drobitencourt
p3drobitencourt merged commit 26a6d25 into main Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant