Skip to content

Phase 2: fix Civil ID upload and removal flow#82

Open
its-amann wants to merge 3 commits into
BAWES-Universe:masterfrom
its-amann:bounty/issue-55-phase-2-civil-id-fixes
Open

Phase 2: fix Civil ID upload and removal flow#82
its-amann wants to merge 3 commits into
BAWES-Universe:masterfrom
its-amann:bounty/issue-55-phase-2-civil-id-fixes

Conversation

@its-amann
Copy link
Copy Markdown

@its-amann its-amann commented May 15, 2026

This PR covers the Phase 2 backend remediation from #55.

What changed

  • make the Civil ID remove endpoints tolerate missing S3 objects, clear the DB field, and mark verification as needed instead of returning a raw 500
  • validate civil_id and civil_expiry_date in update-civil-id-expiry-date and return operation:error on bad input or save failures
  • fix Candidate::deleteFile("civil-id") to use the correct photos/ prefix and attach errors to the Civil ID field instead of candidate_resume
  • change Candidate::updateCivilId() to copy the new file first, verify the destination, and delete the old file only after the new object is confirmed
  • add route / candidate_id / filename context to Civil ID copy and delete failure logs
  • add S3ResourceManager::headObject() and move fileExists() to S3 headObject checks instead of HTTP HEAD
  • add focused unit coverage for the delete/copy/verify path and regression assertions for invalid Civil ID expiry requests

Verification

  • php -l common/components/S3ResourceManager.php
  • php -l common/models/Candidate.php
  • php -l candidate/modules/v1/controllers/AccountController.php
  • php -d extension=gd -d extension=zip vendor\\bin\\codecept run common\\tests\\unit\\models\\CandidateS3BehaviorTest.php --steps
    • OK (2 tests, 10 assertions)

Notes

  • I also ran php -d extension=gd -d extension=zip vendor\\bin\\codecept run candidate\\tests\\functional\\AccountCest.php --steps locally. That suite is currently blocked here by a missing MySQL test database and fails during bootstrap with SQLSTATE[HY000] [2002] against payroll_test, so I am not claiming that functional suite as locally passing.

Summary by CodeRabbit

  • New Features

    • Stronger civil ID and expiry-date validation with clearer localized error responses
    • Deferred cleanup of replaced civil-ID photos to ensure safe replacement
  • Bug Fixes

    • More reliable document existence checks and error handling for storage operations
    • Civil-photo verification state now correctly updated when photos are removed or replaced
  • Tests

    • Added unit and functional tests covering civil ID validation, file handling, and storage URL parsing

Review Change Stack

/claim #55

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

Warning

Rate limit exceeded

@its-amann has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 43 minutes and 18 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8a05087c-18f3-4075-aa43-03d4d05d774f

📥 Commits

Reviewing files that changed from the base of the PR and between 8d35124 and 892dcf8.

📒 Files selected for processing (3)
  • candidate/modules/v1/controllers/AccountController.php
  • common/models/Candidate.php
  • common/tests/unit/models/CandidateS3BehaviorTest.php
📝 Walkthrough

Walkthrough

Adds S3 URL/key resolution and headObject checks; Candidate model stages and verifies civil-ID file copies, defers deletions, centralizes S3 error logging, and exposes deletePendingCivilIdFiles; controller tightens civil-id/expiry validation, propagates model errors, and marks verification state on photo removal; unit and functional tests added.

Changes

Civil ID and Photo Management

Layer / File(s) Summary
S3 Resource Manager
common/components/S3ResourceManager.php
resolveObjectLocation() parses raw keys or S3 URLs to derive bucket/key; headObject() calls AWS S3 headObject; fileExists() now uses headObject() and returns true/false based on S3 metadata checks.
Candidate model: file resolution & logging
common/models/Candidate.php
Adds pendingCivilIdDeletionKeys, resolveStoredFileAttribute, resolveStoredFileKey, and logS3FileFailure() to centralize S3 key resolution and structured failure logging.
Candidate model: delete/copy lifecycle
common/models/Candidate.php
deleteFile() gains $bestEffort and S3-aware resolution and logging; updateCivilId($side) copies temp file to photos/..., verifies destination exists, defers old-file deletion into pendingCivilIdDeletionKeys; deletePendingCivilIdFiles() performs staged deletions.
Controller API validation & photo handlers
candidate/modules/v1/controllers/AccountController.php, candidate/tests/functional/AccountCest.php
actionUpdateCivilIdExpiryDateAndCivilID trims and regex-validates civil_id, validates/parses civil_expiry_date, normalizes to Y-m-d, wraps save() in try/catch with logging; actionUpdateCivilPhotoBack/Front check updateCivilId() results and return model errors on failure and call deletePendingCivilIdFiles() on success; actionRemoveCivilPhotoBack/Front delete files and set candidate_civil_need_verification = true; functional tests added for expiry/civil_id validation errors.
Unit tests for S3 behavior
common/tests/unit/models/CandidateS3BehaviorTest.php
Adds test-only CandidateS3BehaviorTestModel and CandidateS3BehaviorResourceManager; tests cover S3 URL/key resolution, copy+verify+defer-delete lifecycle, and field-specific error behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through buckets, keys in hand,

Verified copies before they land,
Old files queued for a careful sweep,
Errors logged where secrets keep,
Civil IDs trimmed and dates aligned — a tidy, cautious strand.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "Phase 2: fix Civil ID upload and removal flow" clearly describes the main purpose of the PR: fixing the Civil ID upload and removal workflow, which aligns with the substantial changes across multiple files related to Civil ID handling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dosubot
Copy link
Copy Markdown

dosubot Bot commented May 15, 2026

Related Knowledge

1 document with suggested updates is ready for review.

BAWES Universe

Civil ID Upload Flow
View Suggested Changes
@@ -56,9 +56,10 @@
     BE->>BE: urldecode param, guard against undefined
     BE->>BE: S3FileExistValidator — fileExists(key) in temp bucket
     BE->>BE: Candidate::updateCivilId('front')
-    BE->>BE: deleteFile() — remove old civil ID if present
     BE->>PermS3: copyObject(CopySource: tempBucket/key, Key: photos/key, ACL: public-read)
     PermS3-->>BE: copy result
+    BE->>BE: fileExists(photos/key) — verify copy succeeded
+    BE->>BE: deleteFile($bestEffort: true) — remove old civil ID if verified
     BE->>BE: Reset candidate_civil_expiry_date + candidate_civil_id = null
     BE->>BE: model.save()
     BE-->>App: { operation: "success", candidate_civil_photo_front, civilExpired, ... }
@@ -369,7 +370,12 @@
         return ['operation' => 'error', 'message' => '...'];
     }
 
-    $model->updateCivilId('front');
+    if (!$model->updateCivilId('front')) {
+        return [
+            'operation' => 'error',
+            'message' => $model->getErrors()
+        ];
+    }
 
     // Reset old OCR-extracted data — forces re-extraction on next check
     $model->candidate_civil_expiry_date = null;
@@ -411,25 +417,51 @@
         ? 'candidate_civil_photo_front'
         : 'candidate_civil_photo_back';
 
-    // Delete previous file from permanent bucket if one exists
-    if (!empty($this->oldAttributes[$idSide])) {
-        $this->deleteFile('civil-id', $side);
-    }
-
-    $fileName     = $this->$idSide;
+    $fileName = $this->$idSide;
+
+    if (!$fileName) {
+        $this->addError($idSide, Yii::t('app', 'file not available to save.'));
+        return false;
+    }
+
     $sourceBucket = Yii::$app->temporaryBucketResourceManager->bucket;
     $targetPath   = "photos/" . $fileName;
+    $oldFilePath  = $this->resolveStoredFileKey('civil-id', $side, true);
 
     try {
-        return Yii::$app->resourceManager->copy($fileName, $targetPath, $sourceBucket);
+        Yii::$app->resourceManager->copy($fileName, $targetPath, $sourceBucket);
+
+        if (!Yii::$app->resourceManager->fileExists($targetPath)) {
+            throw new \RuntimeException('Copied file could not be verified in the permanent bucket.');
+        }
+
+        if ($oldFilePath && $oldFilePath !== $targetPath) {
+            $this->deleteFile('civil-id', $side, true);
+        }
+
+        return true;
 
     } catch (\Aws\S3\Exception\S3Exception $e) {
-        Yii::error($e->getMessage(), 'candidate');
+        Yii::error([
+            'message' => 'Failed to copy candidate file',
+            'operation' => 'copy',
+            'route' => Yii::$app->requestedRoute,
+            'candidate_id' => $this->candidate_id,
+            'file_key' => $targetPath,
+            'error' => $e->getMessage(),
+        ], 'candidate');
         $this->addError($idSide, Yii::t('app', 'file not available to save.'));
         return false;
 
     } catch (\Exception $e) {
-        Yii::error($e->getMessage(), 'candidate');
+        Yii::error([
+            'message' => 'Failed to copy candidate file',
+            'operation' => 'copy',
+            'route' => Yii::$app->requestedRoute,
+            'candidate_id' => $this->candidate_id,
+            'file_key' => $targetPath,
+            'error' => $e->getMessage(),
+        ], 'candidate');
         $this->addError($idSide, Yii::t('app', 'file not available to save.'));
         return false;
     }
@@ -438,9 +470,10 @@
 
 Key behaviours:
 
-- **Old file cleanup:** If the candidate already has a civil photo stored (`oldAttributes[$idSide]` is set), the old file is deleted from the permanent bucket before the new one is copied. This prevents orphaned files accumulating.
-- **Target path:** Files are stored as `photos/{filename}` in the permanent bucket.
-- **Error handling:** Both `S3Exception` and generic `\Exception` are caught. Errors are logged to the `candidate` category (routed to Slack/Sentry) and surfaced as a model validation error.
+- **Copy first, verify, then delete:** The method copies the new file to the permanent bucket, verifies it exists using `fileExists()`, and only then deletes the old file (with `$bestEffort = true`). This prevents data loss if the copy fails.
+- **Target path:** Files are stored as `photos/{filename}` in the permanent bucket (not `candidate-civil-id/`).
+- **Error handling:** Both `S3Exception` and generic `\Exception` are caught. Errors are logged to the `candidate` category with structured context (route, candidate ID, file key) and surfaced as a model validation error.
+- **Best-effort deletion:** The old file is deleted with `$bestEffort = true`, which tolerates missing S3 objects instead of failing the upload.
 
 ***
 
@@ -479,6 +512,91 @@
 
 ***
 
+### Candidate Model — deleteFile()
+
+**File:** `common/models/Candidate.php`
+
+The `deleteFile()` method removes a file from the permanent S3 bucket. It supports a `$bestEffort` parameter (third parameter, defaults to `false`):
+
+```php
+public function deleteFile($type = 'resume', $side = 'front', $bestEffort = false) {
+    $attribute = $this->resolveStoredFileAttribute($type, $side);
+    $fileKey = $this->resolveStoredFileKey($type, $side, true);
+
+    if (!$fileKey || !isset($this->oldPrimaryKey)) {
+        return true;
+    }
+
+    try {
+        Yii::$app->resourceManager->delete($fileKey);
+        return true;
+
+    } catch (\Aws\S3\Exception\S3Exception $e) {
+        Yii::error([
+            'message' => 'Failed to delete candidate file',
+            'operation' => 'delete',
+            'route' => Yii::$app->requestedRoute,
+            'candidate_id' => $this->candidate_id,
+            'file_key' => $fileKey,
+            'error' => $e->getMessage(),
+        ], 'candidate');
+
+        if ($bestEffort) {
+            return true;
+        }
+
+        $this->addError($attribute, Yii::t('app', 'file not available to delete.'));
+        return false;
+
+    } catch (\Exception $e) {
+        // Same handling
+    }
+}
+```
+
+Key behaviours:
+
+- **Best-effort mode:** When `$bestEffort = true`, the method returns `true` even if the S3 object doesn't exist or deletion fails. This is used during Civil ID removal to tolerate missing files instead of returning a 500 error.
+- **Correct S3 path prefix:** For `$type = 'civil-id'`, files are resolved to `photos/{filename}` (not `candidate-civil-id/`).
+- **Field-specific errors:** Errors are attached to the correct attribute (`candidate_civil_photo_front` or `candidate_civil_photo_back` for civil-id type, not `candidate_resume`).
+- **Structured logging:** Failures are logged with context including route, candidate ID, and file key.
+
+***
+
+### S3ResourceManager — fileExists() and headObject()
+
+**File:** `common/components/S3ResourceManager.php`
+
+The `fileExists()` method checks whether a file exists in S3 using the AWS SDK's `headObject()` method:
+
+```php
+public function fileExists($filenameOrUrl)
+{
+    try {
+        $this->headObject($filenameOrUrl);
+        return true;
+    } catch (AwsException $e) {
+        return false;
+    } catch (\Exception $e) {
+        return false;
+    }
+}
+
+public function headObject($filenameOrUrl)
+{
+    [$bucket, $key] = $this->resolveObjectLocation($filenameOrUrl);
+
+    return $this->getClient()->headObject([
+        'Bucket' => $bucket,
+        'Key' => $key,
+    ]);
+}
+```
+
+The method now uses the S3 client's `headObject()` call instead of GuzzleHttp with HTTP HEAD requests. This means it works for all S3 objects, not just public resources.
+
+***
+
 ### S3FileExistValidator
 
 **File:** `common/components/S3FileExistValidator.php`
@@ -504,7 +622,7 @@
 }
 ```
 
-The validator uses an HTTP `HEAD` request against the public S3 URL to check existence, and optionally validates file extension and size. [[32]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/common/components/S3FileExistValidator.php#L46-L88)
+The validator calls `S3ResourceManager::fileExists()`, which now uses SDK-based `headObject()` checks instead of HTTP HEAD requests. [[32]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/common/components/S3FileExistValidator.php#L46-L88)
 
 ***
 
@@ -545,8 +663,11 @@
 | Method | Endpoint | Auth | Description |
 |---|---|---|---|
 | `GET` | `/aws/config` | None (CORS only) | Returns temp S3 credentials for direct upload |
-| `POST` | `/v1/account/update-civil-photo-front` | Candidate JWT | Validates S3 key, copies front photo to permanent bucket |
-| `POST` | `/v1/account/update-civil-photo-back` | Candidate JWT | Validates S3 key, copies back photo to permanent bucket |
+| `POST` | `/v1/account/update-civil-photo-front` | Candidate JWT | Validates S3 key, copies front photo to permanent bucket, returns error on copy failure |
+| `POST` | `/v1/account/update-civil-photo-back` | Candidate JWT | Validates S3 key, copies back photo to permanent bucket, returns error on copy failure |
+| `POST` | `/v1/account/remove-civil-photo-front` | Candidate JWT | Removes front Civil ID photo, tolerates missing S3 objects, sets `candidate_civil_need_verification = true` |
+| `POST` | `/v1/account/remove-civil-photo-back` | Candidate JWT | Removes back Civil ID photo, tolerates missing S3 objects, sets `candidate_civil_need_verification = true` |
+| `POST` | `/v1/account/update-civil-id-expiry-date` | Candidate JWT | Validates and saves Civil ID number (12 digits) and expiry date, returns structured errors on invalid input |
 
 **Request bodies:**
 
@@ -666,11 +787,11 @@
 
 #### S3FileExistValidator Guards the Copy Operation
 
-Before the backend copies a file from the temp bucket, it validates that the key actually exists in S3 (`HEAD` request via GuzzleHttp). This prevents an attacker from specifying an arbitrary S3 key and tricking the backend into copying a file they did not upload. [[54]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/common/components/S3FileExistValidator.php#L46-L58)
-
-#### Old Files Deleted Before Copy
-
-`updateCivilId()` deletes the candidate's previously stored civil photo before copying the new one, preventing unbounded growth of files in the permanent bucket. [[55]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/common/models/Candidate.php#L2743-L2745)
+Before the backend copies a file from the temp bucket, it validates that the key actually exists in S3 using `S3ResourceManager::fileExists()`, which calls the AWS SDK's `headObject()` method. This prevents an attacker from specifying an arbitrary S3 key and tricking the backend into copying a file they did not upload. [[54]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/common/components/S3FileExistValidator.php#L46-L58)
+
+#### Old Files Deleted After Copy and Verification
+
+`updateCivilId()` copies the new file first, verifies the copy succeeded using `fileExists()`, and only then deletes the candidate's previously stored civil photo (with `$bestEffort = true`). This prevents data loss if the copy or verification fails, and tolerates missing old files instead of failing the upload. [[55]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/common/models/Candidate.php#L2743-L2745)
 
 ***
 
@@ -705,7 +826,3 @@
 Uploaded files are not scanned for malware before being promoted to the permanent bucket. Civil ID uploads are expected to be images, but the `S3FileExistValidator` only checks extension and size — it does not validate MIME content or scan for malicious payloads. [[59]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/common/components/S3FileExistValidator.php#L61-L88)
 
 **Recommendation:** Consider integrating an AWS Lambda function triggered on temp-bucket `PutObject` events to run ClamAV or a commercial AV solution before the backend is permitted to copy the file.
-
-#### 🟢 Low Priority: `fileExists()` Uses HTTP HEAD (Public Access Required)
-
-The `S3FileExistValidator` checks file existence using a GuzzleHttp `HEAD` request to the public URL of the file [[60]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/common/components/S3ResourceManager.php#L174-L188). This means the file must be publicly accessible in the temp bucket for validation to work. If the ACL recommendation above is ever applied to the temp bucket, this validation approach would need to be replaced with an SDK `headObject` call using credentials.

[Accept] [Edit] [Decline]

How did I do? Any feedback?  Join Discord

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
candidate/tests/functional/AccountCest.php (1)

187-213: ⚡ Quick win

Add a regression for the non-empty invalid Civil ID branch.

These tests cover “missing ID” and “bad date”, but the new preg_match('/^\d{12}$/', ...) rejection is still unasserted. A case like civil_id => '70' should verify the "Civil ID must be 12 digit number" response too.

🤖 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 `@candidate/tests/functional/AccountCest.php` around lines 187 - 213, Add a new
functional test to cover the non-empty invalid civil ID branch: create a method
(e.g., tryUpdateCivilIdExpiryDateRejectsNonEmptyInvalidCivilId) alongside
tryUpdateCivilIdExpiryDateRequiresCivilId and
tryUpdateCivilIdExpiryDateRejectsInvalidDate that sends a POST to
'v1/account/update-civil-id-expiry-date' with 'civil_id' => '70' and a valid
'civil_expiry_date' (e.g., date('+1 month')), then assert HttpCode::OK and that
the JSON response contains 'operation' => 'error' and 'message' => 'Civil ID
must be 12 digit number' to exercise the preg_match('/^\d{12}$/', ...)
validation path.
🤖 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 `@candidate/modules/v1/controllers/AccountController.php`:
- Around line 1492-1499: The log call in the catch block (Yii::error) currently
includes the raw government civil ID (variable candidate_civil_id) which is
sensitive; remove that field (and any other sensitive PII like
candidate->candidate_civil_expiry_date if you deem it sensitive) from the array
passed to Yii::error and only include non-PII for debugging (e.g.,
Yii::$app->requestedRoute, candidate->candidate_id, and $e->getMessage());
alternatively, if you must keep a civil-id indicator, replace candidate_civil_id
with a masked version (e.g., last 2 chars only) before passing it to Yii::error.

In `@common/components/S3ResourceManager.php`:
- Around line 179-188: The URL parsing only extracts virtual-hosted bucket names
and leaves path-style URLs broken and percent-encoded; update the logic in
S3ResourceManager (the code handling $filenameOrUrl and returning [$bucket,
$key]) to detect path-style hosts like "s3[.-]..." where the bucket is the first
segment of the parsed path, set $bucket = array_shift($pathSegments) and $key =
implode('/', $pathSegments), and ensure you rawurldecode() the path (or each
path segment) so percent-encoded characters are decoded before returning the key
for headObject() / fileExists().

In `@common/models/Candidate.php`:
- Around line 2818-2820: The deletion of the old Civil ID file is happening
before the new filename is persisted (see variables $oldFilePath, $targetPath
and the call to $this->deleteFile('civil-id', $side, true)), which can lead to
data loss if the subsequent $model->save() fails; change the flow so the old
file is removed only after the DB update successfully commits — either move the
deleteFile call to run after the controller successfully calls $model->save()
(or inside the model's afterSave()/saved() hook) or perform the create/update
inside a DB transaction and register the file deletion in a commit callback so
deletion occurs only on successful commit.

---

Nitpick comments:
In `@candidate/tests/functional/AccountCest.php`:
- Around line 187-213: Add a new functional test to cover the non-empty invalid
civil ID branch: create a method (e.g.,
tryUpdateCivilIdExpiryDateRejectsNonEmptyInvalidCivilId) alongside
tryUpdateCivilIdExpiryDateRequiresCivilId and
tryUpdateCivilIdExpiryDateRejectsInvalidDate that sends a POST to
'v1/account/update-civil-id-expiry-date' with 'civil_id' => '70' and a valid
'civil_expiry_date' (e.g., date('+1 month')), then assert HttpCode::OK and that
the JSON response contains 'operation' => 'error' and 'message' => 'Civil ID
must be 12 digit number' to exercise the preg_match('/^\d{12}$/', ...)
validation path.
🪄 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: 177b727d-c767-44e5-a530-7fcb5f305df8

📥 Commits

Reviewing files that changed from the base of the PR and between 7b023ff and 33839e5.

📒 Files selected for processing (5)
  • candidate/modules/v1/controllers/AccountController.php
  • candidate/tests/functional/AccountCest.php
  • common/components/S3ResourceManager.php
  • common/models/Candidate.php
  • common/tests/unit/models/CandidateS3BehaviorTest.php

Comment thread candidate/modules/v1/controllers/AccountController.php
Comment thread common/components/S3ResourceManager.php
Comment thread common/models/Candidate.php
@its-amann
Copy link
Copy Markdown
Author

/claim #55

Claiming the Phase 2 backend Civil ID remediation slice covered by this PR. Scope is candidate backend handling for missing-object-safe Civil ID removal, Civil ID expiry validation, corrected photos/ S3 paths, SDK headObject existence checks, and copy-before-delete handling.

Follow-up pushed in 8d35124b addresses the CodeRabbit findings:

  • removed raw Civil ID and expiry date from error logs
  • added path-style S3 URL handling and decoded object keys
  • moved old Civil ID object deletion until after the database save succeeds
  • added regressions for invalid non-empty Civil ID and S3 URL parsing

Verification run locally:

  • php -l candidate/modules/v1/controllers/AccountController.php
  • php -l common/components/S3ResourceManager.php
  • php -l common/models/Candidate.php
  • php -l common/tests/unit/models/CandidateS3BehaviorTest.php
  • php -l candidate/tests/functional/AccountCest.php
  • git diff --check
  • php -d extension=gd -d extension=zip vendor\bin\codecept run common\tests\unit\models\CandidateS3BehaviorTest.php --steps passed with 3 tests, 14 assertions

The full candidate/tests/functional/AccountCest.php run is still blocked locally by the missing MySQL test database at mysql:host=127.0.0.1;dbname=payroll_test, which causes all functional tests to fail before endpoint assertions run.

Companion staff-side Phase 3 UI handling is in BAWES-Universe/studenthub-staff#57. I kept it separate because it touches the staff repo.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@common/models/Candidate.php`:
- Around line 2719-2730: The resolveStoredFileKey method assumes $fileName is
always a bare filename and blindly prepends $directory, causing duplicates like
"photos/photos/..."; update resolveStoredFileKey to normalize existing keys by
checking the resolved $fileName (from $this->oldAttributes or $this->$attribute)
and if it already begins with the intended $directory (or already contains a
path separator indicating a bucket-relative key) return it as-is instead of
prepending, otherwise prepend $directory; keep the existing useOldAttributes
behavior and use resolveStoredFileAttribute($type, $side) to locate the
attribute.
- Around line 2809-2821: The catch blocks in Candidate.php that handle S3
deletion currently unset $this->pendingCivilIdDeletionKeys[$pendingSide] and
allow the method to still return $deleted=true; change the error path so that on
any exception thrown by Yii::$app->resourceManager->delete(...) you do NOT unset
$this->pendingCivilIdDeletionKeys[$pendingSide], keep the entry staged for
retry, call $this->logS3FileFailure('delete', $entry['attribute'],
$entry['file_key'], $e) as already done, and ensure the method returns false
(e.g. set $deleted = false or return false immediately) so callers know the
deletion failed and can retry/alert; preserve the existing successful-path
behavior in the try block where the key is unset.
🪄 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: 1c95aae3-c26a-4082-993c-886d5534f1fa

📥 Commits

Reviewing files that changed from the base of the PR and between 33839e5 and 8d35124.

📒 Files selected for processing (5)
  • candidate/modules/v1/controllers/AccountController.php
  • candidate/tests/functional/AccountCest.php
  • common/components/S3ResourceManager.php
  • common/models/Candidate.php
  • common/tests/unit/models/CandidateS3BehaviorTest.php
🚧 Files skipped from review as they are similar to previous changes (3)
  • candidate/tests/functional/AccountCest.php
  • candidate/modules/v1/controllers/AccountController.php
  • common/components/S3ResourceManager.php

Comment thread common/models/Candidate.php
Comment thread common/models/Candidate.php
@its-amann
Copy link
Copy Markdown
Author

Follow-up pushed in 892dcf89 for the latest CodeRabbit findings:

  • normalized historical stored S3 keys so photos/... does not become photos/photos/...
  • kept failed deferred Civil ID deletions staged and returned false for retry/alert handling
  • exposed civil_photo_cleanup_pending in successful Civil ID upload responses when old-object cleanup fails
  • added focused regressions for normalized stored keys and retryable deferred deletion failures

Verification run locally:

  • php -l common/models/Candidate.php
  • php -l candidate/modules/v1/controllers/AccountController.php
  • php -l common/tests/unit/models/CandidateS3BehaviorTest.php
  • git diff --check
  • php -d extension=gd -d extension=zip vendor\\bin\\codecept run common\\tests\\unit\\models\\CandidateS3BehaviorTest.php --steps passed with 4 tests and 23 assertions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant