22 Commits

Author SHA1 Message Date
Patryk
9b64dc8da8 fix return url 2025-05-26 21:59:26 +02:00
Patryk
12cb37127b ad: handling of payment notifications 2025-05-26 21:51:24 +02:00
Patryk
281cc627de add payment notification endpoint 2025-05-26 20:36:39 +02:00
Patryk
6363f966f6 Integrate simple payment handling with WebClient and persist results 2025-05-23 19:06:19 +02:00
Patryk
c642f6f87b create order and change order status 2025-05-20 22:25:00 +02:00
Patryk
65524d0f25 Merge branch 'main' into initOrderAndPayments 2025-05-20 21:25:10 +02:00
Patryk
71fdf1640a change order entities and ad tpay api keys 2025-05-20 21:17:31 +02:00
8fae9f1e55 fix of Kiedy usuwasz ogłoszenie, nie usuwają zdjęcia z bazy i z dysku 2025-05-20 10:37:04 +02:00
d869a18901 boost controller endpoint little fix
+ get rid of not used dependencies
2025-05-16 13:42:24 +02:00
Patryk
1d55f40753 add boostNotice function 2025-05-15 20:29:37 +02:00
3d205df038 few cosmetic fixes 2025-05-14 08:35:33 +02:00
Patryk
5ccfc6ba2c change repository to service in wishlist, add @Lazy 2025-05-13 20:54:44 +02:00
Patryk
f0e3a129d0 change repository to service in wishlist, add @Lazy 2025-05-13 20:54:39 +02:00
Patryk
cdd31fd6b7 init wishlist files 2025-05-12 19:57:00 +02:00
09c15e70d9 New way of categories transfer 2025-05-05 10:10:03 +02:00
3b85b12741 Few improvements such as
application.properties.prod file
new DTO for response when adding notice
2025-05-02 14:49:22 +02:00
039678b90a Categories as map 2025-04-28 18:37:41 +02:00
025f733362 added endpoint for variables 2025-04-28 15:58:59 +02:00
c2f74ab799 little fixes one more time 2025-04-28 15:44:35 +02:00
bf565178f6 fix of images deletion 2025-04-28 15:06:32 +02:00
7f8f13b115 Images are working but still there is need to add isImageMain flag to images. 2025-04-28 14:17:38 +02:00
6b5dded7f8 WIP for images 2025-04-24 07:34:53 +02:00
45 changed files with 1395 additions and 221 deletions

View File

@@ -1,5 +1,7 @@
FROM openjdk:21
COPY target/ArtisanConnectBackend-0.0.1-SNAPSHOT.jar app.jar
WORKDIR /app
ENTRYPOINT ["java","-jar","/app.jar"]
COPY target/ArtisanConnectBackend-0.0.1-SNAPSHOT.jar app/artisan.jar
ENTRYPOINT ["java","-jar","app/artisan.jar"]

View File

@@ -2,10 +2,13 @@ services:
app:
container_name: artisan
build: .
networks:
- artisan_network
depends_on:
- db
networks:
- artisan_network
ports:
- '8085:8080'
db:
container_name: db

29
pom.xml
View File

@@ -10,7 +10,7 @@
</parent>
<groupId>_11.asktpk</groupId>
<artifactId>ArtisanConnectBackend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>1.0.0</version>
<name>ArtisanConnectBackend</name>
<description>ArtisanConnectBackend</description>
<url/>
@@ -34,31 +34,16 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-oauth2-client</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-docker-compose</artifactId>-->
<!-- <scope>runtime</scope>-->
<!-- <optional>true</optional>-->
<!-- </dependency>-->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
@@ -73,15 +58,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.security</groupId>-->
<!-- <artifactId>spring-security-test</artifactId>-->
<!-- <scope>test</scope>-->
<!-- </dependency>-->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
@@ -93,6 +73,11 @@
<artifactId>jakarta.validation-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
<build>

View File

@@ -1,13 +1,27 @@
package _11.asktpk.artisanconnectbackend;
import jakarta.annotation.PostConstruct;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
@SpringBootApplication
public class ArtisanConnectBackendApplication {
private final Environment environment;
public ArtisanConnectBackendApplication(Environment environment) {
this.environment = environment;
}
public static void main(String[] args) {
SpringApplication.run(ArtisanConnectBackendApplication.class, args);
}
@PostConstruct
public void logDataSourceUrl() {
System.out.println("Datasource URL: " + environment.getProperty("spring.datasource.url"));
}
}

View File

@@ -0,0 +1,35 @@
package _11.asktpk.artisanconnectbackend.config;
import _11.asktpk.artisanconnectbackend.dto.RequestResponseDTO;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
@Controller
public class CustomErrorController implements ErrorController {
@RequestMapping("/error")
public ResponseEntity<RequestResponseDTO> handleError(HttpServletRequest request) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if (status != null) {
int statusCode = Integer.parseInt(status.toString());
if (statusCode == HttpStatus.NOT_FOUND.value()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new RequestResponseDTO("Nie znaleziono zasobu. Sprawdź URL i spróbuj ponownie."));
} else if (statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new RequestResponseDTO("Wystąpił wewnętrzny błąd serwera. Spróbuj ponownie później."));
}
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new RequestResponseDTO("Wystąpił nieoczekiwany błąd."));
}
}

View File

@@ -0,0 +1,93 @@
package _11.asktpk.artisanconnectbackend.controller;
import _11.asktpk.artisanconnectbackend.dto.RequestResponseDTO;
import _11.asktpk.artisanconnectbackend.service.ImageService;
import _11.asktpk.artisanconnectbackend.service.NoticeService;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
@RestController
@RequestMapping("/api/v1/images")
public class ImageController {
private final ImageService imageService;
private final NoticeService noticeService;
ImageController(ImageService imageService, NoticeService noticeService) {
this.imageService = imageService;
this.noticeService = noticeService;
}
@Value("${file.upload-dir}")
private String uploadDir;
@PostMapping("/upload/{id}")
public ResponseEntity<RequestResponseDTO> uploadImage(@RequestParam("file") MultipartFile file, @PathVariable("id") Long noticeId) {
try {
if(file.isEmpty()) {
return ResponseEntity.badRequest().body(new RequestResponseDTO("File is empty"));
}
if(!Objects.equals(file.getContentType(), "image/jpeg") && !Objects.equals(file.getContentType(), "image/png")) {
return ResponseEntity.badRequest().body(new RequestResponseDTO("File must be a JPEG or PNG image."));
}
if(noticeId == null || !noticeService.noticeExists(noticeId)) {
return ResponseEntity.badRequest().body(new RequestResponseDTO("Notice ID is invalid or does not exist."));
}
String newImageName = imageService.saveImageToStorage(uploadDir, file);
imageService.addImageNameToDB(newImageName, noticeId);
return ResponseEntity.ok(new RequestResponseDTO("Image uploaded successfully with new name: " + newImageName));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE).body(new RequestResponseDTO(e.getMessage()));
}
}
@GetMapping("/get/{filename}")
public ResponseEntity<Resource> getImage(@PathVariable String filename) {
try {
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_JPEG)
.body(imageService.getImage(uploadDir, filename));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
}
@GetMapping("/list/{id}")
public ResponseEntity<?> getImagesNamesList(@PathVariable("id") Long noticeId) {
List<String> result;
try {
noticeService.getNoticeById(noticeId);
result = imageService.getImagesList(noticeId);
return ResponseEntity.ok(result);
} catch (EntityNotFoundException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new RequestResponseDTO(e.getMessage()));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new RequestResponseDTO(e.getMessage()));
}
}
@DeleteMapping("/delete/{filename}")
public ResponseEntity<RequestResponseDTO> deleteImage(@PathVariable("filename") String filename) {
if(filename == null) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new RequestResponseDTO("Filename is empty."));
}
try {
imageService.deleteImage(uploadDir, filename);
return ResponseEntity.status(HttpStatus.OK).body(new RequestResponseDTO("Image deleted successfully."));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new RequestResponseDTO(e.getMessage()));
}
}
}

View File

@@ -1,5 +1,8 @@
package _11.asktpk.artisanconnectbackend.controller;
import _11.asktpk.artisanconnectbackend.dto.NoticeAdditionDTO;
import _11.asktpk.artisanconnectbackend.dto.NoticeBoostDTO;
import _11.asktpk.artisanconnectbackend.dto.RequestResponseDTO;
import _11.asktpk.artisanconnectbackend.service.ClientService;
import _11.asktpk.artisanconnectbackend.service.NoticeService;
import _11.asktpk.artisanconnectbackend.dto.NoticeDTO;
@@ -7,11 +10,7 @@ import jakarta.persistence.EntityNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
@@ -32,7 +31,7 @@ public class NoticeController {
}
@GetMapping("/get/{id}")
public ResponseEntity getNoticeById(@PathVariable long id) {
public ResponseEntity<?> getNoticeById(@PathVariable long id) {
if (noticeService.noticeExists(id)) {
return ResponseEntity.ok(noticeService.getNoticeById(id));
} else {
@@ -41,21 +40,23 @@ public class NoticeController {
}
@PostMapping("/add")
public ResponseEntity<String> addNotice(@RequestBody NoticeDTO dto) {
public ResponseEntity<NoticeAdditionDTO> addNotice(@RequestBody NoticeDTO dto) {
if (!clientService.clientExists(dto.getClientId())) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body("Nie znaleziono klienta o ID: " + dto.getClientId());
.body(new NoticeAdditionDTO("Nie znaleziono klienta o ID: " + dto.getClientId()));
}
if (dto.getCategory() == null) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new NoticeAdditionDTO("Nie ma takiej kategorii"));
}
dto.setPublishDate(java.time.LocalDateTime.now());
noticeService.addNotice(dto);
Long newNoticeId = noticeService.addNotice(dto);
return ResponseEntity.status(HttpStatus.CREATED).body("Dodano ogłoszenie.");
return ResponseEntity.status(HttpStatus.CREATED).body(new NoticeAdditionDTO(newNoticeId ,"Dodano ogłoszenie."));
}
// TODO: zamiast dodawać tutaj pętlą, musi to robić NoticeService, trzeba zaimplementować odpowienią metodę
@PostMapping("/bulk_add")
public ResponseEntity<String> addNotices(@RequestBody List<NoticeDTO> notices_list) {
@@ -99,61 +100,22 @@ public class NoticeController {
}
@DeleteMapping("/delete/{id}")
public ResponseEntity deleteNotice(@PathVariable("id") long id) {
public ResponseEntity<RequestResponseDTO> deleteNotice(@PathVariable("id") long id) {
if (noticeService.noticeExists(id)) {
noticeService.deleteNotice(id);
return new ResponseEntity<>(HttpStatus.OK);
return ResponseEntity.status(HttpStatus.OK).body(new RequestResponseDTO("Pomyślnie usunięto ogłoszenie o ID: " + id));
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new RequestResponseDTO("Nie znaleziono ogłoszenia o ID: " + id));
}
}
@PostMapping("/upload/{id}")
public ResponseEntity<String> uploadImage(@PathVariable("id") Long id, @RequestParam("file") MultipartFile file) {
if (!noticeService.noticeExists(id)) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Nie znaleziono ogłoszenia o ID: " + id);
@PostMapping("/boost/{id}")
public ResponseEntity<RequestResponseDTO> boostNotice(@PathVariable("id") long clientId, @RequestBody NoticeBoostDTO dto) {
if (!noticeService.isNoticeOwnedByClient(dto.getNoticeId(), clientId)) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new RequestResponseDTO("Ogłoszenie nie istnieje lub nie należy do zalogowanego klienta."));
}
noticeService.boostNotice(dto.getNoticeId());
try {
String filePath = noticeService.saveImage(id, file);
return ResponseEntity.ok("Zdjęcie zapisane pod ścieżką: " + filePath);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Błąd podczas zapisywania zdjęcia: " + e.getMessage());
}
return ResponseEntity.status(HttpStatus.OK).body(new RequestResponseDTO("Ogłoszenie zostało pomyślnie wypromowane."));
}
@GetMapping("/images/{id}")
public ResponseEntity<List<String>> getAllImages(@PathVariable("id") Long id) {
try {
Path directoryPath = Paths.get("src/main/resources/static/images/notices/" + id);
if (!Files.exists(directoryPath) || !Files.isDirectory(directoryPath)) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
List<String> imagePaths = new ArrayList<>();
Files.list(directoryPath).forEach(file -> {
if (Files.isRegularFile(file) && Files.isReadable(file)) {
imagePaths.add(file.toString());
}
});
if (imagePaths.isEmpty()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
return ResponseEntity.ok(imagePaths);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
}
// @GetMapping("/check/{id}")
// public ResponseEntity<String> checkNotice(@PathVariable("id") long id) {
// if (noticeService.noticeExists(id)) {
// return ResponseEntity.ok("Ogłoszenie o ID " + id + " istnieje.");
// } else {
// return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Nie znaleziono ogłoszenia o ID: " + id);
// }
// }
}

View File

@@ -0,0 +1,51 @@
package _11.asktpk.artisanconnectbackend.controller;
import _11.asktpk.artisanconnectbackend.dto.*;
import _11.asktpk.artisanconnectbackend.entities.Order;
import _11.asktpk.artisanconnectbackend.service.ClientService;
import _11.asktpk.artisanconnectbackend.service.OrderService;
import _11.asktpk.artisanconnectbackend.service.PaymentService;
import _11.asktpk.artisanconnectbackend.utils.Enums;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final OrderService orderService;
private final PaymentService paymentService;
public OrderController(OrderService orderService, PaymentService paymentService) {
this.orderService = orderService;
this.paymentService = paymentService;
}
@PostMapping("/add")
public ResponseEntity addClient(@RequestBody OrderDTO orderDTO) {
return new ResponseEntity<>(orderService.addOrder(orderDTO), HttpStatus.CREATED);
}
@PutMapping("/changeStatus")
public ResponseEntity changeStatus(@RequestBody OrderStatusDTO orderStatusDTO) {
return new ResponseEntity<>(orderService.changeOrderStatus(orderStatusDTO.getId(),orderStatusDTO.getStatus()), HttpStatus.OK);
}
@PostMapping("/token")
public ResponseEntity<?> fetchToken() {
Order order = orderService.getOrderById(1L);
OAuthPaymentResponseDTO authPaymentDTO= paymentService.getOAuthToken();
TransactionPaymentRequestDTO.Payer payer = new TransactionPaymentRequestDTO.Payer(
"patryk@test.pl", "Patryk Test");
String paymentDescription = order.getOrderType() == Enums.OrderType.ACTIVATION ? "Aktywacja ogłoszenia" : "Podbicie ogłoszenia";
paymentDescription += order.getNotice().getTitle();
TransactionPaymentRequestDTO request = new TransactionPaymentRequestDTO(
order.getAmount(), paymentDescription, payer);
String response = paymentService.createTransaction(order,authPaymentDTO.getAccess_token(), request);
System.out.println(response);
return ResponseEntity.ok(response);
}
}

View File

@@ -0,0 +1,102 @@
package _11.asktpk.artisanconnectbackend.controller;
import _11.asktpk.artisanconnectbackend.entities.Notice;
import _11.asktpk.artisanconnectbackend.entities.Order;
import _11.asktpk.artisanconnectbackend.entities.Payment;
import _11.asktpk.artisanconnectbackend.repository.PaymentRepository;
import _11.asktpk.artisanconnectbackend.utils.Enums;
import org.springframework.beans.factory.annotation.Value;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/v1/payments")
public class PaymentController {
@Value("${tpay.securityCode}")
private String sellerSecurityCode;
private static final Logger log = LoggerFactory.getLogger(PaymentController.class);
private final PaymentRepository paymentRepository;
public PaymentController(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
@PostMapping(value = "/notification", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ResponseEntity<String> handleTpayNotification(@RequestParam Map<String, String> params) {
log.info("=== ODEBRANO NOTYFIKACJĘ Tpay ===");
log.info("Parametry:\n{}", paramsToLogString(params));
String id = params.get("id");
String trId = params.get("tr_id");
String trAmount = params.get("tr_amount");
String trCrc = params.get("tr_crc");
String md5sum = params.get("md5sum");
String trStatus = params.get("tr_status");
String expectedMd5 = DigestUtils.md5DigestAsHex(
(id + trId + trAmount + trCrc + sellerSecurityCode).getBytes()
);
if (!expectedMd5.equals(md5sum)) {
log.warn("❌ Błędna suma kontrolna! Otrzymano: {}, Oczekiwano: {}", md5sum, expectedMd5);
return ResponseEntity.status(400).body("INVALID CHECKSUM");
}
Optional<Payment> optionalPayment = paymentRepository.findByTransactionId(trId);
if (optionalPayment.isPresent()) {
Payment payment = optionalPayment.get();
if ("true".equalsIgnoreCase(trStatus) || "PAID".equalsIgnoreCase(trStatus)) {
log.info("✅ Transakcja opłacona: tr_id={}, kwota={}", trId, params.get("tr_paid"));
payment.setStatus(Enums.PaymentStatus.CORRECT);
if (payment.getOrder() != null) {
Order order = payment.getOrder();
order.setStatus(Enums.OrderStatus.COMPLETED);
Notice notice = order.getNotice();
if (order.getOrderType() == Enums.OrderType.ACTIVATION) {
notice.setStatus(Enums.Status.ACTIVE);
} else if (order.getOrderType() == Enums.OrderType.BOOST) {
notice.setPublishDate(LocalDateTime.now());
}
}
} else if ("false".equalsIgnoreCase(trStatus)) {
log.warn("❌ Transakcja nieudana: {}", trId);
payment.setStatus(Enums.PaymentStatus.INCORRECT);
if (payment.getOrder() != null) {
payment.getOrder().setStatus(Enums.OrderStatus.CANCELLED);
}
}
paymentRepository.save(payment);
} else {
log.warn("⚠️ Brak płatności o tr_id={}", trId);
}
return ResponseEntity.ok("TRUE");
}
private String paramsToLogString(Map<String, String> params) {
return params.entrySet().stream()
.map(e -> e.getKey() + " = " + e.getValue())
.collect(Collectors.joining("\n"));
}
}

View File

@@ -0,0 +1,40 @@
package _11.asktpk.artisanconnectbackend.controller;
import _11.asktpk.artisanconnectbackend.dto.CategoriesDTO;
import _11.asktpk.artisanconnectbackend.utils.Enums;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/vars")
public class VariablesController {
@GetMapping("/categories")
public List<CategoriesDTO> getAllVariables() {
List<CategoriesDTO> categoriesDTOList = new ArrayList<>();
for (Map.Entry<Enums.Category, String> entry : Enums.categoryPL.entrySet()) {
CategoriesDTO categoriesDTO = new CategoriesDTO();
categoriesDTO.setLabel(entry.getValue());
categoriesDTO.setValue(entry.getKey().toString());
categoriesDTOList.add(categoriesDTO);
}
return categoriesDTOList;
}
@GetMapping("/statuses")
public List<Enums.Status> getAllStatuses() {
return List.of(Enums.Status.values());
}
@GetMapping("/roles")
public List<Enums.Role> getAllRoles() {
return List.of(Enums.Role.values());
}
}

View File

@@ -0,0 +1,59 @@
package _11.asktpk.artisanconnectbackend.controller;
import _11.asktpk.artisanconnectbackend.dto.NoticeDTO;
import _11.asktpk.artisanconnectbackend.dto.RequestResponseDTO;
import _11.asktpk.artisanconnectbackend.dto.WishlistDTO;
import _11.asktpk.artisanconnectbackend.service.ClientService;
import _11.asktpk.artisanconnectbackend.service.NoticeService;
import _11.asktpk.artisanconnectbackend.service.WishlistService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/wishlist")
public class WishlistController {
private final WishlistService wishlistService;
private final ClientService clientService;
private final NoticeService noticeService;
public WishlistController(WishlistService wishlistService, ClientService clientService, NoticeService noticeService) {
this.wishlistService = wishlistService;
this.clientService = clientService;
this.noticeService = noticeService;
}
@PostMapping("/toggle")
public ResponseEntity<RequestResponseDTO> toggleWishlist(@RequestBody WishlistDTO wishlistDTO) {
Long noticeId = wishlistDTO.getNoticeId();
Long clientId = wishlistDTO.getClientId();
NoticeDTO noticeDTO = noticeService.getNoticeById(noticeId);
if (noticeDTO == null) {
return ResponseEntity.badRequest().body(new RequestResponseDTO("Notice not found"));
}
boolean added = wishlistService.toggleWishlist(
clientService.getClientById(clientId),
noticeService.getNoticeByIdEntity(noticeId)
);
if (added) {
return ResponseEntity.ok(new RequestResponseDTO("Wishlist entry added"));
} else {
return ResponseEntity.ok(new RequestResponseDTO("Wishlist entry removed"));
}
}
// @GetMapping("/{clientId}")
// public ResponseEntity<List<WishlistDTO>> getWishlist(@PathVariable Long clientId) {
// List<WishlistDTO> wishlist = wishlistService.getWishlistForClientId(clientId);
// return ResponseEntity.ok(wishlist);
// }
@GetMapping("/")
public List<NoticeDTO> getWishlistForClient() {
// TODO: Replace with actual client ID from authentication context
Long clientId = 1L;
return wishlistService.getNoticesInWishlist(clientId);
}
}

View File

@@ -0,0 +1,16 @@
package _11.asktpk.artisanconnectbackend.dto;
//[
// { "label": "Meble", "value": "Furniture" },
// { "label": "Biżuteria", "value": "Jewelry" },
// { "label": "Ceramika", "value": "Ceramics" }
//]
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class CategoriesDTO {
String label;
String value;
}

View File

@@ -0,0 +1,9 @@
package _11.asktpk.artisanconnectbackend.dto;
import org.springframework.core.io.Resource;
public class ImageRequestDTO {
public Resource image;
public Long noticeId;
public boolean isMainImage;
}

View File

@@ -0,0 +1,19 @@
package _11.asktpk.artisanconnectbackend.dto;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class NoticeAdditionDTO {
public Long noticeId;
public String message;
public NoticeAdditionDTO(String message) {
this.message = message;
}
public NoticeAdditionDTO(Long noticeId, String message) {
this.noticeId = noticeId;
this.message = message;
}
}

View File

@@ -0,0 +1,9 @@
package _11.asktpk.artisanconnectbackend.dto;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class NoticeBoostDTO {
private Long noticeId;
}

View File

@@ -22,30 +22,15 @@ public class NoticeDTO {
private Enums.Category category;
private List<String> images;
private Enums.Status status;
private LocalDateTime publishDate;
private List<AttributesNotice> attributesNotices;
private boolean isWishlisted;
public NoticeDTO() {
}
public NoticeDTO(Long noticeId, String title, Long clientId, String description, Double price,
Enums.Category category, List<String> images, Enums.Status status,
LocalDateTime publishDate, List<AttributesNotice> attributesNotices) {
this.noticeId = noticeId;
this.title = title;
this.clientId = clientId;
this.description = description;
this.price = price;
this.category = category;
this.images = images;
this.status = status;
this.publishDate = publishDate;
this.attributesNotices = attributesNotices;
}
}

View File

@@ -0,0 +1,15 @@
package _11.asktpk.artisanconnectbackend.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class OAuthPaymentResponseDTO {
private long issued_at;
private String scope;
private String token_type;
private int expires_in;
private String client_id;
private String access_token;
}

View File

@@ -0,0 +1,13 @@
package _11.asktpk.artisanconnectbackend.dto;
import _11.asktpk.artisanconnectbackend.utils.Enums;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class OrderDTO {
private Long clientId;
private Long noticeId;
private Enums.OrderType orderType;
}

View File

@@ -0,0 +1,12 @@
package _11.asktpk.artisanconnectbackend.dto;
import _11.asktpk.artisanconnectbackend.utils.Enums;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class OrderStatusDTO {
public long id;
public Enums.OrderStatus status;
}

View File

@@ -0,0 +1,13 @@
package _11.asktpk.artisanconnectbackend.dto;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class RequestResponseDTO {
public String message;
public RequestResponseDTO(String message) {
this.message = message;
}
}

View File

@@ -0,0 +1,25 @@
package _11.asktpk.artisanconnectbackend.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class TransactionPaymentRequestDTO {
private double amount;
private String description;
private Payer payer;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Payer {
private String email;
private String name;
}
}

View File

@@ -0,0 +1,59 @@
package _11.asktpk.artisanconnectbackend.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
public class TransactionPaymentResponseDTO {
private String result;
private String requestId;
private String transactionId;
private String title;
private String posId;
private String status;
private DateInfo date;
private double amount;
private String currency;
private String description;
private String hiddenDescription;
private Payer payer;
private Payments payments;
private String transactionPaymentUrl;
@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
public static class DateInfo {
private String creation;
private String realization;
}
@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Payer {
private String payerId;
private String email;
private String name;
private String phone;
private String address;
private String city;
private String country;
private String postalCode;
}
@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Payments {
private String status;
private String method;
private double amountPaid;
private DateInfo date;
}
}

View File

@@ -0,0 +1,12 @@
package _11.asktpk.artisanconnectbackend.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class WishlistDTO {
private Long id;
private Long clientId;
private Long noticeId;
}

View File

@@ -33,5 +33,5 @@ public class Client {
// private List<Notice> notices;
@OneToMany(mappedBy = "client", cascade = CascadeType.ALL)
private List<Orders> orders;
private List<Order> orders;
}

View File

@@ -0,0 +1,23 @@
package _11.asktpk.artisanconnectbackend.entities;
import jakarta.persistence.*;
import jdk.jfr.BooleanFlag;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "images")
@Getter @Setter
public class Image {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long noticeId;
private String imageName;
@BooleanFlag
private boolean isMainImage;
}

View File

@@ -30,9 +30,6 @@ public class Notice {
@Enumerated(EnumType.STRING)
private Category category;
@ElementCollection
private List<String> images;
@Enumerated(EnumType.STRING)
private Status status;
@@ -42,8 +39,8 @@ public class Notice {
private List<AttributesNotice> attributesNotices;
@OneToMany(mappedBy = "notice", cascade = CascadeType.ALL)
private List<Orders> orders;
private List<Order> orders;
@OneToMany(mappedBy = "notice", cascade = CascadeType.ALL)
private List<Payments> payments;
// @OneToMany(mappedBy = "notice", cascade = CascadeType.ALL)
// private List<Payment> payment;
}

View File

@@ -0,0 +1,43 @@
package _11.asktpk.artisanconnectbackend.entities;
import _11.asktpk.artisanconnectbackend.utils.Enums;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "id_client")
private Client client;
@ManyToOne
@JoinColumn(name = "id_notice")
private Notice notice;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Enums.OrderType orderType;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Enums.OrderStatus status;
@Column(nullable = false)
private Double amount;
@Column(nullable = false)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -1,26 +0,0 @@
package _11.asktpk.artisanconnectbackend.entities;
import _11.asktpk.artisanconnectbackend.utils.Enums.Status;
import jakarta.persistence.*;
@Entity
@Table(name = "orders")
public class Orders {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idOrder;
@ManyToOne
@JoinColumn(name = "id_user")
private Client client;
@ManyToOne
@JoinColumn(name = "id_notice")
private Notice notice;
@Enumerated(EnumType.STRING)
private Status status;
// Getters, setters, and constructors
}

View File

@@ -0,0 +1,30 @@
package _11.asktpk.artisanconnectbackend.entities;
import _11.asktpk.artisanconnectbackend.utils.Enums;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "payment")
@Getter @Setter
public class Payment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idPayment;
@ManyToOne
@JoinColumn(name = "id_order")
private Order order;
private Double amount;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Enums.PaymentStatus status;
private String transactionPaymentUrl;
private String transactionId;
}

View File

@@ -1,30 +0,0 @@
package _11.asktpk.artisanconnectbackend.entities;
import _11.asktpk.artisanconnectbackend.utils.Enums.Status;
import jakarta.persistence.*;
@Entity
@Table(name = "payments")
public class Payments {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idPayment;
@ManyToOne
@JoinColumn(name = "id_order")
private Orders order;
@ManyToOne
@JoinColumn(name = "id_notice")
private Notice notice;
private Double noticePublishPrice;
@Enumerated(EnumType.STRING)
private Status status;
private String sessionId;
// Getters, setters, and constructors
}

View File

@@ -0,0 +1,24 @@
package _11.asktpk.artisanconnectbackend.entities;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "wishlist")
@Getter
@Setter
public class Wishlist {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@ManyToOne
@JoinColumn(name = "notice_id", nullable = false)
private Notice notice;
}

View File

@@ -0,0 +1,14 @@
package _11.asktpk.artisanconnectbackend.repository;
import _11.asktpk.artisanconnectbackend.entities.Image;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ImageRepository extends JpaRepository<Image, Long> {
List<Image> findByNoticeId(Long noticeId);
boolean existsImageByImageNameEqualsIgnoreCase(String imageName);
void deleteByImageNameEquals(String imageName);
}

View File

@@ -5,4 +5,7 @@ import _11.asktpk.artisanconnectbackend.entities.Notice;
import org.springframework.data.jpa.repository.JpaRepository;
public interface NoticeRepository extends JpaRepository<Notice, Long> {
boolean existsByIdNoticeAndClientId(long noticeId, long clientId);
}

View File

@@ -0,0 +1,11 @@
package _11.asktpk.artisanconnectbackend.repository;
import _11.asktpk.artisanconnectbackend.entities.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
}

View File

@@ -0,0 +1,12 @@
package _11.asktpk.artisanconnectbackend.repository;
import _11.asktpk.artisanconnectbackend.entities.Payment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface PaymentRepository extends JpaRepository<Payment, Long> {
Optional<Payment> findByTransactionId(String transactionId);
}

View File

@@ -0,0 +1,16 @@
package _11.asktpk.artisanconnectbackend.repository;
import _11.asktpk.artisanconnectbackend.entities.Client;
import _11.asktpk.artisanconnectbackend.entities.Notice;
import _11.asktpk.artisanconnectbackend.entities.Wishlist;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface WishlistRepository extends JpaRepository<Wishlist, Long> {
List<Wishlist> findAllByClientId(Long clientId);
Optional<Wishlist> findByClientAndNotice(Client client, Notice notice);
}

View File

@@ -0,0 +1,93 @@
package _11.asktpk.artisanconnectbackend.service;
import _11.asktpk.artisanconnectbackend.entities.Image;
import _11.asktpk.artisanconnectbackend.repository.ImageRepository;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
@Transactional
public class ImageService {
private final ImageRepository imageRepository;
ImageService(ImageRepository imageRepository) {
this.imageRepository = imageRepository;
}
public String saveImageToStorage(String uploadDirectory, MultipartFile imageFile) throws IOException {
String uniqueFileName = UUID.randomUUID() + imageFile.getOriginalFilename();
Path uploadPath = Path.of(uploadDirectory);
Path filePath = uploadPath.resolve(uniqueFileName);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
Files.copy(imageFile.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
return uniqueFileName;
}
public void addImageNameToDB(String filename, Long noticeId) {
Image image = new Image();
image.setImageName(filename);
image.setNoticeId(noticeId);
imageRepository.save(image);
}
public Resource getImage(String imageDirectory, String imageName) throws IOException {
Path filePath = Paths.get(imageDirectory).resolve(imageName);
Resource resource = new UrlResource(filePath.toUri());
if(imageName.isEmpty() || imageDirectory.isEmpty()) {
throw new IOException("Filename or folder is empty. Please check your request and try again.");
}
if (!resource.exists()) {
throw new IOException("File not found");
}
return resource;
}
public void deleteImage(String imageDirectory, String imageName) throws IOException {
Path imagePath = Path.of(imageDirectory, imageName);
deleteImageRecordFromDB(imageName);
if (Files.exists(imagePath)) {
Files.delete(imagePath);
} else {
throw new IOException("File not found");
}
}
public List<String> getImagesList(Long noticeID) throws Exception {
List<Image> images = imageRepository.findByNoticeId(noticeID);
if (images.isEmpty()) {
throw new Exception("There is no images for this notice");
}
return images.stream()
.map(Image::getImageName)
.collect(Collectors.toList());
}
public void deleteImageRecordFromDB(String imageName) {
if(imageRepository.existsImageByImageNameEqualsIgnoreCase(imageName)) {
imageRepository.deleteByImageNameEquals(imageName);
}
}
}

View File

@@ -6,35 +6,41 @@ import _11.asktpk.artisanconnectbackend.repository.ClientRepository;
import _11.asktpk.artisanconnectbackend.repository.NoticeRepository;
import _11.asktpk.artisanconnectbackend.dto.NoticeDTO;
import jakarta.persistence.EntityNotFoundException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
public class NoticeService {
private static final Logger logger = LogManager.getLogger(NoticeService.class);
@Value("${file.upload-dir}")
private String uploadDir;
private final NoticeRepository noticeRepository;
private final ClientRepository clientRepository;
private final WishlistService wishlistService;
private final ImageService imageService;
public NoticeService(NoticeRepository noticeRepository, ClientRepository clientRepository) {
public NoticeService(NoticeRepository noticeRepository, ClientRepository clientRepository, WishlistService wishlistService, ImageService imageService) {
this.noticeRepository = noticeRepository;
this.clientRepository = clientRepository;
this.wishlistService = wishlistService;
this.imageService = imageService;
}
private Notice fromDTO(NoticeDTO dto) {
public Notice fromDTO(NoticeDTO dto) {
Notice notice = new Notice();
notice.setTitle(dto.getTitle());
notice.setDescription(dto.getDescription());
notice.setPrice(dto.getPrice());
notice.setCategory(dto.getCategory());
notice.setImages(dto.getImages());
notice.setStatus(dto.getStatus());
notice.setPublishDate(dto.getPublishDate());
notice.setAttributesNotices(dto.getAttributesNotices());
@@ -48,17 +54,23 @@ public class NoticeService {
private NoticeDTO toDTO(Notice notice) {
NoticeDTO dto = new NoticeDTO();
// TODO: To be updated using AuthService after implementing authentication.
Optional<Client> client = clientRepository.findById(1L);
boolean isWishlisted = false;
if (client.isPresent()) {
Client c = client.get();
isWishlisted = wishlistService.isWishlisted(c, notice);
}
dto.setNoticeId(notice.getIdNotice());
dto.setTitle(notice.getTitle());
dto.setClientId(notice.getClient().getId());
dto.setDescription(notice.getDescription());
dto.setPrice(notice.getPrice());
dto.setCategory(notice.getCategory());
dto.setImages(notice.getImages());
dto.setStatus(notice.getStatus());
dto.setPublishDate(notice.getPublishDate());
dto.setAttributesNotices(notice.getAttributesNotices());
dto.setWishlisted(isWishlisted);
return dto;
}
@@ -76,8 +88,13 @@ public class NoticeService {
return toDTO(notice);
}
public void addNotice(NoticeDTO dto) {
noticeRepository.save(fromDTO(dto));
public Notice getNoticeByIdEntity(Long id) {
return noticeRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Nie znaleziono ogłoszenia o ID: " + id));
}
public Long addNotice(NoticeDTO dto) {
return noticeRepository.save(fromDTO(dto)).getIdNotice();
}
public boolean noticeExists(Long id) {
@@ -92,7 +109,6 @@ public class NoticeService {
existingNotice.setDescription(dto.getDescription());
existingNotice.setPrice(dto.getPrice());
existingNotice.setCategory(dto.getCategory());
existingNotice.setImages(dto.getImages());
existingNotice.setStatus(dto.getStatus());
existingNotice.setAttributesNotices(dto.getAttributesNotices());
@@ -108,55 +124,40 @@ public class NoticeService {
public void deleteNotice(Long id) {
if (noticeExists(id)) {
noticeRepository.deleteById(id);
List<String> imagesList = new ArrayList<>();
try {
imagesList = imageService.getImagesList(id);
} catch (Exception e) {
logger.info("There weren't any images for notice with ID: " + id + ". Skipping deletion of images. Message: " + e.getMessage());
}
try {
for (String imageName : imagesList) {
imageService.deleteImage(uploadDir, imageName);
}
} catch (Exception e) {
logger.info("There were some issues while deleting images for notice with ID: " + id + ". Message: " + e.getMessage());
}
} else {
throw new EntityNotFoundException("Nie znaleziono ogłoszenia o ID: " + id);
}
}
public String saveImage(Long noticeId, MultipartFile file) throws IOException {
String uploadDir = "src/main/resources/static/images/notices/" + noticeId;
Path uploadPath = Paths.get(uploadDir);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
// szukanie nazwy pliku
String fileName = file.getOriginalFilename();
if (fileName != null) {
String extension = fileName.substring(fileName.lastIndexOf('.'));
String baseName = fileName.substring(0, fileName.lastIndexOf('.'));
List<Path> filesInDirectory = Files.list(uploadPath)
.filter(Files::isRegularFile)
.toList();
int maxNumber = filesInDirectory.stream()
.map(path -> path.getFileName().toString())
.filter(name -> name.startsWith(baseName) && name.endsWith(extension))
.map(name -> name.substring(baseName.length(), name.length() - extension.length()))
.filter(number -> number.matches("\\d+"))
.mapToInt(Integer::parseInt)
.max()
.orElse(0);
fileName = baseName + (maxNumber + 1) + extension;
}
//koniec szukania nazwy pliku
if(fileName == null) {
throw new IOException("Nie można znaleźć nazwy pliku");
}
Path filePath = uploadPath.resolve(fileName);
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
public boolean isNoticeOwnedByClient(long noticeId, long clientId) {
return noticeRepository.existsByIdNoticeAndClientId(noticeId, clientId);
}
public void boostNotice(long noticeId) {
Notice notice = noticeRepository.findById(noticeId)
.orElseThrow(() -> new EntityNotFoundException("Nie znaleziono ogłoszenia o ID: " + noticeId));
List<String> images = notice.getImages();
images.add(filePath.toString());
notice.setImages(images);
.orElseThrow(() -> new EntityNotFoundException("Ogłoszenie o ID " + noticeId + " nie istnieje."));
notice.setPublishDate(LocalDateTime.now());
noticeRepository.save(notice);
return filePath.toString();
}
}

View File

@@ -0,0 +1,79 @@
package _11.asktpk.artisanconnectbackend.service;
import _11.asktpk.artisanconnectbackend.dto.OrderDTO;
import _11.asktpk.artisanconnectbackend.entities.Client;
import _11.asktpk.artisanconnectbackend.entities.Notice;
import _11.asktpk.artisanconnectbackend.repository.ClientRepository;
import _11.asktpk.artisanconnectbackend.repository.NoticeRepository;
import _11.asktpk.artisanconnectbackend.repository.OrderRepository;
import _11.asktpk.artisanconnectbackend.utils.Enums;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import _11.asktpk.artisanconnectbackend.entities.Order;
import java.time.LocalDateTime;
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final ClientRepository clientRepository;
private final NoticeRepository noticeRepository;
@Autowired
public OrderService(OrderRepository orderRepository, ClientRepository clientRepository, NoticeRepository noticeRepository) {
this.orderRepository = orderRepository;
this.clientRepository = clientRepository;
this.noticeRepository = noticeRepository;
}
public Order fromDTO(OrderDTO orderDTO) {
Order order = new Order();
order.setOrderType(orderDTO.getOrderType());
order.setStatus(Enums.OrderStatus.PENDING);
if(orderDTO.getOrderType() == Enums.OrderType.ACTIVATION){
order.setAmount(10.00);
}else{
order.setAmount(8.00);
}
order.setCreatedAt(LocalDateTime.now()
);
order.setUpdatedAt(LocalDateTime.now()
);
Client client = clientRepository.findById(orderDTO.getClientId())
.orElseThrow(() -> new EntityNotFoundException("Nie znaleziono klienta o ID: " + orderDTO.getClientId()));
order.setClient(client);
Notice notice = noticeRepository.findById(orderDTO.getNoticeId())
.orElseThrow(() -> new EntityNotFoundException("Nie znaleziono ogłoszenia o ID: " + orderDTO.getNoticeId()));
order.setNotice(notice);
return order;
}
public Long addOrder(OrderDTO orderDTO) {
Order order = fromDTO(orderDTO);
return orderRepository.save(order).getId();
}
public Long changeOrderStatus(Long id, Enums.OrderStatus status) {
Order order = orderRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Nie znaleziono zamówienia o ID: " + id));
order.setStatus(status);
order = orderRepository.save(order);
return order.getId();
}
public Order getOrderById(Long id) {
return orderRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Nie znaleziono zamówienia o ID: " + id));
}
}

View File

@@ -0,0 +1,81 @@
package _11.asktpk.artisanconnectbackend.service;
import _11.asktpk.artisanconnectbackend.dto.OAuthPaymentResponseDTO;
import _11.asktpk.artisanconnectbackend.dto.TransactionPaymentRequestDTO;
import _11.asktpk.artisanconnectbackend.dto.TransactionPaymentResponseDTO;
import _11.asktpk.artisanconnectbackend.entities.Order;
import _11.asktpk.artisanconnectbackend.entities.Payment;
import _11.asktpk.artisanconnectbackend.repository.PaymentRepository;
import _11.asktpk.artisanconnectbackend.utils.Enums;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
@Service
public class PaymentService {
private final WebClient webClient;
private final String clientId;
private final String clientSecret;
private final String authUrl;
private final String transactionUrl;
private final PaymentRepository paymentRepository;
public PaymentService(
WebClient.Builder webClientBuilder,
@Value("${tpay.clientId}") String clientId,
@Value("${tpay.clientSecret}") String clientSecret,
@Value("${tpay.authUrl}") String authUrl,
@Value("${tpay.transactionUrl}") String transactionUrl,
PaymentRepository paymentRepository
) {
this.webClient = webClientBuilder.baseUrl(authUrl).build();
this.clientId = clientId;
this.clientSecret = clientSecret;
this.authUrl = authUrl;
this.transactionUrl = transactionUrl;
this.paymentRepository = paymentRepository;
}
public OAuthPaymentResponseDTO getOAuthToken() {
return webClient.post()
.uri("")
.contentType(MediaType.MULTIPART_FORM_DATA)
.header("accept", "application/json")
.body(BodyInserters.fromMultipartData("client_id", clientId)
.with("client_secret", clientSecret))
.retrieve()
.bodyToMono(OAuthPaymentResponseDTO.class)
.block();
}
public String createTransaction(Order order, String accessToken, TransactionPaymentRequestDTO transactionPaymentRequestDTO) {
TransactionPaymentResponseDTO response = webClient.post()
.uri(transactionUrl)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(transactionPaymentRequestDTO)
.retrieve()
.bodyToMono(TransactionPaymentResponseDTO.class)
.block();
if (response != null && "success".equalsIgnoreCase(response.getResult())) {
Payment payment = new Payment();
payment.setOrder(order);
payment.setAmount(response.getAmount());
payment.setStatus(Enums.PaymentStatus.PENDING);
payment.setTransactionId(response.getTransactionId());
payment.setTransactionPaymentUrl(response.getTransactionPaymentUrl());
paymentRepository.save(payment);
return response.getTransactionPaymentUrl();
}
return null;
}
}

View File

@@ -0,0 +1,72 @@
package _11.asktpk.artisanconnectbackend.service;
import _11.asktpk.artisanconnectbackend.dto.WishlistDTO;
import _11.asktpk.artisanconnectbackend.dto.NoticeDTO;
import _11.asktpk.artisanconnectbackend.entities.Client;
import _11.asktpk.artisanconnectbackend.entities.Notice;
import _11.asktpk.artisanconnectbackend.entities.Wishlist;
import _11.asktpk.artisanconnectbackend.repository.WishlistRepository;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class WishlistService {
private final WishlistRepository wishlistRepository;
private final NoticeService noticeService;
public WishlistService(WishlistRepository wishlistRepository, @Lazy NoticeService noticeService) {
this.wishlistRepository = wishlistRepository;
this.noticeService = noticeService;
}
public List<WishlistDTO> getWishlistForClientId(Long clientId) {
List<Wishlist> wishlistEntities = wishlistRepository.findAllByClientId(clientId);
return wishlistEntities.stream()
.map(this::toDTO)
.toList();
}
public boolean isWishlisted(Client client, Notice notice) {
Optional<Wishlist> existingEntry = wishlistRepository.findByClientAndNotice(client, notice);
return existingEntry.isEmpty();
}
public boolean toggleWishlist(Client client, Notice notice) {
Optional<Wishlist> existingEntry = wishlistRepository.findByClientAndNotice(client, notice);
if (existingEntry.isPresent()) {
wishlistRepository.delete(existingEntry.get());
return false;
} else {
Wishlist wishlist = new Wishlist();
wishlist.setClient(client);
wishlist.setNotice(notice);
wishlistRepository.save(wishlist);
return true;
}
}
private WishlistDTO toDTO(Wishlist wishlist) {
WishlistDTO dto = new WishlistDTO();
dto.setId(wishlist.getId());
dto.setClientId(wishlist.getClient().getId());
dto.setNoticeId(wishlist.getNotice().getIdNotice());
return dto;
}
public List<NoticeDTO> getNoticesInWishlist(Long clientId) {
List<Wishlist> wishlistEntries = wishlistRepository.findAllByClientId(clientId);
return wishlistEntries.stream()
.map(wishlist -> noticeService.getNoticeById(wishlist.getNotice().getIdNotice()))
.collect(Collectors.toList());
}
}

View File

@@ -1,5 +1,7 @@
package _11.asktpk.artisanconnectbackend.utils;
import java.util.Map;
public class Enums {
public enum Role {
ADMIN, USER
@@ -13,7 +15,48 @@ public class Enums {
Knitting, Carpentry, Other
}
public static final Map<Category, String> categoryPL = Map.ofEntries(
Map.entry(Category.Handmade, "Rękodzieło"),
Map.entry(Category.Woodworking, "Stolarstwo"),
Map.entry(Category.Metalworking, "Obróbka metalu"),
Map.entry(Category.Ceramics, "Ceramika"),
Map.entry(Category.Textiles, "Tekstylia"),
Map.entry(Category.Jewelry, "Biżuteria"),
Map.entry(Category.Leatherwork, "Wyroby skórzane"),
Map.entry(Category.Painting, "Malarstwo"),
Map.entry(Category.Sculpture, "Rzeźbiarstwo"),
Map.entry(Category.Glasswork, "Szklarstwo"),
Map.entry(Category.Furniture, "Meble"),
Map.entry(Category.Restoration, "Renowacja"),
Map.entry(Category.Tailoring, "Krawiectwo"),
Map.entry(Category.Weaving, "Tkactwo"),
Map.entry(Category.Calligraphy, "Kaligrafia"),
Map.entry(Category.Pottery, "Garncarstwo"),
Map.entry(Category.Blacksmithing, "Kowalstwo"),
Map.entry(Category.Basketry, "Koszykarstwo"),
Map.entry(Category.Embroidery, "Hafciarstwo"),
Map.entry(Category.Knitting, "Dzierganie"),
Map.entry(Category.Carpentry, "Ciesielstwo"),
Map.entry(Category.Other, "Inne")
);
public enum Status {
ACTIVE, INACTIVE
}
public enum OrderType {
ACTIVATION,
BOOST
}
public enum OrderStatus {
PENDING, COMPLETED, CANCELLED
}
public enum PaymentStatus{
PENDING, CORRECT, INCORRECT
}
}

View File

@@ -2,14 +2,27 @@ spring.application.name=ArtisanConnectBackend
## PostgreSQL
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.username=postgres
spring.datasource.password=postgres
#initial data for db injection
spring.sql.init.data-locations=classpath:sql/data.sql
spring.sql.init.mode=always
spring.jpa.defer-datasource-initialization=true
# create and drop table, good for testing, production set to none or comment it
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.ddl-auto=create-drop
spring.web.resources.static-locations=classpath:/static/,file:images/
file.upload-dir=/Users/andsol/Desktop/uploads
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
tpay.clientId = 01JQKC048X62ST9V59HNRSXD92-01JQKC2CQHPYXQFSFX8BKC24BX
tpay.clientSecret = 44898642be53381cdcc47f3e44bf5a15e592f5d270fc3a6cf6fb81a8b8ebffb9
tpay.authUrl = https://openapi.sandbox.tpay.com/oauth/auth
tpay.transactionUrl = https://openapi.sandbox.tpay.com/transactions
tpay.securityCode = )IY7E)YSM!A)Q6O-GN#U7U_33s9qObk8
logging.file.name=logs/payment-notifications.log
logging.level.TpayLogger=INFO

View File

@@ -0,0 +1,20 @@
spring.application.name=ArtisanConnectBackend
## PostgreSQL
spring.datasource.url=jdbc:postgresql://db:5432/postgres
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.username=postgres
spring.datasource.password=postgres
#initial data for db injection
spring.sql.init.data-locations=classpath:sql/data.sql
spring.sql.init.mode=always
spring.jpa.defer-datasource-initialization=true
# create and drop table, good for testing, production set to none or comment it
spring.jpa.hibernate.ddl-auto=update
#file.upload-dir=/Users/andsol/Desktop/uploads
file.upload-dir=/app/images
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB

View File

@@ -0,0 +1,122 @@
<mxfile host="Electron" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/26.2.2 Chrome/134.0.6998.178 Electron/35.1.2 Safari/537.36" version="26.2.2">
<diagram name="Strona-1" id="y3JY7GFactLq4jGyxg2V">
<mxGraphModel dx="2180" dy="884" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="Ht36qwYIb0A0iwGBjog0-3" value="&lt;font style=&quot;font-size: 15px;&quot;&gt;&lt;b&gt;Backend&lt;/b&gt;&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#f5f5f5;fillStyle=auto;strokeColor=#666666;shadow=0;glass=0;fontColor=#333333;" vertex="1" parent="1">
<mxGeometry x="270" y="520" width="534" height="270" as="geometry" />
</mxCell>
<mxCell id="HPrCYg6BNmEDSauX9FRR-4" value="&lt;font style=&quot;font-size: 15px;&quot;&gt;&lt;b&gt;Backend&lt;/b&gt;&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#f5f5f5;fillStyle=auto;strokeColor=#666666;shadow=0;glass=0;fontColor=#333333;" parent="1" vertex="1">
<mxGeometry x="324" y="100" width="470" height="230" as="geometry" />
</mxCell>
<mxCell id="HPrCYg6BNmEDSauX9FRR-16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="HPrCYg6BNmEDSauX9FRR-2" target="HPrCYg6BNmEDSauX9FRR-7" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="HPrCYg6BNmEDSauX9FRR-2" value="Postman" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="34" y="250" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="HPrCYg6BNmEDSauX9FRR-12" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="HPrCYg6BNmEDSauX9FRR-7" target="HPrCYg6BNmEDSauX9FRR-10" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="HPrCYg6BNmEDSauX9FRR-7" value="Controller" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="364" y="250" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="HPrCYg6BNmEDSauX9FRR-13" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="HPrCYg6BNmEDSauX9FRR-10" target="HPrCYg6BNmEDSauX9FRR-11" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="HPrCYg6BNmEDSauX9FRR-10" value="Service" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="554" y="250" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="HPrCYg6BNmEDSauX9FRR-11" value="Repository" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="664" y="130" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="HPrCYg6BNmEDSauX9FRR-17" value="DTO" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="504" y="270" width="30" height="20" as="geometry" />
</mxCell>
<mxCell id="HPrCYg6BNmEDSauX9FRR-19" value="Entity" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="704" y="230" width="40" height="20" as="geometry" />
</mxCell>
<mxCell id="HPrCYg6BNmEDSauX9FRR-1" value="DTO" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="224" y="270" width="40" height="20" as="geometry" />
</mxCell>
<mxCell id="HPrCYg6BNmEDSauX9FRR-44" value="Przesyłanie danych na serwer" style="text;strokeColor=none;fillColor=none;html=1;fontSize=24;fontStyle=1;verticalAlign=middle;align=center;" parent="1" vertex="1">
<mxGeometry x="226" y="40" width="376" height="40" as="geometry" />
</mxCell>
<mxCell id="HPrCYg6BNmEDSauX9FRR-50" value="" style="sketch=0;pointerEvents=1;shadow=0;dashed=0;html=1;strokeColor=none;fillColor=#DF8C42;labelPosition=center;verticalLabelPosition=bottom;verticalAlign=top;align=center;outlineConnect=0;shape=mxgraph.veeam2.restful_api;" parent="1" vertex="1">
<mxGeometry x="10" y="10" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-1" value="Przesyłanie zdjęć na backend" style="text;strokeColor=none;fillColor=none;html=1;fontSize=24;fontStyle=1;verticalAlign=middle;align=center;" vertex="1" parent="1">
<mxGeometry x="194" y="460" width="440" height="40" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="Ht36qwYIb0A0iwGBjog0-2" target="Ht36qwYIb0A0iwGBjog0-4">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-2" value="Postman" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="-20" y="625" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="Ht36qwYIb0A0iwGBjog0-4" target="Ht36qwYIb0A0iwGBjog0-5">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-4" value="ImageController&lt;div&gt;(Validation)&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="307" y="625" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-5" value="ImageService" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="550" y="580" width="196" height="150" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-9" value="MultipartFile + PathVariable" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="140" y="640" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-17" value="saveImageToStorage()" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="580" y="620" width="136" height="30" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-18" value="addImageNameToDB()" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="582" y="680" width="132" height="30" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-20" value="Pobieranie zdjęć z backendu" style="text;strokeColor=none;fillColor=none;html=1;fontSize=24;fontStyle=1;verticalAlign=middle;align=center;" vertex="1" parent="1">
<mxGeometry x="194" y="1010" width="440" height="40" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-22" value="&lt;font style=&quot;font-size: 15px;&quot;&gt;&lt;b&gt;Backend&lt;/b&gt;&lt;/font&gt;" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=#f5f5f5;fillStyle=auto;strokeColor=#666666;shadow=0;glass=0;fontColor=#333333;" vertex="1" parent="1">
<mxGeometry x="260" y="1079" width="534" height="290" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-29" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;" edge="1" parent="1" source="Ht36qwYIb0A0iwGBjog0-23" target="Ht36qwYIb0A0iwGBjog0-26">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-30" value="GetRequest" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="Ht36qwYIb0A0iwGBjog0-29">
<mxGeometry x="0.2401" relative="1" as="geometry">
<mxPoint x="-19" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-23" value="Postman" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="-110" y="1197" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-24" value="/api/v1/images/get/{filename}" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="-135" y="1160" width="170" height="30" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-35" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.25;exitDx=0;exitDy=0;entryX=1;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" source="Ht36qwYIb0A0iwGBjog0-26" target="Ht36qwYIb0A0iwGBjog0-23">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-36" value="MultipartFile(content-type image/jpeg)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="Ht36qwYIb0A0iwGBjog0-35">
<mxGeometry x="0.0779" y="-1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-26" value="ImageController" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="294" y="1197" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-37" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="Ht36qwYIb0A0iwGBjog0-31" target="Ht36qwYIb0A0iwGBjog0-26">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-31" value="ImageService" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;align=center;" vertex="1" parent="1">
<mxGeometry x="534" y="1152" width="196" height="150" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-32" value="getImage()" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="564" y="1209" width="136" height="30" as="geometry" />
</mxCell>
<mxCell id="Ht36qwYIb0A0iwGBjog0-38" value="/api/v1/images/upload/{noticeID}" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="-60" y="590" width="200" height="30" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>