From 425f323867bbb1c58119ac19971e9d4a219691bd Mon Sep 17 00:00:00 2001 From: Joseph Sameh <20220099@stud.fci-cu.edu.eg> Date: Sat, 11 Jul 2026 10:57:27 +0300 Subject: [PATCH 1/6] Add HMAC-SHA256 authentication for AI service calls Implement HMAC-SHA256 request signing for API calls to the AI service when enabled. Includes timestamp and request body in the signature for integrity verification. Also updates transaction propagation for scheduled payments to use REQUIRES_NEW to ensure independent transactions, and simplifies Jackson message converter configuration in ApiClient. --- app/src/main/resources/application.properties | 3 +- .../kotlin/org/spendoo/client/ApiClient.kt | 46 +++++++++++++------ .../service/ScheduledPaymentService.kt | 3 +- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/app/src/main/resources/application.properties b/app/src/main/resources/application.properties index f5c515b..8607ed8 100644 --- a/app/src/main/resources/application.properties +++ b/app/src/main/resources/application.properties @@ -49,4 +49,5 @@ file-size.modules.identity=5 server.forward-headers-strategy=framework internal.api.base-url=${INTERNAL_API_BASE_URL:http://localhost:8080} -ai.service.base-url=${AI_SERVICE_BASE_URL:http://127.0.0.1:8000} \ No newline at end of file +ai.service.base-url=${AI_SERVICE_BASE_URL:http://127.0.0.1:8000} +spendoo.hmac.secret-key=${SPENDOO_HMAC_SECRET_KEY:} \ No newline at end of file diff --git a/http-client/src/main/kotlin/org/spendoo/client/ApiClient.kt b/http-client/src/main/kotlin/org/spendoo/client/ApiClient.kt index e09aabb..a52f22a 100644 --- a/http-client/src/main/kotlin/org/spendoo/client/ApiClient.kt +++ b/http-client/src/main/kotlin/org/spendoo/client/ApiClient.kt @@ -1,33 +1,29 @@ package org.spendoo.client -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter import org.spendoo.identity.security.JwtUtil import org.springframework.beans.factory.annotation.Value +import org.springframework.http.MediaType +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter import org.springframework.security.core.context.SecurityContextHolder import org.springframework.stereotype.Component import org.springframework.web.client.RestClient import org.springframework.web.util.UriComponentsBuilder import java.util.* +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec @Component class ApiClient( private val jwtUtil: JwtUtil, @Value("\${internal.api.base-url:http://localhost:8080}") baseUrl: String, - @Value("\${ai.service.base-url:https://localhost:8000}") private val aiBaseUrl: String + @param:Value("\${ai.service.base-url:https://localhost:8000}") private val aiBaseUrl: String, + @param:Value("\${spendoo.hmac.secret-key:}") private val hmacSecretKey: String ) { private val restClient: RestClient = RestClient.builder() .baseUrl(baseUrl) - .messageConverters { converters -> - val objectMapper = jacksonObjectMapper().registerModule(JavaTimeModule()) - val jacksonConverter = MappingJackson2HttpMessageConverter(objectMapper) - val index = converters.indexOfFirst { it is MappingJackson2HttpMessageConverter } - if (index != -1) { - converters[index] = jacksonConverter - } else { - converters.add(0, jacksonConverter) - } + .configureMessageConverters { builder -> + builder.registerDefaults() + builder.withJsonConverter(JacksonJsonHttpMessageConverter()) } .build() @@ -59,7 +55,29 @@ class ApiClient( } } - if (request.body != null) { + if (request.callAIService && hmacSecretKey.isNotEmpty()) { + val timestamp = java.time.Instant.now().epochSecond.toString() + val bodyBytes = if (request.body != null) { + val mapper = com.fasterxml.jackson.module.kotlin.jacksonObjectMapper() + .registerModule(com.fasterxml.jackson.datatype.jsr310.JavaTimeModule()) + mapper.writeValueAsBytes(request.body) + } else { + ByteArray(0) + } + val keySpec = SecretKeySpec(hmacSecretKey.toByteArray(Charsets.UTF_8), "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256") + mac.init(keySpec) + mac.update(timestamp.toByteArray(Charsets.UTF_8)) + val rawHmac = mac.doFinal(bodyBytes) + val signature = rawHmac.joinToString("") { "%02x".format(it) } + + requestSpec.header("X-Signature", signature) + requestSpec.header("X-Timestamp", timestamp) + if (request.body != null) { + requestSpec.contentType(MediaType.APPLICATION_JSON) + requestSpec.body(bodyBytes) + } + } else if (request.body != null) { requestSpec.body(request.body!!) } diff --git a/transactions/src/main/kotlin/org/spendoo/transactions/service/ScheduledPaymentService.kt b/transactions/src/main/kotlin/org/spendoo/transactions/service/ScheduledPaymentService.kt index 28a0a07..10f5d7e 100644 --- a/transactions/src/main/kotlin/org/spendoo/transactions/service/ScheduledPaymentService.kt +++ b/transactions/src/main/kotlin/org/spendoo/transactions/service/ScheduledPaymentService.kt @@ -10,6 +10,7 @@ import org.spendoo.transactions.repository.ScheduledPaymentRepository import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional import java.math.BigDecimal import java.time.Instant @@ -68,7 +69,7 @@ class ScheduledPaymentService ( paymentRepository.delete(payment) } - @Transactional + @Transactional(propagation = Propagation.REQUIRES_NEW) fun payScheduledItem(userId: UUID, paymentId: UUID){ val currentPayment = getPaymentEntity(paymentId, userId) From 5cea52f9e2df8cf335495bd01301fec6b0f34417 Mon Sep 17 00:00:00 2001 From: Joseph Sameh <20220099@stud.fci-cu.edu.eg> Date: Tue, 28 Jul 2026 02:57:01 +0300 Subject: [PATCH 2/6] Migrate deploy workflow to Azure App Service Reworked the deployment pipeline to target Azure App Service instead of SSH-based server deployment. The workflow now runs on pushes and pull requests to `develop`, logs into Docker Hub, builds/pushes the backend image, authenticates to Azure, syncs required app settings from GitHub secrets, and deploys the container image via `azure/webapps-deploy`. Also added `server.port=${PORT:8080}` in application properties so the app can bind to the platform-provided port. --- .github/workflows/deploy.yml | 58 ++++++++++++++----- app/src/main/resources/application.properties | 3 +- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f2007e4..f48ea84 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,6 +4,9 @@ on: push: branches: - develop + pull_request: + branches: + - develop jobs: deploy: @@ -17,24 +20,47 @@ jobs: mkdir -p app/src/main/resources echo "${{ secrets.FIREBASE_CREDENTIALS_BASE64 }}" | base64 -d > app/src/main/resources/spendoo-firebase-adminsdk.json - - name: Build Docker image - run: docker build -t josephsameh/spendoo-backend:latest . - - - name: Push Docker image + - name: Log in to Docker Hub run: | echo ${{ secrets.DOCKER_HUB_PASSWORD }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin + + - name: Build and Push Docker Image + run: | + docker build -t josephsameh/spendoo-backend:latest . docker push josephsameh/spendoo-backend:latest - - name: Deploy on server - uses: appleboy/ssh-action@v0.1.5 + - name: Log in to Azure + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Sync App Settings to Azure + uses: azure/CLI@v1 + with: + inlineScript: | + az webapp config appsettings set \ + --name ${{ secrets.AZURE_WEBAPP_NAME }} \ + --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ + --settings \ + DATABASE_URL="${{ secrets.DATABASE_URL }}" \ + DATABASE_USERNAME="${{ secrets.DATABASE_USERNAME }}" \ + DATABASE_PASSWORD="${{ secrets.DATABASE_PASSWORD }}" \ + JWT_SECRET="${{ secrets.JWT_SECRET }}" \ + MAIL_USERNAME="${{ secrets.MAIL_USERNAME }}" \ + MAIL_PASSWORD="${{ secrets.MAIL_PASSWORD }}" \ + PROFILE_IMAGE_DIRECTORY="${{ secrets.PROFILE_IMAGE_DIRECTORY }}" \ + STORAGE_SPENDOO_BUCKET="${{ secrets.STORAGE_SPENDOO_BUCKET }}" \ + STORAGE_SPENDOO_CDN_ENDPOINT="${{ secrets.STORAGE_SPENDOO_CDN_ENDPOINT }}" \ + STORAGE_SPENDOO_ENDPOINT="${{ secrets.STORAGE_SPENDOO_ENDPOINT }}" \ + STORAGE_SPENDOO_KEY="${{ secrets.STORAGE_SPENDOO_KEY }}" \ + STORAGE_SPENDOO_SECRET="${{ secrets.STORAGE_SPENDOO_SECRET }}" \ + INTERNAL_API_BASE_URL="${{ secrets.INTERNAL_API_BASE_URL }}" \ + AI_SERVICE_BASE_URL="${{ secrets.AI_SERVICE_BASE_URL }}" \ + SPENDOO_HMAC_SECRET_KEY="${{ secrets.SPENDOO_HMAC_SECRET_KEY }}" + + - name: Deploy to Azure App Service + uses: azure/webapps-deploy@v2 with: - host: ${{ secrets.SERVER_IP }} - username: root - key: ${{ secrets.SSH_PRIVATE_KEY }} - script: | - docker pull josephsameh/spendoo-backend:latest - docker stop springboot-container || true - docker rm springboot-container || true - docker run -d --restart=always --name springboot-container -p 8080:8080 \ - -e DATABASE_URL="${{ secrets.DATABASE_URL }}" -e DATABASE_USERNAME="${{ secrets.DATABASE_USERNAME }}" -e DATABASE_PASSWORD="${{ secrets.DATABASE_PASSWORD }}" -e JWT_SECRET="${{ secrets.JWT_SECRET }}" -e MAIL_USERNAME="${{ secrets.MAIL_USERNAME }}" -e MAIL_PASSWORD="${{ secrets.MAIL_PASSWORD }}" -e PROFILE_IMAGE_DIRECTORY="${{ secrets.PROFILE_IMAGE_DIRECTORY }}" -e STORAGE_SPENDOO_BUCKET="${{ secrets.STORAGE_SPENDOO_BUCKET }}" -e STORAGE_SPENDOO_CDN_ENDPOINT="${{ secrets.STORAGE_SPENDOO_CDN_ENDPOINT }}" -e STORAGE_SPENDOO_ENDPOINT="${{ secrets.STORAGE_SPENDOO_ENDPOINT }}" -e STORAGE_SPENDOO_KEY="${{ secrets.STORAGE_SPENDOO_KEY }}" -e STORAGE_SPENDOO_SECRET="${{ secrets.STORAGE_SPENDOO_SECRET }}" -e INTERNAL_API_BASE_URL="${{ secrets.INTERNAL_API_BASE_URL }}" -e AI_SERVICE_BASE_URL="${{ secrets.AI_SERVICE_BASE_URL }}" josephsameh/spendoo-backend:latest - docker image prune -f \ No newline at end of file + app-name: ${{ secrets.AZURE_WEBAPP_NAME }} + publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }} + images: 'josephsameh/spendoo-backend:latest' \ No newline at end of file diff --git a/app/src/main/resources/application.properties b/app/src/main/resources/application.properties index 8607ed8..a4cbaea 100644 --- a/app/src/main/resources/application.properties +++ b/app/src/main/resources/application.properties @@ -50,4 +50,5 @@ server.forward-headers-strategy=framework internal.api.base-url=${INTERNAL_API_BASE_URL:http://localhost:8080} ai.service.base-url=${AI_SERVICE_BASE_URL:http://127.0.0.1:8000} -spendoo.hmac.secret-key=${SPENDOO_HMAC_SECRET_KEY:} \ No newline at end of file +spendoo.hmac.secret-key=${SPENDOO_HMAC_SECRET_KEY:} +server.port=${PORT:8080} \ No newline at end of file From 5b5b0da129c4650375a891f4466d05bec5acf73b Mon Sep 17 00:00:00 2001 From: Joseph Sameh Fouad <20220099@stud.fci-cu.edu.eg> Date: Thu, 30 Jul 2026 01:24:16 +0300 Subject: [PATCH 3/6] Add forward headers strategy to application properties --- app/src/main/resources/application.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/resources/application.properties b/app/src/main/resources/application.properties index a4cbaea..d264685 100644 --- a/app/src/main/resources/application.properties +++ b/app/src/main/resources/application.properties @@ -51,4 +51,5 @@ server.forward-headers-strategy=framework internal.api.base-url=${INTERNAL_API_BASE_URL:http://localhost:8080} ai.service.base-url=${AI_SERVICE_BASE_URL:http://127.0.0.1:8000} spendoo.hmac.secret-key=${SPENDOO_HMAC_SECRET_KEY:} -server.port=${PORT:8080} \ No newline at end of file +server.port=${PORT:8080} +server.forward-headers-strategy=native From e0ab3f40d8a190025e6905fba4da647beebf7f91 Mon Sep 17 00:00:00 2001 From: Joseph Sameh Fouad <20220099@stud.fci-cu.edu.eg> Date: Thu, 30 Jul 2026 01:35:45 +0300 Subject: [PATCH 4/6] Change server.forward-headers-strategy and disable SSL --- app/src/main/resources/application.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/resources/application.properties b/app/src/main/resources/application.properties index d264685..1961486 100644 --- a/app/src/main/resources/application.properties +++ b/app/src/main/resources/application.properties @@ -52,4 +52,5 @@ internal.api.base-url=${INTERNAL_API_BASE_URL:http://localhost:8080} ai.service.base-url=${AI_SERVICE_BASE_URL:http://127.0.0.1:8000} spendoo.hmac.secret-key=${SPENDOO_HMAC_SECRET_KEY:} server.port=${PORT:8080} -server.forward-headers-strategy=native +server.forward-headers-strategy=framework +server.ssl.enabled=false From 95e5606227e82376f110576de3c2302ba1aa1664 Mon Sep 17 00:00:00 2001 From: Joseph Sameh Fouad <20220099@stud.fci-cu.edu.eg> Date: Mon, 7 Sep 2026 15:51:30 +0300 Subject: [PATCH 5/6] fix: simplify Jackson annotations and configure Kotlin compiler to resolve field-based mapping issues in statistics DTOs with a new deserialization test. --- build.gradle.kts | 3 + gradlew | 0 .../api/dto/response/BudgetStatusBucketDto.kt | 2 +- .../api/dto/response/BudgetStatusResponse.kt | 2 +- .../api/dto/response/CategorySpendingDto.kt | 10 +-- .../api/dto/response/CombinedStatsResponse.kt | 12 ++- .../dto/response/FinancialStatsResponse.kt | 6 +- .../api/dto/response/StatsBucketDto.kt | 6 +- .../api/dto/response/TopCategoriesResponse.kt | 4 +- .../CombinedStatsDeserializationTest.kt | 78 +++++++++++++++++++ 10 files changed, 105 insertions(+), 18 deletions(-) mode change 100644 => 100755 gradlew create mode 100644 statistics/src/test/kotlin/org/spendoo/statistics/CombinedStatsDeserializationTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 461155b..8c7ac22 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -44,6 +44,9 @@ subprojects { plugins.withId("org.jetbrains.kotlin.jvm") { kotlin { jvmToolchain(javaVersion) + compilerOptions { + freeCompilerArgs.addAll("-Xjsr305=strict", "-Xannotation-default-target=param-property") + } } } diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/BudgetStatusBucketDto.kt b/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/BudgetStatusBucketDto.kt index 32b01b4..1aec485 100644 --- a/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/BudgetStatusBucketDto.kt +++ b/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/BudgetStatusBucketDto.kt @@ -8,6 +8,6 @@ data class BudgetStatusBucketDto( val spending: BigDecimal, val status: BudgetStatus, val percentage: BigDecimal, - @field:JsonAlias("start_date") + @JsonAlias("start_date") val startDate: Instant ) diff --git a/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/BudgetStatusResponse.kt b/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/BudgetStatusResponse.kt index 02e297e..742ca78 100644 --- a/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/BudgetStatusResponse.kt +++ b/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/BudgetStatusResponse.kt @@ -5,6 +5,6 @@ import java.math.BigDecimal data class BudgetStatusResponse( val buckets: List, - @field:JsonAlias("highest_spending") + @JsonAlias("highest_spending") val highestSpending: BigDecimal ) diff --git a/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/CategorySpendingDto.kt b/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/CategorySpendingDto.kt index e1a89a4..003c485 100644 --- a/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/CategorySpendingDto.kt +++ b/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/CategorySpendingDto.kt @@ -5,15 +5,15 @@ import java.math.BigDecimal import java.util.UUID data class CategorySpendingDto( - @field:JsonAlias("category_id") + @JsonAlias("category_id") val categoryId: UUID, - @field:JsonAlias("category_name") + @JsonAlias("category_name") val categoryName: String, - @field:JsonAlias("category_icon") + @JsonAlias("category_icon") val categoryIcon: String, val spending: BigDecimal, - @field:JsonAlias("percentage_change") + @JsonAlias("percentage_change") val percentageChange: BigDecimal, - @field:JsonAlias("contribution_percentage") + @JsonAlias("contribution_percentage") val contributionPercentage: BigDecimal ) diff --git a/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/CombinedStatsResponse.kt b/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/CombinedStatsResponse.kt index a2901fe..75ba86d 100644 --- a/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/CombinedStatsResponse.kt +++ b/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/CombinedStatsResponse.kt @@ -1,12 +1,18 @@ package org.spendoo.statistics.api.dto.response import com.fasterxml.jackson.annotation.JsonAlias +import com.fasterxml.jackson.annotation.JsonProperty data class CombinedStatsResponse( - @field:JsonAlias("financial_stats_forecast") + @JsonProperty("financialStats") + @JsonAlias("financial_stats_forecast", "financial_stats") val financialStats: FinancialStatsResponse, - @field:JsonAlias("budget_status") + @JsonProperty("budgetStatus") + @JsonAlias("budget_status") val budgetStatus: BudgetStatusResponse, - @field:JsonAlias("top_categories") + @JsonProperty("topCategories") + @JsonAlias("top_categories") val topCategories: TopCategoriesResponse ) + + diff --git a/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/FinancialStatsResponse.kt b/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/FinancialStatsResponse.kt index 30dd8ce..eae8340 100644 --- a/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/FinancialStatsResponse.kt +++ b/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/FinancialStatsResponse.kt @@ -5,10 +5,10 @@ import java.math.BigDecimal data class FinancialStatsResponse( val buckets: List, - @field:JsonAlias("highest_spending_bucket_index") + @JsonAlias("highest_spending_bucket_index") val highestSpendingBucketIndex: Int, - @field:JsonAlias("highest_value") + @JsonAlias("highest_value") val highestValue: BigDecimal, - @field:JsonAlias("predict") + @JsonAlias("predict") val predicted: Boolean? = null ) diff --git a/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/StatsBucketDto.kt b/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/StatsBucketDto.kt index dd9c570..229b4ff 100644 --- a/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/StatsBucketDto.kt +++ b/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/StatsBucketDto.kt @@ -8,10 +8,10 @@ data class StatsBucketDto( val spending: BigDecimal, val income: BigDecimal, val budget: BigDecimal, - @field:JsonAlias("start_date") + @JsonAlias("start_date") val startDate: Instant, - @field:JsonAlias("predicted") + @JsonAlias("predicted") val predicted: Boolean? = null, - @field:JsonAlias("status") + @JsonAlias("status") val status: BudgetStatus? = null ) diff --git a/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/TopCategoriesResponse.kt b/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/TopCategoriesResponse.kt index c643651..ffd82ec 100644 --- a/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/TopCategoriesResponse.kt +++ b/statistics/src/main/kotlin/org/spendoo/statistics/api/dto/response/TopCategoriesResponse.kt @@ -4,8 +4,8 @@ import com.fasterxml.jackson.annotation.JsonAlias import java.math.BigDecimal data class TopCategoriesResponse( - @field:JsonAlias("total_spending") + @JsonAlias("total_spending") val totalSpending: BigDecimal, - @field:JsonAlias("top_categories") + @JsonAlias("top_categories") val topCategories: List ) diff --git a/statistics/src/test/kotlin/org/spendoo/statistics/CombinedStatsDeserializationTest.kt b/statistics/src/test/kotlin/org/spendoo/statistics/CombinedStatsDeserializationTest.kt new file mode 100644 index 0000000..737d39c --- /dev/null +++ b/statistics/src/test/kotlin/org/spendoo/statistics/CombinedStatsDeserializationTest.kt @@ -0,0 +1,78 @@ +package org.spendoo.statistics + +import com.fasterxml.jackson.annotation.JsonAlias +import com.fasterxml.jackson.annotation.JsonProperty +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertNotNull +import org.spendoo.statistics.api.dto.response.CombinedStatsResponse +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter +import org.springframework.http.MediaType +import org.springframework.mock.http.MockHttpInputMessage + +class CombinedStatsDeserializationTest { + + @Test + fun `test deserialization of AI combined response`() { + val json = """ + { + "financial_stats_forecast": { + "buckets": [ + { + "spending": 150.50, + "income": 0.0, + "budget": 200.0, + "start_date": "2026-07-05T21:00:00Z", + "predicted": false, + "status": "WITHIN" + } + ], + "highest_spending_bucket_index": 0, + "highest_value": 150.50, + "predict": true + }, + "budget_status": { + "buckets": [ + { + "spending": 150.50, + "status": "WITHIN", + "percentage": 75.25, + "start_date": "2026-07-05T21:00:00Z" + } + ], + "highest_spending": 150.50 + }, + "top_categories": { + "total_spending": 150.50, + "top_categories": [ + { + "category_id": "01851ec9-7a36-458f-8788-80799bf95216", + "category_name": "Food", + "category_icon": "food_icon", + "spending": 150.50, + "percentage_change": 5.0, + "contribution_percentage": 100.0 + } + ] + } + } + """.trimIndent() + + + val converter = JacksonJsonHttpMessageConverter() + val inputMessage = MockHttpInputMessage(json.toByteArray(Charsets.UTF_8)) + inputMessage.headers.contentType = MediaType.APPLICATION_JSON + + val result = converter.read(CombinedStatsResponse::class.java, inputMessage) as CombinedStatsResponse + + assertNotNull(result) + assertEquals(0, result.financialStats.highestSpendingBucketIndex) + assertEquals(true, result.financialStats.predicted) + assertEquals(1, result.financialStats.buckets.size) + assertEquals(1, result.budgetStatus.buckets.size) + assertEquals(1, result.topCategories.topCategories.size) + } +} + + + From 2e62c1ce24b5867491d61a93e12c756338476c54 Mon Sep 17 00:00:00 2001 From: Joseph Sameh Fouad <20220099@stud.fci-cu.edu.eg> Date: Mon, 7 Sep 2026 20:44:32 +0300 Subject: [PATCH 6/6] feat: implement unique image tagging for deployments and add comprehensive HTTP client logging for debugging --- .github/workflows/deploy.yml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f48ea84..0bb9b21 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -26,7 +26,10 @@ jobs: - name: Build and Push Docker Image run: | - docker build -t josephsameh/spendoo-backend:latest . + docker build \ + -t josephsameh/spendoo-backend:${{ github.sha }} \ + -t josephsameh/spendoo-backend:latest . + docker push josephsameh/spendoo-backend:${{ github.sha }} docker push josephsameh/spendoo-backend:latest - name: Log in to Azure @@ -56,11 +59,20 @@ jobs: STORAGE_SPENDOO_SECRET="${{ secrets.STORAGE_SPENDOO_SECRET }}" \ INTERNAL_API_BASE_URL="${{ secrets.INTERNAL_API_BASE_URL }}" \ AI_SERVICE_BASE_URL="${{ secrets.AI_SERVICE_BASE_URL }}" \ - SPENDOO_HMAC_SECRET_KEY="${{ secrets.SPENDOO_HMAC_SECRET_KEY }}" + SPENDOO_HMAC_SECRET_KEY="${{ secrets.SPENDOO_HMAC_SECRET_KEY }}" \ + DOCKER_ENABLE_CI="true" - name: Deploy to Azure App Service uses: azure/webapps-deploy@v2 with: app-name: ${{ secrets.AZURE_WEBAPP_NAME }} publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }} - images: 'josephsameh/spendoo-backend:latest' \ No newline at end of file + images: 'josephsameh/spendoo-backend:${{ github.sha }}' + + - name: Restart Azure App Service + uses: azure/CLI@v1 + with: + inlineScript: | + az webapp restart \ + --name ${{ secrets.AZURE_WEBAPP_NAME }} \ + --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \ No newline at end of file