feat(back-end, front-enc): twint payment
This commit is contained in:
@@ -7,6 +7,7 @@ import com.printcalculator.service.InvoicePdfRenderingService;
|
||||
import com.printcalculator.service.OrderService;
|
||||
import com.printcalculator.service.QrBillService;
|
||||
import com.printcalculator.service.StorageService;
|
||||
import com.printcalculator.service.TwintPaymentService;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -24,7 +25,9 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.Base64;
|
||||
import java.util.stream.Collectors;
|
||||
import java.net.URI;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/orders")
|
||||
@@ -39,6 +42,7 @@ public class OrderController {
|
||||
private final StorageService storageService;
|
||||
private final InvoicePdfRenderingService invoiceService;
|
||||
private final QrBillService qrBillService;
|
||||
private final TwintPaymentService twintPaymentService;
|
||||
|
||||
|
||||
public OrderController(OrderService orderService,
|
||||
@@ -49,7 +53,8 @@ public class OrderController {
|
||||
CustomerRepository customerRepo,
|
||||
StorageService storageService,
|
||||
InvoicePdfRenderingService invoiceService,
|
||||
QrBillService qrBillService) {
|
||||
QrBillService qrBillService,
|
||||
TwintPaymentService twintPaymentService) {
|
||||
this.orderService = orderService;
|
||||
this.orderRepo = orderRepo;
|
||||
this.orderItemRepo = orderItemRepo;
|
||||
@@ -59,6 +64,7 @@ public class OrderController {
|
||||
this.storageService = storageService;
|
||||
this.invoiceService = invoiceService;
|
||||
this.qrBillService = qrBillService;
|
||||
this.twintPaymentService = twintPaymentService;
|
||||
}
|
||||
|
||||
|
||||
@@ -185,6 +191,51 @@ public class OrderController {
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(pdf);
|
||||
}
|
||||
|
||||
@GetMapping("/{orderId}/twint")
|
||||
public ResponseEntity<Map<String, String>> getTwintPayment(@PathVariable UUID orderId) {
|
||||
if (!orderRepo.existsById(orderId)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
byte[] qrPng = twintPaymentService.generateQrPng(360);
|
||||
String qrDataUri = "data:image/png;base64," + Base64.getEncoder().encodeToString(qrPng);
|
||||
|
||||
Map<String, String> data = new HashMap<>();
|
||||
data.put("paymentUrl", twintPaymentService.getTwintPaymentUrl());
|
||||
data.put("openUrl", "/api/orders/" + orderId + "/twint/open");
|
||||
data.put("qrImageUrl", "/api/orders/" + orderId + "/twint/qr");
|
||||
data.put("qrImageDataUri", qrDataUri);
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/{orderId}/twint/open")
|
||||
public ResponseEntity<Void> openTwintPayment(@PathVariable UUID orderId) {
|
||||
if (!orderRepo.existsById(orderId)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.status(302)
|
||||
.location(URI.create(twintPaymentService.getTwintPaymentUrl()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@GetMapping("/{orderId}/twint/qr")
|
||||
public ResponseEntity<byte[]> getTwintQr(
|
||||
@PathVariable UUID orderId,
|
||||
@RequestParam(defaultValue = "320") int size
|
||||
) {
|
||||
if (!orderRepo.existsById(orderId)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
int normalizedSize = Math.max(200, Math.min(size, 600));
|
||||
byte[] png = twintPaymentService.generateQrPng(normalizedSize);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.IMAGE_PNG)
|
||||
.body(png);
|
||||
}
|
||||
|
||||
private String getExtension(String filename) {
|
||||
if (filename == null) return "stl";
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.printcalculator.service;
|
||||
|
||||
import io.nayuki.qrcodegen.QrCode;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
@Service
|
||||
public class TwintPaymentService {
|
||||
|
||||
private final String twintPaymentUrl;
|
||||
|
||||
public TwintPaymentService(
|
||||
@Value("${payment.twint.url:https://go.twint.ch/1/e/tw?tw=acq.gERQQytOTnyIMuQHUqn4hlxgciHE5X7nnqHnNSPAr2OF2K3uBlXJDr2n9JU3sgxa.}")
|
||||
String twintPaymentUrl
|
||||
) {
|
||||
this.twintPaymentUrl = twintPaymentUrl;
|
||||
}
|
||||
|
||||
public String getTwintPaymentUrl() {
|
||||
return twintPaymentUrl;
|
||||
}
|
||||
|
||||
public byte[] generateQrPng(int sizePx) {
|
||||
try {
|
||||
// Use High Error Correction for financial QR codes
|
||||
QrCode qrCode = QrCode.encodeText(twintPaymentUrl, QrCode.Ecc.HIGH);
|
||||
|
||||
// Standard QR quiet zone is 4 modules
|
||||
int borderModules = 4;
|
||||
int fullModules = qrCode.size + borderModules * 2;
|
||||
int scale = Math.max(1, sizePx / fullModules);
|
||||
int imageSize = fullModules * scale;
|
||||
|
||||
BufferedImage image = new BufferedImage(imageSize, imageSize, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
try {
|
||||
graphics.setColor(Color.WHITE);
|
||||
graphics.fillRect(0, 0, imageSize, imageSize);
|
||||
graphics.setColor(Color.BLACK);
|
||||
|
||||
for (int y = 0; y < qrCode.size; y++) {
|
||||
for (int x = 0; x < qrCode.size; x++) {
|
||||
if (qrCode.getModule(x, y)) {
|
||||
int px = (x + borderModules) * scale;
|
||||
int py = (y + borderModules) * scale;
|
||||
graphics.fillRect(px, py, scale, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
graphics.dispose();
|
||||
}
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "png", outputStream);
|
||||
return outputStream.toByteArray();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Unable to generate TWINT QR image.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,3 +23,6 @@ spring.servlet.multipart.max-request-size=200MB
|
||||
clamav.host=${CLAMAV_HOST:clamav}
|
||||
clamav.port=${CLAMAV_PORT:3310}
|
||||
clamav.enabled=${CLAMAV_ENABLED:false}
|
||||
|
||||
# TWINT Configuration
|
||||
payment.twint.url=${TWINT_PAYMENT_URL:https://go.twint.ch/1/e/tw?tw=acq.gERQQytOTnyIMuQHUqn4hlxgciHE5X7nnqHnNSPAr2OF2K3uBlXJDr2n9JU3sgxa.}
|
||||
|
||||
Reference in New Issue
Block a user