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 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 health() { return Map.of( "status", "ok", "app", "SHiNE-promo-solana-devnet" ); } private ResponseEntity 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,}", " "); } }