From 15d5d31d068e26f657a75be218fb09b6fc9b6ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Fri, 20 Feb 2026 17:09:42 +0100 Subject: [PATCH] feat(back-end, front-enc): twint payment --- .../controller/OrderController.java | 53 +++++++- .../service/TwintPaymentService.java | 66 ++++++++++ .../src/main/resources/application.properties | 3 + docker-compose.yml | 3 + frontend/src/app/app.routes.ts | 4 + .../calculator/calculator-page.component.html | 4 +- .../quote-result/quote-result.component.html | 4 +- .../upload-form/upload-form.component.html | 8 +- .../user-details/user-details.component.html | 28 ++--- .../services/quote-estimator.service.ts | 7 ++ .../features/checkout/checkout.component.html | 10 +- .../contact-form/contact-form.component.html | 4 +- .../contact/contact-page.component.html | 2 +- .../src/app/features/home/home.component.html | 98 +++++++-------- .../order-confirmed.component.html | 25 ++++ .../order-confirmed.component.scss | 62 ++++++++++ .../order-confirmed.component.ts | 28 +++++ .../features/payment/payment.component.html | 22 +++- .../features/payment/payment.component.scss | 36 ++++-- .../app/features/payment/payment.component.ts | 51 +++++++- .../shop/product-detail.component.html | 2 +- frontend/src/assets/i18n/en.json | 13 ++ frontend/src/assets/i18n/it.json | 117 +++++++++++++++++- 23 files changed, 543 insertions(+), 107 deletions(-) create mode 100644 backend/src/main/java/com/printcalculator/service/TwintPaymentService.java create mode 100644 frontend/src/app/features/order-confirmed/order-confirmed.component.html create mode 100644 frontend/src/app/features/order-confirmed/order-confirmed.component.scss create mode 100644 frontend/src/app/features/order-confirmed/order-confirmed.component.ts diff --git a/backend/src/main/java/com/printcalculator/controller/OrderController.java b/backend/src/main/java/com/printcalculator/controller/OrderController.java index 3c70513..272b41b 100644 --- a/backend/src/main/java/com/printcalculator/controller/OrderController.java +++ b/backend/src/main/java/com/printcalculator/controller/OrderController.java @@ -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> 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 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 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 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"; diff --git a/backend/src/main/java/com/printcalculator/service/TwintPaymentService.java b/backend/src/main/java/com/printcalculator/service/TwintPaymentService.java new file mode 100644 index 0000000..97982eb --- /dev/null +++ b/backend/src/main/java/com/printcalculator/service/TwintPaymentService.java @@ -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); + } + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 3d70ca6..82887cc 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -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.} diff --git a/docker-compose.yml b/docker-compose.yml index f347611..39c038b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -58,7 +58,10 @@ services: container_name: print-calculator-clamav ports: - "3310:3310" + volumes: + - clamav_db:/var/lib/clamav restart: unless-stopped volumes: postgres_data: + clamav_db: diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index c74044d..ac184d1 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -33,6 +33,10 @@ export const routes: Routes = [ path: 'payment/:orderId', loadComponent: () => import('./features/payment/payment.component').then(m => m.PaymentComponent) }, + { + path: 'order-confirmed/:orderId', + loadComponent: () => import('./features/order-confirmed/order-confirmed.component').then(m => m.OrderConfirmedComponent) + }, { path: '', loadChildren: () => import('./features/legal/legal.routes').then(m => m.LEGAL_ROUTES) diff --git a/frontend/src/app/features/calculator/calculator-page.component.html b/frontend/src/app/features/calculator/calculator-page.component.html index 928f957..72b1ed3 100644 --- a/frontend/src/app/features/calculator/calculator-page.component.html +++ b/frontend/src/app/features/calculator/calculator-page.component.html @@ -46,8 +46,8 @@
-

Analisi in corso...

-

Stiamo analizzando la geometria e calcolando il percorso utensile.

+

{{ 'CALC.ANALYZING_TITLE' | translate }}

+

{{ 'CALC.ANALYZING_TEXT' | translate }}

} @else if (result()) { diff --git a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html index f33740d..769cb0e 100644 --- a/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html +++ b/frontend/src/app/features/calculator/components/quote-result/quote-result.component.html @@ -21,7 +21,7 @@
- * Include {{ result().setupCost | currency:result().currency }} Setup Cost + {{ 'CALC.SETUP_NOTE' | translate:{cost: (result().setupCost | currency:result().currency)} }}
@if (result().notes) { @@ -46,7 +46,7 @@
- +
- +
- +
@@ -151,7 +151,7 @@ type="submit" [disabled]="items().length === 0 || loading()" [fullWidth]="true"> - {{ loading() ? (uploadProgress() < 100 ? 'Uploading...' : 'Processing...') : ('CALC.CALCULATE' | translate) }} + {{ loading() ? (uploadProgress() < 100 ? ('CALC.UPLOADING' | translate) : ('CALC.PROCESSING' | translate)) : ('CALC.CALCULATE' | translate) }}
diff --git a/frontend/src/app/features/calculator/components/user-details/user-details.component.html b/frontend/src/app/features/calculator/components/user-details/user-details.component.html index a16b8e2..3f934cd 100644 --- a/frontend/src/app/features/calculator/components/user-details/user-details.component.html +++ b/frontend/src/app/features/calculator/components/user-details/user-details.component.html @@ -9,8 +9,8 @@
@@ -18,8 +18,8 @@
@@ -31,9 +31,9 @@
@@ -41,9 +41,9 @@
@@ -53,8 +53,8 @@ @@ -64,8 +64,8 @@
@@ -73,8 +73,8 @@
diff --git a/frontend/src/app/features/calculator/services/quote-estimator.service.ts b/frontend/src/app/features/calculator/services/quote-estimator.service.ts index 55fa6ae..6841caa 100644 --- a/frontend/src/app/features/calculator/services/quote-estimator.service.ts +++ b/frontend/src/app/features/calculator/services/quote-estimator.service.ts @@ -161,6 +161,13 @@ export class QuoteEstimatorService { responseType: 'blob' }); } + + getTwintPayment(orderId: string): Observable { + const headers: any = {}; + // @ts-ignore + if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth); + return this.http.get(`${environment.apiUrl}/api/orders/${orderId}/twint`, { headers }); + } calculate(request: QuoteRequest): Observable { console.log('QuoteEstimatorService: Calculating quote...', request); diff --git a/frontend/src/app/features/checkout/checkout.component.html b/frontend/src/app/features/checkout/checkout.component.html index d3972a3..7c83827 100644 --- a/frontend/src/app/features/checkout/checkout.component.html +++ b/frontend/src/app/features/checkout/checkout.component.html @@ -21,7 +21,7 @@

{{ 'CHECKOUT.CONTACT_INFO' | translate }}

- +
@@ -41,8 +41,8 @@
- - + +
@@ -87,8 +87,8 @@
- - + +
diff --git a/frontend/src/app/features/contact/components/contact-form/contact-form.component.html b/frontend/src/app/features/contact/components/contact-form/contact-form.component.html index c7e33a1..86a2335 100644 --- a/frontend/src/app/features/contact/components/contact-form/contact-form.component.html +++ b/frontend/src/app/features/contact/components/contact-form/contact-form.component.html @@ -60,8 +60,8 @@
- PDF - 3D + {{ 'CONTACT.FILE_TYPE_PDF' | translate }} + {{ 'CONTACT.FILE_TYPE_3D' | translate }}
{{ file.file.name }}
diff --git a/frontend/src/app/features/contact/contact-page.component.html b/frontend/src/app/features/contact/contact-page.component.html index 75d5c3c..efd8bb5 100644 --- a/frontend/src/app/features/contact/contact-page.component.html +++ b/frontend/src/app/features/contact/contact-page.component.html @@ -1,7 +1,7 @@

{{ 'CONTACT.TITLE' | translate }}

-

Siamo qui per aiutarti. Compila il modulo sottostante per qualsiasi richiesta.

+

{{ 'CONTACT.HERO_SUBTITLE' | translate }}

diff --git a/frontend/src/app/features/home/home.component.html b/frontend/src/app/features/home/home.component.html index 4d4b943..5aca4a3 100644 --- a/frontend/src/app/features/home/home.component.html +++ b/frontend/src/app/features/home/home.component.html @@ -2,22 +2,18 @@
-

Stampa 3D tecnica per aziende, freelance e maker

-

- Prezzo e tempi in pochi secondi.
- Dal file 3D al pezzo finito. -

+

{{ 'HOME.HERO_EYEBROW' | translate }}

+

- Il calcolatore più avanzato per le tue stampe 3D: precisione assoluta e zero sorprese. + {{ 'HOME.HERO_LEAD' | translate }}

- Realizziamo pezzi unici e produzioni in serie. Se hai il file, il preventivo è istantaneo. - Se devi ancora crearlo, il nostro team di design lo progetterà per te. + {{ 'HOME.HERO_SUBTITLE' | translate }}

- Calcola Preventivo - Vai allo shop - Parla con noi + {{ 'HOME.BTN_CALCULATE' | translate }} + {{ 'HOME.BTN_SHOP' | translate }} + {{ 'HOME.BTN_CONTACT' | translate }}
@@ -26,31 +22,31 @@
-

Preventivo immediato in pochi secondi

+

{{ 'HOME.SEC_CALC_TITLE' | translate }}

- Nessuna registrazione necessaria. Non salviamo il tuo file fino al momento dell'ordine e la stima è effettuata tramite vero slicing. + {{ 'HOME.SEC_CALC_SUBTITLE' | translate }}

    -
  • Formati supportati: STL, 3MF, STEP, OBJ
  • -
  • Qualità: bozza, standard, alta definizione
  • +
  • {{ 'HOME.SEC_CALC_LIST_1' | translate }}
  • +
  • {{ 'HOME.SEC_CALC_LIST_2' | translate }}
-

Calcolo automatico

-

Prezzo e tempi in un click

+

{{ 'HOME.CARD_CALC_EYEBROW' | translate }}

+

{{ 'HOME.CARD_CALC_TITLE' | translate }}

- Senza registrazione + {{ 'HOME.CARD_CALC_TAG' | translate }}
    -
  • Carica il file 3D
  • -
  • Scegli materiale e qualità
  • -
  • Ricevi subito costo e tempo
  • +
  • {{ 'HOME.CARD_CALC_STEP_1' | translate }}
  • +
  • {{ 'HOME.CARD_CALC_STEP_2' | translate }}
  • +
  • {{ 'HOME.CARD_CALC_STEP_3' | translate }}
- Apri calcolatore - Parla con noi + {{ 'HOME.BTN_OPEN_CALC' | translate }} + {{ 'HOME.BTN_CONTACT' | translate }}
@@ -60,9 +56,9 @@
-

Cosa puoi ottenere

+

{{ 'HOME.SEC_CAP_TITLE' | translate }}

- Produzione su misura per prototipi, piccole serie e pezzi personalizzati. + {{ 'HOME.SEC_CAP_SUBTITLE' | translate }}

@@ -70,29 +66,29 @@
-

Prototipazione veloce

-

Dal file digitale al modello fisico in 24/48 ore. Verifica ergonomia, incastri e funzionamento prima dello stampo definitivo.

+

{{ 'HOME.CAP_1_TITLE' | translate }}

+

{{ 'HOME.CAP_1_TEXT' | translate }}

-

Pezzi personalizzati

-

Componenti unici impossibili da trovare in commercio. Riproduciamo parti rotte o creiamo adattamenti su misura.

+

{{ 'HOME.CAP_2_TITLE' | translate }}

+

{{ 'HOME.CAP_2_TEXT' | translate }}

-

Piccole serie

-

Produzione ponte o serie limitate (10-500 pezzi) con qualità ripetibile. Ideale per validazione di mercato.

+

{{ 'HOME.CAP_3_TITLE' | translate }}

+

{{ 'HOME.CAP_3_TEXT' | translate }}

-

Consulenza e CAD

-

Non hai il file 3D? Ti aiutiamo a progettarlo, ottimizzarlo per la stampa e scegliere il materiale giusto.

+

{{ 'HOME.CAP_4_TITLE' | translate }}

+

{{ 'HOME.CAP_4_TEXT' | translate }}

@@ -101,33 +97,32 @@
-

Shop di soluzioni tecniche pronte

+

{{ 'HOME.SEC_SHOP_TITLE' | translate }}

- Prodotti selezionati, testati in laboratorio e pronti all'uso. Risolvono problemi reali con - funzionalità concrete. + {{ 'HOME.SEC_SHOP_TEXT' | translate }}

    -
  • Accessori funzionali per officine e laboratori
  • -
  • Ricambi e componenti difficili da reperire
  • -
  • Supporti e organizzatori per migliorare i flussi di lavoro
  • +
  • {{ 'HOME.SEC_SHOP_LIST_1' | translate }}
  • +
  • {{ 'HOME.SEC_SHOP_LIST_2' | translate }}
  • +
  • {{ 'HOME.SEC_SHOP_LIST_3' | translate }}
- Scopri i prodotti - Richiedi una soluzione + {{ 'HOME.BTN_DISCOVER' | translate }} + {{ 'HOME.BTN_REQ_SOLUTION' | translate }}
-

Best seller tecnici

-

Soluzioni provate sul campo e già pronte alla spedizione.

+

{{ 'HOME.CARD_SHOP_1_TITLE' | translate }}

+

{{ 'HOME.CARD_SHOP_1_TEXT' | translate }}

-

Kit pronti all'uso

-

Componenti compatibili e facili da montare senza sorprese.

+

{{ 'HOME.CARD_SHOP_2_TITLE' | translate }}

+

{{ 'HOME.CARD_SHOP_2_TEXT' | translate }}

-

Su richiesta

-

Non trovi quello che serve? Lo progettiamo e lo produciamo per te.

+

{{ 'HOME.CARD_SHOP_3_TITLE' | translate }}

+

{{ 'HOME.CARD_SHOP_3_TEXT' | translate }}

@@ -136,17 +131,16 @@
-

Su di noi

+

{{ 'HOME.SEC_ABOUT_TITLE' | translate }}

- 3D fab è un laboratorio tecnico di stampa 3D. Seguiamo progetti dalla consulenza iniziale - alla produzione, con tempi chiari e supporto diretto. + {{ 'HOME.SEC_ABOUT_TEXT' | translate }}

- Contattaci + {{ 'HOME.BTN_CONTACT' | translate }}
- Foto Founders + {{ 'HOME.FOUNDERS_PHOTO' | translate }}
diff --git a/frontend/src/app/features/order-confirmed/order-confirmed.component.html b/frontend/src/app/features/order-confirmed/order-confirmed.component.html new file mode 100644 index 0000000..c443bce --- /dev/null +++ b/frontend/src/app/features/order-confirmed/order-confirmed.component.html @@ -0,0 +1,25 @@ +
+

{{ 'ORDER_CONFIRMED.TITLE' | translate }}

+

{{ 'ORDER_CONFIRMED.SUBTITLE' | translate }}

+
+ +
+
+ +
{{ 'ORDER_CONFIRMED.STATUS' | translate }}
+

{{ 'ORDER_CONFIRMED.HEADING' | translate }}

+

+ {{ 'ORDER_CONFIRMED.ORDER_REF' | translate }}: #{{ orderId.substring(0, 8) }} +

+ +
+

{{ 'ORDER_CONFIRMED.PROCESSING_TEXT' | translate }}

+

{{ 'ORDER_CONFIRMED.EMAIL_TEXT' | translate }}

+
+ +
+ {{ 'ORDER_CONFIRMED.BACK_HOME' | translate }} +
+
+
+
diff --git a/frontend/src/app/features/order-confirmed/order-confirmed.component.scss b/frontend/src/app/features/order-confirmed/order-confirmed.component.scss new file mode 100644 index 0000000..b38000b --- /dev/null +++ b/frontend/src/app/features/order-confirmed/order-confirmed.component.scss @@ -0,0 +1,62 @@ +.hero { + padding: var(--space-12) 0 var(--space-8); + text-align: center; + + h1 { + font-size: 2.4rem; + margin-bottom: var(--space-2); + } +} + +.subtitle { + font-size: 1.1rem; + color: var(--color-text-muted); + max-width: 720px; + margin: 0 auto; +} + +.confirmation-layout { + max-width: 760px; + margin: 0 auto var(--space-12); +} + +.status-badge { + display: inline-block; + padding: 0.35rem 0.65rem; + border-radius: 999px; + background: #eef8f0; + color: #136f2d; + font-weight: 700; + font-size: 0.85rem; + margin-bottom: var(--space-4); +} + +h2 { + margin: 0 0 var(--space-3); +} + +.order-ref { + margin: 0 0 var(--space-4); + color: var(--color-text-muted); +} + +.message-block { + background: var(--color-neutral-100); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: var(--space-5); + margin-bottom: var(--space-6); + + p { + margin: 0; + line-height: 1.45; + } + + p + p { + margin-top: var(--space-3); + } +} + +.actions { + max-width: 320px; +} diff --git a/frontend/src/app/features/order-confirmed/order-confirmed.component.ts b/frontend/src/app/features/order-confirmed/order-confirmed.component.ts new file mode 100644 index 0000000..60abd3f --- /dev/null +++ b/frontend/src/app/features/order-confirmed/order-confirmed.component.ts @@ -0,0 +1,28 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { AppButtonComponent } from '../../shared/components/app-button/app-button.component'; +import { AppCardComponent } from '../../shared/components/app-card/app-card.component'; + +@Component({ + selector: 'app-order-confirmed', + standalone: true, + imports: [CommonModule, TranslateModule, AppButtonComponent, AppCardComponent], + templateUrl: './order-confirmed.component.html', + styleUrl: './order-confirmed.component.scss' +}) +export class OrderConfirmedComponent implements OnInit { + private route = inject(ActivatedRoute); + private router = inject(Router); + + orderId: string | null = null; + + ngOnInit(): void { + this.orderId = this.route.snapshot.paramMap.get('orderId'); + } + + goHome(): void { + this.router.navigate(['/']); + } +} diff --git a/frontend/src/app/features/payment/payment.component.html b/frontend/src/app/features/payment/payment.component.html index a86265b..7728b4b 100644 --- a/frontend/src/app/features/payment/payment.component.html +++ b/frontend/src/app/features/payment/payment.component.html @@ -17,26 +17,35 @@ class="type-option" [class.selected]="selectedPaymentMethod === 'twint'" (click)="selectPayment('twint')"> - TWINT + {{ 'PAYMENT.METHOD_TWINT' | translate }}
- QR Bill / Bank Transfer + {{ 'PAYMENT.METHOD_BANK' | translate }}
-
+

{{ 'PAYMENT.TWINT_TITLE' | translate }}

-
- QR CODE -
+ TWINT payment QR

{{ 'PAYMENT.TWINT_DESC' | translate }}

+

{{ 'PAYMENT.BILLING_INFO_HINT' | translate }}

+
+ + {{ 'PAYMENT.TWINT_OPEN' | translate }} + +

{{ 'PAYMENT.TOTAL' | translate }}: {{ o.totalChf | currency:'CHF' }}

@@ -49,6 +58,7 @@

{{ 'PAYMENT.BANK_OWNER' | translate }}: 3D Fab Switzerland

{{ 'PAYMENT.BANK_IBAN' | translate }}: CH98 0000 0000 0000 0000 0

{{ 'PAYMENT.BANK_REF' | translate }}: {{ o.id }}

+

{{ 'PAYMENT.BILLING_INFO_HINT' | translate }}

diff --git a/frontend/src/app/features/payment/payment.component.scss b/frontend/src/app/features/payment/payment.component.scss index 4d3ba7f..3008d79 100644 --- a/frontend/src/app/features/payment/payment.component.scss +++ b/frontend/src/app/features/payment/payment.component.scss @@ -105,23 +105,33 @@ } } +.payment-details-twint { + border: 1px solid #dcd5ff; + background: linear-gradient(180deg, #fcfbff 0%, #f6f4ff 100%); +} + .qr-placeholder { display: flex; flex-direction: column; align-items: center; text-align: center; - .qr-box { - width: 180px; - height: 180px; - background-color: white; - border: 2px solid var(--color-neutral-900); - display: flex; - align-items: center; - justify-content: center; - font-weight: bold; - margin-bottom: var(--space-4); + .twint-qr { + width: 240px; + height: 240px; + background-color: #fff; + border: 1px solid var(--color-border); border-radius: var(--radius-md); + padding: var(--space-2); + margin-bottom: var(--space-4); + object-fit: contain; + box-shadow: 0 6px 18px rgba(44, 37, 84, 0.08); + } + + .twint-mobile-action { + width: 100%; + max-width: 320px; + margin-top: var(--space-3); } .amount { @@ -132,6 +142,12 @@ } } +.billing-hint { + margin-top: var(--space-3); + font-size: 0.95rem; + color: var(--color-text-muted); +} + .bank-details { p { margin-bottom: var(--space-2); diff --git a/frontend/src/app/features/payment/payment.component.ts b/frontend/src/app/features/payment/payment.component.ts index 520e486..9083732 100644 --- a/frontend/src/app/features/payment/payment.component.ts +++ b/frontend/src/app/features/payment/payment.component.ts @@ -5,6 +5,7 @@ import { AppButtonComponent } from '../../shared/components/app-button/app-butto import { AppCardComponent } from '../../shared/components/app-card/app-card.component'; import { QuoteEstimatorService } from '../calculator/services/quote-estimator.service'; import { TranslateModule } from '@ngx-translate/core'; +import { environment } from '../../../environments/environment'; @Component({ selector: 'app-payment', @@ -23,11 +24,14 @@ export class PaymentComponent implements OnInit { order = signal(null); loading = signal(true); error = signal(null); + twintOpenUrl = signal(null); + twintQrUrl = signal(null); ngOnInit(): void { this.orderId = this.route.snapshot.paramMap.get('orderId'); if (this.orderId) { this.loadOrder(); + this.loadTwintPayment(); } else { this.error.set('Order ID not found.'); this.loading.set(false); @@ -68,8 +72,51 @@ export class PaymentComponent implements OnInit { }); } + loadTwintPayment() { + if (!this.orderId) return; + this.quoteService.getTwintPayment(this.orderId).subscribe({ + next: (res) => { + const qrPath = typeof res.qrImageUrl === 'string' ? `${res.qrImageUrl}?size=360` : null; + const qrDataUri = typeof res.qrImageDataUri === 'string' ? res.qrImageDataUri : null; + this.twintOpenUrl.set(this.resolveApiUrl(res.openUrl)); + this.twintQrUrl.set(qrDataUri ?? this.resolveApiUrl(qrPath)); + }, + error: (err) => { + console.error('Failed to load TWINT payment details', err); + } + }); + } + + openTwintPayment(): void { + const openUrl = this.twintOpenUrl(); + if (typeof window !== 'undefined' && openUrl) { + window.location.href = openUrl; + } + } + + getTwintQrUrl(): string { + return this.twintQrUrl() ?? ''; + } + + onTwintQrError(): void { + this.twintQrUrl.set(null); + } + + private resolveApiUrl(urlOrPath: string | null | undefined): string | null { + if (!urlOrPath) return null; + if (urlOrPath.startsWith('http://') || urlOrPath.startsWith('https://')) { + return urlOrPath; + } + const base = (environment.apiUrl || '').replace(/\/$/, ''); + const path = urlOrPath.startsWith('/') ? urlOrPath : `/${urlOrPath}`; + return `${base}${path}`; + } + completeOrder(): void { - alert('Payment Simulated! Order marked as PAID.'); - this.router.navigate(['/']); + if (!this.orderId) { + this.router.navigate(['/']); + return; + } + this.router.navigate(['/order-confirmed', this.orderId]); } } diff --git a/frontend/src/app/features/shop/product-detail.component.html b/frontend/src/app/features/shop/product-detail.component.html index a08b543..58f2937 100644 --- a/frontend/src/app/features/shop/product-detail.component.html +++ b/frontend/src/app/features/shop/product-detail.component.html @@ -20,6 +20,6 @@
} @else { -

Prodotto non trovato.

+

{{ 'SHOP.NOT_FOUND' | translate }}

}
diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index 23ce97f..45217d3 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -180,10 +180,13 @@ "METHOD": "Payment Method", "TWINT_TITLE": "Pay with TWINT", "TWINT_DESC": "Scan the code with your TWINT app", + "TWINT_OPEN": "Open directly in TWINT", + "TWINT_LINK": "Open payment link", "BANK_TITLE": "Bank Transfer", "BANK_OWNER": "Owner", "BANK_IBAN": "IBAN", "BANK_REF": "Reference", + "BILLING_INFO_HINT": "Add the same information used in billing.", "DOWNLOAD_QR": "Download QR-Invoice (PDF)", "CONFIRM": "Confirm Order", "SUMMARY_TITLE": "Order Summary", @@ -192,5 +195,15 @@ "SETUP_FEE": "Setup Fee", "TOTAL": "Total", "LOADING": "Loading order details..." + }, + "ORDER_CONFIRMED": { + "TITLE": "Order Confirmed", + "SUBTITLE": "Payment received. Your order is now being processed.", + "STATUS": "Processing", + "HEADING": "We are preparing your order", + "ORDER_REF": "Order reference", + "PROCESSING_TEXT": "As soon as payment is confirmed, your order will move to production.", + "EMAIL_TEXT": "We will send you an email update with status and next steps.", + "BACK_HOME": "Back to Home" } } diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index 70ce4f9..9ccaca4 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -11,6 +11,52 @@ "TERMS": "Termini & Condizioni", "CONTACT": "Contattaci" }, + "HOME": { + "HERO_EYEBROW": "Stampa 3D tecnica per aziende, freelance e maker", + "HERO_TITLE": "Prezzo e tempi in pochi secondi.
Dal file 3D al pezzo finito.", + "HERO_LEAD": "Il calcolatore più avanzato per le tue stampe 3D: precisione assoluta e zero sorprese.", + "HERO_SUBTITLE": "Realizziamo pezzi unici e produzioni in serie. Se hai il file, il preventivo è istantaneo. Se devi ancora crearlo, il nostro team di design lo progetterà per te.", + "BTN_CALCULATE": "Calcola Preventivo", + "BTN_SHOP": "Vai allo shop", + "BTN_CONTACT": "Parla con noi", + "SEC_CALC_TITLE": "Preventivo immediato in pochi secondi", + "SEC_CALC_SUBTITLE": "Nessuna registrazione necessaria. Non salviamo il tuo file fino al momento dell'ordine e la stima è effettuata tramite vero slicing.", + "SEC_CALC_LIST_1": "Formati supportati: STL, 3MF, STEP, OBJ", + "SEC_CALC_LIST_2": "Qualità: bozza, standard, alta definizione", + "CARD_CALC_EYEBROW": "Calcolo automatico", + "CARD_CALC_TITLE": "Prezzo e tempi in un click", + "CARD_CALC_TAG": "Senza registrazione", + "CARD_CALC_STEP_1": "Carica il file 3D", + "CARD_CALC_STEP_2": "Scegli materiale e qualità", + "CARD_CALC_STEP_3": "Ricevi subito costo e tempo", + "BTN_OPEN_CALC": "Apri calcolatore", + "SEC_CAP_TITLE": "Cosa puoi ottenere", + "SEC_CAP_SUBTITLE": "Produzione su misura per prototipi, piccole serie e pezzi personalizzati.", + "CAP_1_TITLE": "Prototipazione veloce", + "CAP_1_TEXT": "Dal file digitale al modello fisico in 24/48 ore. Verifica ergonomia, incastri e funzionamento prima dello stampo definitivo.", + "CAP_2_TITLE": "Pezzi personalizzati", + "CAP_2_TEXT": "Componenti unici impossibili da trovare in commercio. Riproduciamo parti rotte o creiamo adattamenti su misura.", + "CAP_3_TITLE": "Piccole serie", + "CAP_3_TEXT": "Produzione ponte o serie limitate (10-500 pezzi) con qualità ripetibile. Ideale per validazione di mercato.", + "CAP_4_TITLE": "Consulenza e CAD", + "CAP_4_TEXT": "Non hai il file 3D? Ti aiutiamo a progettarlo, ottimizzarlo per la stampa e scegliere il materiale giusto.", + "SEC_SHOP_TITLE": "Shop di soluzioni tecniche pronte", + "SEC_SHOP_TEXT": "Prodotti selezionati, testati in laboratorio e pronti all'uso. Risolvono problemi reali con funzionalità concrete.", + "SEC_SHOP_LIST_1": "Accessori funzionali per officine e laboratori", + "SEC_SHOP_LIST_2": "Ricambi e componenti difficili da reperire", + "SEC_SHOP_LIST_3": "Supporti e organizzatori per migliorare i flussi di lavoro", + "BTN_DISCOVER": "Scopri i prodotti", + "BTN_REQ_SOLUTION": "Richiedi una soluzione", + "CARD_SHOP_1_TITLE": "Best seller tecnici", + "CARD_SHOP_1_TEXT": "Soluzioni provate sul campo e già pronte alla spedizione.", + "CARD_SHOP_2_TITLE": "Kit pronti all'uso", + "CARD_SHOP_2_TEXT": "Componenti compatibili e facili da montare senza sorprese.", + "CARD_SHOP_3_TITLE": "Su richiesta", + "CARD_SHOP_3_TEXT": "Non trovi quello che serve? Lo progettiamo e lo produciamo per te.", + "SEC_ABOUT_TITLE": "Su di noi", + "SEC_ABOUT_TEXT": "3D fab è un laboratorio tecnico di stampa 3D. Seguiamo progetti dalla consulenza iniziale alla produzione, con tempi chiari e supporto diretto.", + "FOUNDERS_PHOTO": "Foto Founders" + }, "CALC": { "TITLE": "Calcola Preventivo 3D", "SUBTITLE": "Carica il tuo file 3D (STL, 3MF, STEP...) e ricevi una stima immediata di costi e tempi di stampa.", @@ -47,13 +93,53 @@ "BENEFITS_1": "Preventivo automatico con costo e tempo immediati", "BENEFITS_2": "Materiali selezionati e qualità controllata", "BENEFITS_3": "Consulenza CAD se il file ha bisogno di modifiche", - "ERR_FILE_REQUIRED": "Il file è obbligatorio." + "ERR_FILE_REQUIRED": "Il file è obbligatorio.", + "ANALYZING_TITLE": "Analisi in corso...", + "ANALYZING_TEXT": "Stiamo analizzando la geometria e calcolando il percorso utensile.", + "QTY_SHORT": "QTÀ", + "COLOR_LABEL": "COLORE", + "ADD_FILES": "Aggiungi file", + "UPLOADING": "Caricamento...", + "PROCESSING": "Elaborazione...", + "NOTES_PLACEHOLDER": "Istruzioni specifiche...", + "SETUP_NOTE": "* Include {{cost}} Costo di Setup" + }, + "QUOTE": { + "PROCEED_ORDER": "Procedi con l'ordine", + "CONSULT": "Richiedi Consulenza", + "TOTAL": "Totale" + }, + "USER_DETAILS": { + "TITLE": "I tuoi dati", + "NAME": "Nome", + "NAME_PLACEHOLDER": "Il tuo nome", + "SURNAME": "Cognome", + "SURNAME_PLACEHOLDER": "Il tuo cognome", + "EMAIL": "Email", + "EMAIL_PLACEHOLDER": "tua@email.com", + "PHONE": "Telefono", + "PHONE_PLACEHOLDER": "+41 ...", + "ADDRESS": "Indirizzo", + "ADDRESS_PLACEHOLDER": "Via e numero", + "ZIP": "CAP", + "ZIP_PLACEHOLDER": "0000", + "CITY": "Città", + "CITY_PLACEHOLDER": "Città", + "SUBMIT": "Procedi", + "SUMMARY_TITLE": "Riepilogo" + }, + "COMMON": { + "REQUIRED": "Campo obbligatorio", + "INVALID_EMAIL": "Email non valida", + "BACK": "Indietro", + "OPTIONAL": "(Opzionale)" }, "SHOP": { "TITLE": "Soluzioni tecniche", "SUBTITLE": "Prodotti pronti che risolvono problemi pratici", "ADD_CART": "Aggiungi al Carrello", - "BACK": "Torna allo Shop" + "BACK": "Torna allo Shop", + "NOT_FOUND": "Prodotto non trovato." }, "ABOUT": { "TITLE": "Chi Siamo", @@ -126,7 +212,10 @@ "ERR_MAX_FILES": "Limite massimo di 15 file raggiunto.", "SUCCESS_TITLE": "Messaggio Inviato con Successo", "SUCCESS_DESC": "Grazie per averci contattato. Abbiamo ricevuto il tuo messaggio e ti invieremo una email di riepilogo a breve.", - "SEND_ANOTHER": "Invia un altro messaggio" + "SEND_ANOTHER": "Invia un altro messaggio", + "HERO_SUBTITLE": "Siamo qui per aiutarti. Compila il modulo sottostante per qualsiasi richiesta.", + "FILE_TYPE_PDF": "PDF", + "FILE_TYPE_3D": "3D" }, "CHECKOUT": { "TITLE": "Checkout", @@ -152,17 +241,23 @@ "SETUP_FEE": "Costo di Avvio", "TOTAL": "Totale", "QTY": "Qtà", - "SHIPPING": "Spedizione" + "SHIPPING": "Spedizione", + "INVALID_EMAIL": "Email non valida", + "COMPANY_OPTIONAL": "Nome Azienda (Opzionale)", + "REF_PERSON_OPTIONAL": "Persona di Riferimento (Opzionale)" }, "PAYMENT": { "TITLE": "Pagamento", "METHOD": "Metodo di Pagamento", "TWINT_TITLE": "Paga con TWINT", "TWINT_DESC": "Inquadra il codice con l'app TWINT", + "TWINT_OPEN": "Apri direttamente in TWINT", + "TWINT_LINK": "Apri link di pagamento", "BANK_TITLE": "Bonifico Bancario", "BANK_OWNER": "Titolare", "BANK_IBAN": "IBAN", "BANK_REF": "Riferimento", + "BILLING_INFO_HINT": "Aggiungi le informazioni uguali a quelle della fatturazione.", "DOWNLOAD_QR": "Scarica QR-Fattura (PDF)", "CONFIRM": "Conferma Ordine", "SUMMARY_TITLE": "Riepilogo Ordine", @@ -170,6 +265,18 @@ "SHIPPING": "Spedizione", "SETUP_FEE": "Costo Setup", "TOTAL": "Totale", - "LOADING": "Caricamento dettagli ordine..." + "LOADING": "Caricamento dettagli ordine...", + "METHOD_TWINT": "TWINT", + "METHOD_BANK": "Fattura QR / Bonifico" + }, + "ORDER_CONFIRMED": { + "TITLE": "Ordine Confermato", + "SUBTITLE": "Pagamento registrato. Il tuo ordine è ora in elaborazione.", + "STATUS": "In elaborazione", + "HEADING": "Stiamo preparando il tuo ordine", + "ORDER_REF": "Riferimento ordine", + "PROCESSING_TEXT": "Non appena confermiamo il pagamento, il tuo ordine passerà in produzione.", + "EMAIL_TEXT": "Ti invieremo una email con aggiornamento stato e prossimi step.", + "BACK_HOME": "Torna alla Home" } }