From 5d627d3fc505e5b7320f77bd20ebba1fb1d0dafb Mon Sep 17 00:00:00 2001 From: TobogganDev Date: Mon, 20 Jul 2026 11:15:14 +0200 Subject: [PATCH 1/2] Add receipt scanning with Tabscanner OCR and fix related backend issues --- .../config/JwtAuthenticationFilter.java | 8 +- .../controller/AllowanceController.java | 3 +- .../controller/ReceiptScanController.java | 5 +- .../fr/moneybuddy/dtos/TaskUpdate.java | 14 +- .../exception/GlobalExceptionHandler.java | 23 +++ .../fr/moneybuddy/service/DiscordService.java | 7 +- .../fr/moneybuddy/service/IncomeService.java | 7 +- .../moneybuddy/service/ReceiptOcrService.java | 175 ++++++++++++------ .../service/RefreshTokenService.java | 19 +- .../fr/moneybuddy/service/TaskService.java | 7 +- .../utils/operations/Operations.java | 3 +- src/main/resources/application.properties | 7 + 12 files changed, 191 insertions(+), 87 deletions(-) diff --git a/src/main/java/moneybuddy/fr/moneybuddy/config/JwtAuthenticationFilter.java b/src/main/java/moneybuddy/fr/moneybuddy/config/JwtAuthenticationFilter.java index 7791a6c..fd4106e 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/config/JwtAuthenticationFilter.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/config/JwtAuthenticationFilter.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; +import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -46,7 +47,12 @@ protected void doFilterInternal( } jwt = authHeader.substring(7); - userEmail = jwtService.extractUsername(jwt); + try { + userEmail = jwtService.extractUsername(jwt); + } catch (JwtException e) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token expired or invalid"); + return; + } if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); diff --git a/src/main/java/moneybuddy/fr/moneybuddy/controller/AllowanceController.java b/src/main/java/moneybuddy/fr/moneybuddy/controller/AllowanceController.java index 4e83133..a1936a0 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/controller/AllowanceController.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/controller/AllowanceController.java @@ -33,7 +33,7 @@ public class AllowanceController { public ResponseEntity createAllowance( @RequestHeader("Authorization") String authHeader, @Valid @RequestBody CreateAllowance req) { String token = authHeader.substring(7); - return ResponseEntity.status(HttpStatus.ACCEPTED) + return ResponseEntity.status(HttpStatus.OK) .body(allowanceService.createAllowance(req, token)); } @@ -57,4 +57,5 @@ public ResponseEntity updateAllowance( return ResponseEntity.status(HttpStatus.OK) .body(allowanceService.updateAllowance(req, id, token)); } + } diff --git a/src/main/java/moneybuddy/fr/moneybuddy/controller/ReceiptScanController.java b/src/main/java/moneybuddy/fr/moneybuddy/controller/ReceiptScanController.java index 713cd28..68e19fd 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/controller/ReceiptScanController.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/controller/ReceiptScanController.java @@ -10,7 +10,6 @@ import moneybuddy.fr.moneybuddy.dtos.Receipt.Receipt; import moneybuddy.fr.moneybuddy.dtos.Receipt.SendReceipt; import moneybuddy.fr.moneybuddy.service.ReceiptOcrService; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; @@ -31,8 +30,8 @@ public ResponseEntity scanReceipt( throws IOException { String token = authHeader.substring(7); - boolean res = receiptOcrService.updateMoneyAndInsertReceipt(body.getFile(), token); + Receipt receipt = receiptOcrService.updateMoneyAndInsertReceipt(body.getFile(), token); - return ResponseEntity.status(res ? HttpStatus.OK : HttpStatus.BAD_REQUEST).body(null); + return ResponseEntity.ok(receipt); } } diff --git a/src/main/java/moneybuddy/fr/moneybuddy/dtos/TaskUpdate.java b/src/main/java/moneybuddy/fr/moneybuddy/dtos/TaskUpdate.java index df9072c..8d2dbc6 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/dtos/TaskUpdate.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/dtos/TaskUpdate.java @@ -33,21 +33,23 @@ public class TaskUpdate { @PositiveOrZero(message = "coinReward doit etre positif ou égale a 0") @Max(message = "Doit pas exceder 50", value = 50) - private int coinReward; + private Integer coinReward; @PositiveOrZero(message = "moneyReward doit etre positif ou égale a 0") private BigDecimal moneyReward; private LocalDateTime dateLimit; private List weeklyDays; - private int monthlyDay; - private boolean preValidate; - private boolean disable; + private Integer monthlyDay; + private Boolean preValidate; + private Boolean disable; @AssertTrue(message = "Au moins une récompense (coinReward ou moneyReward) doit être fournie") @Schema(hidden = true) public boolean isRewardValid() { - return (coinReward == 0 && moneyReward == null) || coinReward != 0 || moneyReward != null; + return (coinReward == null || coinReward == 0) && moneyReward == null + || (coinReward != null && coinReward != 0) + || moneyReward != null; } @AssertTrue( @@ -61,6 +63,6 @@ public boolean isWeeklyDays() { @AssertTrue(message = "Si c'est mensuel alors monthlyDay doit avoir une valeur") public boolean isMonthlyDay() { - return type == null || !TaskType.MONTHLY.equals(type) || monthlyDay != 0; + return type == null || !TaskType.MONTHLY.equals(type) || (monthlyDay != null && monthlyDay != 0); } } diff --git a/src/main/java/moneybuddy/fr/moneybuddy/exception/GlobalExceptionHandler.java b/src/main/java/moneybuddy/fr/moneybuddy/exception/GlobalExceptionHandler.java index 575667d..ee30d00 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/exception/GlobalExceptionHandler.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/exception/GlobalExceptionHandler.java @@ -18,10 +18,15 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.WebRequest; +import org.springframework.web.multipart.MultipartException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @RestControllerAdvice public class GlobalExceptionHandler { + private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); + @Autowired private DiscordService discordService; @ExceptionHandler(MoneyBuddyException.class) @@ -120,8 +125,26 @@ public ResponseEntity handleIndexOutOfBoundsException( return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse); } + @ExceptionHandler(MultipartException.class) + public ResponseEntity handleMultipartException( + MultipartException ex, WebRequest request) { + ErrorResponse errorResponse = + ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.BAD_REQUEST.value()) + .error("Fichier invalide") + .message("Le fichier envoyé est invalide ou dépasse la taille maximale autorisée (10MB)") + .errorCode("MULTIPART_ERROR") + .path(request.getDescription(false).replace("uri=", "")) + .build(); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleGenericException(Exception ex, WebRequest request) { + logger.error("Unhandled exception on {}", request.getDescription(false), ex); + ErrorResponse errorResponse = ErrorResponse.builder() .timestamp(LocalDateTime.now()) diff --git a/src/main/java/moneybuddy/fr/moneybuddy/service/DiscordService.java b/src/main/java/moneybuddy/fr/moneybuddy/service/DiscordService.java index 7fdff1f..c960798 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/service/DiscordService.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/service/DiscordService.java @@ -37,7 +37,11 @@ public class DiscordService { @EventListener(ApplicationReadyEvent.class) @Async public void init() { - this.jda = JDABuilder.createDefault(token).build(); + try { + this.jda = JDABuilder.createDefault(token).build(); + } catch (Exception e) { + System.err.println("[DiscordService] Failed to initialize Discord bot (monitoring disabled): " + e.getMessage()); + } } public void SendMessage(String text) { @@ -75,6 +79,7 @@ public void sendNewAccountMessage(String email, SubAccount subAccount, Boolean i } public void sendErroMessage(MoneyBuddyException ex, WebRequest req) { + if (jda == null) return; TextChannel channel = jda.getTextChannelById(monitoring_errors_channel); Boolean error = Integer.toString(ex.getStatus().value()).startsWith("4"); diff --git a/src/main/java/moneybuddy/fr/moneybuddy/service/IncomeService.java b/src/main/java/moneybuddy/fr/moneybuddy/service/IncomeService.java index 22c86a4..382e4b9 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/service/IncomeService.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/service/IncomeService.java @@ -40,15 +40,14 @@ public class IncomeService { private final JwtService jwtService; public void createTransactionForIncome(Income income) { + if (!income.getStatus().equals(IncomeStatus.ACCEPTED)) return; + Transaction transaction = Transaction.builder() .accountId(income.getAccountId()) .childId(income.getSubAccountIdChild()) .parentId(income.getSubAccountId()) - .type( - income.getStatus().equals(IncomeStatus.ACCEPTED) - ? TransactionType.CREDIT - : TransactionType.DEBIT) + .type(TransactionType.CREDIT) .category(TransactionCategory.MONEY) .amount(income.getAmount().toString()) .description(income.getTask().getDescription()) diff --git a/src/main/java/moneybuddy/fr/moneybuddy/service/ReceiptOcrService.java b/src/main/java/moneybuddy/fr/moneybuddy/service/ReceiptOcrService.java index 5dbacc5..591ed6a 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/service/ReceiptOcrService.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/service/ReceiptOcrService.java @@ -6,16 +6,23 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigDecimal; +import java.math.RoundingMode; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.List; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import moneybuddy.fr.moneybuddy.dtos.Money.AddMoney; import moneybuddy.fr.moneybuddy.dtos.Receipt.Receipt; import moneybuddy.fr.moneybuddy.exception.CustomError; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -24,14 +31,24 @@ @RequiredArgsConstructor public class ReceiptOcrService { + private static final Logger logger = LoggerFactory.getLogger(ReceiptOcrService.class); + private final JwtService jwtService; private final MoneyService moneyService; - private String OCR_ENDPOINT = "https://ocr.asprise.com/api/v1/receipt"; + @Value("${tabscanner.api_key}") + private String tabscannerApiKey; + + private static final String PROCESS_ENDPOINT = "https://api.tabscanner.com/api/2/process"; + private static final String RESULT_ENDPOINT = "https://api.tabscanner.com/api/result/"; + private static final int POLL_INITIAL_WAIT_MS = 5000; + private static final int POLL_INTERVAL_MS = 1000; + private static final int POLL_MAX_ATTEMPTS = 15; + HttpClient client = HttpClient.newHttpClient(); private final ObjectMapper objectMapper; - public boolean updateMoneyAndInsertReceipt(MultipartFile file, String token) throws IOException { + public Receipt updateMoneyAndInsertReceipt(MultipartFile file, String token) throws IOException { String subAccountId = jwtService.extractSubAccountId(token); Receipt receipts = scanReceipt(file); @@ -52,38 +69,113 @@ public boolean updateMoneyAndInsertReceipt(MultipartFile file, String token) thr .subAccountId(subAccountId) .amount(receipt.getTotal()) .description( - String.format("%s | %s%", receipt.getMerchant_name(), receipt.getDate())) + String.format("%s | %s", receipt.getMerchant_name(), receipt.getDate())) .build(); moneyService.updateMoney(addMoney, token, false); }); - return receipts.isSuccess(); + return receipts; } public Receipt scanReceipt(MultipartFile file) throws IOException { - String boundary = "Boundary" + System.currentTimeMillis(); + if (tabscannerApiKey == null || tabscannerApiKey.isBlank()) + throw new CustomError( + "Le scan de reçu n'est pas configuré (clé Tabscanner manquante)", + HttpStatus.SERVICE_UNAVAILABLE, + "TABSCANNER_ERROR"); try { - HttpRequest request = - HttpRequest.newBuilder() - .uri(URI.create(OCR_ENDPOINT)) - .header("Content-Type", "multipart/form-data; boundary=" + boundary) - .POST(HttpRequest.BodyPublishers.ofByteArray(buildBody(boundary, file))) - .build(); + String token = submitReceipt(file); + JsonNode result = pollForResult(token); + return toReceipt(result); + } catch (CustomError e) { + throw e; + } catch (Exception e) { + throw new CustomError(e.getMessage(), HttpStatus.FORBIDDEN, "TABSCANNER_ERROR"); + } + } - HttpResponse response = - client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).join(); + private String submitReceipt(MultipartFile file) throws IOException, InterruptedException { + String boundary = "Boundary" + System.currentTimeMillis(); - Receipt receipt = objectMapper.readValue(response.body(), Receipt.class); + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(PROCESS_ENDPOINT)) + .header("apikey", tabscannerApiKey) + .header("Content-Type", "multipart/form-data; boundary=" + boundary) + .POST(HttpRequest.BodyPublishers.ofByteArray(buildBody(boundary, file))) + .build(); - if (!receipt.isSuccess()) - throw new CustomError(receipt.getMessage(), HttpStatus.FORBIDDEN, "ASPRISE_ERROR"); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + logger.info("Tabscanner submit response [{}]: {}", response.statusCode(), response.body()); + JsonNode body = objectMapper.readTree(response.body()); - return receipt; - } catch (Exception e) { - throw new CustomError(e.getMessage(), HttpStatus.FORBIDDEN, "ASPRISE_ERROR"); + JsonNode tokenNode = body.get("token"); + if (tokenNode == null || tokenNode.isNull()) + throw new CustomError( + "Impossible de soumettre le reçu au service OCR", HttpStatus.FORBIDDEN, "TABSCANNER_ERROR"); + + return tokenNode.asText(); + } + + private JsonNode pollForResult(String token) throws IOException, InterruptedException { + Thread.sleep(POLL_INITIAL_WAIT_MS); + + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(RESULT_ENDPOINT + token)) + .header("apikey", tabscannerApiKey) + .GET() + .build(); + + for (int attempt = 0; attempt < POLL_MAX_ATTEMPTS; attempt++) { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + JsonNode body = objectMapper.readTree(response.body()); + String status = body.path("status").asText(""); + + if ("done".equals(status)) return body; + if ("failed".equals(status)) + throw new CustomError( + "Le scan du reçu a échoué", HttpStatus.FORBIDDEN, "TABSCANNER_ERROR"); + + Thread.sleep(POLL_INTERVAL_MS); } + + throw new CustomError( + "Le scan du reçu a pris trop de temps, réessaie", HttpStatus.FORBIDDEN, "TABSCANNER_ERROR"); + } + + private Receipt toReceipt(JsonNode body) { + JsonNode result = body.path("result"); + + Receipt receipt = new Receipt(); + receipt.setSuccess(true); + receipt.setMessage("OK"); + receipt.setOcr_type("tabscanner"); + + Receipt.ReceiptRow row = new Receipt.ReceiptRow(); + row.setMerchant_name(result.path("establishment").asText("")); + row.setDate(result.path("date").asText("")); + row.setCurrency(result.path("currency").asText("")); + row.setTotal(BigDecimal.valueOf(result.path("total").asDouble(0))); + + double totalConfidence = result.path("totalConfidence").asDouble(0); + double establishmentConfidence = result.path("establishmentConfidence").asDouble(0); + double confidence = Math.min(totalConfidence, establishmentConfidence) * 100; + row.setOcr_confidence(BigDecimal.valueOf(confidence).setScale(2, RoundingMode.HALF_UP)); + + List items = new ArrayList<>(); + for (JsonNode lineItem : result.path("lineItems")) { + Receipt.ReceiptRow.ReceiptItem item = new Receipt.ReceiptRow.ReceiptItem(); + item.setDescription(lineItem.path("descClean").asText(lineItem.path("desc").asText(""))); + item.setAmount(BigDecimal.valueOf(lineItem.path("lineTotal").asDouble(0))); + items.add(item); + } + row.setItems(items); + + receipt.setReceipts(List.of(row)); + return receipt; } private byte[] buildBody(String boundary, MultipartFile file) throws IOException { @@ -92,45 +184,12 @@ private byte[] buildBody(String boundary, MultipartFile file) throws IOException ByteArrayOutputStream out = new ByteArrayOutputStream(); - // api_key - write( - out, - dashes - + CRLF - + "Content-Disposition: form-data; name=\"api_key\"" - + CRLF - + CRLF - + "TEST" - + CRLF); // remplace TEST par ta vraie clé en prod - - // recognizer - write( - out, - dashes - + CRLF - + "Content-Disposition: form-data; name=\"recognizer\"" - + CRLF - + CRLF - + "auto" - + CRLF); // 'auto' détecte la langue, ou force "FR" - - // ref_no - write( - out, - dashes - + CRLF - + "Content-Disposition: form-data; name=\"ref_no\"" - + CRLF - + CRLF - + "moneybuddy_" - + System.currentTimeMillis() - + CRLF); - - // file String fileName = file.getOriginalFilename() != null ? file.getOriginalFilename().replace(" ", "_") - : "receipt.png"; + : "receipt.jpg"; + + String contentType = file.getContentType() != null ? file.getContentType() : "image/jpeg"; write( out, @@ -140,14 +199,14 @@ private byte[] buildBody(String boundary, MultipartFile file) throws IOException + fileName + "\"" + CRLF - + "Content-Type: application/octet-stream" + + "Content-Type: " + + contentType + CRLF - + CRLF); // ← ici le fix + + CRLF); out.write(file.getBytes()); write(out, CRLF); - // Fermeture write(out, dashes + "--" + CRLF); return out.toByteArray(); diff --git a/src/main/java/moneybuddy/fr/moneybuddy/service/RefreshTokenService.java b/src/main/java/moneybuddy/fr/moneybuddy/service/RefreshTokenService.java index 86e8a2d..6404707 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/service/RefreshTokenService.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/service/RefreshTokenService.java @@ -36,12 +36,12 @@ public String createRefreshToken(String accountId) { } public void deleteRefreshToken(String token, String refreshToken) { - String accoundId = jwtService.extractSubAccountAccountId(token); - refreshTokenRepository.deleteByRefreshTokenAndAccountId(refreshToken, accoundId); + refreshTokenRepository.findByRefreshToken(refreshToken) + .ifPresent(refreshTokenRepository::delete); } public AuthResponse refreshToken(String refreshToken) { - RefreshToken newRefreshToken = + RefreshToken existing = refreshTokenRepository .findByRefreshToken(refreshToken) .orElseThrow( @@ -49,14 +49,17 @@ public AuthResponse refreshToken(String refreshToken) { new CustomError( "Refresh token not found", HttpStatus.CONFLICT, "REFRESH_TOKEN_ERROR")); - if (newRefreshToken.getExpiryDate().isBefore(LocalDateTime.now())) { - refreshTokenRepository.delete(newRefreshToken); + if (existing.getExpiryDate().isBefore(LocalDateTime.now())) { + refreshTokenRepository.delete(existing); throw new RefreshTokenExpired(); } - Account account = accountService.getAccount(newRefreshToken.getAccountId()); - String token = jwtService.generateToken(account, account.getId(), account.getRole()); + Account account = accountService.getAccount(existing.getAccountId()); + String newToken = jwtService.generateToken(account, account.getId(), account.getRole()); - return AuthResponse.builder().token(token).build(); + refreshTokenRepository.delete(existing); + String newRefreshToken = createRefreshToken(account.getId()); + + return AuthResponse.builder().token(newToken).refreshToken(newRefreshToken).build(); } } diff --git a/src/main/java/moneybuddy/fr/moneybuddy/service/TaskService.java b/src/main/java/moneybuddy/fr/moneybuddy/service/TaskService.java index 71cd467..eb03caa 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/service/TaskService.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/service/TaskService.java @@ -116,8 +116,7 @@ public ResponseEntity> getTasks( } else { criteria.and("subaccountIdChild").is(id); criteria.and("disable").is(false); - criteria.and("status").ne(TaskStatus.COMPLETED.name()); - criteria.andOperator(dateLimitCriteria, weeklyCriteria); + if (status == null) criteria.and("status").ne(TaskStatus.COMPLETED.name()); } if (isParent && childId != null && !childId.isEmpty()) @@ -257,11 +256,11 @@ public ResponseEntity modifyTask(TaskUpdate request, String token, String task.setCoinReward(Optional.ofNullable(request.getCoinReward()).orElse(task.getCoinReward())); - task.setPreValidate(Optional.ofNullable(request.isPreValidate()).orElse(task.isPreValidate())); + task.setPreValidate(Optional.ofNullable(request.getPreValidate()).orElse(task.isPreValidate())); task.setDateLimit(Optional.ofNullable(request.getDateLimit()).orElse(task.getDateLimit())); - task.setDisable(Optional.ofNullable(request.isDisable()).orElse(task.isDisable())); + task.setDisable(Optional.ofNullable(request.getDisable()).orElse(task.isDisable())); switch (type) { case TaskType.WEEKLY: diff --git a/src/main/java/moneybuddy/fr/moneybuddy/utils/operations/Operations.java b/src/main/java/moneybuddy/fr/moneybuddy/utils/operations/Operations.java index 23bcfb7..3ab72db 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/utils/operations/Operations.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/utils/operations/Operations.java @@ -4,6 +4,7 @@ package moneybuddy.fr.moneybuddy.utils.operations; import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDateTime; import moneybuddy.fr.moneybuddy.dtos.Money.AddMoney; @@ -29,7 +30,7 @@ public Operations(TransactionRepository transactionRepository, MoneyService mone public void updateProgression(Goal goal, BigDecimal depositStatement) { Number updateProgression = - depositStatement.multiply(BigDecimal.valueOf(100)).divide(goal.getAmount()); + depositStatement.multiply(BigDecimal.valueOf(100)).divide(goal.getAmount(), 2, RoundingMode.HALF_UP); if (updateProgression.floatValue() == 100) goal.setGoalStatus(GoalStatus.DONE); goal.setProgression(updateProgression); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7ed289d..ee3cd76 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -23,6 +23,13 @@ jwt.expiration=${JWT_EXP} resend.email=${RESEND_EMAIL} resend.api_key=${RESEND_API_KEY} +# Tabscanner OCR +tabscanner.api_key=${TABSCANNER_API_KEY:} + +# Multipart uploads (receipt scan photos) +spring.servlet.multipart.max-file-size=10MB +spring.servlet.multipart.max-request-size=10MB + #Domain domain=${DOMAIN} From 1a7eb4c8240ab66ff5924adea10487f25cdbe23a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 09:19:48 +0000 Subject: [PATCH 2/2] fix: apply spotless formatting for CI lint --- .../fr/moneybuddy/controller/AllowanceController.java | 4 +--- .../java/moneybuddy/fr/moneybuddy/dtos/TaskUpdate.java | 4 +++- .../fr/moneybuddy/exception/GlobalExceptionHandler.java | 7 ++++--- .../moneybuddy/fr/moneybuddy/service/DiscordService.java | 4 +++- .../java/moneybuddy/fr/moneybuddy/service/QuizService.java | 2 +- .../fr/moneybuddy/service/ReceiptOcrService.java | 7 ++++--- .../fr/moneybuddy/service/RefreshTokenService.java | 3 ++- .../fr/moneybuddy/utils/operations/Operations.java | 4 +++- 8 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/main/java/moneybuddy/fr/moneybuddy/controller/AllowanceController.java b/src/main/java/moneybuddy/fr/moneybuddy/controller/AllowanceController.java index a1936a0..938f9e9 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/controller/AllowanceController.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/controller/AllowanceController.java @@ -33,8 +33,7 @@ public class AllowanceController { public ResponseEntity createAllowance( @RequestHeader("Authorization") String authHeader, @Valid @RequestBody CreateAllowance req) { String token = authHeader.substring(7); - return ResponseEntity.status(HttpStatus.OK) - .body(allowanceService.createAllowance(req, token)); + return ResponseEntity.status(HttpStatus.OK).body(allowanceService.createAllowance(req, token)); } @GetMapping("") @@ -57,5 +56,4 @@ public ResponseEntity updateAllowance( return ResponseEntity.status(HttpStatus.OK) .body(allowanceService.updateAllowance(req, id, token)); } - } diff --git a/src/main/java/moneybuddy/fr/moneybuddy/dtos/TaskUpdate.java b/src/main/java/moneybuddy/fr/moneybuddy/dtos/TaskUpdate.java index 8d2dbc6..dde9cd7 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/dtos/TaskUpdate.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/dtos/TaskUpdate.java @@ -63,6 +63,8 @@ public boolean isWeeklyDays() { @AssertTrue(message = "Si c'est mensuel alors monthlyDay doit avoir une valeur") public boolean isMonthlyDay() { - return type == null || !TaskType.MONTHLY.equals(type) || (monthlyDay != null && monthlyDay != 0); + return type == null + || !TaskType.MONTHLY.equals(type) + || (monthlyDay != null && monthlyDay != 0); } } diff --git a/src/main/java/moneybuddy/fr/moneybuddy/exception/GlobalExceptionHandler.java b/src/main/java/moneybuddy/fr/moneybuddy/exception/GlobalExceptionHandler.java index ee30d00..f85ce34 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/exception/GlobalExceptionHandler.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/exception/GlobalExceptionHandler.java @@ -10,6 +10,8 @@ import jakarta.validation.UnexpectedTypeException; import moneybuddy.fr.moneybuddy.dtos.ErrorResponse; import moneybuddy.fr.moneybuddy.service.DiscordService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -19,8 +21,6 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.WebRequest; import org.springframework.web.multipart.MultipartException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @RestControllerAdvice public class GlobalExceptionHandler { @@ -133,7 +133,8 @@ public ResponseEntity handleMultipartException( .timestamp(LocalDateTime.now()) .status(HttpStatus.BAD_REQUEST.value()) .error("Fichier invalide") - .message("Le fichier envoyé est invalide ou dépasse la taille maximale autorisée (10MB)") + .message( + "Le fichier envoyé est invalide ou dépasse la taille maximale autorisée (10MB)") .errorCode("MULTIPART_ERROR") .path(request.getDescription(false).replace("uri=", "")) .build(); diff --git a/src/main/java/moneybuddy/fr/moneybuddy/service/DiscordService.java b/src/main/java/moneybuddy/fr/moneybuddy/service/DiscordService.java index c960798..1fda4a0 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/service/DiscordService.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/service/DiscordService.java @@ -40,7 +40,9 @@ public void init() { try { this.jda = JDABuilder.createDefault(token).build(); } catch (Exception e) { - System.err.println("[DiscordService] Failed to initialize Discord bot (monitoring disabled): " + e.getMessage()); + System.err.println( + "[DiscordService] Failed to initialize Discord bot (monitoring disabled): " + + e.getMessage()); } } diff --git a/src/main/java/moneybuddy/fr/moneybuddy/service/QuizService.java b/src/main/java/moneybuddy/fr/moneybuddy/service/QuizService.java index 543f28b..897ea94 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/service/QuizService.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/service/QuizService.java @@ -86,7 +86,7 @@ public Quiz updateQuiz(String quizId, UpdateQuizRequest req) throws FileUploadException, JsonMappingException, JsonProcessingException { Quiz quiz = getById(quizId); - quiz.setCorrectAnswerIndex(req.getCorrectAnswerIndex()); + quiz.setCorrectAnswerIndex(req.getCorrectAnswerIndex()); quiz.setQuestion(Optional.ofNullable(req.getQuestion()).orElse(quiz.getQuestion())); quiz.setResponse(Optional.ofNullable(req.getResponse()).orElse(quiz.getResponse())); diff --git a/src/main/java/moneybuddy/fr/moneybuddy/service/ReceiptOcrService.java b/src/main/java/moneybuddy/fr/moneybuddy/service/ReceiptOcrService.java index 591ed6a..2ca8ec4 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/service/ReceiptOcrService.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/service/ReceiptOcrService.java @@ -114,7 +114,9 @@ private String submitReceipt(MultipartFile file) throws IOException, Interrupted JsonNode tokenNode = body.get("token"); if (tokenNode == null || tokenNode.isNull()) throw new CustomError( - "Impossible de soumettre le reçu au service OCR", HttpStatus.FORBIDDEN, "TABSCANNER_ERROR"); + "Impossible de soumettre le reçu au service OCR", + HttpStatus.FORBIDDEN, + "TABSCANNER_ERROR"); return tokenNode.asText(); } @@ -136,8 +138,7 @@ private JsonNode pollForResult(String token) throws IOException, InterruptedExce if ("done".equals(status)) return body; if ("failed".equals(status)) - throw new CustomError( - "Le scan du reçu a échoué", HttpStatus.FORBIDDEN, "TABSCANNER_ERROR"); + throw new CustomError("Le scan du reçu a échoué", HttpStatus.FORBIDDEN, "TABSCANNER_ERROR"); Thread.sleep(POLL_INTERVAL_MS); } diff --git a/src/main/java/moneybuddy/fr/moneybuddy/service/RefreshTokenService.java b/src/main/java/moneybuddy/fr/moneybuddy/service/RefreshTokenService.java index 6404707..2e740f2 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/service/RefreshTokenService.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/service/RefreshTokenService.java @@ -36,7 +36,8 @@ public String createRefreshToken(String accountId) { } public void deleteRefreshToken(String token, String refreshToken) { - refreshTokenRepository.findByRefreshToken(refreshToken) + refreshTokenRepository + .findByRefreshToken(refreshToken) .ifPresent(refreshTokenRepository::delete); } diff --git a/src/main/java/moneybuddy/fr/moneybuddy/utils/operations/Operations.java b/src/main/java/moneybuddy/fr/moneybuddy/utils/operations/Operations.java index 3ab72db..f4c9ca2 100644 --- a/src/main/java/moneybuddy/fr/moneybuddy/utils/operations/Operations.java +++ b/src/main/java/moneybuddy/fr/moneybuddy/utils/operations/Operations.java @@ -30,7 +30,9 @@ public Operations(TransactionRepository transactionRepository, MoneyService mone public void updateProgression(Goal goal, BigDecimal depositStatement) { Number updateProgression = - depositStatement.multiply(BigDecimal.valueOf(100)).divide(goal.getAmount(), 2, RoundingMode.HALF_UP); + depositStatement + .multiply(BigDecimal.valueOf(100)) + .divide(goal.getAmount(), 2, RoundingMode.HALF_UP); if (updateProgression.floatValue() == 100) goal.setGoalStatus(GoalStatus.DONE); goal.setProgression(updateProgression);