feat: implement saldo preview functionality in payment process and update receipt generation

This commit is contained in:
Marco Pedone 2026-03-27 20:23:21 +01:00
parent a6e4ace3b6
commit 18ff13a64d
6 changed files with 145 additions and 27 deletions

View file

@ -1,8 +1,5 @@
TODOS: TODOS:
- Log email fallite e retry
NEW IDEA TODOS:
Minimal Viable Product:
AFTER MVP: AFTER MVP:
- TODO migrazione app router https://nextjs.org/docs/pages/building-your-application/upgrading/app-router-migration#migrating-from-pages-to-app - TODO migrazione app router https://nextjs.org/docs/pages/building-your-application/upgrading/app-router-migration#migrating-from-pages-to-app

View file

@ -9,6 +9,7 @@ import { Card } from "~/components/ui/card";
import { PaymentMethodToString, type PaymentType } from "~/i18n/stripe"; import { PaymentMethodToString, type PaymentType } from "~/i18n/stripe";
import { formatCurrency } from "~/lib/utils"; import { formatCurrency } from "~/lib/utils";
import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
import type { Prezziario } from "~/schemas/public/Prezziario";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
export function ReceiptGenerator({ export function ReceiptGenerator({
@ -83,6 +84,7 @@ export type PurchaseData = {
sconto: number; sconto: number;
amount_cent: number; amount_cent: number;
previewSaldo: Prezziario | null;
}; };
interface ReceiptProps { interface ReceiptProps {
@ -149,17 +151,17 @@ function Receipt({ data }: ReceiptProps) {
</div> </div>
</div> </div>
<div> <div>
<table className="w-full"> <table className="w-full table-auto">
<thead> <thead className="flex w-full">
<tr className="border-b"> <tr className="grid w-full grow grid-cols-[1fr_auto_auto] gap-2 border-b">
<th className="py-2 text-left">Descrizione</th> <th className="py-2 text-left">Oggetto</th>
<th className="py-2 text-right">Sconto applicato</th> <th className="py-2 text-right">Sconto applicato</th>
<th className="py-2 text-right">Importo</th> <th className="py-2 text-right">Importo</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr className="border-b"> <tr className="grid w-full grow grid-cols-[1fr_auto_auto] gap-2 border-b">
<td className="py-2">{data.packName}</td> <td className="py-2">{data.packName}</td>
<td className="py-2 text-right"> <td className="py-2 text-right">
@ -169,6 +171,21 @@ function Receipt({ data }: ReceiptProps) {
{formatCurrency(data.amount_cent / 100)} {formatCurrency(data.amount_cent / 100)}
</td> </td>
</tr> </tr>
{data.previewSaldo && (
<tr className="grid w-full grow grid-cols-[1fr_auto_auto] gap-2 border-b">
<td className="py-2">
{data.previewSaldo.nome_it}{" "}
<span className="text-sm">
pagamento differito, non è dovuto in questo momento
</span>
</td>
<td className="py-2 text-right">&nbsp;</td>
<td className="py-2 text-right text-muted-foreground line-through">
{formatCurrency(data.previewSaldo.prezzo_cent / 100)}
</td>
</tr>
)}
</tbody> </tbody>
</table> </table>
</div> </div>

View file

@ -5,10 +5,19 @@ import { useRouter } from "next/router";
import z from "zod"; import z from "zod";
import { AreaRiservataLayout } from "~/components/Layout"; import { AreaRiservataLayout } from "~/components/Layout";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { formatCurrency, redirectTo500 } from "~/lib/utils"; import { formatCurrency, redirectTo500 } from "~/lib/utils";
import type { NextPageWithLayout } from "~/pages/_app"; import type { NextPageWithLayout } from "~/pages/_app";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
import type { Prezziario } from "~/schemas/public/Prezziario";
import type { PreparedPaymentData } from "~/server/controllers/pagamenti.controller"; import type { PreparedPaymentData } from "~/server/controllers/pagamenti.controller";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
import { zRinnovoId, zServizioId } from "~/server/utils/zod_types"; import { zRinnovoId, zServizioId } from "~/server/utils/zod_types";
@ -16,6 +25,7 @@ import { zRinnovoId, zServizioId } from "~/server/utils/zod_types";
type PreviewPurchaseProps = { type PreviewPurchaseProps = {
stripeDisabled: boolean; stripeDisabled: boolean;
data: PreparedPaymentData; data: PreparedPaymentData;
saldoPreview: Prezziario | null;
}; };
/** /**
* /servizio/pagamento?servizioId=<servizioId>|rinnovoId=<rinnovoId>&type=<OrderTypeEnum> \ * /servizio/pagamento?servizioId=<servizioId>|rinnovoId=<rinnovoId>&type=<OrderTypeEnum> \
@ -24,26 +34,26 @@ type PreviewPurchaseProps = {
const PreviewPurchasePage: NextPageWithLayout<PreviewPurchaseProps> = ({ const PreviewPurchasePage: NextPageWithLayout<PreviewPurchaseProps> = ({
stripeDisabled, stripeDisabled,
data, data,
saldoPreview,
}: PreviewPurchaseProps) => { }: PreviewPurchaseProps) => {
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="mx-auto w-full max-w-6xl grow space-y-4 p-2 sm:px-8 sm:py-4"> <div className="mt-1 flex w-full justify-center p-2 sm:mt-14">
<div className="mx-auto mt-4 max-w-2xl p-2 sm:mt-10"> <Card className="h-fit">
<div className="space-y-8"> <CardHeader>
<div className="space-y-6"> <CardTitle className="text-3xl">{data.packName}</CardTitle>
<p className="text-3xl">{data.packName}</p> <CardDescription>Cod: &ldquo;{data.packid}&rdquo;</CardDescription>
<p className="text-lg">Cod: &ldquo;{data.packid}&rdquo;</p> </CardHeader>
<p className="font-bold text-4xl"> <CardContent className="space-y-3">
{formatCurrency(data.amount_cent / 100)}{" "} <p className="font-bold text-4xl">
{data.sconto > 0 && ( {formatCurrency(data.amount_cent / 100)}{" "}
<span className="text-green-500">(Sconto: {data.sconto}%)</span> {data.sconto > 0 && (
)} <span className="text-green-500">(Sconto: {data.sconto}%)</span>
</p> )}
<p>{data.packDesc}</p> </p>
</div> <p>{data.packDesc}</p>
<Link <Link
aria-label="Leggi le condizioni" aria-label="Leggi le condizioni"
className="block w-fit" className="block w-fit"
@ -56,6 +66,28 @@ const PreviewPurchasePage: NextPageWithLayout<PreviewPurchaseProps> = ({
</Button> </Button>
</Link> </Link>
{saldoPreview && (
<div className="mt-6 space-y-4 rounded-md bg-muted/80 p-2">
<h1 className="font-medium">{saldoPreview.nome_it}</h1>
<div className="flex items-baseline gap-2">
<p className="font-bold text-2xl text-muted-foreground line-through">
{formatCurrency(saldoPreview.prezzo_cent / 100)}
</p>
<p className="font-bold text-4xl text-primary">
{formatCurrency(0)}
</p>
</div>
<span className="font-bold text-primary">
Il Saldo non è dovuto in questo momento, ma al termine della
ricerca.
</span>
<p className="text-muted-foreground text-sm">
Il saldo verrà aggiornato in base ai parametri del servizio.
</p>
</div>
)}
</CardContent>
<CardFooter>
<Button <Button
className="w-full text-lg" className="w-full text-lg"
onClick={async () => onClick={async () =>
@ -68,8 +100,8 @@ const PreviewPurchasePage: NextPageWithLayout<PreviewPurchaseProps> = ({
> >
<span>{t.acquisto.procedi_pagamento}</span> <ArrowRight /> <span>{t.acquisto.procedi_pagamento}</span> <ArrowRight />
</Button> </Button>
</div> </CardFooter>
</div> </Card>
</div> </div>
); );
}; };
@ -125,6 +157,7 @@ export const getServerSideProps = (async (context) => {
props: { props: {
stripeDisabled: flag || false, stripeDisabled: flag || false,
data: result.data, data: result.data,
saldoPreview: null,
}, },
}; };
} }
@ -147,10 +180,21 @@ export const getServerSideProps = (async (context) => {
console.error("Error preparing payment", result.message); console.error("Error preparing payment", result.message);
return redirectTo500; return redirectTo500;
} }
let saldoPreview: Prezziario | null = null;
if (type === OrderTypeEnum.Acconto) {
const saldoResult = await helper.trpc.pagamenti.previewSaldo.fetch({
servizioId: parsedServizioId.data,
});
if (saldoResult.success) {
saldoPreview = saldoResult.pack;
}
}
return { return {
props: { props: {
stripeDisabled: flag || false, stripeDisabled: flag || false,
data: result.data, data: result.data,
saldoPreview,
}, },
}; };
}) satisfies GetServerSideProps<PreviewPurchaseProps>; }) satisfies GetServerSideProps<PreviewPurchaseProps>;

View file

@ -6,6 +6,7 @@ import {
getPagamentoDataForCheckout, getPagamentoDataForCheckout,
type PreparedPaymentData, type PreparedPaymentData,
preparePayment, preparePayment,
previewSaldo,
} from "~/server/controllers/pagamenti.controller"; } from "~/server/controllers/pagamenti.controller";
import { db } from "~/server/db"; import { db } from "~/server/db";
@ -69,6 +70,15 @@ export const pagamentiRouter = createTRPCRouter({
servizioId: input.servizioId, servizioId: input.servizioId,
}); });
}), }),
previewSaldo: protectedProcedure
.input(
z.object({
servizioId: zServizioId,
}),
)
.query(async ({ input }) => {
return await previewSaldo(input.servizioId);
}),
preparePaymentRinnovi: protectedProcedure preparePaymentRinnovi: protectedProcedure
.input( .input(
z.object({ z.object({

View file

@ -130,7 +130,11 @@ export const preparePayment = async (
} }
if (!servizio.isOkSaldo) { if (!servizio.isOkSaldo) {
//find saldo pack based on servizio tipologia and permanenza/budget, if tipologia posizione //find saldo pack based on servizio tipologia and permanenza/budget, if tipologia posizione
const saldo = await SaldoSolver(servizio); const saldo = await SaldoSolver({
tipologia: servizio.tipologia,
permanenza: servizio.permanenza,
budget: servizio.budget,
});
if (!saldo.success) { if (!saldo.success) {
return { return {
success: false, success: false,
@ -592,3 +596,22 @@ export const getPacksPerServizio = async (
}), }),
}; };
}; };
export const previewSaldo = async (
servizioId: ServizioServizioId,
): Promise<{ success: false; message: string } | GetPackResult> => {
const servizio = await getServizioById(servizioId);
if (!servizio) {
return {
success: false,
message: "Servizio non trovato",
};
}
const saldo = await SaldoSolver({
tipologia: servizio.tipologia,
permanenza: servizio.permanenza,
budget: servizio.budget,
});
return saldo;
};

View file

@ -1394,6 +1394,8 @@ export const getOrdineRicevutaData = async ({
"ordini.sconto", "ordini.sconto",
"ordini.created_at", "ordini.created_at",
"ordini.paymentmethod", "ordini.paymentmethod",
"ordini.type",
"ordini.servizio_id",
]) ])
.innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid") .innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid")
.select("prezziario.nome_it") .select("prezziario.nome_it")
@ -1427,6 +1429,30 @@ export const getOrdineRicevutaData = async ({
const parsedComune = parseDBComune(anagrafica.comune_residenza, comuni); const parsedComune = parseDBComune(anagrafica.comune_residenza, comuni);
const parsedNazione = parseDBNazione(anagrafica.nazione_residenza, nazioni); const parsedNazione = parseDBNazione(anagrafica.nazione_residenza, nazioni);
let previewSaldo: Prezziario | null = null;
if (data.type === OrderTypeEnum.Acconto && data.servizio_id) {
const servizio = await getServizioById(data.servizio_id);
if (servizio) {
const saldo = await SaldoSolver({
tipologia: servizio.tipologia,
permanenza: servizio.permanenza,
budget: servizio.budget,
});
if (saldo.success) {
previewSaldo = saldo.pack;
} else {
console.error(
`Errore nel calcolo del saldo per il servizio ${data.servizio_id}: ${saldo.message}, procedura getOrdineRicevutaData`,
);
}
} else {
console.error(
`Servizio non trovato per ordine ${ordineId} con servizio_id ${data.servizio_id}, procedura getOrdineRicevutaData`,
);
}
}
return { return {
clienteIndirizzo: `${anagrafica.via_residenza} ${anagrafica.civico_residenza}\n${parsedComune?.nome} (${anagrafica.provincia_residenza}) ${anagrafica.cap_residenza}\n${parsedNazione?.nome}`, clienteIndirizzo: `${anagrafica.via_residenza} ${anagrafica.civico_residenza}\n${parsedComune?.nome} (${anagrafica.provincia_residenza}) ${anagrafica.cap_residenza}\n${parsedNazione?.nome}`,
clienteNome: anagrafica.username, clienteNome: anagrafica.username,
@ -1439,6 +1465,7 @@ export const getOrdineRicevutaData = async ({
ordineId: data.ordine_id, ordineId: data.ordine_id,
paymentMethod: (data.paymentmethod || "manual") as PaymentType, paymentMethod: (data.paymentmethod || "manual") as PaymentType,
previewSaldo,
}; };
} catch (e) { } catch (e) {
throw new TRPCError({ throw new TRPCError({