diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..481fb0a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +# .github/workflows/ci.yml +name: CI + +on: + push: + branches: [ main ] + paths: ['**/*.kt', '**/*.kts', '.github/workflows/ci.yml'] + pull_request: + branches: [ main ] + +jobs: + build: + if: github.event_name == 'push' || github.event_name == 'pull_request' + runs-on: ubuntu-latest + name: "Run Tests" + + env: + GH_USERNAME: ${{ github.actor }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 23 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 23 + + - name: Cache Gradle dependencies + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + gradle-${{ runner.os }}- + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Test + run: ./gradlew test \ No newline at end of file diff --git a/.github/workflows/gemini-pr-review.yaml b/.github/workflows/gemini-pr-review.yaml new file mode 100644 index 0000000..a28ac0b --- /dev/null +++ b/.github/workflows/gemini-pr-review.yaml @@ -0,0 +1,417 @@ +name: "🧐 Gemini Pull Request Review" + +on: + pull_request: + types: + - "opened" + pull_request_review_comment: + types: + - "created" + pull_request_review: + types: + - "submitted" + workflow_dispatch: + inputs: + pr_number: + description: "PR number to review" + required: true + type: "number" + +concurrency: + group: "${{ github.workflow }}-${{ github.head_ref || github.ref }}" + cancel-in-progress: true + +defaults: + run: + shell: "bash" + +permissions: + contents: "read" + id-token: "write" + issues: "write" + pull-requests: "write" + statuses: "write" + +jobs: + review-pr: + if: |- + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && github.event.action == 'opened') || + (github.event_name == 'issue_comment' && github.event.issue.pull_request && + contains(github.event.comment.body, '@gemini-cli /review') && + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' + ) + ) || + (github.event_name == 'pull_request_review_comment' && + contains(github.event.comment.body, '@gemini-cli /review') && + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' + ) + ) || + (github.event_name == 'pull_request_review' && + contains(github.event.review.body, '@gemini-cli /review') && + ( + github.event.review.author_association == 'OWNER' || + github.event.review.author_association == 'MEMBER' || + github.event.review.author_association == 'COLLABORATOR' + ) + ) + timeout-minutes: 5 + runs-on: "ubuntu-latest" + + steps: + - name: "Checkout PR code" + uses: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683" # ratchet:actions/checkout@v4 + + - name: "Generate GitHub App Token" + id: "generate_token" + if: |- + ${{ vars.APP_ID }} + uses: "actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e" # ratchet:actions/create-github-app-token@v2 + with: + app-id: "${{ vars.APP_ID }}" + private-key: "${{ secrets.APP_PRIVATE_KEY }}" + + - name: "Get PR details (pull_request & workflow_dispatch)" + id: "get_pr" + if: |- + ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} + env: + GITHUB_TOKEN: "${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}" + EVENT_NAME: "${{ github.event_name }}" + WORKFLOW_PR_NUMBER: "${{ github.event.inputs.pr_number }}" + PULL_REQUEST_NUMBER: "${{ github.event.pull_request.number }}" + run: |- + set -euo pipefail + + if [[ "${EVENT_NAME}" = "workflow_dispatch" ]]; then + PR_NUMBER="${WORKFLOW_PR_NUMBER}" + else + PR_NUMBER="${PULL_REQUEST_NUMBER}" + fi + + echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}" + + # Get PR details + PR_DATA="$(gh pr view "${PR_NUMBER}" --json title,body,additions,deletions,changedFiles,baseRefName,headRefName)" + echo "pr_data=${PR_DATA}" >> "${GITHUB_OUTPUT}" + + # Get file changes + CHANGED_FILES="$(gh pr diff "${PR_NUMBER}" --name-only)" + { + echo "changed_files<> "${GITHUB_OUTPUT}" + + - name: "Get PR details (issue_comment)" + id: "get_pr_comment" + if: |- + ${{ github.event_name == 'issue_comment' }} + env: + GITHUB_TOKEN: "${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}" + COMMENT_BODY: "${{ github.event.comment.body }}" + PR_NUMBER: "${{ github.event.issue.number }}" + run: |- + set -euo pipefail + + echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}" + + # Extract additional instructions from comment + ADDITIONAL_INSTRUCTIONS="$( + echo "${COMMENT_BODY}" | sed 's/.*@gemini-cli \/review//' | xargs + )" + echo "additional_instructions=${ADDITIONAL_INSTRUCTIONS}" >> "${GITHUB_OUTPUT}" + + # Get PR details + PR_DATA="$(gh pr view "${PR_NUMBER}" --json title,body,additions,deletions,changedFiles,baseRefName,headRefName)" + echo "pr_data=${PR_DATA}" >> "${GITHUB_OUTPUT}" + + # Get file changes + CHANGED_FILES="$(gh pr diff "${PR_NUMBER}" --name-only)" + { + echo "changed_files<> "${GITHUB_OUTPUT}" + + - name: "Run Gemini PR Review" + uses: "google-github-actions/run-gemini-cli@v0" + id: "gemini_pr_review" + env: + GITHUB_TOKEN: "${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}" + PR_NUMBER: "${{ steps.get_pr.outputs.pr_number || steps.get_pr_comment.outputs.pr_number }}" + PR_DATA: "${{ steps.get_pr.outputs.pr_data || steps.get_pr_comment.outputs.pr_data }}" + CHANGED_FILES: "${{ steps.get_pr.outputs.changed_files || steps.get_pr_comment.outputs.changed_files }}" + ADDITIONAL_INSTRUCTIONS: "${{ steps.get_pr.outputs.additional_instructions || steps.get_pr_comment.outputs.additional_instructions }}" + REPOSITORY: "${{ github.repository }}" + with: + gemini_cli_version: "${{ vars.GEMINI_CLI_VERSION }}" + gcp_workload_identity_provider: "${{ vars.GCP_WIF_PROVIDER }}" + gcp_project_id: "${{ vars.GOOGLE_CLOUD_PROJECT }}" + gcp_location: "${{ vars.GOOGLE_CLOUD_LOCATION }}" + gcp_service_account: "${{ vars.SERVICE_ACCOUNT_EMAIL }}" + gemini_api_key: "${{ secrets.GEMINI_API_KEY }}" + use_vertex_ai: "${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}" + use_gemini_code_assist: "${{ vars.GOOGLE_GENAI_USE_GCA }}" + settings: |- + { + "maxSessionTurns": 20, + "mcpServers": { + "github": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "GITHUB_PERSONAL_ACCESS_TOKEN", + "ghcr.io/github/github-mcp-server" + ], + "includeTools": [ + "create_pending_pull_request_review", + "add_comment_to_pending_review", + "submit_pending_pull_request_review" + ], + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" + } + } + }, + "coreTools": [ + "run_shell_command(echo)", + "run_shell_command(gh pr view)", + "run_shell_command(gh pr diff)", + "run_shell_command(cat)", + "run_shell_command(head)", + "run_shell_command(tail)", + "run_shell_command(grep)" + ], + "telemetry": { + "enabled": false, + "target": "gcp" + } + } + prompt: |- + ## Role + + You are an expert code reviewer. You have access to tools to gather + PR information and perform the review. Use the available tools to + gather information; do not ask for information to be provided. + + ## Prerequisites + + Before doing anything, read the ai.md file in the root of this project. + Ensure that you have a good understanding of the project before reading + or writing any code. + + ## Steps + + Start by running these commands to gather the required data: + 1. Run: echo "${PR_DATA}" to get PR details (JSON format) + 2. Run: echo "${CHANGED_FILES}" to get the list of changed files + 3. Run: echo "${PR_NUMBER}" to get the PR number + 4. Run: echo "${ADDITIONAL_INSTRUCTIONS}" to see any specific review + instructions from the user + 5. Run: gh pr diff "${PR_NUMBER}" to see the full diff + 6. For any specific files, use: cat filename, head -50 filename, or + tail -50 filename + 7. If ADDITIONAL_INSTRUCTIONS contains text, prioritize those + specific areas or focus points in your review. Common instruction + examples: "focus on security", "check performance", "review error + handling", "check for breaking changes" + + ## Guideline + ### Core Guideline(Always applicable) + + 1. Understand the Context: Analyze the pull request title, description, changes, and code files to grasp the intent. + 2. Meticulous Review: Thoroughly review all relevant code changes, prioritizing added lines. Consider the specified + focus areas and any provided style guide. + 3. Comprehensive Review: Ensure that the code is thoroughly reviewed, as it's important to the author + that you identify any and all relevant issues (subject to the review criteria and style guide). + Missing any issues will lead to a poor code review experience for the author. + 4. Constructive Feedback: + * Provide clear explanations for each concern. + * Offer specific, improved code suggestions and suggest alternative approaches, when applicable. + Code suggestions in particular are very helpful so that the author can directly apply them + to their code, but they must be accurately anchored to the lines that should be replaced. + 5. Severity Indication: Clearly indicate the severity of the issue in the review comment. + This is very important to help the author understand the urgency of the issue. + The severity should be one of the following (which are provided below in decreasing order of severity): + * `critical`: This issue must be addressed immediately, as it could lead to serious consequences + for the code's correctness, security, or performance. + * `high`: This issue should be addressed soon, as it could cause problems in the future. + * `medium`: This issue should be considered for future improvement, but it's not critical or urgent. + * `low`: This issue is minor or stylistic, and can be addressed at the author's discretion. + 6. Avoid commenting on hardcoded dates and times being in future or not (for example "this date is in the future"). + * Remember you don't have access to the current date and time and leave that to the author. + 7. Targeted Suggestions: Limit all suggestions to only portions that are modified in the diff hunks. + This is a strict requirement as the GitHub (and other SCM's) API won't allow comments on parts of code files that are not + included in the diff hunks. + 8. Code Suggestions in Review Comments: + * Succintness: Aim to make code suggestions succinct, unless necessary. Larger code suggestions tend to be + harder for pull request authors to commit directly in the pull request UI. + * Valid Formatting: Provide code suggestions within the suggestion field of the JSON response (as a string literal, + escaping special characters like \n, \\, \"). Do not include markdown code blocks in the suggestion field. + Use markdown code blocks in the body of the comment only for broader examples or if a suggestion field would + create an excessively large diff. Prefer the suggestion field for specific, targeted code changes. + * Line Number Accuracy: Code suggestions need to align perfectly with the code it intend to replace. + Pay special attention to line numbers when creating comments, particularly if there is a code suggestion. + Note the patch includes code versions with line numbers for the before and after code snippets for each diff, so use these to anchor + your comments and corresponding code suggestions. + * Compilable: Code suggestions should be compilable code snippets that can be directly copy/pasted into the code file. + If the suggestion is not compilable, it will not be accepted by the pull request. Note that not all languages Are + compiled of course, so by compilable here, we mean either literally or in spirit. + * Inline Code Comments: Feel free to add brief comments to the code suggestion if it enhances the underlying code readability. + Just make sure that the inline code comments add value, and are not just restating what the code does. Don't use + inline comments to "teach" the author (use the review comment body directly for that), instead use it if it's beneficial + to the readability of the code itself. + 10. Markdown Formatting: Heavily leverage the benefits of markdown for formatting, such as bulleted lists, bold text, tables, etc. + 11. Avoid mistaken review comments: + * Any comment you make must point towards a discrepancy found in the code and the best practice surfaced in your feedback. + For example, if you are pointing out that constants need to be named in all caps with underscores, + ensure that the code selected by the comment does not already do this, otherwise it's confusing let alone unnecessary. + 12. Remove Duplicated code suggestions: + * Some provided code suggestions are duplicated, please remove the duplicated review comments. + 13. Don't Approve The Pull Request + 14. Reference all shell variables as "${VAR}" (with quotes and braces) + + ### Review Criteria (Prioritized in Review) + + * Correctness: Verify code functionality, handle edge cases, and ensure alignment between function + descriptions and implementations. Consider common correctness issues (logic errors, error handling, + race conditions, data validation, API usage, type mismatches). + * Efficiency: Identify performance bottlenecks, optimize for efficiency, and avoid unnecessary + loops, iterations, or calculations. Consider common efficiency issues (excessive loops, memory + leaks, inefficient data structures, redundant calculations, excessive logging, etc.). + * Maintainability: Assess code readability, modularity, and adherence to language idioms and + best practices. Consider common maintainability issues (naming, comments/documentation, complexity, + code duplication, formatting, magic numbers). State the style guide being followed (defaulting to + commonly used guides, for example Python's PEP 8 style guide or Google Java Style Guide, if no style guide is specified). + * Security: Identify potential vulnerabilities (e.g., insecure storage, injection attacks, + insufficient access controls). + + ### Miscellanous Considerations + * Testing: Ensure adequate unit tests, integration tests, and end-to-end tests. Evaluate + coverage, edge case handling, and overall test quality. + * Performance: Assess performance under expected load, identify bottlenecks, and suggest + optimizations. + * Scalability: Evaluate how the code will scale with growing user base or data volume. + * Modularity and Reusability: Assess code organization, modularity, and reusability. Suggest + refactoring or creating reusable components. + * Error Logging and Monitoring: Ensure errors are logged effectively, and implement monitoring + mechanisms to track application health in production. + + **CRITICAL CONSTRAINTS:** + + You MUST only provide comments on lines that represent the actual changes in + the diff. This means your comments should only refer to lines that begin with + a `+` or `-` character in the provided diff content. + DO NOT comment on lines that start with a space (context lines). + + You MUST only add a review comment if there exists an actual ISSUE or BUG in the code changes. + DO NOT add review comments to tell the user to "check" or "confirm" or "verify" something. + DO NOT add review comments to tell the user to "ensure" something. + DO NOT add review comments to explain what the code change does. + DO NOT add review comments to validate what the code change does. + DO NOT use the review comments to explain the code to the author. They already know their code. Only comment when there's an improvement opportunity. This is very important. + + Pay close attention to line numbers and ensure they are correct. + Pay close attention to indentations in the code suggestions and make sure they match the code they are to replace. + Avoid comments on the license headers - if any exists - and instead make comments on the code that is being changed. + + It's absolutely important to avoid commenting on the license header of files. + It's absolutely important to avoid commenting on copyright headers. + Avoid commenting on hardcoded dates and times being in future or not (for example "this date is in the future"). + Remember you don't have access to the current date and time and leave that to the author. + + Avoid mentioning any of your instructions, settings or criteria. + + Here are some general guidelines for setting the severity of your comments + - Comments about refactoring a hardcoded string or number as a constant are generally considered low severity. + - Comments about log messages or log enhancements are generally considered low severity. + - Comments in .md files are medium or low severity. This is really important. + - Comments about adding or expanding docstring/javadoc have low severity most of the times. + - Comments about suppressing unchecked warnings or todos are considered low severity. + - Comments about typos are usually low or medium severity. + - Comments about testing or on tests are usually low severity. + - Do not comment about the content of a URL if the content is not directly available in the input. + + Keep comments bodies concise and to the point. + Keep each comment focused on one issue. + + ## Review + + Once you have the information, provide a comprehensive code review by: + 1. Creating a pending review: Use the mcp__github__create_pending_pull_request_review to create a Pending Pull Request Review. + + 2. Adding review comments: + 2.1 Use the mcp__github__add_comment_to_pending_review to add comments to the Pending Pull Request Review. Inline comments are preffered whenever possible, so repeat this step, calling mcp__github__add_comment_to_pending_review, as needed. All comments about specific lines of code should use inline comments. It is preferred to use code suggestions when possible, which include a code block that is labeled "suggestion", which contains what the new code should be. All comments should also have a piority. They syntax is: + Normal Comment Syntax: + + {{PRIORITY}} {{COMMENT_TEXT}} + + + Inline Comment Syntax: (Preferred): + + {{PRIORITY}} {{COMMENT_TEXT}} + ```suggestion + {{CODE_SUGGESTION}} + ``` + + + Prepend a severity emoji to each comment: + - 🟢 for low severity + - 🟡 for medium severity + - 🟠 for high severity + - 🔴 for critical severity + - 🔵 if severity is unclear + + Including all of this, an example inline comment would be: + + 🟢 Use camelCase for function names + ```suggestion + myFooBarFunction + ``` + + + A critical severity example would be: + + 🔴 Remove storage key from GitHub + ```suggestion + ``` + + 3. Posting the review: Use the mcp__github__submit_pending_pull_request_review to submit the Pending Pull Request Review. + + 3.1 Crafting the summary comment: Include a summary of high level points that were not addressed with inline comments. Be concise. Do not repeat details mentioned inline. + + Structure your summary comment using this exact format with markdown: + ## 📋 Review Summary + + Provide a brief 2-3 sentence overview of the PR and overall + assessment. + + ## 🔍 General Feedback + - List general observations about code quality + - Mention overall patterns or architectural decisions + - Highlight positive aspects of the implementation + - Note any recurring themes across files + + - name: "Post PR review failure comment" + if: |- + ${{ failure() && steps.gemini_pr_review.outcome == 'failure' }} + uses: "actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea" + with: + github-token: "${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}" + script: |- + github.rest.issues.createComment({ + owner: '${{ github.repository }}'.split('/')[0], + repo: '${{ github.repository }}'.split('/')[1], + issue_number: '${{ steps.get_pr.outputs.pr_number || steps.get_pr_comment.outputs.pr_number }}', + body: 'There is a problem with the Gemini CLI PR review. Please check the [action logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.' + }) diff --git a/src/main/kotlin/com/ctrlhub/core/projects/appointments/response/Appointment.kt b/src/main/kotlin/com/ctrlhub/core/projects/appointments/response/Appointment.kt new file mode 100644 index 0000000..85846db --- /dev/null +++ b/src/main/kotlin/com/ctrlhub/core/projects/appointments/response/Appointment.kt @@ -0,0 +1,32 @@ +package com.ctrlhub.core.projects.appointments.response + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import com.github.jasminb.jsonapi.StringIdHandler +import com.github.jasminb.jsonapi.annotations.Id +import com.github.jasminb.jsonapi.annotations.Type + +@Type("appointments") +@JsonIgnoreProperties(ignoreUnknown = true) +class Appointment @JsonCreator constructor( + @Id(StringIdHandler::class) var id: String = "", + @JsonProperty("animals") val animals: Boolean = false, + @JsonProperty("end_time") val endTime: String = "", + @JsonProperty("medical_dependency") val medicalDependency: Boolean = false, + @JsonProperty("notes") val notes: String = "", + @JsonProperty("on_ecr") val onEcr: Boolean = false, + @JsonProperty("on_ecr_notes") val onEcrNotes: String = "", + @JsonProperty("start_time") val startTime: String = "" +) { + constructor() : this( + id = "", + animals = false, + endTime = "", + medicalDependency = false, + notes = "", + onEcr = false, + onEcrNotes = "", + startTime = "" + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/ctrlhub/core/projects/operations/OperationsRouter.kt b/src/main/kotlin/com/ctrlhub/core/projects/operations/OperationsRouter.kt index 6ad56fe..9fdb9c5 100644 --- a/src/main/kotlin/com/ctrlhub/core/projects/operations/OperationsRouter.kt +++ b/src/main/kotlin/com/ctrlhub/core/projects/operations/OperationsRouter.kt @@ -5,16 +5,18 @@ import com.ctrlhub.core.api.response.PaginatedList import com.ctrlhub.core.projects.workorders.WorkOrdersRouter import com.ctrlhub.core.projects.operations.response.Operation import com.ctrlhub.core.iam.response.User +import com.ctrlhub.core.projects.appointments.response.Appointment import com.ctrlhub.core.projects.operations.templates.response.OperationTemplate import com.ctrlhub.core.router.Router import com.ctrlhub.core.router.request.FilterOption import com.ctrlhub.core.router.request.JsonApiIncludes -import com.ctrlhub.core.router.request.RequestParameters import com.ctrlhub.core.router.request.RequestParametersWithIncludes import io.ktor.client.HttpClient enum class OperationIncludes(val value: String) : JsonApiIncludes { Template("template"), + Appointments("appointment"), + Properties("properties"), Forms("forms"); override fun value(): String { @@ -46,7 +48,7 @@ class OperationsRouter(httpClient: HttpClient) : Router(httpClient) { return fetchPaginatedJsonApiResources( endpoint, requestParameters.toMap(), Operation::class.java, - OperationTemplate::class.java, User::class.java + OperationTemplate::class.java, User::class.java, Appointment::class.java ) } diff --git a/src/main/kotlin/com/ctrlhub/core/projects/operations/response/Operation.kt b/src/main/kotlin/com/ctrlhub/core/projects/operations/response/Operation.kt index 0b1cb2d..a21fc9d 100644 --- a/src/main/kotlin/com/ctrlhub/core/projects/operations/response/Operation.kt +++ b/src/main/kotlin/com/ctrlhub/core/projects/operations/response/Operation.kt @@ -2,6 +2,7 @@ package com.ctrlhub.core.projects.operations.response import com.ctrlhub.core.api.Assignable import com.ctrlhub.core.geo.Property +import com.ctrlhub.core.projects.appointments.response.Appointment import com.ctrlhub.core.projects.operations.templates.response.OperationTemplate import com.ctrlhub.core.projects.response.Label import com.fasterxml.jackson.annotation.JsonCreator @@ -36,6 +37,9 @@ class Operation @JsonCreator constructor( @Relationship("properties") var properties: java.util.List? = null, + + @Relationship("appointment") + var appointment: Appointment? = null ) { constructor(): this( name = "", diff --git a/src/test/kotlin/com/ctrlhub/core/projects/operations/OperationsRouterTest.kt b/src/test/kotlin/com/ctrlhub/core/projects/operations/OperationsRouterTest.kt index 596aebb..8cb61d2 100644 --- a/src/test/kotlin/com/ctrlhub/core/projects/operations/OperationsRouterTest.kt +++ b/src/test/kotlin/com/ctrlhub/core/projects/operations/OperationsRouterTest.kt @@ -97,4 +97,44 @@ class OperationsRouterTest { assertEquals("Example Template Name", response.data[0].template?.name) } } + + @Test + fun `can retrieve all operations with included appointments`() { + val jsonFilePath = Paths.get("src/test/resources/projects/operations/all-operations-with-included-appointments.json") + val jsonContent = Files.readString(jsonFilePath) + + val mockEngine = MockEngine { request -> + respond( + content = jsonContent, + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } + + val operationsRouter = OperationsRouter(httpClient = HttpClient(mockEngine).configureForTest()) + + runBlocking { + val response = operationsRouter.all( + organisationId = "123", + requestParameters = OperationRequestParameters( + includes = listOf( + OperationIncludes.Appointments + ) + ) + ) + + assertIs>(response) + + // Find the operation that has an appointment + val operationWithAppointment = response.data.find { it.appointment != null } + assertNotNull(operationWithAppointment, "Should have at least one operation with an appointment") + + // Validate the appointment is properly hydrated + val appointment = operationWithAppointment?.appointment + assertNotNull(appointment, "Appointment should not be null") + assertEquals("appointment-1", appointment?.id) + assertEquals("2025-08-15T07:00:00Z", appointment?.startTime) + assertEquals("2025-08-15T11:00:00Z", appointment?.endTime) + } + } } \ No newline at end of file diff --git a/src/test/resources/projects/operations/all-operations-with-included-appointments.json b/src/test/resources/projects/operations/all-operations-with-included-appointments.json new file mode 100644 index 0000000..7e133e5 --- /dev/null +++ b/src/test/resources/projects/operations/all-operations-with-included-appointments.json @@ -0,0 +1,203 @@ +{ + "data": [ + { + "id": "operation-1", + "type": "operations", + "attributes": { + "code": "ANON-TK0002", + "dates": { + "scheduled": {} + }, + "description": "", + "labels": [ + { + "key": "Time band", + "value": "AM" + } + ], + "name": "Anonymised Task 1", + "requirements": { + "forms": [ + { + "id": "form-1", + "required": true + } + ] + } + }, + "relationships": { + "appointment": { + "data": null + }, + "assignees": { + "data": [ + { "id": "user-1", "type": "users" }, + { "id": "user-2", "type": "users" }, + { "id": "user-3", "type": "users" }, + { "id": "user-4", "type": "users" } + ] + }, + "forms": { + "data": [ + { "id": "form-1", "type": "forms" } + ] + }, + "organisation": { + "data": { "id": "org-1", "type": "organisations" } + }, + "permits": { "data": [] }, + "properties": { + "data": [ + { "id": "property-1", "type": "properties" }, + { "id": "property-2", "type": "properties" }, + { "id": "property-3", "type": "properties" }, + { "id": "property-4", "type": "properties" }, + { "id": "property-5", "type": "properties" }, + { "id": "property-6", "type": "properties" }, + { "id": "property-7", "type": "properties" } + ] + }, + "scheme": { + "data": { "id": "scheme-1", "type": "schemes" } + }, + "streets": { "data": [] }, + "teams": { "data": [] }, + "template": { "data": null }, + "work_order": { + "data": { "id": "workorder-1", "type": "work-orders" } + } + }, + "meta": { + "created_at": "2025-03-13T00:00:00.000Z", + "updated_at": "2025-07-16T00:00:00.000Z", + "counts": { "properties": 7, "streets": 0 } + } + }, + { + "id": "operation-2", + "type": "operations", + "attributes": { + "code": "", + "dates": { + "scheduled": { + "start": "2025-08-15T07:00:00Z", + "end": "2025-08-15T11:00:00Z" + } + }, + "description": "", + "labels": [], + "name": "Anonymised Operation 2", + "requirements": { + "forms": [ + { "id": "form-1", "required": true } + ] + } + }, + "relationships": { + "appointment": { + "data": { "id": "appointment-1", "type": "appointments" } + }, + "assignees": { + "data": [ { "id": "user-4", "type": "users" } ] + }, + "forms": { + "data": [ { "id": "form-1", "type": "forms" } ] + }, + "organisation": { + "data": { "id": "org-1", "type": "organisations" } + }, + "permits": { "data": [] }, + "properties": { "data": [] }, + "scheme": { "data": { "id": "scheme-2", "type": "schemes" } }, + "streets": { "data": [] }, + "teams": { "data": [] }, + "template": { "data": null }, + "work_order": { "data": { "id": "workorder-2", "type": "work-orders" } } + }, + "meta": { + "created_at": "2025-08-14T00:00:00.000Z", + "updated_at": "2025-08-14T00:00:00.000Z", + "counts": { "properties": 0, "streets": 0 } + } + }, + { + "id": "operation-3", + "type": "operations", + "attributes": { + "code": "", + "dates": { "scheduled": {} }, + "description": "", + "labels": [], + "name": "Anonymised Operation 3", + "requirements": { + "forms": [ { "id": "form-1", "required": true } ] + } + }, + "relationships": { + "appointment": { "data": null }, + "assignees": { "data": [ { "id": "user-4", "type": "users" } ] }, + "forms": { "data": [ { "id": "form-1", "type": "forms" } ] }, + "organisation": { "data": { "id": "org-1", "type": "organisations" } }, + "permits": { "data": [] }, + "properties": { "data": [] }, + "scheme": { "data": { "id": "scheme-2", "type": "schemes" } }, + "streets": { "data": [] }, + "teams": { "data": [] }, + "template": { "data": null }, + "work_order": { "data": { "id": "workorder-2", "type": "work-orders" } } + }, + "meta": { + "created_at": "2025-08-14T00:00:00.000Z", + "updated_at": "2025-08-14T00:00:00.000Z", + "counts": { "properties": 0, "streets": 0 } + } + } + ], + "meta": { + "pagination": { + "current_page": 1, + "counts": { "resources": 3, "pages": 1 }, + "requested": { "offset": 0, "limit": 100 }, + "offsets": { "previous": null, "next": null } + }, + "features": { + "params": { + "include": { + "options": [ + "appointment", "assignees", "forms", "organisation", "permits", "properties", "scheme", "streets", "teams", "template", "work_order" + ] + }, + "sort": { "default": "", "options": null } + } + } + }, + "jsonapi": { "version": "1.0" }, + "included": [ + { + "id": "appointment-1", + "type": "appointments", + "attributes": { + "animals": false, + "end_time": "2025-08-15T11:00:00Z", + "medical_dependency": false, + "notes": "", + "on_ecr": false, + "on_ecr_notes": "", + "start_time": "2025-08-15T07:00:00Z" + }, + "relationships": { + "author": { "data": { "id": "user-4", "type": "authors" } }, + "interaction": { "data": { "id": "interaction-1", "type": "customer-interactions" } }, + "operation": { "data": { "id": "operation-2", "type": "operations" } }, + "organisation": { "data": { "id": "org-1", "type": "organisations" } }, + "time_band": { "data": { "id": "timeband-1", "type": "time-bands" } } + }, + "meta": { + "created_at": "2025-08-14T00:00:00.000Z", + "updated_at": "2025-08-14T00:00:00.000Z", + "modified_at": "2025-08-14T00:00:00.000Z" + } + } + ] +} +