Files
SHiNE-server/SHiNE-promo-solana-devnet/src/main/java/ru/shine/promo/controller/PromoApiController.java
T

137 lines
5.7 KiB
Java

package ru.shine.promo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import ru.shine.promo.config.AppProperties;
import ru.shine.promo.dto.PromoRequest;
import ru.shine.promo.dto.PromoResponse;
import ru.shine.promo.service.PromoCodeService;
import ru.shine.promo.service.PromoException;
import ru.shine.promo.service.PromoTransferService;
import ru.shine.promo.service.UsedPromoStorageService;
import java.util.Map;
@RestController
public class PromoApiController {
private static final Logger log = LoggerFactory.getLogger(PromoApiController.class);
private final AppProperties appProperties;
private final PromoCodeService promoCodeService;
private final PromoTransferService promoTransferService;
private final UsedPromoStorageService usedPromoStorageService;
public PromoApiController(
AppProperties appProperties,
PromoCodeService promoCodeService,
PromoTransferService promoTransferService,
UsedPromoStorageService usedPromoStorageService
) {
this.appProperties = appProperties;
this.promoCodeService = promoCodeService;
this.promoTransferService = promoTransferService;
this.usedPromoStorageService = usedPromoStorageService;
}
@PostMapping("/api/promo/top-up")
public ResponseEntity<PromoResponse> topUp(@RequestBody PromoRequest request) {
String wallet = trimToEmpty(request == null ? null : request.getWallet());
String name = trimToEmpty(request == null ? null : request.getName());
String promoCode = promoCodeService.normalizePromoCode(request == null ? null : request.getPromoCode());
if (wallet.isEmpty()) {
return error(HttpStatus.BAD_REQUEST, "Пустой wallet");
}
if (!promoTransferService.isSolanaWalletValid(wallet)) {
return error(HttpStatus.BAD_REQUEST, "Неверный формат Solana-адреса");
}
if (name.isEmpty()) {
return error(HttpStatus.BAD_REQUEST, "Пустое имя");
}
if (name.length() < 2 || name.length() > 120) {
return error(HttpStatus.BAD_REQUEST, "Имя должно быть длиной от 2 до 120 символов");
}
if (promoCode.isEmpty()) {
return error(HttpStatus.BAD_REQUEST, "Пустой промокод");
}
boolean eternalPromo = promoCodeService.isEternalPromoCode(promoCode);
if (!eternalPromo && !promoCodeService.isPromoCodeFormatValid(promoCode)) {
return error(HttpStatus.BAD_REQUEST, "Неверный формат промокода");
}
try {
if (!eternalPromo && !promoCodeService.promoCodeExists(promoCode)) {
return error(HttpStatus.BAD_REQUEST, "Промокод не найден");
}
String signature = usedPromoStorageService.executeLocked(() -> {
if (!eternalPromo && usedPromoStorageService.isPromoUsed(promoCode)) {
throw new PromoException(HttpStatus.CONFLICT, "Промокод уже использован");
}
String txSignature = promoTransferService.sendPromoTransfer(wallet);
// Для вечного промокода ведём только лог использования, но не блокируем повторное применение.
usedPromoStorageService.appendUsedPromo(promoCode, wallet, name, txSignature);
return txSignature;
});
String explorerUrl = promoTransferService.buildExplorerUrl(signature);
String amount = appProperties.getPromoTransferAmountSol().stripTrailingZeros().toPlainString();
log.info(
"Promo top-up success: wallet={}, name={}, promoCode={}, signature={}",
wallet,
name,
promoCode,
signature
);
PromoResponse response = PromoResponse.success(
"Тестовое пополнение выполнено",
wallet,
name,
amount,
signature,
explorerUrl
);
return ResponseEntity.ok(response);
} catch (PromoException e) {
if (e.getStatus().is5xxServerError()) {
log.error("Top-up failed: {}", e.getMessage(), e);
} else {
log.warn("Top-up rejected: {}", e.getMessage());
}
return error(e.getStatus(), e.getMessage());
} catch (Exception e) {
log.error("Unexpected top-up error", e);
return error(HttpStatus.INTERNAL_SERVER_ERROR, "Внутренняя ошибка сервера");
}
}
@GetMapping("/health")
public Map<String, String> health() {
return Map.of(
"status", "ok",
"app", "SHiNE-promo-solana-devnet"
);
}
private ResponseEntity<PromoResponse> error(HttpStatus status, String message) {
return ResponseEntity.status(status).body(PromoResponse.error(message));
}
private String trimToEmpty(String value) {
if (value == null) {
return "";
}
return value.trim().replaceAll("\\s{2,}", " ");
}
}