feat: refactor error handling and response structure in authentication and payment processes
This commit is contained in:
parent
a03326f036
commit
2676dc61ed
13 changed files with 391 additions and 257 deletions
|
|
@ -19,6 +19,7 @@ import { env } from "~/env";
|
||||||
import { useZodForm } from "~/lib/zodForm";
|
import { useZodForm } from "~/lib/zodForm";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
import { assertNever } from "~/utils/utils";
|
||||||
|
|
||||||
export const FormLogin = () => {
|
export const FormLogin = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -46,26 +47,29 @@ export const FormLogin = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutate, isPending } = api.auth.loginUser.useMutation({
|
const { mutate, isPending } = api.auth.loginUser.useMutation({
|
||||||
onError: ({ message }) => {
|
onError: (error) => {
|
||||||
toast.error(`${t.auth.login.fail}\n${message}`);
|
toast.error(`${t.auth.login.fail}\n${error.message}`);
|
||||||
},
|
},
|
||||||
onSuccess: async (data) => {
|
onSuccess: async (data) => {
|
||||||
// FLOWS: login | 3 | Login Result Handling
|
// FLOWS: login | 3 | Login Result Handling
|
||||||
switch (data.status) {
|
if (!data.ok) {
|
||||||
case "not-found":
|
switch (data.error._tag) {
|
||||||
|
case "NotFound":
|
||||||
setUserNotFound(true);
|
setUserNotFound(true);
|
||||||
toast.error(data.message);
|
toast.error(`${t.auth.login.fail}\n${data.error.message}`);
|
||||||
return;
|
return;
|
||||||
case "fail":
|
case "Fail":
|
||||||
toast.error(data.message);
|
toast.error(`${t.auth.login.fail}\n${data.error.message}`);
|
||||||
return;
|
return;
|
||||||
case "success":
|
default:
|
||||||
|
assertNever(data.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await utils.auth.getSession.invalidate();
|
await utils.auth.getSession.invalidate();
|
||||||
toast.success(t.auth.login.success);
|
toast.success(t.auth.login.success);
|
||||||
await router.push(redirect ? (redirect as string) : "/");
|
await router.push(redirect ? (redirect as string) : "/");
|
||||||
//window.location.replace(redirect ? (redirect as string) : "/");
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [userNotFound, setUserNotFound] = useState(false);
|
const [userNotFound, setUserNotFound] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export type Result<T> =
|
|
||||||
| { success: true; data: T }
|
|
||||||
| { success: false; message: string };
|
|
||||||
|
|
@ -58,7 +58,7 @@ import { useMediaQuery } from "~/hooks/use-media-query";
|
||||||
import { handleConsegna } from "~/lib/annuncio_details";
|
import { handleConsegna } from "~/lib/annuncio_details";
|
||||||
import { replaceWithBr } from "~/lib/newlineToBr";
|
import { replaceWithBr } from "~/lib/newlineToBr";
|
||||||
import { getStorageUrl } from "~/lib/storage_utils";
|
import { getStorageUrl } from "~/lib/storage_utils";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn, redirectTo500 } from "~/lib/utils";
|
||||||
import { AnnuncioContext, useAnnuncio } from "~/providers/AnnuncioProvider";
|
import { AnnuncioContext, useAnnuncio } from "~/providers/AnnuncioProvider";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import { buildRicercaUrl } from "~/providers/RicercaProvider";
|
import { buildRicercaUrl } from "~/providers/RicercaProvider";
|
||||||
|
|
@ -176,8 +176,21 @@ export async function getStaticProps(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const annuncio = await ssg.annunci.getAnnuncioData.fetch({ cod: cod });
|
const result = await ssg.annunci.getAnnuncioData.fetch({ cod: cod });
|
||||||
if (!annuncio.success || annuncio.data.stato === "Sospeso") {
|
|
||||||
|
if (!result.ok) {
|
||||||
|
if (result.error._tag === "InternalError") {
|
||||||
|
return redirectTo500;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/annuncio-non-trovato",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (result.value.stato === "Sospeso") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/annuncio-non-trovato",
|
destination: "/annuncio-non-trovato",
|
||||||
|
|
@ -191,7 +204,7 @@ export async function getStaticProps(
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
data: annuncio.data,
|
data: result.value,
|
||||||
//cod,
|
//cod,
|
||||||
flag,
|
flag,
|
||||||
//meta,
|
//meta,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import {
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "~/components/custom_ui/form";
|
} from "~/components/custom_ui/form";
|
||||||
import LoadingButton from "~/components/custom_ui/loading-button";
|
import LoadingButton from "~/components/custom_ui/loading-button";
|
||||||
|
import { Status500 } from "~/components/status-page";
|
||||||
import { PasswordSVG } from "~/components/svgs";
|
import { PasswordSVG } from "~/components/svgs";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
|
|
@ -38,27 +39,22 @@ import {
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import Input from "~/components/ui/input";
|
import Input from "~/components/ui/input";
|
||||||
import { usePassword } from "~/hooks/usePassword";
|
import { usePassword } from "~/hooks/usePassword";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn, redirectTo500 } from "~/lib/utils";
|
||||||
import { useZodForm } from "~/lib/zodForm";
|
import { useZodForm } from "~/lib/zodForm";
|
||||||
import type { NextPageWithLayout } from "~/pages/_app";
|
import type { NextPageWithLayout } from "~/pages/_app";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
import type {
|
||||||
|
InviteErrors,
|
||||||
|
TokenVerErrors,
|
||||||
|
} from "~/server/controllers/invites.controller";
|
||||||
import { generateSSGHelper } from "~/server/utils/ssgHelper";
|
import { generateSSGHelper } from "~/server/utils/ssgHelper";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
import type { Result } from "~/utils/result";
|
||||||
|
import { assertNever } from "~/utils/utils";
|
||||||
|
|
||||||
type InviteTokenProps = {
|
type InviteTokenProps = {
|
||||||
token: string;
|
token: string;
|
||||||
verification:
|
verification: Result<string, InviteErrors>;
|
||||||
| {
|
|
||||||
status: "valid";
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
status: "invalid";
|
|
||||||
reason: "not_found" | "expired";
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
status: "already_user";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -68,16 +64,20 @@ const AcceptInvitePage: NextPageWithLayout<InviteTokenProps> = ({
|
||||||
verification,
|
verification,
|
||||||
token,
|
token,
|
||||||
}: InviteTokenProps) => {
|
}: InviteTokenProps) => {
|
||||||
switch (verification.status) {
|
if (!verification.ok) {
|
||||||
case "valid":
|
switch (verification.error._tag) {
|
||||||
return <ValidInvitePage email={verification.email} token={token} />;
|
case "AlreadyUser":
|
||||||
case "invalid":
|
|
||||||
return <InvalidInvitePage reason={verification.reason} />;
|
|
||||||
case "already_user":
|
|
||||||
return <AlreadyUserPage />;
|
return <AlreadyUserPage />;
|
||||||
|
case "Expired":
|
||||||
|
case "TokenNotFound":
|
||||||
|
return <InvalidInvitePage reason={verification.error._tag} />;
|
||||||
|
case "InternalError":
|
||||||
|
return <Status500 />;
|
||||||
default:
|
default:
|
||||||
return <InvalidInvitePage reason="not_found" />;
|
assertNever(verification.error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return <ValidInvitePage email={verification.value} token={token} />;
|
||||||
};
|
};
|
||||||
const ValidInvitePage = ({
|
const ValidInvitePage = ({
|
||||||
token,
|
token,
|
||||||
|
|
@ -131,10 +131,28 @@ const ValidInvitePage = ({
|
||||||
|
|
||||||
const { mutate: acceptInvite, isPending } =
|
const { mutate: acceptInvite, isPending } =
|
||||||
api.invite.acceptInvite.useMutation({
|
api.invite.acceptInvite.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async (result) => {
|
||||||
toast.success("Account creato! Effettua il login.");
|
if (!result.ok) {
|
||||||
|
switch (result.error._tag) {
|
||||||
|
case "UserNotFound":
|
||||||
|
toast.error(result.error.message);
|
||||||
|
return;
|
||||||
|
case "TokenNotFound":
|
||||||
|
case "Expired":
|
||||||
|
toast.error(
|
||||||
|
"Il token di invito non è più valido. Richiedi un nuovo invito.",
|
||||||
|
);
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
|
||||||
|
default:
|
||||||
|
assertNever(result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toast.success(
|
||||||
|
"Successo! La tua password è stata impostata. Reindirizzamento in corso...",
|
||||||
|
);
|
||||||
await router.push("/area-riservata/dashboard");
|
await router.push("/area-riservata/dashboard");
|
||||||
//await router.push("/login?redirect=/area-riservata/dashboard");
|
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
|
|
@ -246,9 +264,9 @@ const AlreadyUserPage = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const InvalidInvitePage = ({ reason }: { reason: "not_found" | "expired" }) => {
|
const InvalidInvitePage = ({ reason }: { reason: TokenVerErrors["_tag"] }) => {
|
||||||
const message =
|
const message =
|
||||||
reason === "expired"
|
reason === "Expired"
|
||||||
? "L'invito è scaduto, è passato troppo tempo dall'invio."
|
? "L'invito è scaduto, è passato troppo tempo dall'invio."
|
||||||
: "Non è stato trovato alcun invito valido per questo collegamento.";
|
: "Non è stato trovato alcun invito valido per questo collegamento.";
|
||||||
|
|
||||||
|
|
@ -354,6 +372,10 @@ export const getServerSideProps = (async (context) => {
|
||||||
|
|
||||||
const verification = await helper.invite.verifyInvite.fetch({ token, email });
|
const verification = await helper.invite.verifyInvite.fetch({ token, email });
|
||||||
|
|
||||||
|
if (!verification.ok && verification.error._tag === "InternalError") {
|
||||||
|
return redirectTo500;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
verification,
|
verification,
|
||||||
|
|
|
||||||
|
|
@ -149,14 +149,15 @@ export const getServerSideProps = (async (context) => {
|
||||||
const result = await helper.trpc.pagamenti.preparePaymentRinnovi.fetch({
|
const result = await helper.trpc.pagamenti.preparePaymentRinnovi.fetch({
|
||||||
rinnovoId: parsedRinnovoId.data,
|
rinnovoId: parsedRinnovoId.data,
|
||||||
});
|
});
|
||||||
if (!result.success) {
|
if (!result.ok) {
|
||||||
console.error("Error preparing payment", result.message);
|
// error type non usato prienamente per ora, logghiamo tutto con messaggio
|
||||||
|
console.error("Error preparing payment", result.error.message);
|
||||||
return redirectTo500;
|
return redirectTo500;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
stripeDisabled: flag || false,
|
stripeDisabled: flag || false,
|
||||||
data: result.data,
|
data: result.value,
|
||||||
saldoPreview: null,
|
saldoPreview: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -176,8 +177,9 @@ export const getServerSideProps = (async (context) => {
|
||||||
type,
|
type,
|
||||||
servizioId: parsedServizioId.data,
|
servizioId: parsedServizioId.data,
|
||||||
});
|
});
|
||||||
if (!result.success) {
|
if (!result.ok) {
|
||||||
console.error("Error preparing payment", result.message);
|
// error type non usato prienamente per ora, logghiamo tutto con messaggio
|
||||||
|
console.error("Error preparing payment", result.error.message);
|
||||||
return redirectTo500;
|
return redirectTo500;
|
||||||
}
|
}
|
||||||
let saldoPreview: Prezziario | null = null;
|
let saldoPreview: Prezziario | null = null;
|
||||||
|
|
@ -193,7 +195,7 @@ export const getServerSideProps = (async (context) => {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
stripeDisabled: flag || false,
|
stripeDisabled: flag || false,
|
||||||
data: result.data,
|
data: result.value,
|
||||||
saldoPreview,
|
saldoPreview,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,13 @@ import {
|
||||||
createTRPCRouter,
|
createTRPCRouter,
|
||||||
publicProcedure,
|
publicProcedure,
|
||||||
} from "~/server/api/trpc";
|
} from "~/server/api/trpc";
|
||||||
import { swapAuth } from "~/server/controllers/auth.controller";
|
|
||||||
import {
|
import {
|
||||||
consumeInviteToken,
|
|
||||||
createInviteToken,
|
createInviteToken,
|
||||||
|
InviteAcceptance,
|
||||||
|
InviteVerification,
|
||||||
sendInvitoEmail,
|
sendInvitoEmail,
|
||||||
verifyInviteToken,
|
|
||||||
} from "~/server/controllers/invites.controller";
|
} from "~/server/controllers/invites.controller";
|
||||||
import { editAccountHandler } from "~/server/controllers/user.controller";
|
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import { generateSalt, hashPassword } from "~/server/services/password.service";
|
|
||||||
import { findUser_byEmail, getUser } from "~/server/services/user.service";
|
import { findUser_byEmail, getUser } from "~/server/services/user.service";
|
||||||
import { zUserId } from "~/server/utils/zod_types";
|
import { zUserId } from "~/server/utils/zod_types";
|
||||||
|
|
||||||
|
|
@ -90,31 +87,8 @@ export const inviteRouter = createTRPCRouter({
|
||||||
// Verify invite token (public - user clicking the link)
|
// Verify invite token (public - user clicking the link)
|
||||||
verifyInvite: publicProcedure
|
verifyInvite: publicProcedure
|
||||||
.input(z.object({ token: z.string(), email: z.string().nullable() }))
|
.input(z.object({ token: z.string(), email: z.string().nullable() }))
|
||||||
.output(
|
|
||||||
z.discriminatedUnion("status", [
|
|
||||||
z.object({ status: z.literal("valid"), email: z.email() }),
|
|
||||||
z.object({
|
|
||||||
status: z.literal("invalid"),
|
|
||||||
reason: z.enum(["not_found", "expired"]),
|
|
||||||
}),
|
|
||||||
z.object({ status: z.literal("already_user") }),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
if (input.email) {
|
return await InviteVerification(input.token, input.email);
|
||||||
const existingUser = await findUser_byEmail({ email: input.email });
|
|
||||||
if (existingUser?.usedInvite) {
|
|
||||||
return { status: "already_user" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ver = await verifyInviteToken(input.token);
|
|
||||||
|
|
||||||
if (!ver.status) {
|
|
||||||
return { status: "invalid", reason: ver.reason };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { status: "valid", email: ver.invite.email };
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Accept invite and set password
|
// Accept invite and set password
|
||||||
|
|
@ -127,39 +101,11 @@ export const inviteRouter = createTRPCRouter({
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const user = await findUser_byEmail({ email: input.email });
|
return await InviteAcceptance(
|
||||||
if (!user) {
|
ctx,
|
||||||
throw new TRPCError({
|
input.token,
|
||||||
code: "NOT_FOUND",
|
input.email,
|
||||||
message: `Accept Invite: Utente non trovato con email: ${input.email}`,
|
input.password,
|
||||||
});
|
);
|
||||||
}
|
|
||||||
const ver = await verifyInviteToken(input.token);
|
|
||||||
|
|
||||||
if (!ver.status) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: `Accept Invite: Invito non valido o scaduto per email: ${input.email}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create user with password
|
|
||||||
const salt = generateSalt();
|
|
||||||
const hashedPassword = await hashPassword(input.password, salt);
|
|
||||||
|
|
||||||
await editAccountHandler({
|
|
||||||
id: user.id,
|
|
||||||
data: {
|
|
||||||
password: hashedPassword,
|
|
||||||
salt,
|
|
||||||
usedInvite: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mark invite as used
|
|
||||||
await consumeInviteToken(input.token);
|
|
||||||
await swapAuth({ ctx, userId: user.id });
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
getPagamentoDataForCheckout,
|
getPagamentoDataForCheckout,
|
||||||
getRicevutaData,
|
getRicevutaData,
|
||||||
type PreparedPaymentData,
|
type PreparedPaymentData,
|
||||||
|
type PreparePaymentError,
|
||||||
preparePayment,
|
preparePayment,
|
||||||
previewSaldo,
|
previewSaldo,
|
||||||
} from "~/server/controllers/pagamenti.controller";
|
} from "~/server/controllers/pagamenti.controller";
|
||||||
|
|
@ -13,6 +14,7 @@ import {
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import { TypstGenerate } from "~/server/services/typst.service";
|
import { TypstGenerate } from "~/server/services/typst.service";
|
||||||
import { zOrdineId, zRinnovoId, zServizioId } from "~/server/utils/zod_types";
|
import { zOrdineId, zRinnovoId, zServizioId } from "~/server/utils/zod_types";
|
||||||
|
import { zResult } from "~/utils/result";
|
||||||
// Create a new ratelimiter, that allows 10 requests per 10 seconds
|
// Create a new ratelimiter, that allows 10 requests per 10 seconds
|
||||||
/*
|
/*
|
||||||
const ratelimit = new RateLimiterHandler({
|
const ratelimit = new RateLimiterHandler({
|
||||||
|
|
@ -47,23 +49,15 @@ export const pagamentiRouter = createTRPCRouter({
|
||||||
type: z.enum(OrderTypeEnum),
|
type: z.enum(OrderTypeEnum),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(
|
.output(zResult<PreparedPaymentData, PreparePaymentError>())
|
||||||
z.discriminatedUnion("success", [
|
|
||||||
z.object({
|
|
||||||
success: z.literal(true),
|
|
||||||
data: z.custom<PreparedPaymentData>(),
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
success: z.literal(false),
|
|
||||||
message: z.string(),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
if (input.type === OrderTypeEnum.Rinnovo) {
|
if (input.type === OrderTypeEnum.Rinnovo) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "InvalidInput",
|
||||||
message: "Use preparePaymentRinnovi for Rinnovo payments",
|
message: "Use preparePaymentRinnovi for Rinnovo payments",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return await preparePayment({
|
return await preparePayment({
|
||||||
|
|
@ -86,18 +80,7 @@ export const pagamentiRouter = createTRPCRouter({
|
||||||
rinnovoId: zRinnovoId,
|
rinnovoId: zRinnovoId,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(
|
.output(zResult<PreparedPaymentData, PreparePaymentError>())
|
||||||
z.discriminatedUnion("success", [
|
|
||||||
z.object({
|
|
||||||
success: z.literal(true),
|
|
||||||
data: z.custom<PreparedPaymentData>(),
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
success: z.literal(false),
|
|
||||||
message: z.string(),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await preparePayment({
|
return await preparePayment({
|
||||||
type: OrderTypeEnum.Rinnovo,
|
type: OrderTypeEnum.Rinnovo,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import { filteredCaratteristiche } from "~/hooks/schedaAnnuncioUtils";
|
import { filteredCaratteristiche } from "~/hooks/schedaAnnuncioUtils";
|
||||||
import type { Result } from "~/lib/result";
|
|
||||||
import { getStorageUrl } from "~/lib/storage_utils";
|
import { getStorageUrl } from "~/lib/storage_utils";
|
||||||
import type {
|
import type {
|
||||||
Annunci,
|
Annunci,
|
||||||
|
|
@ -14,6 +13,7 @@ import type { VideosRefs } from "~/schemas/public/VideosRefs";
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import type { AnnuncioTemplateData } from "~/server/services/typst.service";
|
import type { AnnuncioTemplateData } from "~/server/services/typst.service";
|
||||||
import { withImages, withVideos } from "~/utils/kysely-helper";
|
import { withImages, withVideos } from "~/utils/kysely-helper";
|
||||||
|
import type { Result } from "~/utils/result";
|
||||||
import { revalidate } from "../utils/revalidationHelper";
|
import { revalidate } from "../utils/revalidationHelper";
|
||||||
|
|
||||||
// const ratelimit = new RateLimiterHandler({
|
// const ratelimit = new RateLimiterHandler({
|
||||||
|
|
@ -112,11 +112,16 @@ export type AnnuncioData = Pick<
|
||||||
ogUrl: string;
|
ogUrl: string;
|
||||||
ogImage: string;
|
ogImage: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type getAnnuncioDataError =
|
||||||
|
| { _tag: "NoAnnuncio" }
|
||||||
|
| { _tag: "MalformedAnnuncio" }
|
||||||
|
| { _tag: "InternalError"; cause: unknown };
|
||||||
export const getAnnuncioData = async ({
|
export const getAnnuncioData = async ({
|
||||||
cod,
|
cod,
|
||||||
}: {
|
}: {
|
||||||
cod: string;
|
cod: string;
|
||||||
}): Promise<Result<AnnuncioData>> => {
|
}): Promise<Result<AnnuncioData, getAnnuncioDataError>> => {
|
||||||
try {
|
try {
|
||||||
const annuncio = await db
|
const annuncio = await db
|
||||||
.selectFrom("annunci")
|
.selectFrom("annunci")
|
||||||
|
|
@ -157,11 +162,11 @@ export const getAnnuncioData = async ({
|
||||||
.where("codice", "=", cod)
|
.where("codice", "=", cod)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
if (!annuncio) {
|
if (!annuncio) {
|
||||||
return { success: false, message: "Annuncio non trovato" };
|
return { ok: false, error: { _tag: "NoAnnuncio" } };
|
||||||
}
|
}
|
||||||
const { tipo, media_updated_at, ...rest } = annuncio;
|
const { tipo, media_updated_at, ...rest } = annuncio;
|
||||||
if (tipo === null) {
|
if (tipo === null) {
|
||||||
return { success: false, message: "Annuncio non trovato" };
|
return { ok: false, error: { _tag: "MalformedAnnuncio" } };
|
||||||
}
|
}
|
||||||
const ogImage =
|
const ogImage =
|
||||||
annuncio.images && annuncio.images.length > 0 && annuncio.images[0]
|
annuncio.images && annuncio.images.length > 0 && annuncio.images[0]
|
||||||
|
|
@ -175,8 +180,8 @@ export const getAnnuncioData = async ({
|
||||||
})
|
})
|
||||||
: `${env.BASE_URL}/og.jpg`;
|
: `${env.BASE_URL}/og.jpg`;
|
||||||
return {
|
return {
|
||||||
success: true,
|
ok: true,
|
||||||
data: {
|
value: {
|
||||||
...rest,
|
...rest,
|
||||||
media_updated_at: media_updated_at?.toISOString() || null,
|
media_updated_at: media_updated_at?.toISOString() || null,
|
||||||
tipo,
|
tipo,
|
||||||
|
|
@ -185,11 +190,7 @@ export const getAnnuncioData = async ({
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new TRPCError({
|
return { ok: false, error: { _tag: "InternalError", cause: e } };
|
||||||
cause: (e as Error).cause,
|
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
|
||||||
message: `Errore query getAnnunciByCod: ${(e as Error).message}`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
findUser_byId_MINI,
|
findUser_byId_MINI,
|
||||||
} from "~/server/services/user.service";
|
} from "~/server/services/user.service";
|
||||||
import { checkCtx } from "~/server/utils/ctxChecker";
|
import { checkCtx } from "~/server/utils/ctxChecker";
|
||||||
|
import type { Result } from "~/utils/result";
|
||||||
import { TOKEN_CONFIG } from "../auth/configs";
|
import { TOKEN_CONFIG } from "../auth/configs";
|
||||||
|
|
||||||
export const createUserAdmin = async ({
|
export const createUserAdmin = async ({
|
||||||
|
|
@ -68,6 +69,10 @@ export const createUserAdmin = async ({
|
||||||
return { status: "success" as const };
|
return { status: "success" as const };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type LoginErrors =
|
||||||
|
| { _tag: "NotFound"; message: string }
|
||||||
|
| { _tag: "Fail"; message: string };
|
||||||
|
|
||||||
// FLOWS: login | 2 | Login function
|
// FLOWS: login | 2 | Login function
|
||||||
export const logIn = async ({
|
export const logIn = async ({
|
||||||
email,
|
email,
|
||||||
|
|
@ -77,7 +82,7 @@ export const logIn = async ({
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
ctx: Context;
|
ctx: Context;
|
||||||
}) => {
|
}): Promise<Result<null, LoginErrors>> => {
|
||||||
try {
|
try {
|
||||||
checkCtx({ req, res });
|
checkCtx({ req, res });
|
||||||
|
|
||||||
|
|
@ -86,17 +91,23 @@ export const logIn = async ({
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
status: "not-found" as const,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "NotFound",
|
||||||
message:
|
message:
|
||||||
"Questo utente non esiste. Devi avere un account per effettuare il login.",
|
"Questo utente non esiste. Devi avere un account per effettuare il login.",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
//Check if user is blocked or unverified
|
//Check if user is blocked or unverified
|
||||||
|
|
||||||
if (user.isBlocked) {
|
if (user.isBlocked) {
|
||||||
return {
|
return {
|
||||||
status: "fail" as const,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "Fail",
|
||||||
message: "Errore in Login, non è possibile effettuare il login.",
|
message: "Errore in Login, non è possibile effettuare il login.",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,8 +123,11 @@ export const logIn = async ({
|
||||||
|
|
||||||
if (!passworIsMatch) {
|
if (!passworIsMatch) {
|
||||||
return {
|
return {
|
||||||
status: "fail" as const,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "Fail",
|
||||||
message: "Password errata, riprova.",
|
message: "Password errata, riprova.",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +153,6 @@ export const logIn = async ({
|
||||||
"refreshToken",
|
"refreshToken",
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
|
||||||
await setCookie(TOKEN_CONFIG.ACCESS_TOKEN_COOKIE_NAME, accessToken, {
|
await setCookie(TOKEN_CONFIG.ACCESS_TOKEN_COOKIE_NAME, accessToken, {
|
||||||
...TOKEN_CONFIG.COOKIE_OPTIONS,
|
...TOKEN_CONFIG.COOKIE_OPTIONS,
|
||||||
expires: add(new Date(), {
|
expires: add(new Date(), {
|
||||||
|
|
@ -157,19 +170,12 @@ export const logIn = async ({
|
||||||
res,
|
res,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { status: "success" as const };
|
return { ok: true, value: null };
|
||||||
} catch (cookieError) {
|
|
||||||
console.error("Error setting cookies:", cookieError);
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
|
||||||
message: `Errore durante l'impostazione dei cookies: ${(cookieError as Error).message}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Login error:", e);
|
console.error("Login error:", e);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: `Errore login: ${(e as Error).message}`,
|
message: `Errore durante il login: ${(e as Error).message}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,13 @@ import crypto from "node:crypto";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import type { UserInvites } from "~/schemas/public/UserInvites";
|
import type { UserInvites } from "~/schemas/public/UserInvites";
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
|
import type { Context } from "~/server/api/trpc";
|
||||||
|
import { swapAuth } from "~/server/controllers/auth.controller";
|
||||||
|
import { editAccountHandler } from "~/server/controllers/user.controller";
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
|
import { generateSalt, hashPassword } from "~/server/services/password.service";
|
||||||
|
import { findUser_byEmail } from "~/server/services/user.service";
|
||||||
|
import type { Result } from "~/utils/result";
|
||||||
import { NewMail } from "../services/mailer";
|
import { NewMail } from "../services/mailer";
|
||||||
|
|
||||||
const INVITE_EXPIRY_HOURS = 48;
|
const INVITE_EXPIRY_HOURS = 48;
|
||||||
|
|
@ -31,19 +37,109 @@ export async function createInviteToken(email: string) {
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
export type InviteErrors =
|
||||||
|
| { _tag: "AlreadyUser" }
|
||||||
|
| { _tag: "InternalError"; message: string }
|
||||||
|
| TokenVerErrors;
|
||||||
|
|
||||||
type VerifyInviteResult =
|
export const InviteVerification = async (
|
||||||
| {
|
token: string,
|
||||||
status: true;
|
email: string | null,
|
||||||
invite: UserInvites;
|
): Promise<Result<string, InviteErrors>> => {
|
||||||
|
try {
|
||||||
|
if (email) {
|
||||||
|
const existingUser = await findUser_byEmail({ email });
|
||||||
|
if (existingUser?.usedInvite) {
|
||||||
|
return { ok: false, error: { _tag: "AlreadyUser" } };
|
||||||
}
|
}
|
||||||
| {
|
}
|
||||||
status: false;
|
|
||||||
reason: "not_found" | "expired";
|
const ver = await verifyInviteToken(token);
|
||||||
|
|
||||||
|
if (!ver.ok) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: ver.error,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, value: ver.value.email };
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error verifying invite token:", e);
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "InternalError",
|
||||||
|
message: `Errore durante la verifica del token di invito.: ${(e as Error).message}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
type InviteAcceptanceErrors =
|
||||||
|
| {
|
||||||
|
_tag: "UserNotFound";
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
| TokenVerErrors;
|
||||||
|
export const InviteAcceptance = async (
|
||||||
|
ctx: Context,
|
||||||
|
token: string,
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<Result<UserInvites, InviteAcceptanceErrors>> => {
|
||||||
|
try {
|
||||||
|
const user = await findUser_byEmail({ email });
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "UserNotFound",
|
||||||
|
message: `Accept Invite: Utente non trovato con email: ${email}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ver = await verifyInviteToken(token);
|
||||||
|
|
||||||
|
if (!ver.ok) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: ver.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user with password
|
||||||
|
const salt = generateSalt();
|
||||||
|
const hashedPassword = await hashPassword(password, salt);
|
||||||
|
|
||||||
|
await editAccountHandler({
|
||||||
|
id: user.id,
|
||||||
|
data: {
|
||||||
|
password: hashedPassword,
|
||||||
|
salt,
|
||||||
|
usedInvite: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark invite as used
|
||||||
|
await consumeInviteToken(token);
|
||||||
|
await swapAuth({ ctx, userId: user.id });
|
||||||
|
|
||||||
|
return { ok: true, value: ver.value };
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error verifying invite token:", e);
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: `Errore durante l'accettazione del token di invito.: ${(e as Error).message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TokenVerErrors = { _tag: "TokenNotFound" } | { _tag: "Expired" };
|
||||||
|
|
||||||
export async function verifyInviteToken(
|
export async function verifyInviteToken(
|
||||||
token: string,
|
token: string,
|
||||||
): Promise<VerifyInviteResult> {
|
): Promise<Result<UserInvites, TokenVerErrors>> {
|
||||||
try {
|
try {
|
||||||
const invite = await db
|
const invite = await db
|
||||||
.selectFrom("user_invites")
|
.selectFrom("user_invites")
|
||||||
|
|
@ -52,13 +148,13 @@ export async function verifyInviteToken(
|
||||||
//.where("expires_at", ">", new Date())
|
//.where("expires_at", ">", new Date())
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
if (!invite) {
|
if (!invite) {
|
||||||
return { status: false, reason: "not_found" };
|
return { ok: false, error: { _tag: "TokenNotFound" } };
|
||||||
}
|
}
|
||||||
if (invite.expires_at < new Date()) {
|
if (invite.expires_at < new Date()) {
|
||||||
return { status: false, reason: "expired" };
|
return { ok: false, error: { _tag: "Expired" } };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { status: true, invite };
|
return { ok: true, value: invite };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error verifying invite token:", e);
|
console.error("Error verifying invite token:", e);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import type { ServizioServizioId } from "~/schemas/public/Servizio";
|
||||||
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
||||||
import { getComuni, getNazioni } from "~/server/controllers/catasto.controller";
|
import { getComuni, getNazioni } from "~/server/controllers/catasto.controller";
|
||||||
import type { RicevutaProps } from "~/server/services/typst.service";
|
import type { RicevutaProps } from "~/server/services/typst.service";
|
||||||
|
import type { Result } from "~/utils/result";
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import { createOrdine } from "../services/ordini.service";
|
import { createOrdine } from "../services/ordini.service";
|
||||||
import {
|
import {
|
||||||
|
|
@ -40,12 +41,14 @@ type preparePaymentInput =
|
||||||
type: OrderTypeEnum.Rinnovo;
|
type: OrderTypeEnum.Rinnovo;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PreparePaymentError =
|
||||||
|
| { _tag: "PrezziarioNotFound"; message: string }
|
||||||
|
| { _tag: "AlreadyPaid"; message: string }
|
||||||
|
| { _tag: "InvalidInput"; message: string }
|
||||||
|
| { _tag: "InternalError"; message: string };
|
||||||
export const preparePayment = async (
|
export const preparePayment = async (
|
||||||
props: preparePaymentInput,
|
props: preparePaymentInput,
|
||||||
): Promise<
|
): Promise<Result<PreparedPaymentData, PreparePaymentError>> => {
|
||||||
| { success: true; data: PreparedPaymentData }
|
|
||||||
| { success: false; message: string }
|
|
||||||
> => {
|
|
||||||
try {
|
try {
|
||||||
switch (props.type) {
|
switch (props.type) {
|
||||||
case OrderTypeEnum.Acconto:
|
case OrderTypeEnum.Acconto:
|
||||||
|
|
@ -68,8 +71,11 @@ export const preparePayment = async (
|
||||||
const acconto = await AccontoSolver(servizio.tipologia);
|
const acconto = await AccontoSolver(servizio.tipologia);
|
||||||
if (!acconto.success) {
|
if (!acconto.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "PrezziarioNotFound",
|
||||||
message: `Acconto non trovato per il servizio: ${acconto.message}`,
|
message: `Acconto non trovato per il servizio: ${acconto.message}`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,8 +88,8 @@ export const preparePayment = async (
|
||||||
if (preExistingAcconto) {
|
if (preExistingAcconto) {
|
||||||
// acconto già creato, procedere con questo
|
// acconto già creato, procedere con questo
|
||||||
return {
|
return {
|
||||||
success: true,
|
ok: true,
|
||||||
data: {
|
value: {
|
||||||
ordine_id: preExistingAcconto.ordine_id,
|
ordine_id: preExistingAcconto.ordine_id,
|
||||||
packid: preExistingAcconto.packid,
|
packid: preExistingAcconto.packid,
|
||||||
amount_cent: preExistingAcconto.amount_cent,
|
amount_cent: preExistingAcconto.amount_cent,
|
||||||
|
|
@ -105,8 +111,8 @@ export const preparePayment = async (
|
||||||
userid: servizio.user_id,
|
userid: servizio.user_id,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
success: true,
|
ok: true,
|
||||||
data: {
|
value: {
|
||||||
ordine_id: ordine.ordine_id,
|
ordine_id: ordine.ordine_id,
|
||||||
packid: acconto.pack.idprezziario,
|
packid: acconto.pack.idprezziario,
|
||||||
amount_cent: acconto.pack.prezzo_cent,
|
amount_cent: acconto.pack.prezzo_cent,
|
||||||
|
|
@ -119,17 +125,23 @@ export const preparePayment = async (
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
success: false,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "AlreadyPaid",
|
||||||
message:
|
message:
|
||||||
"Il servizio ha già un acconto attivo, non è possibile preparare un nuovo pagamento",
|
"Il servizio ha già un acconto attivo, non è possibile preparare un nuovo pagamento",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case OrderTypeEnum.Saldo: {
|
case OrderTypeEnum.Saldo: {
|
||||||
if (!servizio.isOkAcconto) {
|
if (!servizio.isOkAcconto) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "InvalidInput",
|
||||||
message:
|
message:
|
||||||
"Non è possibile procedere con il pagamento del saldo prima di aver attivato l'acconto",
|
"Non è possibile procedere con il pagamento del saldo prima di aver attivato l'acconto",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!servizio.isOkSaldo) {
|
if (!servizio.isOkSaldo) {
|
||||||
|
|
@ -141,8 +153,11 @@ export const preparePayment = async (
|
||||||
});
|
});
|
||||||
if (!saldo.success) {
|
if (!saldo.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "PrezziarioNotFound",
|
||||||
message: `Saldo non trovato per il servizio: ${saldo.message}`,
|
message: `Saldo non trovato per il servizio: ${saldo.message}`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,8 +167,8 @@ export const preparePayment = async (
|
||||||
if (preExistingSaldo) {
|
if (preExistingSaldo) {
|
||||||
// saldo già creato, procedere con questo
|
// saldo già creato, procedere con questo
|
||||||
return {
|
return {
|
||||||
success: true,
|
ok: true,
|
||||||
data: {
|
value: {
|
||||||
ordine_id: preExistingSaldo.ordine_id,
|
ordine_id: preExistingSaldo.ordine_id,
|
||||||
packid: preExistingSaldo.packid,
|
packid: preExistingSaldo.packid,
|
||||||
amount_cent: preExistingSaldo.amount_cent,
|
amount_cent: preExistingSaldo.amount_cent,
|
||||||
|
|
@ -174,8 +189,8 @@ export const preparePayment = async (
|
||||||
userid: servizio.user_id,
|
userid: servizio.user_id,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
success: true,
|
ok: true,
|
||||||
data: {
|
value: {
|
||||||
ordine_id: ordine.ordine_id,
|
ordine_id: ordine.ordine_id,
|
||||||
packid: saldo.pack.idprezziario,
|
packid: saldo.pack.idprezziario,
|
||||||
amount_cent: saldo.pack.prezzo_cent,
|
amount_cent: saldo.pack.prezzo_cent,
|
||||||
|
|
@ -187,18 +202,24 @@ export const preparePayment = async (
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
success: false,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "AlreadyPaid",
|
||||||
message:
|
message:
|
||||||
"Il servizio ha già un saldo attivo, non è possibile preparare un nuovo pagamento",
|
"Il servizio ha già un saldo attivo, non è possibile preparare un nuovo pagamento",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case OrderTypeEnum.Consulenza: {
|
case OrderTypeEnum.Consulenza: {
|
||||||
if (!servizio.isOkAcconto || !servizio.isOkSaldo) {
|
if (!servizio.isOkAcconto || !servizio.isOkSaldo) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "InvalidInput",
|
||||||
message:
|
message:
|
||||||
"Non è possibile procedere con il pagamento della consulenza prima di aver attivato acconto e saldo",
|
"Non è possibile procedere con il pagamento della consulenza prima di aver attivato acconto e saldo",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!servizio.isOkConsulenza) {
|
if (!servizio.isOkConsulenza) {
|
||||||
|
|
@ -212,14 +233,17 @@ export const preparePayment = async (
|
||||||
);
|
);
|
||||||
if (!consulenzaPack) {
|
if (!consulenzaPack) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "PrezziarioNotFound",
|
||||||
message: `Pack della consulenza non trovato: ${existingConsulenza.packid}`,
|
message: `Pack della consulenza non trovato: ${existingConsulenza.packid}`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
ok: true,
|
||||||
data: {
|
value: {
|
||||||
ordine_id: existingConsulenza.ordine_id,
|
ordine_id: existingConsulenza.ordine_id,
|
||||||
packid: existingConsulenza.packid,
|
packid: existingConsulenza.packid,
|
||||||
amount_cent: existingConsulenza.amount_cent,
|
amount_cent: existingConsulenza.amount_cent,
|
||||||
|
|
@ -233,16 +257,22 @@ export const preparePayment = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "InvalidInput",
|
||||||
message:
|
message:
|
||||||
"Il servizio non ha un pacchetto di consulenza, è necessario inserirlo prima di procedere",
|
"Il servizio non ha un pacchetto di consulenza, è necessario inserirlo prima di procedere",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "AlreadyPaid",
|
||||||
message:
|
message:
|
||||||
"Il servizio ha già una consulenza attiva, non è possibile preparare un nuovo pagamento",
|
"Il servizio ha già una consulenza attiva, non è possibile preparare un nuovo pagamento",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case OrderTypeEnum.Altro: {
|
case OrderTypeEnum.Altro: {
|
||||||
|
|
@ -256,14 +286,17 @@ export const preparePayment = async (
|
||||||
);
|
);
|
||||||
if (!altroPack) {
|
if (!altroPack) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "PrezziarioNotFound",
|
||||||
message: `Pack dell'altro non trovato: ${existingAltro.packid}`,
|
message: `Pack dell'altro non trovato: ${existingAltro.packid}`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
ok: true,
|
||||||
data: {
|
value: {
|
||||||
ordine_id: existingAltro.ordine_id,
|
ordine_id: existingAltro.ordine_id,
|
||||||
packid: existingAltro.packid,
|
packid: existingAltro.packid,
|
||||||
amount_cent: existingAltro.amount_cent,
|
amount_cent: existingAltro.amount_cent,
|
||||||
|
|
@ -276,9 +309,12 @@ export const preparePayment = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "InvalidInput",
|
||||||
message:
|
message:
|
||||||
"La preparazione del pagamento per questa tipologia non è supportata",
|
"La preparazione del pagamento per questa tipologia non è supportata",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
@ -300,9 +336,12 @@ export const preparePayment = async (
|
||||||
const alreadyPaid = ordiniRinnovo.some((ordine) => ordine.isActive);
|
const alreadyPaid = ordiniRinnovo.some((ordine) => ordine.isActive);
|
||||||
if (alreadyPaid) {
|
if (alreadyPaid) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "AlreadyPaid",
|
||||||
message:
|
message:
|
||||||
"Il rinnovo ha già un ordine attivo, non è possibile preparare un nuovo pagamento",
|
"Il rinnovo ha già un ordine attivo, non è possibile preparare un nuovo pagamento",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,14 +353,17 @@ export const preparePayment = async (
|
||||||
);
|
);
|
||||||
if (!rinnovoPack) {
|
if (!rinnovoPack) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "PrezziarioNotFound",
|
||||||
message: `Pack del rinnovo non trovato: ${unusedOrdine.packid}`,
|
message: `Pack del rinnovo non trovato: ${unusedOrdine.packid}`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
ok: true,
|
||||||
data: {
|
value: {
|
||||||
ordine_id: unusedOrdine.ordine_id,
|
ordine_id: unusedOrdine.ordine_id,
|
||||||
packid: unusedOrdine.packid,
|
packid: unusedOrdine.packid,
|
||||||
amount_cent: unusedOrdine.amount_cent,
|
amount_cent: unusedOrdine.amount_cent,
|
||||||
|
|
@ -343,8 +385,11 @@ export const preparePayment = async (
|
||||||
const pack = await RinnovoSolver(rinnovo.decorrenza, rinnovo.scadenza);
|
const pack = await RinnovoSolver(rinnovo.decorrenza, rinnovo.scadenza);
|
||||||
if (!pack.success) {
|
if (!pack.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
ok: false,
|
||||||
|
error: {
|
||||||
|
_tag: "PrezziarioNotFound",
|
||||||
message: `Pack non trovato per il rinnovo: ${pack.message}`,
|
message: `Pack non trovato per il rinnovo: ${pack.message}`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -356,8 +401,8 @@ export const preparePayment = async (
|
||||||
userid: rinnovo.userId,
|
userid: rinnovo.userId,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
success: true,
|
ok: true,
|
||||||
data: {
|
value: {
|
||||||
ordine_id: ordine.ordine_id,
|
ordine_id: ordine.ordine_id,
|
||||||
packid: pack.pack.idprezziario,
|
packid: pack.pack.idprezziario,
|
||||||
amount_cent: pack.pack.prezzo_cent,
|
amount_cent: pack.pack.prezzo_cent,
|
||||||
|
|
|
||||||
15
apps/infoalloggi/src/utils/result.ts
Normal file
15
apps/infoalloggi/src/utils/result.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
|
||||||
|
|
||||||
|
export const zResult = <T, E>() =>
|
||||||
|
z.discriminatedUnion("ok", [
|
||||||
|
z.object({
|
||||||
|
ok: z.literal(true),
|
||||||
|
value: z.custom<T>(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
ok: z.literal(false),
|
||||||
|
error: z.custom<E>(),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
@ -31,3 +31,7 @@ export function titleCase(str: string | null | undefined): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const startsWithVowel = (s: string) => /^[aeiouàèéìòù]/i.test(s);
|
export const startsWithVowel = (s: string) => /^[aeiouàèéìòù]/i.test(s);
|
||||||
|
|
||||||
|
export function assertNever(x: never): never {
|
||||||
|
throw new Error(`Unexpected: ${JSON.stringify(x)}`);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue