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:
NEW IDEA TODOS:
Minimal Viable Product:
- Log email fallite e retry
AFTER MVP:
- 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 { formatCurrency } from "~/lib/utils";
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
import type { Prezziario } from "~/schemas/public/Prezziario";
import { api } from "~/utils/api";
export function ReceiptGenerator({
@ -83,6 +84,7 @@ export type PurchaseData = {
sconto: number;
amount_cent: number;
previewSaldo: Prezziario | null;
};
interface ReceiptProps {
@ -149,17 +151,17 @@ function Receipt({ data }: ReceiptProps) {
</div>
</div>
<div>
<table className="w-full">
<thead>
<tr className="border-b">
<th className="py-2 text-left">Descrizione</th>
<table className="w-full table-auto">
<thead className="flex w-full">
<tr className="grid w-full grow grid-cols-[1fr_auto_auto] gap-2 border-b">
<th className="py-2 text-left">Oggetto</th>
<th className="py-2 text-right">Sconto applicato</th>
<th className="py-2 text-right">Importo</th>
</tr>
</thead>
<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 text-right">
@ -169,6 +171,21 @@ function Receipt({ data }: ReceiptProps) {
{formatCurrency(data.amount_cent / 100)}
</td>
</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>
</table>
</div>

View file

@ -5,10 +5,19 @@ import { useRouter } from "next/router";
import z from "zod";
import { AreaRiservataLayout } from "~/components/Layout";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { formatCurrency, redirectTo500 } from "~/lib/utils";
import type { NextPageWithLayout } from "~/pages/_app";
import { useTranslation } from "~/providers/I18nProvider";
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
import type { Prezziario } from "~/schemas/public/Prezziario";
import type { PreparedPaymentData } from "~/server/controllers/pagamenti.controller";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
import { zRinnovoId, zServizioId } from "~/server/utils/zod_types";
@ -16,6 +25,7 @@ import { zRinnovoId, zServizioId } from "~/server/utils/zod_types";
type PreviewPurchaseProps = {
stripeDisabled: boolean;
data: PreparedPaymentData;
saldoPreview: Prezziario | null;
};
/**
* /servizio/pagamento?servizioId=<servizioId>|rinnovoId=<rinnovoId>&type=<OrderTypeEnum> \
@ -24,17 +34,19 @@ type PreviewPurchaseProps = {
const PreviewPurchasePage: NextPageWithLayout<PreviewPurchaseProps> = ({
stripeDisabled,
data,
saldoPreview,
}: PreviewPurchaseProps) => {
const router = useRouter();
const { t } = useTranslation();
return (
<div className="mx-auto w-full max-w-6xl grow space-y-4 p-2 sm:px-8 sm:py-4">
<div className="mx-auto mt-4 max-w-2xl p-2 sm:mt-10">
<div className="space-y-8">
<div className="space-y-6">
<p className="text-3xl">{data.packName}</p>
<p className="text-lg">Cod: &ldquo;{data.packid}&rdquo;</p>
<div className="mt-1 flex w-full justify-center p-2 sm:mt-14">
<Card className="h-fit">
<CardHeader>
<CardTitle className="text-3xl">{data.packName}</CardTitle>
<CardDescription>Cod: &ldquo;{data.packid}&rdquo;</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<p className="font-bold text-4xl">
{formatCurrency(data.amount_cent / 100)}{" "}
{data.sconto > 0 && (
@ -42,8 +54,6 @@ const PreviewPurchasePage: NextPageWithLayout<PreviewPurchaseProps> = ({
)}
</p>
<p>{data.packDesc}</p>
</div>
<Link
aria-label="Leggi le condizioni"
className="block w-fit"
@ -56,6 +66,28 @@ const PreviewPurchasePage: NextPageWithLayout<PreviewPurchaseProps> = ({
</Button>
</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
className="w-full text-lg"
onClick={async () =>
@ -68,8 +100,8 @@ const PreviewPurchasePage: NextPageWithLayout<PreviewPurchaseProps> = ({
>
<span>{t.acquisto.procedi_pagamento}</span> <ArrowRight />
</Button>
</div>
</div>
</CardFooter>
</Card>
</div>
);
};
@ -125,6 +157,7 @@ export const getServerSideProps = (async (context) => {
props: {
stripeDisabled: flag || false,
data: result.data,
saldoPreview: null,
},
};
}
@ -147,10 +180,21 @@ export const getServerSideProps = (async (context) => {
console.error("Error preparing payment", result.message);
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 {
props: {
stripeDisabled: flag || false,
data: result.data,
saldoPreview,
},
};
}) satisfies GetServerSideProps<PreviewPurchaseProps>;

View file

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

View file

@ -130,7 +130,11 @@ export const preparePayment = async (
}
if (!servizio.isOkSaldo) {
//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) {
return {
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.created_at",
"ordini.paymentmethod",
"ordini.type",
"ordini.servizio_id",
])
.innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid")
.select("prezziario.nome_it")
@ -1427,6 +1429,30 @@ export const getOrdineRicevutaData = async ({
const parsedComune = parseDBComune(anagrafica.comune_residenza, comuni);
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 {
clienteIndirizzo: `${anagrafica.via_residenza} ${anagrafica.civico_residenza}\n${parsedComune?.nome} (${anagrafica.provincia_residenza}) ${anagrafica.cap_residenza}\n${parsedNazione?.nome}`,
clienteNome: anagrafica.username,
@ -1439,6 +1465,7 @@ export const getOrdineRicevutaData = async ({
ordineId: data.ordine_id,
paymentMethod: (data.paymentmethod || "manual") as PaymentType,
previewSaldo,
};
} catch (e) {
throw new TRPCError({