Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(MPDZBS-877): zmscitizenapi into next #674

Open
wants to merge 99 commits into
base: next
Choose a base branch
from

Conversation

ThomasAFink
Copy link
Member

@ThomasAFink ThomasAFink commented Nov 6, 2024

Description

Short description or comments

Reference

Issues #XXX

Summary by CodeRabbit

  • New Features

    • Introduced a new module zmscitizenapi with functionalities for managing appointments, services, and scopes.
    • Added support for building and pushing Docker images for the zmscitizenapi module.
    • New routing configuration for the /zmscitizenapi endpoint.
    • Comprehensive API documentation generation and validation functionalities.
    • Added several new controllers for managing appointments, services, and scopes.
    • Implemented new classes for handling captcha verification and managing office-service relations.
    • Enhanced functionality for handling available appointments and days.
    • Introduced new classes for structured error handling and validation processes.
    • Added a new Makefile for development and deployment tasks.
    • Added a new build_swagger.js script for generating OpenAPI specifications.
  • Bug Fixes

    • Improved error handling and response management across various endpoints.
  • Documentation

    • Updated README with instructions for API documentation and setup.
    • Added JSON schemas for various entities and collections.
  • Tests

    • Introduced unit tests for new functionalities including appointment management and service retrieval.
    • Added tests for captcha verification and error handling.
  • Chores

    • Added .gitignore files to streamline version control.
    • Updated PHPUnit configuration for better test management.

ThomasAFink and others added 20 commits September 10, 2024 14:48
#574)

* endpoint working

* fix unit tests

* one working test for post-reserve

* some clean up

* feat(ZMS-2517): fix up response for the reserve post request

* feat(ZMS-2517): add more unit tests for validations

* feat(ZMS-2517): add more unit tests for validations

* feat(ZMS-2517): Zmscitizenapi Refactor Part 1 of 3 move controllers back to root

* feat(ZMS-2517): Zmscitizenapi Refactor Part 2 of 3 create ZmsApiFacadeService ZmsApiClientService ValidationService MapperService UtilityHelper and delete fragmented services

* feat(ZMS-2517): Zmscitizenapi Refactor Part 3 of 3 clean up validations and unit tests

---------

Co-authored-by: Thomas Fink <[email protected]>
* (feat MPDZBS-877) readd citizenapi to  htaccess

* feat(MPDZBS-877 ZMS-2517): Working Appointment update with too many emails exception

* feat(MPDZBS-877 ZMS-2517): Test rendering working for update appointment test

* feat(MPDZBS-877 ZMS-2517): Add 63 validation unit tests for appointment update

* feat(MPDZBS-877 ZMS-2517): Refactor exception appointmentNotFound

* feat(MPDZBS-877 ZMS-2517): Add two new edge case exception tests

* feat(MPDZBS-877 ZMS-2517): Remove error_logs

---------

Co-authored-by: Thomas Fink <[email protected]>
@ThomasAFink ThomasAFink force-pushed the feature-MPDZBS-877-zmscitizenapi branch from b57e687 to 1a34881 Compare November 6, 2024 15:41
@ThomasAFink ThomasAFink force-pushed the feature-MPDZBS-877-zmscitizenapi branch from 1a34881 to c15dea6 Compare November 6, 2024 15:44
Copy link
Contributor

@tobiasholler tobiasholler left a comment

Choose a reason for hiding this comment

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

Bitte schau nochmal wegen Validation und Error Handling, siehe Kommentare.

zmscitizenapi/composer.json Outdated Show resolved Hide resolved
zmscitizenapi/public/doc/README.md Outdated Show resolved Hide resolved
zmscitizenapi/public/doc/partials/basic.yaml Outdated Show resolved Hide resolved
zmscitizenapi/public/doc/partials/definitions.yaml Outdated Show resolved Hide resolved
zmscitizenapi/public/doc/partials/info.yaml Outdated Show resolved Hide resolved
zmsclient/phpunit.xml Outdated Show resolved Hide resolved
zmsdb/paratest.xml Show resolved Hide resolved
zmsdldb/phpunit.xml.dist Show resolved Hide resolved
zmsslim/phpunit.xml.dist Show resolved Hide resolved
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@ThomasAFink
Copy link
Member Author

@coderabbitai I need a new model and schema ThinnedContact

                    'city' => $contact->city ?? '',
                    'country' => $contact->country ?? '',
                    'lat' => $contact->lat ?? 0.0,
                    'lon' => $contact->lon ?? 0.0,
                    'name' => $contact->name ?? '',
                    'postalCode' => $contact->postalCode ?? '',
                    'region' => $contact->region ?? '',
                    'street' => $contact->street ?? '',
                    'streetNumber' => $contact->streetNumber ?? '',

@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@ThomasAFink
Copy link
Member Author

ThomasAFink commented Dec 30, 2024

ddev-zms-web | [30-Dec-2024 16:12:52] WARNING: [pool www] child 65140 said into stderr: "NOTICE: PHP message: Call to undefined method BO\Zmsentities\Collection\ProviderList::first()"

    public static function getScopes(): ScopeList
    {
        $sources = self::fetchSourceData();
        
        $scopeList = $sources->getScopeList() ?? new ScopeList();
    
        $providerList = $sources->getProviderList() ?? new ProviderList();
    
        foreach ($scopeList as $scope) {
            if ($scope->provider->id) {
                $provider = $providerList->first(fn($p) => $p->id === $scope->provider->id);
                if ($provider) {
                    $scope->setProvider($provider);
                }
            }
        }
    
        return $scopeList;
    }
    ```
    
    @coderabbitai

Copy link
Contributor

coderabbitai bot commented Dec 30, 2024

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink: The error occurs because ProviderList does not have a first() method in its implementation. While some frameworks provide a ->first() or ->where() convenience method on collections, your custom BO\Zmsentities\Collection\ProviderList apparently does not. You have a few options to fix this:


1) Add a helper method like getById() to ProviderList

If ProviderList is your own collection class, you can define a method to search by ID. For instance:

// In BO\Zmsentities\Collection\ProviderList
public function getById(int $id): ?Provider
{
    foreach ($this->entities as $provider) {
        if ((int) $provider->id === $id) {
            return $provider;
        }
    }
    return null; // Not found
}

(Adjust property names as appropriate—e.g. $this->data vs. $this->entities—depending on how your collection is stored.)

Then, in your loop, you can do:

$provider = $providerList->getById((int) $scope->provider->id);
if ($provider) {
    $scope->setProvider($provider);
}

This custom approach mirrors what “first()” was intended to do (find the first matching entity), but uses your own helper method.


2) Loop Manually

If you only need to do a small check, you can inline a simple loop:

$foundProvider = null;
foreach ($providerList as $provider) {
    if ((int) $provider->id === (int) $scope->provider->id) {
        $foundProvider = $provider;
        break;
    }
}

if ($foundProvider) {
    $scope->setProvider($foundProvider);
}

This is less elegant but works without modifying ProviderList.


3) Extend your Collection to support a “first()” style method

If you want a “Laravel-like” approach, you could add a first(callable $callback) method to your custom collection class:

public function first(callable $callback = null)
{
    if (is_null($callback)) {
        // Return the first item in the collection (if you want that behavior).
        return isset($this->entities[0]) ? $this->entities[0] : null;
    }

    // If a callback is provided, search for the first match.
    foreach ($this->entities as $entity) {
        if ($callback($entity)) {
            return $entity;
        }
    }
    return null;
}

Then your existing line $providerList->first(fn($p) => $p->id === $scope->provider->id); will work. Just be sure to adapt property names ($this->data, $this->entities, etc.) to your actual ProviderList class.


Conclusion

Since first() is not defined by default in your custom ProviderList, you can either:

  1. Implement a getById() or first() method in ProviderList.
  2. Write a small manual loop in your code.

Both approaches ensure you can locate the matching Provider by ID to attach to each Scope.

@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
Copy link
Contributor

@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: 16

♻️ Duplicate comments (5)
zmscitizenapi/tests/Zmscitizenapi/AppointmentUpdateTest.php (1)

208-1723: 🛠️ Refactor suggestion

Refactor validation tests using data providers.

The validation test methods from line 208 onwards are highly repetitive, varying only in input parameters and expected errors. This leads to:

  • Excessive code duplication
  • Harder maintenance
  • Reduced readability

Refactor using a PHPUnit data provider:

/**
 * @dataProvider validationTestProvider
 */
public function testParameterValidation(array $parameters, array $expectedErrors)
{
    $response = $this->render([], $parameters, [], 'POST');
    $responseBody = json_decode((string) $response->getBody(), true);
    
    $this->assertEquals(400, $response->getStatusCode());
    $this->assertEqualsCanonicalizing(['errors' => $expectedErrors], $responseBody);
}

public function validationTestProvider(): array
{
    return [
        'all_invalid' => [
            'parameters' => [
                'processId' => null,
                'authKey' => '',
                'familyName' => '',
                'email' => 'invalid-email',
                'telephone' => '123',
                'customTextfield' => 123
            ],
            'expectedErrors' => [
                ErrorMessages::get('invalidProcessId'),
                ErrorMessages::get('invalidAuthKey'),
                ErrorMessages::get('invalidFamilyName'),
                ErrorMessages::get('invalidEmail'),
                ErrorMessages::get('invalidTelephone'),
                ErrorMessages::get('invalidCustomTextfield')
            ]
        ],
        // Add more test cases following the same pattern
    ];
}

Benefits:

  • Reduces code duplication
  • Improves maintainability
  • Makes test cases more readable
  • Easier to add new test cases
zmsentities/schema/citizenapi/thinnedProvider.json (3)

3-7: ⚠️ Potential issue

Restrict root level schema type to object

The schema allows the root to be an array, object, or null. Since this represents a single provider entity, it should be restricted to just "object" type to ensure consistent data structures.

    "type": [
-        "array",
-        "object",
-        "null"
-    ],
+    "type": "object",

8-19: ⚠️ Potential issue

Add required field constraints

The id field appears to be mandatory but is not marked as required in the schema.

    "properties": {
        "id": {
            "type": "string",
-            "description": "Provider ID"
+            "description": "Provider ID",
+            "minLength": 1
        },
        "name": {
            "type": ["string", "null"],
            "description": "Provider name"
        }
    },
+    "required": ["id"],
+    "additionalProperties": false

38-97: ⚠️ Potential issue

Revise contact object structure

The contact property type includes "array" which seems incorrect for an address structure.

        "contact": {
            "title": "ThinnedContact",
            "description": "Represents a simplified contact object for the citizen API.",
-            "type": [
-                "array",
-                "object",
-                "null"
-            ],
+            "type": ["object", "null"],
zmscitizenapi/src/Zmscitizenapi/Services/MapperService.php (1)

262-275: 🛠️ Refactor suggestion

Replace global variable usage with dependency injection.

Using the global App::$source_name makes the code harder to test and maintain.

🧹 Nitpick comments (24)
zmsentities/schema/citizenapi/thinnedContact.json (1)

5-43: Consider revising the required/nullable field strategy.

Currently, all fields are marked as required but also nullable. This creates a confusing contract where fields must always be present in the JSON but can have null values. This might lead to unnecessarily verbose JSON payloads.

Consider one of these approaches:

  1. Make truly optional fields not required, removing them from the "required" array.
  2. Make required fields non-nullable if they must always have a value.

Example of approach #1 - assuming name is truly optional:

     "name": {
       "type": ["string", "null"],
       "description": "Optional name field displayed for contact."
     },
     // ... other properties ...
     "required": [
       "city",
       "country",
-      "name",
       "postalCode",
       "region",
       "street",
       "streetNumber"
     ]
zmscitizenapi/tests/Zmscitizenapi/AppointmentUpdateTest.php (1)

12-78: Remove duplicate response structure.

The expectedResponse array at lines 50-75 is defined with identical content as the fixture. Consider extracting this into a shared test data provider or helper method.

-        $expectedResponse = [
-            "processId" => 101002,
-            "timestamp" => "1727865900",
-            "authKey" => "fb43",
-            // ... (rest of the array)
-        ];
+        $expectedResponse = $this->getExpectedSuccessResponse();

Add this helper method:

private function getExpectedSuccessResponse(): array
{
    return [
        "processId" => 101002,
        "timestamp" => "1727865900",
        "authKey" => "fb43",
        // ... (rest of the array)
    ];
}
zmscitizenapi/src/Zmscitizenapi/Models/ThinnedContact.php (1)

21-29: Consider preserving null values for optional fields
Currently, passing null into the constructor results in setting empty strings. This can blur the distinction between genuinely unknown/unspecified values and empty strings in subsequent usage. If it’s important to differentiate between “no data” (null) and “empty string,” consider storing the original null.

- $this->city         = $city     ?? '';
- $this->country      = $country  ?? '';
+ $this->city         = $city;
+ $this->country      = $country;
zmsentities/schema/citizenapi/thinnedProvider.json (1)

38-97: Add examples for contact fields

Consider adding examples to the schema to illustrate the expected format and improve developer experience.

        "city": {
            "type": ["string", "null"],
-            "description": "The city name."
+            "description": "The city name.",
+            "examples": ["München"]
        },
        "country": {
            "type": ["string", "null"],
-            "description": "The country name."
+            "description": "The country name.",
+            "examples": ["DE"]
        },
        "postalCode": {
            "type": ["string", "null"],
-            "description": "The postal (ZIP) code."
+            "description": "The postal (ZIP) code.",
+            "examples": ["80331"]
        }
zmscitizenapi/src/Zmscitizenapi/Models/Office.php (3)

14-28: Enhance property documentation

While the type hints are correct, the PHPDoc comments could be more descriptive to better explain each property's purpose and constraints.

Example improvement:

-    /** @var int */
+    /**
+     * Unique identifier for the office
+     * @var int
+     */
     public int $id;

-    /** @var string */
+    /**
+     * Official name of the office
+     * @var string
+     */
     public string $name;

-    /** @var array|null */
+    /**
+     * Physical address details of the office
+     * @var array{street: string, city: string, postal_code: string}|null
+     */
     public ?array $address = null;

29-48: Add parameter validation in constructor

Consider validating parameters before assignment to ensure data integrity at the point of object creation.

     public function __construct(int $id, string $name, ?array $address = null, ?array $geo = null, ?ThinnedScope $scope = null)
     {
+        if ($id <= 0) {
+            throw new InvalidArgumentException('Office ID must be a positive integer');
+        }
+        if (empty(trim($name))) {
+            throw new InvalidArgumentException('Office name cannot be empty');
+        }
         $this->id = $id;
         $this->name = $name;
         $this->address = $address;

49-54: Enhance validation error message

The current error message is generic. Consider including specific validation failures to help with debugging.

     private function ensureValid()
     {
-        if (!$this->testValid()) {
-            throw new InvalidArgumentException("The provided data is invalid according to the schema.");
+        $validationResult = $this->testValid();
+        if ($validationResult !== true) {
+            $errors = json_encode($this->getErrors());
+            throw new InvalidArgumentException("Schema validation failed: $errors");
         }
     }
zmscitizenapi/tests/Zmscitizenapi/OfficesListTest.php (2)

12-21: Consider enhancing API mock setup with data provider

The test setup could benefit from using a data provider to test different scenarios (e.g., empty list, single office, multiple offices).

Example implementation:

/**
 * @dataProvider provideOfficeListScenarios
 */
public function testRendering(array $apiResponse, array $expectedResponse)
{
    $this->setApiCalls([[
        'function' => 'readGetResult',
        'url' => '/source/unittest/',
        'parameters' => ['resolveReferences' => 2],
        'response' => $apiResponse
    ]]);
    
    $response = $this->render();
    $this->assertEquals(200, $response->getStatusCode());
    $this->assertEqualsCanonicalizing($expectedResponse, json_decode((string)$response->getBody(), true));
}

public function provideOfficeListScenarios(): array
{
    return [
        'multiple_offices' => [
            'apiResponse' => $this->readFixture("GET_SourceGet_dldb.json"),
            'expectedResponse' => ['offices' => [ /* ... */ ]]
        ],
        'empty_list' => [
            'apiResponse' => ['data' => ['providers' => []]],
            'expectedResponse' => ['offices' => []]
        ]
    ];
}

102-103: Add more specific assertions for response validation

The current assertions only check the status code and response structure. Consider adding more specific assertions to validate critical business rules.

Example:

// Validate specific business rules
$this->assertNotEmpty($responseBody['offices'], 'Office list should not be empty');
foreach ($responseBody['offices'] as $office) {
    $this->assertArrayHasKey('scope', $office, 'Each office must have a scope');
    $this->assertNotNull($office['scope']['provider'], 'Each scope must have a provider');
    $this->assertEquals($office['id'], $office['scope']['provider']['id'], 'Office ID must match provider ID');
}
zmscitizenapi/tests/Zmscitizenapi/fixtures/GET_SourceGet_dldb.json (2)

6-6: Update the generated timestamp to a more recent date

The fixture uses a static timestamp from 2019, which might not reflect current testing scenarios.

-    "generated": "2019-02-08T14:45:15+01:00",
+    "generated": "2024-12-01T14:45:15+01:00",

81-83: Consider expanding combinable services test data

The combinable array in the request data could be enhanced to test more complex service combinations.

         "data": {
           "json": "data",
           "key": "value",
-          "combinable": ["1", "2"]
+          "combinable": ["1", "2"],
+          "restrictions": {
+            "maxCombined": 3,
+            "excludedServices": ["3"],
+            "requiredServices": ["1"]
+          }
         }
zmscitizenapi/src/Zmscitizenapi/Services/MapperService.php (3)

94-130: Consider optimizing service mapping for better performance.

While the implementation is correct, consider these optimizations:

  1. Pre-calculate service provider mappings
  2. Use array_filter for filtering common providers
 public static function mapServicesWithCombinations(RequestList $requestList, RequestRelationList $relationList): ServiceList
 {
+    // Pre-calculate service provider mappings
+    /** @var array<string, array<int>> $servicesProviderIds */
+    $servicesProviderIds = array_reduce(
+        iterator_to_array($relationList),
+        function (array $carry, $relation) {
+            $serviceId = $relation->request->id;
+            $carry[$serviceId][] = $relation->provider->id;
+            return $carry;
+        },
+        []
+    );
-    /** @var array<string, array<int>> $servicesProviderIds */
-    $servicesProviderIds = [];
-    foreach ($relationList as $relation) {
-        $serviceId = $relation->request->id;
-        $servicesProviderIds[$serviceId] ??= [];
-        $servicesProviderIds[$serviceId][] = $relation->provider->id;
-    }

184-230: Optimize process mapping for better readability and performance.

The process mapping logic is complex and could benefit from:

  1. Extracting request counting logic to a separate method
  2. Using null coalescing operator consistently
  3. Reducing nested conditionals
 public static function processToThinnedProcess(Process $myProcess): ThinnedProcess
 {
     if (!$myProcess || !isset($myProcess->id)) {
         return new ThinnedProcess();
     }
 
+    private static function countRequests(array $requests): array
+    {
+        $mainServiceId = $requests[0]->id ?? null;
+        $counts = ['main' => 0, 'sub' => []];
+        
+        foreach ($requests as $request) {
+            if ($request->id === $mainServiceId) {
+                $counts['main']++;
+                continue;
+            }
+            
+            $counts['sub'][$request->id]['id'] ??= $request->id;
+            $counts['sub'][$request->id]['count'] ??= 0;
+            $counts['sub'][$request->id]['count']++;
+        }
+        
+        return $counts;
+    }

300-311: Improve error handling in contact mapping.

The method should validate input types and handle invalid data more gracefully.

 public static function contactToThinnedContact($contact): ThinnedContact
 {
+    if (!is_array($contact) && !is_object($contact)) {
+        throw new \InvalidArgumentException('Contact must be an array or object');
+    }
+
     return new ThinnedContact(
-        $contact['city']         ?? $contact->city         ?? '',
-        $contact['country']      ?? $contact->country      ?? '',
+        self::getContactValue($contact, 'city'),
+        self::getContactValue($contact, 'country'),
         // ... similar for other fields
     );
 }
+
+private static function getContactValue($contact, string $field): string
+{
+    if (is_array($contact)) {
+        return $contact[$field] ?? '';
+    }
+    return $contact->$field ?? '';
+}
zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiFacadeService.php (3)

156-161: Address TODO comments with implementation plans.

Multiple TODO comments indicate missing functionality. Please provide implementation details or create GitHub issues to track these pending features:

  • getCombinableServicesByIds
  • getOfficeByIds
  • getOfficesThatProvideService
  • preconfirmAppointment
  • confirmAppointment
  • cancelAppointment

Would you like me to help create GitHub issues for tracking these pending implementations?

Also applies to: 220-225, 412-417, 736-741, 744-749, 751-756


508-539: Optimize array operations in getFreeAppointments.

The current implementation uses array_merge in a loop which can be inefficient for large datasets.

Consider pre-allocating the array:

-            $requests = [];
-            foreach ($serviceIds as $index => $serviceId) {
-                $service = [
-                    'id' => $serviceId,
-                    'source' => \App::$source_name,
-                    'slotCount' => $serviceCounts[$index]
-                ];
-                $requests = array_merge($requests, array_fill(0, $service['slotCount'], $service));
-            }
+            $totalSlots = array_sum($serviceCounts);
+            $requests = array_pad([], $totalSlots, []);
+            $currentIndex = 0;
+            
+            foreach ($serviceIds as $index => $serviceId) {
+                $service = [
+                    'id' => $serviceId,
+                    'source' => \App::$source_name,
+                    'slotCount' => $serviceCounts[$index]
+                ];
+                for ($i = 0; $i < $service['slotCount']; $i++) {
+                    $requests[$currentIndex++] = $service;
+                }
+            }

586-623: Optimize processFreeSlots method using array operations.

The method can be optimized using array operations for better performance.

The current implementation using array_reduce with iterator_to_array is good, but consider adding early validation:

     private static function processFreeSlots(ProcessList $freeSlots): ProcessFreeSlots|array
     {
+        if ($freeSlots->count() === 0) {
+            return new ProcessFreeSlots([]);
+        }
+
         $errors = ValidationService::validateGetProcessFreeSlots($freeSlots);
         if (!empty($errors['errors'])) {
             return $errors;
         }
zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProvider.php (3)

1-2: Consider adding a file-level docblock or PHP type declarations.

Although this is a new file, adding a short description or docblock at the file level (or using strict types by adding declare(strict_types=1);) can improve readability and maintainability.


9-30: Use of public fields is acceptable, but consider an immutable or encapsulated design.

The direct usage of public properties is fine for data models, especially for entities. However, opting for getters/setters (or an immutable object pattern) can offer clearer change tracking, validation, and enhanced control over property state changes.


49-54: Validation method is concise, but consider expanding error detail.

While throwing an InvalidArgumentException is fine, providing additional context (e.g., which fields failed validation) can help debugging. Consider expanding the exception message to identify the problematic property or validation detail.

- throw new InvalidArgumentException("The provided data is invalid according to the schema.");
+ throw new InvalidArgumentException(
+     "The provided data is invalid. Please check the schema constraints for properties like 'id', 'name', etc."
+ );
zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiClientService.php (1)

197-225: Reduce duplication in email methods.

Extract common URL construction logic and add parameter validation.

Consider this improvement:

+    private function constructEmailUrl(Process $process, string $type): string
+    {
+        if (!$process->id || !$process->authKey) {
+            throw new \InvalidArgumentException('Process must have valid ID and auth key');
+        }
+        return "/process/{$process->id}/{$process->authKey}/{$type}/mail/";
+    }

     public function sendConfirmationEmail(Process $process): Process
     {
         try {
-            $url = "/process/{$process->id}/{$process->authKey}/confirmation/mail/";
+            $url = $this->constructEmailUrl($process, 'confirmation');
             return \App::$http->readPostResult($url, $process)->getEntity();
zmscitizenapi/src/Zmscitizenapi/Models/Service.php (1)

27-30: Add null to the docblock for $combinable.

To remain consistent with the actual property type, the docblock should reflect @var Combinable|null.

-/** @var Combinable */
+/** @var Combinable|null */
zmscitizenapi/src/Zmscitizenapi/Models/ProcessFreeSlots.php (1)

23-25: Validate whether frequent schema checks are needed in the constructor.

Calling $this->ensureValid(); on every instantiation can be beneficial for data integrity but might introduce runtime overhead if objects are created repeatedly in performance-critical sections. If performance becomes an issue, consider providing an option to skip schema validation when the data source is already trusted or validated.

zmsentities/schema/citizenapi/processFreeSlots.json (1)

7-12: Reconcile nullable timestamps in model vs. schema.

The schema allows "type": ["array", "null"] for appointmentTimestamps, yet the PHP property is typed solely as an array and does not handle null. Consider revising either the schema or the model to avoid inconsistencies that could lead to runtime errors.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6f3630e and 5b38021.

📒 Files selected for processing (33)
  • zmscitizenapi/src/Zmscitizenapi/Models/AvailableAppointments.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Models/AvailableDays.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeList.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeServiceAndRelationList.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeServiceRelationList.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Models/Collections/ServiceList.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Models/Collections/ThinnedScopeList.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Models/Combinable.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Models/Office.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Models/OfficeServiceRelation.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Models/ProcessFreeSlots.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Models/Service.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Models/ThinnedContact.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProcess.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProvider.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Models/ThinnedScope.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Services/MapperService.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiClientService.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiFacadeService.php (1 hunks)
  • zmscitizenapi/tests/Zmscitizenapi/AppointmentByIdTest.php (1 hunks)
  • zmscitizenapi/tests/Zmscitizenapi/AppointmentReserveTest.php (1 hunks)
  • zmscitizenapi/tests/Zmscitizenapi/AppointmentUpdateTest.php (1 hunks)
  • zmscitizenapi/tests/Zmscitizenapi/OfficesByServiceListTest.php (1 hunks)
  • zmscitizenapi/tests/Zmscitizenapi/OfficesListTest.php (1 hunks)
  • zmscitizenapi/tests/Zmscitizenapi/OfficesServicesRelationsTest.php (1 hunks)
  • zmscitizenapi/tests/Zmscitizenapi/ScopeByIdTest.php (1 hunks)
  • zmscitizenapi/tests/Zmscitizenapi/ScopesListTest.php (1 hunks)
  • zmscitizenapi/tests/Zmscitizenapi/fixtures/GET_SourceGet_dldb.json (1 hunks)
  • zmsentities/schema/citizenapi/processFreeSlots.json (1 hunks)
  • zmsentities/schema/citizenapi/thinnedContact.json (1 hunks)
  • zmsentities/schema/citizenapi/thinnedProcess.json (1 hunks)
  • zmsentities/schema/citizenapi/thinnedProvider.json (1 hunks)
  • zmsentities/schema/citizenapi/thinnedScope.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (18)
  • zmscitizenapi/tests/Zmscitizenapi/ScopeByIdTest.php
  • zmscitizenapi/tests/Zmscitizenapi/ScopesListTest.php
  • zmscitizenapi/src/Zmscitizenapi/Models/AvailableDays.php
  • zmscitizenapi/tests/Zmscitizenapi/AppointmentReserveTest.php
  • zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProcess.php
  • zmscitizenapi/tests/Zmscitizenapi/OfficesServicesRelationsTest.php
  • zmscitizenapi/src/Zmscitizenapi/Models/OfficeServiceRelation.php
  • zmscitizenapi/tests/Zmscitizenapi/OfficesByServiceListTest.php
  • zmsentities/schema/citizenapi/thinnedProcess.json
  • zmsentities/schema/citizenapi/thinnedScope.json
  • zmscitizenapi/src/Zmscitizenapi/Models/AvailableAppointments.php
  • zmscitizenapi/src/Zmscitizenapi/Models/Collections/ThinnedScopeList.php
  • zmscitizenapi/src/Zmscitizenapi/Models/Combinable.php
  • zmscitizenapi/src/Zmscitizenapi/Models/Collections/ServiceList.php
  • zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeServiceRelationList.php
  • zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeServiceAndRelationList.php
  • zmscitizenapi/src/Zmscitizenapi/Models/Collections/OfficeList.php
  • zmscitizenapi/src/Zmscitizenapi/Models/ThinnedScope.php
🔇 Additional comments (25)
zmscitizenapi/tests/Zmscitizenapi/AppointmentUpdateTest.php (2)

1-10: LGTM! Class structure follows PSR-4 autoloading standard.

The namespace and class declaration are properly structured. Good use of importing the ErrorMessages class for centralized error handling.


80-206: LGTM! Comprehensive error case testing.

Good coverage of important error scenarios:

  • Too many appointments with same email
  • Appointment not found
  • Auth key mismatch
zmscitizenapi/src/Zmscitizenapi/Models/ThinnedContact.php (3)

8-8: Class declaration is consistent and aligns with project naming conventions
The class name ThinnedContact clearly conveys its purpose—a reduced or simplified representation of contact data. Inheritance from Entity and usage of \JsonSerializable appear consistent with the project’s design patterns.


38-50: Ensure the JSON schema accommodates empty strings
With the current approach, fields default to '' rather than null. Confirm that the JSON schema in thinnedContact.json allows empty strings even if the original intent was for null. If it doesn’t, validation might pass/ fail unexpectedly.

Would you like me to open a new GitHub issue or add a test script to verify that the schema handles empty strings consistently?


52-69: Use of toArray() and jsonSerialize() is coherent
The pattern of returning $this->toArray() from jsonSerialize() is standard and offers a single point of truth for marshaling data into arrays/ JSON. This makes the code more maintainable.

zmscitizenapi/src/Zmscitizenapi/Models/Office.php (2)

1-9: LGTM!

The namespace and imports are well-organized and include all necessary dependencies.


10-12: Update schema path to match project convention

The schema path should follow the project convention used by other models.

-    public static $schema = 'citizenapi/office.json';
+    public static $schema = 'zmsentities/schema/citizenapi/office.json';
zmscitizenapi/tests/Zmscitizenapi/fixtures/GET_SourceGet_dldb.json (1)

141-179: Verify appointment dates and availability patterns

The appointment dates are set for August 2024 with specific availability patterns. Consider:

  1. Adding edge cases for appointment availability
  2. Including non-bookable days to test different status scenarios
  3. Testing boundary conditions for the appointment window

Run this script to check the appointment date handling in the codebase:

✅ Verification successful

Based on the search results, I can now provide a comprehensive verification of the appointment dates and test coverage:

Test fixtures appropriately cover appointment date scenarios

The test fixtures and codebase show robust date handling and validation:

  1. The system has comprehensive date validation through DateTimeFormatHelper which validates ISO date formats and timestamps
  2. The test fixtures include:
    • Regular bookable days with varying availability (public, intern, callcenter slots)
    • Proper date format validation (YYYY-MM-DD) in ValidationService
    • Error handling for invalid dates and unavailable appointments
  3. Edge cases are covered through:
    • Validation of future dates (timestamps > current time)
    • Different appointment status scenarios (bookable, not available)
    • Boundary checks in appointment window validation

The test data in GET_SourceGet_dldb.json with August 2024 dates is appropriate as:

  • It provides multiple consecutive days to test date sequence handling
  • It includes varying slot availability patterns
  • The dates are within a reasonable future timeframe
  • The schema validation is properly enforced
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify appointment date handling logic
# Check for date validation and processing in the codebase

# Look for date handling logic
ast-grep --pattern 'function $FUNC($$$) {
  $$$
  if ($DATE $OP $$$) {
    $$$
  }
  $$$
}'

# Search for date-related validation
rg -A 3 'validateDate|isValidDate|checkDate'

Length of output: 140


Script:

#!/bin/bash
# Let's try a different approach to find date-related logic and test files

# Search for date-related patterns in test files
rg -g "*.php" -g "*.json" "(date|appointment|booking|schedule)" --type-add 'json:*.json' -A 3

# Find all test fixture files related to appointments or dates
fd -e json -e php "appointment|date|book" tests/

# Look for specific date format patterns (YYYY-MM-DD or similar)
rg -g "*.json" -g "*.php" "\d{4}[-./]\d{2}[-./]\d{2}" --type-add 'json:*.json'

Length of output: 79729

zmscitizenapi/src/Zmscitizenapi/Services/MapperService.php (1)

1-29: LGTM! Well-organized file structure with clear namespace and import declarations.

The file follows PSR-4 autoloading standards and has a clear separation between models from different namespaces.

zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiFacadeService.php (1)

1-29: LGTM! Well-organized imports and declarations.

The file follows PSR-4 standards and has proper strict typing declaration. All necessary dependencies are clearly imported and organized by their types (Models, Collections, Services).

zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProvider.php (4)

3-8: Namespace and imports look consistent.

The namespace BO\Zmscitizenapi\Models is appropriate, and importing relevant classes (Entity, InvalidArgumentException, JsonSerializable) aligns with best practices.


31-47: Constructor leverages nullable typed properties effectively.

The approach of using nullable typed parameters is clear and ensures flexible creation of the object. The call to ensureValid() in the constructor helps maintain data integrity from the start.


56-72: Excellent use of a toArray() helper.

This method ensures consistent serialization across the codebase. Especially beneficial if you later decide to modify object structure or property names without affecting external consumers.


73-76: jsonSerialize() correctly delegates to toArray().

Leveraging toArray() in jsonSerialize() is a good approach for code reuse and consistency.

zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiClientService.php (1)

1-15: Well-structured file with proper organization!

The file follows PSR standards with strict typing, proper namespace declaration, and well-organized imports.

zmscitizenapi/src/Zmscitizenapi/Models/Service.php (5)

13-13: Schema path may be incorrect.

Based on earlier findings (see previous review comments), the file citizenapi/service.json might not exist in this location. Ensure the path is correct; otherwise, update this reference to the actual schema location.


27-44: Consider validating $maxQuantity.

As suggested in a prior review, it would be beneficial to validate that $maxQuantity > 0 when provided, to avoid potential logical errors.


39-47: Constructor usage of ensureValid() looks good.

Invoking ensureValid() at the end of construction ensures the object is validated early. This pattern promotes better data integrity.


49-54: Good use of schema-based validation.

Using the parent class method testValid() enforces schema compliance. Ensure the schema itself is up to date regarding optional properties like maxQuantity and combinable.


61-74: Serialization approach is clean and straightforward.

Exposing an associative array and utilizing the built-in jsonSerialize() method follows best practices for data models in PHP.

zmscitizenapi/src/Zmscitizenapi/Models/ProcessFreeSlots.php (2)

28-33: Schema-based validation appears solid.

Your validation logic aligns with the JSON schema, throwing an InvalidArgumentException when the data doesn't match. This helps maintain robust data integrity at the model layer.


40-53: Good use of JsonSerializable and structured serialization.

The toArray method is clean and consistent, and returning it from jsonSerialize ensures the data is always in a predictable format. This is a neat and maintainable approach to object serialization.

zmscitizenapi/tests/Zmscitizenapi/AppointmentByIdTest.php (3)

10-82: Test coverage is comprehensive for the successful scenario.
It effectively handles API call setup, HTTP response rendering, and result validation. Good job ensuring the critical fields are validated (e.g., timestamp, office details, scope, etc.).


84-116: Consolidate repetitive parameter validation tests with data providers (duplicate of a previous suggestion).
These tests for missing or invalid parameters (e.g., testMissingProcessId, testMissingAuthKey, testInvalidProcessId, etc.) mostly follow the same pattern of rendering the response, parsing it, and verifying a 400 status code. This can be streamlined through PHPUnit data providers to reduce duplication and simplify maintenance.

Also applies to: 118-152, 154-168


206-238: Confirm correctness of 406 status code upon 'authKeyMismatch'.
Typically, 401 or 403 might be used when authorization fails. Please verify that 406 aligns with the intended semantics.

Would you like to run a repository-wide search to check whether other controllers or tests also use 406 in similar situations or if there's a discrepancy?

✅ Verification successful

Let me search for more specific information about HTTP status codes usage in the codebase.


Let me check how authKey validation is handled in other parts of the codebase.


406 status code is consistently used for authKey validation failures

The codebase shows a deliberate pattern of using HTTP 406 (Not Acceptable) for authentication key mismatches:

  • The HTTP_NOT_ACCEPTABLE (406) status code is defined in ErrorMessages.php and consistently used for authKeyMismatch errors
  • This is part of a broader error handling system where 406 is used for various validation failures, not just auth
  • The implementation is consistent across multiple endpoints that handle auth key validation

While 401 or 403 might seem more semantically appropriate for auth failures, changing this would require a broader refactoring of the error handling system. The current implementation appears to be an intentional design choice that's consistently applied.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Searching for 406 usage and references to 'authKeyMismatch' across the codebase
rg -A 5 '(406|authKeyMismatch)'

Length of output: 75653


Script:

#!/bin/bash
# Search for HTTP status code constants and their usage
rg -A 2 "HTTP_NOT_ACCEPTABLE|406" zmscitizenapi/src/

Length of output: 2666


Script:

#!/bin/bash
# Search for authKey related error handling
rg -A 5 "authKeyMismatch|AuthKeyMatchFailed" zmscitizenapi/src/

Length of output: 8862

Comment on lines +20 to +33
"lat": {
"type": [
"number",
"null"
],
"description": "Latitude in decimal degrees."
},
"lon": {
"type": [
"number",
"null"
],
"description": "Longitude in decimal degrees."
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add coordinate range validation

The latitude and longitude fields should include range validation to ensure valid coordinates:

  • Latitude: -90 to 90 degrees
  • Longitude: -180 to 180 degrees
        "lat": {
            "type": [
                "number",
                "null"
            ],
-            "description": "Latitude in decimal degrees."
+            "description": "Latitude in decimal degrees.",
+            "minimum": -90,
+            "maximum": 90
        },
        "lon": {
            "type": [
                "number",
                "null"
            ],
-            "description": "Longitude in decimal degrees."
+            "description": "Longitude in decimal degrees.",
+            "minimum": -180,
+            "maximum": 180
        }
📝 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
"lat": {
"type": [
"number",
"null"
],
"description": "Latitude in decimal degrees."
},
"lon": {
"type": [
"number",
"null"
],
"description": "Longitude in decimal degrees."
},
"lat": {
"type": [
"number",
"null"
],
"description": "Latitude in decimal degrees.",
"minimum": -90,
"maximum": 90
},
"lon": {
"type": [
"number",
"null"
],
"description": "Longitude in decimal degrees.",
"minimum": -180,
"maximum": 180
},

zmscitizenapi/src/Zmscitizenapi/Models/Office.php Outdated Show resolved Hide resolved
Comment on lines +47 to +80
public static function mapOfficesWithScope(ProviderList $providerList): OfficeList
{
$offices = [];
$scopes = ZmsApiFacadeService::getScopes();

if (is_array($scopes)) {
return new OfficeList();
}

foreach ($providerList as $provider) {
$providerScope = self::mapScopeForProvider((int) $provider->id, $scopes);

$offices[] = new Office(
id: isset($provider->id) ? (int) $provider->id : 0,
name: isset($provider->displayName) ? $provider->displayName : (isset($provider->name) ? $provider->name : null),
address: isset($provider->data['address']) ? $provider->data['address'] : null,
geo: isset($provider->data['geo']) ? $provider->data['geo'] : null,
scope: isset($providerScope) && !isset($providerScope['errors']) ? new ThinnedScope(
id: isset($providerScope->id) ? (int) $providerScope->id : 0,
provider: isset($providerScope->provider) ? $providerScope->provider : null,
shortName: isset($providerScope->shortName) ? $providerScope->shortName : null,
telephoneActivated: isset($providerScope->telephoneActivated) ? (bool) $providerScope->telephoneActivated : null,
telephoneRequired: isset($providerScope->telephoneRequired) ? (bool) $providerScope->telephoneRequired : null,
customTextfieldActivated: isset($providerScope->customTextfieldActivated) ? (bool) $providerScope->customTextfieldActivated : null,
customTextfieldRequired: isset($providerScope->customTextfieldRequired) ? (bool) $providerScope->customTextfieldRequired : null,
customTextfieldLabel: isset($providerScope->customTextfieldLabel) ? $providerScope->customTextfieldLabel : null,
captchaActivatedRequired: isset($providerScope->captchaActivatedRequired) ? (bool) $providerScope->captchaActivatedRequired : null,
displayInfo: isset($providerScope->displayInfo) ? $providerScope->displayInfo : null
) : null
);
}

return new OfficeList($offices);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve type safety and error handling in office mapping.

The method has potential issues with type confusion and complex object construction.

  1. Replace is_array($scopes) check with more specific type checking
  2. Consider extracting the office construction logic to a separate method
  3. Add error logging for edge cases
 public static function mapOfficesWithScope(ProviderList $providerList): OfficeList
 {
     $offices = [];
     $scopes = ZmsApiFacadeService::getScopes();
     
-    if (is_array($scopes)) {
+    if (!$scopes instanceof ThinnedScopeList) {
+        error_log('Invalid scopes returned from ZmsApiFacadeService::getScopes()');
         return new OfficeList();
     }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 35 to 73
try {
$providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
$scopeList = ZmsApiClientService::getScopes() ?? new ScopeList();
$offices = [];

$scopeMap = [];
foreach ($scopeList as $scope) {
if ($scope->getProvider()) {
$scopeMap[$scope->getProvider()->source . '_' . $scope->getProvider()->id] = $scope;
}
}

foreach ($providerList as $provider) {
$matchingScope = $scopeMap[$provider->source . '_' . $provider->id] ?? null;

$offices[] = new Office(
id: (int) $provider->id,
name: $provider->displayName ?? $provider->name,
address: $provider->data['address'] ?? null,
geo: $provider->data['geo'] ?? null,
scope: $matchingScope ? new ThinnedScope(
id: (int) $matchingScope->id,
provider: MapperService::providerToThinnedProvider($provider),
shortName: $matchingScope->getShortName(),
telephoneActivated: (bool) $matchingScope->getTelephoneActivated(),
telephoneRequired: (bool) $matchingScope->getTelephoneRequired(),
customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(),
customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(),
customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(),
captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(),
displayInfo: $matchingScope->getDisplayInfo()
) : null
);
}

return new OfficeList($offices);
} catch (\RuntimeException $e) {
return ExceptionService::officesNotFound();
}
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 30, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add explicit null checks for API responses.

The null coalescing operator provides a default empty collection, but we should handle API failures explicitly.

Apply this pattern:

         try {
-            $providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
-            $scopeList = ZmsApiClientService::getScopes() ?? new ScopeList();
+            $providerList = ZmsApiClientService::getOffices();
+            if ($providerList === null) {
+                \App::$log->error('Failed to retrieve offices from API');
+                return ExceptionService::officesNotFound();
+            }
+            
+            $scopeList = ZmsApiClientService::getScopes();
+            if ($scopeList === null) {
+                \App::$log->error('Failed to retrieve scopes from API');
+                return ExceptionService::scopesNotFound();
+            }
📝 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
try {
$providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
$scopeList = ZmsApiClientService::getScopes() ?? new ScopeList();
$offices = [];
$scopeMap = [];
foreach ($scopeList as $scope) {
if ($scope->getProvider()) {
$scopeMap[$scope->getProvider()->source . '_' . $scope->getProvider()->id] = $scope;
}
}
foreach ($providerList as $provider) {
$matchingScope = $scopeMap[$provider->source . '_' . $provider->id] ?? null;
$offices[] = new Office(
id: (int) $provider->id,
name: $provider->displayName ?? $provider->name,
address: $provider->data['address'] ?? null,
geo: $provider->data['geo'] ?? null,
scope: $matchingScope ? new ThinnedScope(
id: (int) $matchingScope->id,
provider: MapperService::providerToThinnedProvider($provider),
shortName: $matchingScope->getShortName(),
telephoneActivated: (bool) $matchingScope->getTelephoneActivated(),
telephoneRequired: (bool) $matchingScope->getTelephoneRequired(),
customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(),
customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(),
customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(),
captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(),
displayInfo: $matchingScope->getDisplayInfo()
) : null
);
}
return new OfficeList($offices);
} catch (\RuntimeException $e) {
return ExceptionService::officesNotFound();
}
try {
$providerList = ZmsApiClientService::getOffices();
if ($providerList === null) {
\App::$log->error('Failed to retrieve offices from API');
return ExceptionService::officesNotFound();
}
$scopeList = ZmsApiClientService::getScopes();
if ($scopeList === null) {
\App::$log->error('Failed to retrieve scopes from API');
return ExceptionService::scopesNotFound();
}
$offices = [];
$scopeMap = [];
foreach ($scopeList as $scope) {
if ($scope->getProvider()) {
$scopeMap[$scope->getProvider()->source . '_' . $scope->getProvider()->id] = $scope;
}
}
foreach ($providerList as $provider) {
$matchingScope = $scopeMap[$provider->source . '_' . $provider->id] ?? null;
$offices[] = new Office(
id: (int) $provider->id,
name: $provider->displayName ?? $provider->name,
address: $provider->data['address'] ?? null,
geo: $provider->data['geo'] ?? null,
scope: $matchingScope ? new ThinnedScope(
id: (int) $matchingScope->id,
provider: MapperService::providerToThinnedProvider($provider),
shortName: $matchingScope->getShortName(),
telephoneActivated: (bool) $matchingScope->getTelephoneActivated(),
telephoneRequired: (bool) $matchingScope->getTelephoneRequired(),
customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(),
customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(),
customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(),
captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(),
displayInfo: $matchingScope->getDisplayInfo()
) : null
);
}
return new OfficeList($offices);
} catch (\RuntimeException $e) {
return ExceptionService::officesNotFound();
}

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai why when it already gets caught by the large try catch block?

Copy link
Contributor

Choose a reason for hiding this comment

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

@ThomasAFink The main idea behind these explicit null checks is to handle scenarios where the API might return null without actually throwing an exception. The existing try/catch block will catch runtime exceptions, but if the underlying service just fails quietly and returns null (instead of throwing), the code in catch will never be triggered. By adding explicit null checks, we ensure that if we receive an unexpected null result—rather than a thrown exception—we can gracefully handle that condition (log an error, return a default, or generate a specific error response).

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai but why would ZmsApiClientService return null?

Copy link
Contributor

Choose a reason for hiding this comment

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

@ThomasAFink If you’ve verified in your codebase that ZmsApiClientService never returns null—for example, by consistently throwing exceptions on error conditions—then these extra null checks may indeed be redundant. The suggestion to check for null is often a safeguard against edge cases where a downstream call might fail quietly and return null instead of throwing. If you can confirm that such a scenario can’t happen, relying on the try/catch alone is sufficient.

Copy link
Contributor

@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: 7

🧹 Nitpick comments (7)
zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiFacadeService.php (3)

177-183: Track TODO comments in the issue system.

There are several TODO comments for planned methods:

  • getCombinableServicesByIds
  • getOfficeByIds
  • getOfficesThatProvideService
  • preconfirmAppointment
  • confirmAppointment
  • cancelAppointment

Would you like me to help create GitHub issues to track these planned implementations?

Also applies to: 247-252, 451-456, 778-799


547-578: Optimize array operations for better performance.

The current implementation uses array_merge in a loop which can be inefficient for large datasets. Consider pre-allocating the array.

Apply this diff:

-            $requests = [];
+            $totalSlots = array_sum($serviceCounts);
+            $requests = array_pad([], $totalSlots, []);
+            $currentIndex = 0;
+
             foreach ($serviceIds as $index => $serviceId) {
                 $service = [
                     'id' => $serviceId,
                     'source' => \App::$source_name,
                     'slotCount' => $serviceCounts[$index]
                 ];
-                $requests = array_merge($requests, array_fill(0, $service['slotCount'], $service));
+                for ($i = 0; $i < $service['slotCount']; $i++) {
+                    $requests[$currentIndex++] = $service;
+                }
             }

736-747: Add logging for better error tracking.

Add logging statements before returning error responses to help with debugging and monitoring.

Apply this pattern to all catch blocks:

         } catch (\RuntimeException $e) {
+            \App::$log->error('Error in getThinnedProcessById: ' . $e->getMessage());
             if (strpos($e->getMessage(), 'appointmentNotFound') !== false) {
                 return ExceptionService::appointmentNotFound();
             }
             if (strpos($e->getMessage(), 'authKeyMismatch') !== false) {
                 return ExceptionService::authKeyMismatch();
             }
             if (strpos($e->getMessage(), 'officesNotFound') !== false) {
                 return ExceptionService::officesNotFound();
             }
             return ExceptionService::internalError();
         }
zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiClientService.php (3)

30-80: Reduce code duplication in data retrieval methods.

The four data retrieval methods share similar patterns and could benefit from a more DRY approach.

Consider extracting the common pattern into a generic method:

+    private static function getEntityList<T>(string $getter, string $entityClass): T
+    {
+        try {
+            $sources = self::fetchSourceData();
+            $result = $sources->$getter();
+            
+            if (!$result instanceof $entityClass) {
+                return new $entityClass();
+            }
+            
+            return $result;
+        } catch (\Exception $e) {
+            ExceptionService::handleException($e, __FUNCTION__);
+        }
+    }

     public static function getOffices(): ProviderList
     {
-        try {
-            $sources = self::fetchSourceData();
-            $providerList = $sources->getProviderList() ?? new ProviderList();
-            return $providerList;
-        } catch (\Exception $e) {
-            ExceptionService::handleException($e, __FUNCTION__);
-        }
+        return self::getEntityList('getProviderList', ProviderList::class);
     }

82-119: Extract common calendar initialization logic.

Both getFreeDays and getFreeTimeslots share calendar initialization code.

Consider extracting the common code:

+    private static function initializeCalendar(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): Calendar
+    {
+        $calendar = new Calendar();
+        $calendar->firstDay = $firstDay;
+        $calendar->lastDay = $lastDay;
+        $calendar->providers = $providers;
+        $calendar->requests = $requests;
+        return $calendar;
+    }

     public static function getFreeDays(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): Calendar
     {
         try {
-            $calendar = new Calendar();
-            $calendar->firstDay = $firstDay;
-            $calendar->lastDay = $lastDay;
-            $calendar->providers = $providers;
-            $calendar->requests = $requests;
+            $calendar = self::initializeCalendar($providers, $requests, $firstDay, $lastDay);

245-253: Improve error handling in getScopesByProviderId.

The method should have more specific error handling and input validation.

Apply this improvement:

     public static function getScopesByProviderId(string $source, string|int $providerId): ScopeList
     {
+        if (empty($source) || empty($providerId)) {
+            throw new \InvalidArgumentException('Source and provider ID are required');
+        }
         try {
             $scopeList = self::getScopes() ?? new ScopeList();
             return $scopeList->withProviderID($source, (string) $providerId);
         } catch (\Exception $e) {
-            ExceptionService::handleException($e, __FUNCTION__);
+            throw new \RuntimeException(
+                "Failed to get scopes for provider {$providerId}: " . $e->getMessage(),
+                0,
+                $e
+            );
         }
     }
zmscitizenapi/src/Zmscitizenapi/Models/Office.php (1)

49-54: Centralize error messages to support multi-lingual usage
The error message here is hard-coded in English. If you plan to support multiple languages or a centralized error messaging system, consider retrieving this string from a localization or constants class.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b38021 and 5f2f718.

📒 Files selected for processing (4)
  • zmscitizenapi/src/Zmscitizenapi/Models/Office.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Models/ProcessFreeSlots.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiClientService.php (1 hunks)
  • zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiFacadeService.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • zmscitizenapi/src/Zmscitizenapi/Models/ProcessFreeSlots.php
🔇 Additional comments (9)
zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiFacadeService.php (2)

1-29: LGTM! Well-organized file structure.

The file has proper strict typing declaration and well-organized imports for all necessary dependencies.


625-662: LGTM! Efficient implementation of slot processing.

The method uses array_reduce for efficient processing of free slots and maintains a clean structure with proper error handling.

zmscitizenapi/src/Zmscitizenapi/Services/ZmsApiClientService.php (2)

1-17: LGTM! Well-structured file with proper imports.

The file follows PHP best practices with strict types, proper namespacing, and all necessary imports.


231-243: 🛠️ Refactor suggestion

Add input validation to getProcessById.

The method should validate input parameters before making the API call.

Apply this improvement:

     public static function getProcessById(int $processId, string $authKey): Process
     {
+        if ($processId <= 0 || empty($authKey)) {
+            throw new \InvalidArgumentException('Invalid process ID or auth key');
+        }
         try {
             $resolveReferences = 2;
             $process = \App::$http->readGetResult("/process/{$processId}/{$authKey}/", [
                 'resolveReferences' => $resolveReferences
             ])->getEntity();
+            
+            if (!$process instanceof Process) {
+                throw new \UnexpectedValueException('Invalid response from API');
+            }

             return $process;

Likely invalid or redundant comment.

zmscitizenapi/src/Zmscitizenapi/Models/Office.php (5)

12-12: Schema path should match the established convention
This issue was previously flagged in an older comment. Currently, the $schema property points to 'citizenapi/office.json', which may cause inconsistencies if your other models reference 'zmsentities/schema/citizenapi/...'.

-    public static $schema = 'citizenapi/office.json';
+    public static $schema = 'zmsentities/schema/citizenapi/office.json';

14-27: Typed properties look clean
Your usage of typed properties (e.g. public int $id, public string $name) and nullable arrays for address and geo help keep the model clear. Consider introducing dedicated classes for address and geo if they become more complex in the future to enhance maintainability.


38-47: Validate the data early in the constructor
Calling $this->ensureValid() directly after all assignments is a good practice to catch invalid data as soon as possible. This approach helps ensure every instantiated object is valid right from initialization.


61-70: Clean and clear serialization
The toArray() method consistently converts your typed properties to an array, with graceful handling of a nullable $scope. This design is straightforward and maintainable.


72-75: Consistent JSON serialization
Deferring to toArray() for JSON serialization is a best practice that ensures consistency. This implementation aligns with the JsonSerializable interface expectations.

Comment on lines +33 to +80
public static function getOffices(): OfficeList|array
{
try {
$providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
$scopeList = ZmsApiClientService::getScopes() ?? new ScopeList();
$offices = [];

$scopeMap = [];
foreach ($scopeList as $scope) {
if ($scope->getProvider()) {
$scopeMap[$scope->getProvider()->source . '_' . $scope->getProvider()->id] = $scope;
}
}

foreach ($providerList as $provider) {
$matchingScope = $scopeMap[$provider->source . '_' . $provider->id] ?? null;

$offices[] = new Office(
id: (int) $provider->id,
name: $provider->displayName ?? $provider->name,
address: $provider->data['address'] ?? null,
geo: $provider->data['geo'] ?? null,
scope: $matchingScope ? new ThinnedScope(
id: (int) $matchingScope->id,
provider: MapperService::providerToThinnedProvider($provider),
shortName: $matchingScope->getShortName(),
telephoneActivated: (bool) $matchingScope->getTelephoneActivated(),
telephoneRequired: (bool) $matchingScope->getTelephoneRequired(),
customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(),
customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(),
customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(),
captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(),
displayInfo: $matchingScope->getDisplayInfo()
) : null
);
}

return new OfficeList($offices);
} catch (\RuntimeException $e) {
if (strpos($e->getMessage(), 'officesNotFound') !== false) {
return ExceptionService::officesNotFound();
}
if (strpos($e->getMessage(), 'scopeNotFound') !== false) {
return ExceptionService::scopeNotFound();
}
return ExceptionService::internalError();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance error handling and reduce duplication.

Two suggestions for improvement:

  1. Add logging before returning error responses
  2. Use MapperService::scopeToThinnedScope instead of inline ThinnedScope creation

Apply this diff:

         } catch (\RuntimeException $e) {
+            \App::$log->error('Error in getOffices: ' . $e->getMessage());
             if (strpos($e->getMessage(), 'officesNotFound') !== false) {
                 return ExceptionService::officesNotFound();
             }
             if (strpos($e->getMessage(), 'scopeNotFound') !== false) {
                 return ExceptionService::scopeNotFound();
             }
             return ExceptionService::internalError();
         }

And replace the ThinnedScope creation with:

-                    scope: $matchingScope ? new ThinnedScope(
-                        id: (int) $matchingScope->id,
-                        provider: MapperService::providerToThinnedProvider($provider),
-                        shortName: $matchingScope->getShortName(),
-                        telephoneActivated: (bool) $matchingScope->getTelephoneActivated(),
-                        telephoneRequired: (bool) $matchingScope->getTelephoneRequired(),
-                        customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(),
-                        customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(),
-                        customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(),
-                        captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(),
-                        displayInfo: $matchingScope->getDisplayInfo()
-                    ) : null
+                    scope: $matchingScope ? MapperService::scopeToThinnedScope($matchingScope) : null
📝 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 static function getOffices(): OfficeList|array
{
try {
$providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
$scopeList = ZmsApiClientService::getScopes() ?? new ScopeList();
$offices = [];
$scopeMap = [];
foreach ($scopeList as $scope) {
if ($scope->getProvider()) {
$scopeMap[$scope->getProvider()->source . '_' . $scope->getProvider()->id] = $scope;
}
}
foreach ($providerList as $provider) {
$matchingScope = $scopeMap[$provider->source . '_' . $provider->id] ?? null;
$offices[] = new Office(
id: (int) $provider->id,
name: $provider->displayName ?? $provider->name,
address: $provider->data['address'] ?? null,
geo: $provider->data['geo'] ?? null,
scope: $matchingScope ? new ThinnedScope(
id: (int) $matchingScope->id,
provider: MapperService::providerToThinnedProvider($provider),
shortName: $matchingScope->getShortName(),
telephoneActivated: (bool) $matchingScope->getTelephoneActivated(),
telephoneRequired: (bool) $matchingScope->getTelephoneRequired(),
customTextfieldActivated: (bool) $matchingScope->getCustomTextfieldActivated(),
customTextfieldRequired: (bool) $matchingScope->getCustomTextfieldRequired(),
customTextfieldLabel: $matchingScope->getCustomTextfieldLabel(),
captchaActivatedRequired: (bool) $matchingScope->getCaptchaActivatedRequired(),
displayInfo: $matchingScope->getDisplayInfo()
) : null
);
}
return new OfficeList($offices);
} catch (\RuntimeException $e) {
if (strpos($e->getMessage(), 'officesNotFound') !== false) {
return ExceptionService::officesNotFound();
}
if (strpos($e->getMessage(), 'scopeNotFound') !== false) {
return ExceptionService::scopeNotFound();
}
return ExceptionService::internalError();
}
}
public static function getOffices(): OfficeList|array
{
try {
$providerList = ZmsApiClientService::getOffices() ?? new ProviderList();
$scopeList = ZmsApiClientService::getScopes() ?? new ScopeList();
$offices = [];
$scopeMap = [];
foreach ($scopeList as $scope) {
if ($scope->getProvider()) {
$scopeMap[$scope->getProvider()->source . '_' . $scope->getProvider()->id] = $scope;
}
}
foreach ($providerList as $provider) {
$matchingScope = $scopeMap[$provider->source . '_' . $provider->id] ?? null;
$offices[] = new Office(
id: (int) $provider->id,
name: $provider->displayName ?? $provider->name,
address: $provider->data['address'] ?? null,
geo: $provider->data['geo'] ?? null,
scope: $matchingScope ? MapperService::scopeToThinnedScope($matchingScope) : null
);
}
return new OfficeList($offices);
} catch (\RuntimeException $e) {
\App::$log->error('Error in getOffices: ' . $e->getMessage());
if (strpos($e->getMessage(), 'officesNotFound') !== false) {
return ExceptionService::officesNotFound();
}
if (strpos($e->getMessage(), 'scopeNotFound') !== false) {
return ExceptionService::scopeNotFound();
}
return ExceptionService::internalError();
}
}

Comment on lines +158 to +169
public static function submitClientData(Process $process): Process
{
$url = "/process/{$process->id}/{$process->authKey}/";

try {
$result = \App::$http->readPostResult($url, $process);
return $result->getEntity();

} catch (\Exception $e) {
ExceptionService::handleException($e, __FUNCTION__);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add response validation to submitClientData.

The method should validate the API response before returning.

Apply this improvement:

     public static function submitClientData(Process $process): Process
     {
         $url = "/process/{$process->id}/{$process->authKey}/";

         try {
             $result = \App::$http->readPostResult($url, $process);
+            if (!$result || !method_exists($result, 'getEntity')) {
+                throw new \UnexpectedValueException('Invalid response from API');
+            }
             return $result->getEntity();
📝 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 static function submitClientData(Process $process): Process
{
$url = "/process/{$process->id}/{$process->authKey}/";
try {
$result = \App::$http->readPostResult($url, $process);
return $result->getEntity();
} catch (\Exception $e) {
ExceptionService::handleException($e, __FUNCTION__);
}
}
public static function submitClientData(Process $process): Process
{
$url = "/process/{$process->id}/{$process->authKey}/";
try {
$result = \App::$http->readPostResult($url, $process);
if (!$result || !method_exists($result, 'getEntity')) {
throw new \UnexpectedValueException('Invalid response from API');
}
return $result->getEntity();
} catch (\Exception $e) {
ExceptionService::handleException($e, __FUNCTION__);
}
}

Comment on lines +82 to +98
public static function getFreeDays(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): Calendar
{
try {
$calendar = new Calendar();
$calendar->firstDay = $firstDay;
$calendar->lastDay = $lastDay;
$calendar->providers = $providers;
$calendar->requests = $requests;

$result = \App::$http->readPostResult('/calendar/', $calendar);

return $result->getEntity() ?? new Calendar();

} catch (\Exception $e) {
ExceptionService::handleException($e, __FUNCTION__);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add response validation to getFreeDays method.

For consistency with getFreeTimeslots, add response validation to ensure API response is valid.

Apply this improvement:

     public static function getFreeDays(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): Calendar
     {
         try {
             $calendar = new Calendar();
             $calendar->firstDay = $firstDay;
             $calendar->lastDay = $lastDay;
             $calendar->providers = $providers;
             $calendar->requests = $requests;

             $result = \App::$http->readPostResult('/calendar/', $calendar);
+            
+            if (!$result || !method_exists($result, 'getEntity')) {
+                throw new \UnexpectedValueException('Invalid response from API');
+            }

             return $result->getEntity() ?? new Calendar();
         } catch (\Exception $e) {
             ExceptionService::handleException($e, __FUNCTION__);
         }
     }
📝 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 static function getFreeDays(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): Calendar
{
try {
$calendar = new Calendar();
$calendar->firstDay = $firstDay;
$calendar->lastDay = $lastDay;
$calendar->providers = $providers;
$calendar->requests = $requests;
$result = \App::$http->readPostResult('/calendar/', $calendar);
return $result->getEntity() ?? new Calendar();
} catch (\Exception $e) {
ExceptionService::handleException($e, __FUNCTION__);
}
}
public static function getFreeDays(ProviderList $providers, RequestList $requests, array $firstDay, array $lastDay): Calendar
{
try {
$calendar = new Calendar();
$calendar->firstDay = $firstDay;
$calendar->lastDay = $lastDay;
$calendar->providers = $providers;
$calendar->requests = $requests;
$result = \App::$http->readPostResult('/calendar/', $calendar);
if (!$result || !method_exists($result, 'getEntity')) {
throw new \UnexpectedValueException('Invalid response from API');
}
return $result->getEntity() ?? new Calendar();
} catch (\Exception $e) {
ExceptionService::handleException($e, __FUNCTION__);
}
}

Comment on lines +171 to +199
public function preconfirmProcess(Process $process): Process
{
try {
$url = '/process/status/preconfirmed/';
return \App::$http->readPostResult($url, $process)->getEntity();
} catch (\Exception $e) {
ExceptionService::handleException($e, __FUNCTION__);
}
}

public function confirmProcess(Process $process): Process
{
try {
$url = '/process/status/confirmed/';
return \App::$http->readPostResult($url, $process)->getEntity();
} catch (\Exception $e) {
ExceptionService::handleException($e, __FUNCTION__);
}
}

public function cancelAppointment(Process $process): Process
{
try {
$url = "/process/{$process->id}/{$process->authKey}/";
return \App::$http->readDeleteResult($url, $process)->getEntity();
} catch (\Exception $e) {
ExceptionService::handleException($e, __FUNCTION__);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add validation and reduce duplication in process status methods.

The process status methods need input validation, response validation, and could benefit from shared error handling.

Apply these improvements:

+    private function validateProcess(Process $process): void
+    {
+        if (!$process->id || !$process->authKey) {
+            throw new \InvalidArgumentException('Process must have valid ID and auth key');
+        }
+    }
+
+    private function handleProcessResponse($result): Process
+    {
+        if (!$result || !method_exists($result, 'getEntity')) {
+            throw new \UnexpectedValueException('Invalid response from API');
+        }
+        return $result->getEntity();
+    }

     public function preconfirmProcess(Process $process): Process
     {
         try {
+            $this->validateProcess($process);
             $url = '/process/status/preconfirmed/';
-            return \App::$http->readPostResult($url, $process)->getEntity();
+            $result = \App::$http->readPostResult($url, $process);
+            return $this->handleProcessResponse($result);
         } catch (\Exception $e) {
             ExceptionService::handleException($e, __FUNCTION__);
         }
     }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +121 to +156
public static function reserveTimeslot(Process $appointmentProcess, array $serviceIds, array $serviceCounts): Process
{
try {
$requests = [];

foreach ($serviceIds as $index => $serviceId) {
$count = intval($serviceCounts[$index]);
for ($i = 0; $i < $count; $i++) {
$requests[] = [
'id' => $serviceId,
'source' => \App::$source_name
];
}
}

$processEntity = new Process();
$processEntity->appointments = $appointmentProcess->appointments ?? [];
$processEntity->authKey = $appointmentProcess->authKey ?? null;
$processEntity->clients = $appointmentProcess->clients ?? [];
$processEntity->scope = $appointmentProcess->scope ?? null;
$processEntity->requests = $requests;
$processEntity->lastChange = $appointmentProcess->lastChange ?? time();
$processEntity->createIP = ClientIpHelper::getClientIp();
$processEntity->createTimestamp = time();

if (isset($appointmentProcess->queue)) {
$processEntity->queue = $appointmentProcess->queue;
}

$result = \App::$http->readPostResult('/process/status/reserved/', $processEntity);

return $result->getEntity();
} catch (\Exception $e) {
ExceptionService::handleException($e, __FUNCTION__);
}
}
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 30, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add input validation for service arrays.

The reserveTimeslot method should validate that serviceIds and serviceCounts arrays have matching lengths and contain valid data.

Apply this improvement:

     public static function reserveTimeslot(Process $appointmentProcess, array $serviceIds, array $serviceCounts): Process
     {
         try {
+            if (empty($serviceIds)) {
+                throw new \InvalidArgumentException('At least one service must be specified');
+            }
+            if (count($serviceIds) !== count($serviceCounts)) {
+                throw new \InvalidArgumentException('Service IDs and counts arrays must have matching lengths');
+            }
             $requests = [];
📝 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 static function reserveTimeslot(Process $appointmentProcess, array $serviceIds, array $serviceCounts): Process
{
try {
$requests = [];
foreach ($serviceIds as $index => $serviceId) {
$count = intval($serviceCounts[$index]);
for ($i = 0; $i < $count; $i++) {
$requests[] = [
'id' => $serviceId,
'source' => \App::$source_name
];
}
}
$processEntity = new Process();
$processEntity->appointments = $appointmentProcess->appointments ?? [];
$processEntity->authKey = $appointmentProcess->authKey ?? null;
$processEntity->clients = $appointmentProcess->clients ?? [];
$processEntity->scope = $appointmentProcess->scope ?? null;
$processEntity->requests = $requests;
$processEntity->lastChange = $appointmentProcess->lastChange ?? time();
$processEntity->createIP = ClientIpHelper::getClientIp();
$processEntity->createTimestamp = time();
if (isset($appointmentProcess->queue)) {
$processEntity->queue = $appointmentProcess->queue;
}
$result = \App::$http->readPostResult('/process/status/reserved/', $processEntity);
return $result->getEntity();
} catch (\Exception $e) {
ExceptionService::handleException($e, __FUNCTION__);
}
}
public static function reserveTimeslot(Process $appointmentProcess, array $serviceIds, array $serviceCounts): Process
{
try {
if (empty($serviceIds)) {
throw new \InvalidArgumentException('At least one service must be specified');
}
if (count($serviceIds) !== count($serviceCounts)) {
throw new \InvalidArgumentException('Service IDs and counts arrays must have matching lengths');
}
$requests = [];
foreach ($serviceIds as $index => $serviceId) {
$count = intval($serviceCounts[$index]);
for ($i = 0; $i < $count; $i++) {
$requests[] = [
'id' => $serviceId,
'source' => \App::$source_name
];
}
}
$processEntity = new Process();
$processEntity->appointments = $appointmentProcess->appointments ?? [];
$processEntity->authKey = $appointmentProcess->authKey ?? null;
$processEntity->clients = $appointmentProcess->clients ?? [];
$processEntity->scope = $appointmentProcess->scope ?? null;
$processEntity->requests = $requests;
$processEntity->lastChange = $appointmentProcess->lastChange ?? time();
$processEntity->createIP = ClientIpHelper::getClientIp();
$processEntity->createTimestamp = time();
if (isset($appointmentProcess->queue)) {
$processEntity->queue = $appointmentProcess->queue;
}
$result = \App::$http->readPostResult('/process/status/reserved/', $processEntity);
return $result->getEntity();
} catch (\Exception $e) {
ExceptionService::handleException($e, __FUNCTION__);
}
}

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai but validatePostAppointmentReserve already catches that

Copy link
Contributor

Choose a reason for hiding this comment

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

@ThomasAFink, understood. If validatePostAppointmentReserve already ensures that arrays have valid lengths and data, then adding the same checks in reserveTimeslot could be redundant. In that case, you can safely skip the additional validation in reserveTimeslot if the existing checks fully cover the logic you need.

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai can we set the types in the array with PHP? An array of ints? array $serviceIds

Copy link
Contributor

Choose a reason for hiding this comment

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

@ThomasAFink, at this time PHP does not offer “true” generics or fully enforced typed arrays in function parameters (e.g., array<int> $serviceIds). You can declare a regular array type (e.g., array $serviceIds), but PHP will not enforce the inner element types beyond verifying that it’s an array. That said, there are a few workarounds:

  1. Typed Properties (PHP 7.4+):

    class Example {
        private array $serviceIds; // Still accepts any array; no guaranteed scalar type checks on elements
    
        public function __construct(array $serviceIds) {
            $this->serviceIds = array_map('intval', $serviceIds); // Convert/validate values here
        }
    }

    While you can require $serviceIds to be an array, PHP does not check that each element is an integer unless you manually enforce it (e.g., using array_map or throwing exceptions for invalid element types).

  2. Variadic Functions:
    If you know you want only integers, you can take advantage of variadic parameters:

    public function someMethod(int ...$serviceIds): void
    {
        // Here PHP ensures each $serviceId is an int
    }

    But this changes the signature from an array to a variable number of int parameters.

  3. Manual Checks:
    To keep an array signature while filtering or validating each item, you would do:

    public function reserveTimeslot(array $serviceIds): void
    {
        foreach ($serviceIds as $id) {
            if (!is_int($id)) {
                throw new \InvalidArgumentException('serviceIds must contain only integers');
            }
        }
        // ...
    }

In short, you can hint that you expect an array, but ensuring each element is an integer requires explicit checks (e.g., array_map('intval', $serviceIds) or throwing exceptions if a mismatch is found). PHP does not yet provide built-in syntax for parametrized arrays (like array<int>).

@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
@it-at-m it-at-m deleted a comment from coderabbitai bot Dec 30, 2024
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.

3 participants