From 52801bebd19bfd3743cf42d22426c35df448a518 Mon Sep 17 00:00:00 2001 From: Marco Pedone Date: Fri, 20 Feb 2026 11:36:47 +0100 Subject: [PATCH] feat: refactor payment processing and checkout flow with Stripe integration - Removed AcquistoProcessing component and replaced it with a new CheckoutForm component for better separation of concerns. - Added new payment checkout page to handle payment intents and display payment status. - Updated server-side logic to create payment intents and retrieve payment data for checkout. - Enhanced error handling and user feedback during payment processing. - Introduced new utility functions for fetching payment data and managing Stripe integration. --- .../src/components/acquisto_processing.tsx | 243 ------------------ apps/infoalloggi/src/components/checkout.tsx | 187 ++++++++++++++ .../src/components/tables/payments-table.tsx | 1 + apps/infoalloggi/src/lib/stripe.ts | 12 + .../src/pages/api/stripe-webhook.ts | 5 +- .../area-riservata/admin/impostazioni.tsx | 8 + .../pages/servizio/acquisto-processing.tsx | 208 --------------- .../pagamento-checkout/[paymentId].tsx | 117 +++++++++ .../src/pages/servizio/pagamento-status.tsx | 185 +++++++++++++ .../pages/servizio/pagamento/[ordineId].tsx | 81 +++++- .../src/server/api/routers/pagamenti.ts | 8 + .../server/controllers/servizio.controller.ts | 20 ++ .../server/controllers/stripe.controller.ts | 14 +- 13 files changed, 625 insertions(+), 464 deletions(-) delete mode 100644 apps/infoalloggi/src/components/acquisto_processing.tsx create mode 100644 apps/infoalloggi/src/components/checkout.tsx create mode 100644 apps/infoalloggi/src/lib/stripe.ts delete mode 100644 apps/infoalloggi/src/pages/servizio/acquisto-processing.tsx create mode 100644 apps/infoalloggi/src/pages/servizio/pagamento-checkout/[paymentId].tsx create mode 100644 apps/infoalloggi/src/pages/servizio/pagamento-status.tsx diff --git a/apps/infoalloggi/src/components/acquisto_processing.tsx b/apps/infoalloggi/src/components/acquisto_processing.tsx deleted file mode 100644 index 4f21295..0000000 --- a/apps/infoalloggi/src/components/acquisto_processing.tsx +++ /dev/null @@ -1,243 +0,0 @@ -"use client"; -import { - Elements, - PaymentElement, - useElements, - useStripe, -} from "@stripe/react-stripe-js"; -import type { Stripe } from "@stripe/stripe-js"; -import { ArrowRight, ExternalLink } from "lucide-react"; -import Link from "next/link"; -import { type FormEvent, useState } from "react"; -import LoadingButton from "~/components/custom_ui/loading-button"; -import { LoadingPage } from "~/components/loading"; -import { Status500 } from "~/components/status-page"; -import { Button } from "~/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; -import { env } from "~/env"; -import { formatCurrency } from "~/lib/utils"; -import { useTranslation } from "~/providers/I18nProvider"; -import type { PaymentsId } from "~/schemas/public/Payments"; -import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario"; -import { api } from "~/utils/api"; - -type AcquistoProcessingProps = { - paymentId: PaymentsId; - packId: PrezziarioIdprezziario; - stripePromise: Promise; -}; - -export const AcquistoProcessing = ({ - paymentId, - packId, - stripePromise, -}: AcquistoProcessingProps) => { - const [stripeIntent, setStripeIntent] = useState<{ - clientSecret: string | null; - titolo: string; - prezzo: number; - descrizione: string; - } | null>(null); - const { t } = useTranslation(); - const [generatedIntent, setGeneratedIntent] = useState(false); - - const { mutate, isPending } = api.stripe.createIntent.useMutation({ - onSuccess: (data) => { - setStripeIntent(data); - }, - }); - - if (generatedIntent && !isPending && !stripeIntent) { - return ; - } - - return ( -
-

- {t.acquisto.titolo} -

- {(() => { - if (!generatedIntent) { - return ( - { - setGeneratedIntent(true); - mutate({ - paymentId, - }); - }} - packId={packId} - /> - ); - } - if (isPending) { - return ; - } - if (stripeIntent?.clientSecret) { - return ( - <> - - - - - ); - } - })()} -
- ); -}; - -const PricePreparation = ({ - packId, - onProcedi, -}: { - packId: PrezziarioIdprezziario; - onProcedi: () => void; -}) => { - const { data: prezziario, isLoading } = - api.prezziario.getPrezzoPerServizio.useQuery({ - idprezziario: packId, - }); - const { t } = useTranslation(); - return ( -
- {isLoading || !prezziario ? ( - <> -

{t.acquisto.preparazione}

- - - ) : ( -
-
-

{prezziario.nome_it}

-

- Cod: “{prezziario.idprezziario}” -

-

- {formatCurrency(prezziario.prezzo_cent / 100)} -

-

{prezziario.desc_it}

-
- - {prezziario.testo_condizioni && ( - - - - )} - - -
- )} -
- ); -}; - -const CheckoutForm = ({ - titolo, - prezzo, - descrizione, -}: { - titolo: string; - prezzo: number; - descrizione: string; -}) => { - const stripe = useStripe(); - const elements = useElements(); - - const [message, setMessage] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - const { t } = useTranslation(); - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - console.log("submitting payment"); - if (!stripe || !elements) { - return; - } - - setIsLoading(true); - const { error } = await stripe.confirmPayment({ - confirmParams: { - return_url: `${env.NEXT_PUBLIC_BASE_URL}/servizio/acquisto-processing`, - }, - elements, - }); - if (error.type === "card_error" || error.type === "validation_error") { - setMessage(error.message || "An unexpected error occurred."); - } else { - setMessage("An unexpected error occurred."); - } - setIsLoading(false); - }; - - return ( - - - -

{titolo}

-
-

{descrizione}

- - - {formatCurrency(prezzo / 100)} - {" "} - - {t.pricing_cmp.risultati_iva} - - -
- -
- - - - {t.procedi} - - {message &&
{message}
} - -
-
- ); -}; diff --git a/apps/infoalloggi/src/components/checkout.tsx b/apps/infoalloggi/src/components/checkout.tsx new file mode 100644 index 0000000..3ccb6a4 --- /dev/null +++ b/apps/infoalloggi/src/components/checkout.tsx @@ -0,0 +1,187 @@ +import { + Elements, + PaymentElement, + useElements, + useStripe, +} from "@stripe/react-stripe-js"; +import { loadStripe } from "@stripe/stripe-js"; +import { type FormEvent, useState } from "react"; +import { env } from "~/env"; +import { formatCurrency } from "~/lib/utils"; +import { useTranslation } from "~/providers/I18nProvider"; +import LoadingButton from "./custom_ui/loading-button"; +import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; + +const stripePromise = loadStripe(env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY); + +export default function CheckoutForm({ + clientSecret, + titolo, + prezzo, + descrizione, +}: { + clientSecret: string; + titolo: string; + prezzo: number; + descrizione: string; +}) { + return ( + + + + ); +} + +const PaymentForm = ({ + titolo, + prezzo, + descrizione, +}: { + titolo: string; + prezzo: number; + descrizione: string; +}) => { + const stripe = useStripe(); + const elements = useElements(); + + const [message, setMessage] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const { t } = useTranslation(); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + console.log("submitting payment"); + if (!stripe || !elements) { + return; + } + + setIsLoading(true); + const { error } = await stripe.confirmPayment({ + confirmParams: { + return_url: `${env.NEXT_PUBLIC_BASE_URL}/servizio/pagamento-status`, + }, + elements, + }); + if (error.type === "card_error" || error.type === "validation_error") { + setMessage(error.message || "An unexpected error occurred."); + } else { + setMessage("An unexpected error occurred."); + } + setIsLoading(false); + }; + + return ( + + + +

{titolo}

+
+

{descrizione}

+ + + {formatCurrency(prezzo / 100)} + {" "} + + {t.pricing_cmp.risultati_iva} + + +
+ +
+ + + + {t.procedi} + + {message &&
{message}
} + +
+
+ ); +}; +/* +function PaymentForm2() { + const stripe = useStripe(); + const elements = useElements(); + + const [message, setMessage] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!stripe || !elements) { + // Stripe.js hasn't yet loaded. + // Make sure to disable form submission until Stripe.js has loaded. + return; + } + + setIsLoading(true); + + const { error } = await stripe.confirmPayment({ + elements, + confirmParams: { + // Make sure to change this to your payment completion page + return_url: "http://localhost:3000/success", + }, + }); + + // This point will only be reached if there is an immediate error when + // confirming the payment. Otherwise, your customer will be redirected to + // your `return_url`. For some payment methods like iDEAL, your customer will + // be redirected to an intermediate site first to authorize the payment, then + // redirected to the `return_url`. + if (error.type === "card_error" || error.type === "validation_error") { + setMessage(error.message || "Validation error occurred."); + } else { + setMessage("An unexpected error occurred."); + } + + setIsLoading(false); + }; + + return ( +
+ + + + {message &&
{message}
} + + ); +} +*/ diff --git a/apps/infoalloggi/src/components/tables/payments-table.tsx b/apps/infoalloggi/src/components/tables/payments-table.tsx index 1930dba..d8793ea 100644 --- a/apps/infoalloggi/src/components/tables/payments-table.tsx +++ b/apps/infoalloggi/src/components/tables/payments-table.tsx @@ -153,6 +153,7 @@ export const PaymentsTable = ({ { cell: ({ row }) => { const data = row.original; + if (!isAdmin) return null; return ( diff --git a/apps/infoalloggi/src/lib/stripe.ts b/apps/infoalloggi/src/lib/stripe.ts new file mode 100644 index 0000000..2e6325e --- /dev/null +++ b/apps/infoalloggi/src/lib/stripe.ts @@ -0,0 +1,12 @@ +// lib/stripe.js +import Stripe from "stripe"; +import { env } from "~/env"; + +// This check ensures we don't accidentally run this on the client +if (typeof window !== "undefined") { + throw new Error("Stripe Secret Key should not be loaded on the client!"); +} + +const stripe = new Stripe(env.STRIPE_SECRET_KEY); + +export default stripe; diff --git a/apps/infoalloggi/src/pages/api/stripe-webhook.ts b/apps/infoalloggi/src/pages/api/stripe-webhook.ts index 29af077..86c7f23 100644 --- a/apps/infoalloggi/src/pages/api/stripe-webhook.ts +++ b/apps/infoalloggi/src/pages/api/stripe-webhook.ts @@ -1,6 +1,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import Stripe from "stripe"; +import type Stripe from "stripe"; import { env } from "~/env"; +import stripe from "~/lib/stripe"; import type { PaymentsId } from "~/schemas/public/Payments"; import { appRouter } from "~/server/api/root"; import { createTRPCContext, t } from "~/server/api/trpc"; @@ -9,8 +10,6 @@ const handler = async ( req: NextApiRequest, res: NextApiResponse, ): Promise => { - const stripe = new Stripe(env.STRIPE_SECRET_KEY); - if (req.method === "POST") { const ctx = await createTRPCContext({ req: req, res: res }); const createCaller = t.createCallerFactory(appRouter); diff --git a/apps/infoalloggi/src/pages/area-riservata/admin/impostazioni.tsx b/apps/infoalloggi/src/pages/area-riservata/admin/impostazioni.tsx index cd1e4fc..f143f5b 100644 --- a/apps/infoalloggi/src/pages/area-riservata/admin/impostazioni.tsx +++ b/apps/infoalloggi/src/pages/area-riservata/admin/impostazioni.tsx @@ -47,6 +47,14 @@ const StripeSection = () => {

Stripe Status: {data?.success ? "Sano" : "Non sano"}

+

Keys:

+

+ Secret Key: {data?.secretKey} +

+

+ Public Key: {data?.publicKey} +

+

Endpoint Webhook:

{data?.endpoints?.data.map((endpoint) => (
diff --git a/apps/infoalloggi/src/pages/servizio/acquisto-processing.tsx b/apps/infoalloggi/src/pages/servizio/acquisto-processing.tsx deleted file mode 100644 index 2b0c736..0000000 --- a/apps/infoalloggi/src/pages/servizio/acquisto-processing.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import { Elements, useStripe } from "@stripe/react-stripe-js"; -import { loadStripe } from "@stripe/stripe-js"; -import { ArrowLeft } from "lucide-react"; -import type { GetServerSideProps } from "next"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; -import PaymentStatus from "~/components/payment_status"; -import { ProgressRedirect } from "~/components/progress_redirect"; -import { Button, buttonVariants } from "~/components/ui/button"; -import { env } from "~/env"; -import { cn } from "~/lib/utils"; -import { useTranslation } from "~/providers/I18nProvider"; -import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum"; -import { api } from "~/utils/api"; - -type AcquistoProcessingProps = { - client_secret: string; -}; - -export const getServerSideProps = (async (context) => { - if ( - !context.query.payment_intent_client_secret || - typeof context.query.payment_intent_client_secret !== "string" - ) { - return { - redirect: { - destination: "/500", - permanent: false, - statusCode: 500, - }, - }; - } - - return { - props: { - client_secret: context.query.payment_intent_client_secret, - }, - }; -}) satisfies GetServerSideProps; - -export default function AcquistoProcessing({ - client_secret, -}: AcquistoProcessingProps) { - const { t } = useTranslation(); - - const stripePromise = loadStripe(env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY); - - return ( -
-

- {t.acquisto_elaborazione.titolo} -

- -
- - - -
-
- ); -} -const CompletePage = ({ secret }: { secret: string }) => { - const stripe = useStripe(); - const utils = api.useUtils(); - const router = useRouter(); - const { t } = useTranslation(); - const [status, setStatus] = useState< - "success" | "processing" | "failed" | "internal_error" | "not_found" - >("processing"); - - useEffect(() => { - const fetchPaymentIntent = async () => { - if (!stripe) { - return; - } - - try { - const { paymentIntent } = await stripe.retrievePaymentIntent(secret); - if (!paymentIntent) { - setStatus("internal_error"); - return; - } - const payment = await utils.pagamenti.getPaymentFromIntent.fetch({ - pIntentId: paymentIntent.id, - }); - - if (!payment) { - setStatus("not_found"); - return; - } - - if (paymentIntent.status === "succeeded") { - if (payment.paymentstatus !== PaymentStatusEnum.success) { - setStatus("internal_error"); - } - setStatus("success"); - } else if (paymentIntent.status === "processing") { - if (payment.paymentstatus !== PaymentStatusEnum.processing) { - setStatus("internal_error"); - } - setStatus("processing"); - } else { - setStatus("failed"); - } - } catch (error) { - console.error("Error retrieving payment intent:", error); - } - }; - void fetchPaymentIntent(); - }, [stripe, secret]); - - if (!status) { - return null; - } - - return ( -
- {status === "success" && ( - <> - - - - - )} - {status === "processing" && ( - <> - - - {t.acquisto_elaborazione.to_dashboard} - - - )} - {status === "failed" && ( - <> - - - {t.acquisto_elaborazione.to_chat} - - - )} - {status === "internal_error" && ( - <> - - - {t.acquisto_elaborazione.to_chat} - - - )} - {status === "not_found" && ( - <> - - - - {t.acquisto_elaborazione.to_chat} - - - )} -
- ); -}; diff --git a/apps/infoalloggi/src/pages/servizio/pagamento-checkout/[paymentId].tsx b/apps/infoalloggi/src/pages/servizio/pagamento-checkout/[paymentId].tsx new file mode 100644 index 0000000..ce4e8a7 --- /dev/null +++ b/apps/infoalloggi/src/pages/servizio/pagamento-checkout/[paymentId].tsx @@ -0,0 +1,117 @@ +import type { GetServerSideProps } from "next"; +import CheckoutForm from "~/components/checkout"; +import { AreaRiservataLayout } from "~/components/Layout"; +import stripe from "~/lib/stripe"; +import type { NextPageWithLayout } from "~/pages/_app"; +import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; +import { zPagamentoId } from "~/server/utils/zod_types"; + +type PagamentoPageProps = { + clientSecret: string | null; + titolo: string; + prezzo: number; + descrizione: string; +}; + +const PagamentoPage: NextPageWithLayout = ({ + clientSecret, + titolo, + prezzo, + descrizione, +}: PagamentoPageProps) => { + if (!clientSecret) { + return
Errore nella creazione del pagamento. Riprova più tardi.
; + } + return ( +
+ +
+ ); +}; + +PagamentoPage.getLayout = function getLayout(page) { + return {page}; +}; + +export default PagamentoPage; + +export const getServerSideProps = (async (context) => { + const id = context.params?.paymentId; + console.log("Received paymentId:", id); + if (typeof id !== "string") { + console.error("Error: paymentId is not a string"); + return { + redirect: { + destination: "/500", + permanent: false, + }, + }; + } + + const parsed = zPagamentoId.safeParse(id); + if (!parsed.success) { + console.error("Error parsing paymentId", parsed.error); + return { + redirect: { + destination: "/500", + permanent: false, + }, + }; + } + + const access_token = context.req.cookies.access_token; + + const helper = await TrpcAuthedFetchingIstance({ access_token }); + if (helper) { + const paymentData = + await helper.trpc.pagamenti.getPagamentoDataForCheckout.fetch({ + paymentId: parsed.data, + }); + + const { client_secret: clientSecret } = await stripe.paymentIntents.create({ + amount: paymentData.amount_cent, + + automatic_payment_methods: { + enabled: true, + }, + currency: "eur", + description: paymentData.nome_it, + metadata: { + paymentId: paymentData.id, + servizioId: paymentData.servizio_id, + userId: paymentData.userid, + }, + receipt_email: paymentData.email, + }); + console.log("Created payment intent with client secret:", clientSecret); + + if (!clientSecret) { + return { + redirect: { + destination: "/500", + permanent: false, + }, + }; + } + + return { + props: { + clientSecret, + titolo: paymentData.nome_it, + prezzo: paymentData.amount_cent, + descrizione: paymentData.desc_it, + }, + }; + } + return { + redirect: { + destination: "/500", + permanent: false, + }, + }; +}) satisfies GetServerSideProps; diff --git a/apps/infoalloggi/src/pages/servizio/pagamento-status.tsx b/apps/infoalloggi/src/pages/servizio/pagamento-status.tsx new file mode 100644 index 0000000..9c0154e --- /dev/null +++ b/apps/infoalloggi/src/pages/servizio/pagamento-status.tsx @@ -0,0 +1,185 @@ +import { ArrowLeft } from "lucide-react"; +import type { GetServerSideProps } from "next"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import PaymentStatus from "~/components/payment_status"; +import { ProgressRedirect } from "~/components/progress_redirect"; +import { Button, buttonVariants } from "~/components/ui/button"; +import stripe from "~/lib/stripe"; +import { cn } from "~/lib/utils"; +import { useTranslation } from "~/providers/I18nProvider"; +import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum"; +import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; + +enum AcquistoStatusEnum { + success = "success", + processing = "processing", + failed = "failed", + internal_error = "internal_error", + not_found = "not_found", +} + +type AcquistoProcessingProps = { + status: AcquistoStatusEnum; +}; + +export const getServerSideProps = (async (context) => { + const paymentIntentId = context.query?.payment_intent; + if (!paymentIntentId || typeof paymentIntentId !== "string") { + return { + redirect: { + destination: "/500", + permanent: false, + }, + }; + } + + const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId); + if (!paymentIntent) { + return { + props: { + status: AcquistoStatusEnum.internal_error, + }, + }; + } + + const access_token = context.req.cookies.access_token; + + const helper = await TrpcAuthedFetchingIstance({ access_token }); + if (helper) { + const payment = await helper.trpc.pagamenti.getPaymentFromIntent.fetch({ + pIntentId: paymentIntent.id, + }); + + if (!payment) { + return { + props: { + status: AcquistoStatusEnum.not_found, + }, + }; + } + + let resultStatus: AcquistoStatusEnum; + if (paymentIntent.status === "succeeded") { + if (payment.paymentstatus !== PaymentStatusEnum.success) { + resultStatus = AcquistoStatusEnum.internal_error; + } + resultStatus = AcquistoStatusEnum.success; + } else if (paymentIntent.status === "processing") { + if (payment.paymentstatus !== PaymentStatusEnum.processing) { + resultStatus = AcquistoStatusEnum.internal_error; + } + resultStatus = AcquistoStatusEnum.processing; + } else { + resultStatus = AcquistoStatusEnum.failed; + } + return { + props: { + status: resultStatus, + }, + }; + } + + return { + redirect: { + destination: "/500", + permanent: false, + }, + }; +}) satisfies GetServerSideProps; + +export default function AcquistoProcessing({ + status, +}: AcquistoProcessingProps) { + const { t } = useTranslation(); + const router = useRouter(); + return ( +
+

+ {t.acquisto_elaborazione.titolo} +

+ +
+ {status === AcquistoStatusEnum.success && ( + <> + + + + + )} + {status === AcquistoStatusEnum.processing && ( + <> + + + {t.acquisto_elaborazione.to_dashboard} + + + )} + {status === AcquistoStatusEnum.failed && ( + <> + + + {t.acquisto_elaborazione.to_chat} + + + )} + {status === AcquistoStatusEnum.internal_error && ( + <> + + + {t.acquisto_elaborazione.to_chat} + + + )} + {status === AcquistoStatusEnum.not_found && ( + <> + + + + {t.acquisto_elaborazione.to_chat} + + + )} +
+
+ ); +} diff --git a/apps/infoalloggi/src/pages/servizio/pagamento/[ordineId].tsx b/apps/infoalloggi/src/pages/servizio/pagamento/[ordineId].tsx index 61ae974..4674f1c 100644 --- a/apps/infoalloggi/src/pages/servizio/pagamento/[ordineId].tsx +++ b/apps/infoalloggi/src/pages/servizio/pagamento/[ordineId].tsx @@ -1,12 +1,16 @@ -import { loadStripe } from "@stripe/stripe-js"; +import { ArrowRight, ExternalLink } from "lucide-react"; import type { GetServerSideProps } from "next"; -import { AcquistoProcessing } from "~/components/acquisto_processing"; +import Link from "next/link"; +import { useRouter } from "next/router"; import { AreaRiservataLayout } from "~/components/Layout"; import { LoadingPage } from "~/components/loading"; import { Status500 } from "~/components/status-page"; -import { env } from "~/env"; +import { Button } from "~/components/ui/button"; +import { formatCurrency } from "~/lib/utils"; import type { NextPageWithLayout } from "~/pages/_app"; +import { useTranslation } from "~/providers/I18nProvider"; import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; +import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario"; import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; import { zOrdineId } from "~/server/utils/zod_types"; import { api } from "~/utils/api"; @@ -18,12 +22,11 @@ type ServizioPurchaseProps = { const ServicePurchasePage: NextPageWithLayout = ({ ordineId, }: ServizioPurchaseProps) => { + const router = useRouter(); const { data, isLoading } = api.servizio.getServizioPaymentData.useQuery({ ordineId, }); - const stripePromise = loadStripe(env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY); - if (isLoading) { return ; } @@ -32,15 +35,77 @@ const ServicePurchasePage: NextPageWithLayout = ({ } return (
- + await router.push(`/servizio/pagamento-checkout/${data.payment.id}`) + } packId={data.ordine.packid} - paymentId={data.payment.id} - stripePromise={stripePromise} />
); }; +const PricePreparation = ({ + packId, + onProcedi, +}: { + packId: PrezziarioIdprezziario; + onProcedi: () => void; +}) => { + const { data: prezziario, isLoading } = + api.prezziario.getPrezzoPerServizio.useQuery({ + idprezziario: packId, + }); + const { t } = useTranslation(); + return ( +
+ {isLoading || !prezziario ? ( + <> +

{t.acquisto.preparazione}

+ + + ) : ( +
+
+

{prezziario.nome_it}

+

+ Cod: “{prezziario.idprezziario}” +

+

+ {formatCurrency(prezziario.prezzo_cent / 100)} +

+

{prezziario.desc_it}

+
+ + {prezziario.testo_condizioni && ( + + + + )} + + +
+ )} +
+ ); +}; + ServicePurchasePage.getLayout = function getLayout(page) { return {page}; }; diff --git a/apps/infoalloggi/src/server/api/routers/pagamenti.ts b/apps/infoalloggi/src/server/api/routers/pagamenti.ts index 8d25679..fd0f4a4 100644 --- a/apps/infoalloggi/src/server/api/routers/pagamenti.ts +++ b/apps/infoalloggi/src/server/api/routers/pagamenti.ts @@ -1,7 +1,9 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod/v4"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; +import { getPagamentoDataForCheckout } from "~/server/controllers/servizio.controller"; import { db } from "~/server/db"; +import { zPagamentoId } from "~/server/utils/zod_types"; // Create a new ratelimiter, that allows 10 requests per 10 seconds /* const ratelimit = new RateLimiterHandler({ @@ -30,4 +32,10 @@ export const pagamentiRouter = createTRPCRouter({ }); } }), + + getPagamentoDataForCheckout: protectedProcedure + .input(z.object({ paymentId: zPagamentoId })) + .query(async ({ input }) => { + return await getPagamentoDataForCheckout(input.paymentId); + }), }); diff --git a/apps/infoalloggi/src/server/controllers/servizio.controller.ts b/apps/infoalloggi/src/server/controllers/servizio.controller.ts index d7c44c3..7d19f16 100644 --- a/apps/infoalloggi/src/server/controllers/servizio.controller.ts +++ b/apps/infoalloggi/src/server/controllers/servizio.controller.ts @@ -1535,6 +1535,26 @@ export const getServizioPagamentoData = async (ordineId: OrdiniOrdineId) => { } }; +export const getPagamentoDataForCheckout = async (paymentId: PaymentsId) => { + try { + return await db + .selectFrom("payments") + .selectAll("payments") + .innerJoin("users", "users.id", "payments.userid") + .select("users.email") + .innerJoin("ordini", "ordini.ordine_id", "payments.ordine_id") + .innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid") + .select(["prezziario.nome_it", "prezziario.desc_it"]) + .where("payments.id", "=", paymentId) + .executeTakeFirstOrThrow(); + } catch (e) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Payment not found: ${(e as Error).message}`, + }); + } +}; + type Pagamento = Payments & Pick; export const getAllPagamenti = async (): Promise => { try { diff --git a/apps/infoalloggi/src/server/controllers/stripe.controller.ts b/apps/infoalloggi/src/server/controllers/stripe.controller.ts index 6ac8989..94e3232 100644 --- a/apps/infoalloggi/src/server/controllers/stripe.controller.ts +++ b/apps/infoalloggi/src/server/controllers/stripe.controller.ts @@ -14,10 +14,20 @@ const stripeApi = new Stripe(env.STRIPE_SECRET_KEY); export const stripeHealthCheck = async () => { try { const endpoints = await stripeApi.webhookEndpoints.list(); - return { success: true, endpoints }; + return { + success: true, + endpoints, + secretKey: env.STRIPE_SECRET_KEY, + publicKey: env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY, + }; } catch (e) { console.error("Stripe health check failed:", e); - return { success: false, endpoints: null }; + return { + success: false, + endpoints: null, + secretKey: env.STRIPE_SECRET_KEY, + publicKey: env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY, + }; } };