feat: implement saldo preview functionality in payment process and update receipt generation
This commit is contained in:
parent
a6e4ace3b6
commit
18ff13a64d
6 changed files with 145 additions and 27 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"> </td>
|
||||
<td className="py-2 text-right text-muted-foreground line-through">
|
||||
{formatCurrency(data.previewSaldo.prezzo_cent / 100)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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: “{data.packid}”</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: “{data.packid}”</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>;
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue