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:
|
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
|
||||||
|
|
|
||||||
|
|
@ -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"> </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>
|
||||||
|
|
|
||||||
|
|
@ -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: “{data.packid}”</CardDescription>
|
||||||
<p className="text-lg">Cod: “{data.packid}”</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>;
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue