feat: update payment recap links and add new receipt page
- Changed payment recap link in OrdiniModal to point to the new receipt page. - Added new PaymentRecapPage for displaying receipt details. - Updated PaymentsTable to set paid_at date based on payment status. - Introduced manual payment type in PaymentType and updated related strings. - Refactored getOrdineRicevutaData to use overridableProcedure for access control. - Enhanced mail sending functionality to include PDF attachments for receipts and conditions. - Updated PDF generation service to handle new receipt PDF generation. - Cleaned up context checking utilities by removing unnecessary session checks.
This commit is contained in:
parent
6c9524e54b
commit
280e0ab5c2
21 changed files with 384 additions and 279 deletions
|
|
@ -64,8 +64,8 @@ const Base = ({
|
|||
</Text>
|
||||
)}
|
||||
</Section>
|
||||
<Section className="mt-3">
|
||||
<Text className="text-left text-sm">
|
||||
<Section className="mt-3 text-center">
|
||||
<Text className="text-sm">
|
||||
Arcenia S.r.l. Via Beata Giovanna 1, Bassano del Grappa (VI) -
|
||||
tel: 0424529869
|
||||
</Text>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
import { Button, Heading, Row, Section, Text } from "@react-email/components";
|
||||
import { Heading, Row, Section, Text } from "@react-email/components";
|
||||
import Base from "./base";
|
||||
export type PagamentoConferma_NewMail = {
|
||||
mailType: "pagamentoConferma";
|
||||
props: PagamentoConfermaProps;
|
||||
};
|
||||
|
||||
type PagamentoConfermaProps = {
|
||||
receiptHref: string;
|
||||
};
|
||||
|
||||
const PagamentoConferma = ({ receiptHref }: PagamentoConfermaProps) => {
|
||||
const PagamentoConferma = () => {
|
||||
return (
|
||||
<Base preview="Conferma Pagamento">
|
||||
<>
|
||||
|
|
@ -19,20 +14,18 @@ const PagamentoConferma = ({ receiptHref }: PagamentoConfermaProps) => {
|
|||
<Section className="px-5 text-left text-base md:px-12">
|
||||
<Row>
|
||||
<Text>
|
||||
{/**TODO correggere */}
|
||||
Gentile Cliente, la informiamo che il pagamento è stato confermato
|
||||
con successo. Procederemo ad attivare i servizi acquistati se non
|
||||
lo fossero già. Può controllare lo stato del suo account nella
|
||||
sezione dedicata.
|
||||
con successo.
|
||||
</Text>
|
||||
<Text>
|
||||
Puoi controllare lo stato del tuo account nella sezione dedicata.
|
||||
</Text>
|
||||
<Text>
|
||||
In allegato troverai una ricevuta di cortesia, la fattura
|
||||
eletnica arriverà separatamente.
|
||||
</Text>
|
||||
</Row>
|
||||
<Section className="mb-5">
|
||||
<Button
|
||||
className="mx-auto box-border inline-flex h-10 items-center justify-center whitespace-nowrap rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold text-neutral-900"
|
||||
href={receiptHref}
|
||||
>
|
||||
Vai al tuo account
|
||||
</Button>
|
||||
</Section>
|
||||
</Section>
|
||||
</>
|
||||
</Base>
|
||||
|
|
|
|||
34
apps/infoalloggi/emails/servizio-attivato.tsx
Normal file
34
apps/infoalloggi/emails/servizio-attivato.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { Heading, Row, Section, Text } from "@react-email/components";
|
||||
import Base from "./base";
|
||||
export type ServizioAttivato_NewMail = {
|
||||
mailType: "servizioAttivato";
|
||||
};
|
||||
|
||||
const ServizioAttivato = () => {
|
||||
return (
|
||||
<Base noreply preview="Conferma Servizio Attivato">
|
||||
<>
|
||||
<Heading className="text-center text-3xl leading-8">
|
||||
Conferma Attivazione Servizio
|
||||
</Heading>
|
||||
<Section className="px-5 text-left text-base md:px-12">
|
||||
<Row>
|
||||
<Text>
|
||||
Gentile Cliente, ti informiamo che il pagamento è stato confermato
|
||||
con successo e il servizio è stato attivato.
|
||||
</Text>
|
||||
<Text>
|
||||
Puoi controllare lo stato del tuo account nella sezione dedicata.
|
||||
</Text>
|
||||
<Text>
|
||||
In allegato troverai una ricevuta di cortesia e le condizioni
|
||||
contrattuali accettate relative al servizio attivato.
|
||||
</Text>
|
||||
</Row>
|
||||
</Section>
|
||||
</>
|
||||
</Base>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServizioAttivato;
|
||||
|
|
@ -10,12 +10,21 @@ import { Card } from "~/components/ui/card";
|
|||
import { PaymentMethodToString, type PaymentType } from "~/i18n/stripe";
|
||||
import { formatCurrency } from "~/lib/utils";
|
||||
|
||||
export function ReceiptGenerator({ data }: { data: PurchaseData }) {
|
||||
export function ReceiptGenerator({
|
||||
data,
|
||||
raw,
|
||||
}: {
|
||||
data: PurchaseData;
|
||||
raw: boolean;
|
||||
}) {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const reactToPrintFn = useReactToPrint({
|
||||
contentRef,
|
||||
//print: async (target) => {console.log("Printing...", target.contentDocument);},
|
||||
});
|
||||
if (raw) {
|
||||
return <Receipt data={data} />;
|
||||
}
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<div className="space-y-6">
|
||||
|
|
@ -27,7 +36,9 @@ export function ReceiptGenerator({ data }: { data: PurchaseData }) {
|
|||
</Button>
|
||||
</div>
|
||||
<div ref={contentRef}>
|
||||
<Card className="p-4 print:border-none print:shadow-none">
|
||||
<Receipt data={data} />
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -65,11 +76,10 @@ function Receipt({ data }: ReceiptProps) {
|
|||
0,
|
||||
);
|
||||
const note =
|
||||
"La presente è non costituisce ricevuta o fattura fiscale.\nRiceverai la fattura elettronica direttamente al tuo indirizzo di posta.";
|
||||
"La presente è non costituisce ricevuta o fattura fiscale ai sensi dell'Art.21, DPR 633/72.\nSeguirà fattura elettronica direttamente al tuo indirizzo di posta.";
|
||||
const footer =
|
||||
"Arcenia Srl. | Tel. +39 0424529869 | Email: arca@infoalloggi.it";
|
||||
return (
|
||||
<Card className="p-4 print:border-none print:shadow-none">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
|
|
@ -114,9 +124,7 @@ function Receipt({ data }: ReceiptProps) {
|
|||
<div>
|
||||
<h3 className="mb-1 font-semibold">Intestatario:</h3>
|
||||
<p className="font-medium">{data.clienteNome}</p>
|
||||
<p className="whitespace-pre-line text-sm">
|
||||
{data.clienteIndirizzo}
|
||||
</p>
|
||||
<p className="whitespace-pre-line text-sm">{data.clienteIndirizzo}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -182,6 +190,5 @@ function Receipt({ data }: ReceiptProps) {
|
|||
<p>{footer}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ export const OrdiniModal = ({
|
|||
) && (
|
||||
<div>
|
||||
<Link
|
||||
href={`/area-riservata/payment-recap/${data.ordine_id}`}
|
||||
href={`/servizio/ricevuta-cortesia/${data.ordine_id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Button variant="outline">
|
||||
|
|
|
|||
|
|
@ -176,7 +176,10 @@ export const PaymentsTable = ({
|
|||
<DropdownMenuRadioGroup
|
||||
onValueChange={(v) => {
|
||||
update({
|
||||
data: { paymentstatus: EnumFromString(v) },
|
||||
data: {
|
||||
paymentstatus: EnumFromString(v),
|
||||
paid_at: v === "Successo" ? new Date() : null,
|
||||
},
|
||||
pagamentoId: data.id,
|
||||
});
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export type PaymentType =
|
||||
| "manual"
|
||||
| "acss_debit"
|
||||
| "affirm"
|
||||
| "afterpay_clearpay"
|
||||
|
|
@ -50,6 +51,8 @@ export type PaymentType =
|
|||
| "zip";
|
||||
export const PaymentMethodToString = (type: PaymentType) => {
|
||||
switch (type) {
|
||||
case "manual":
|
||||
return "Pagamento in sede";
|
||||
case "card":
|
||||
return "Carta di credito";
|
||||
case "sepa_debit":
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { LoadingPage } from "~/components/loading";
|
|||
import { Status500 } from "~/components/status-page";
|
||||
import type { NextPageWithLayout } from "~/pages/_app";
|
||||
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
||||
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||
import { generateSSGHelper } from "~/server/utils/ssgHelper";
|
||||
import { zTestiEStringheStingaId } from "~/server/utils/zod_types";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
|
|
@ -38,6 +38,9 @@ const CondizioniPage: NextPageWithLayout<CondizioniProps> = ({
|
|||
};
|
||||
|
||||
CondizioniPage.getLayout = function getLayout(page) {
|
||||
if (page.props.raw) {
|
||||
return <>{page}</>;
|
||||
}
|
||||
return <AreaRiservataLayout noSidebar>{page}</AreaRiservataLayout>;
|
||||
};
|
||||
|
||||
|
|
@ -45,7 +48,7 @@ export default CondizioniPage;
|
|||
|
||||
export const getServerSideProps = (async (context) => {
|
||||
const id = context.params?.stringaId;
|
||||
|
||||
const raw = context.query?.raw;
|
||||
if (typeof id !== "string") {
|
||||
console.error("Error: stringaId is not a string");
|
||||
return {
|
||||
|
|
@ -67,18 +70,17 @@ export const getServerSideProps = (async (context) => {
|
|||
};
|
||||
}
|
||||
|
||||
const access_token = context.req.cookies.access_token;
|
||||
|
||||
const helper = await TrpcAuthedFetchingIstance({ access_token });
|
||||
const helper = generateSSGHelper();
|
||||
if (helper) {
|
||||
await helper.trpc.strings.getStringa.prefetch({
|
||||
await helper.strings.getStringa.prefetch({
|
||||
stringaId: parsed.data,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
raw: raw === "true",
|
||||
stringaId: parsed.data,
|
||||
trpcState: helper.trpc.dehydrate(),
|
||||
trpcState: helper.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,15 +5,17 @@ import { LoadingPage } from "~/components/loading";
|
|||
import { Status500 } from "~/components/status-page";
|
||||
import type { NextPageWithLayout } from "~/pages/_app";
|
||||
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
||||
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||
import { generateSSGHelper } from "~/server/utils/ssgHelper";
|
||||
import { zOrdineId } from "~/server/utils/zod_types";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
type PagamentoRecapProps = {
|
||||
raw: boolean;
|
||||
ordineId: OrdiniOrdineId;
|
||||
};
|
||||
|
||||
const PaymentRecapPage: NextPageWithLayout<PagamentoRecapProps> = ({
|
||||
raw,
|
||||
ordineId,
|
||||
}: PagamentoRecapProps) => {
|
||||
const { data, isLoading } = api.servizio.getOrdineRicevutaData.useQuery({
|
||||
|
|
@ -28,12 +30,15 @@ const PaymentRecapPage: NextPageWithLayout<PagamentoRecapProps> = ({
|
|||
}
|
||||
return (
|
||||
<div className="mx-auto w-full p-4">
|
||||
<ReceiptGenerator data={data} />
|
||||
<ReceiptGenerator data={data} raw={raw} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PaymentRecapPage.getLayout = function getLayout(page) {
|
||||
if (page.props.raw) {
|
||||
return <>{page}</>;
|
||||
}
|
||||
return <AreaRiservataLayout noSidebar>{page}</AreaRiservataLayout>;
|
||||
};
|
||||
|
||||
|
|
@ -41,7 +46,7 @@ export default PaymentRecapPage;
|
|||
|
||||
export const getServerSideProps = (async (context) => {
|
||||
const id = context.params?.ordineId;
|
||||
|
||||
const raw = context.query?.raw;
|
||||
if (typeof id !== "string") {
|
||||
console.error("Error: ordineId is not a string");
|
||||
return {
|
||||
|
|
@ -63,18 +68,17 @@ export const getServerSideProps = (async (context) => {
|
|||
};
|
||||
}
|
||||
|
||||
const access_token = context.req.cookies.access_token;
|
||||
|
||||
const helper = await TrpcAuthedFetchingIstance({ access_token });
|
||||
const helper = generateSSGHelper();
|
||||
if (helper) {
|
||||
await helper.trpc.servizio.getOrdineRicevutaData.prefetch({
|
||||
await helper.servizio.getOrdineRicevutaData.prefetch({
|
||||
ordineId: parsed.data,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
raw: raw === "true",
|
||||
ordineId: parsed.data,
|
||||
trpcState: helper.trpc.dehydrate(),
|
||||
trpcState: helper.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -9,8 +9,12 @@ import { signToken, verifyToken } from "~/server/auth/jwt";
|
|||
export const authProxy: ProxyFn = async (req: NextRequest) => {
|
||||
const path = req.nextUrl.pathname;
|
||||
|
||||
const isOverridableRoute =
|
||||
path.startsWith("/area-riservata/admin/scheda-annuncio-stampa/") ||
|
||||
path.startsWith("/servizio/condizioni/") ||
|
||||
path.startsWith("/servizio/ricevuta-cortesia/");
|
||||
// permette a puppeteer di accedere alla pagina di generazione PDF bypassando l'autenticazione normale
|
||||
if (path.startsWith("/area-riservata/admin/scheda-annuncio-stampa/")) {
|
||||
if (isOverridableRoute) {
|
||||
const overrideToken = req.cookies.get(
|
||||
TOKEN_CONFIG.OVERRIDE_TOKEN_COOKIE_NAME,
|
||||
)?.value;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica";
|
|||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
overridableProcedure,
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
} from "~/server/api/trpc";
|
||||
|
|
@ -169,14 +170,14 @@ export const servizioRouter = createTRPCRouter({
|
|||
.query(async ({ input }) => {
|
||||
return await getOrdineById(input.ordineId);
|
||||
}),
|
||||
getOrdineRicevutaData: protectedProcedure
|
||||
getOrdineRicevutaData: overridableProcedure
|
||||
.input(
|
||||
z.object({
|
||||
ordineId: zOrdineId,
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
return await getOrdineRicevutaData({ ctx, ordineId: input.ordineId });
|
||||
.query(async ({ input }) => {
|
||||
return await getOrdineRicevutaData({ ordineId: input.ordineId });
|
||||
}),
|
||||
getServizioOrdini: protectedProcedure
|
||||
.input(
|
||||
|
|
|
|||
|
|
@ -14,11 +14,10 @@ export const testRouter = createTRPCRouter({
|
|||
console.error("test console.error");
|
||||
throw new Error("This is a test error");
|
||||
}),
|
||||
|
||||
});
|
||||
/*
|
||||
PROTECTED API CALL EXAMPLE
|
||||
curl --location 'localhost:3000/api/trpc/test.testApi' \
|
||||
--header 'Authorization: Basic aW5mb2FsbG9nZ2ktYWRtaW46Y3lZazI0dTlmUm41RHNxakZXcFRWOEdLQk53aFF2RXRiTW1IeEplVTczWlNhWHJkQ0xBUGc2TnREa0M0M1pRYTlkVktIYnhNdjdmV3B5NjVnRm1Bc0VSZVh3dVBZVEw4MmpKblNVaHJ6Y3FHa3F2c0hQeXhtRUFGellWU250R2VRVHIyNWRKYjNVTkNNdVJwZ3dhRFc2OVpoYzdLajhMZkI0WkNIUjlrWWZXVnNHcng3dHlncWR2YzgyU0ZYajU0TmhucG02UEVCTGJLVXVhTVRRSmVBM3dEZ1BTQ1h3RFZtaG5lZDZKOFpRM3F6TnNyNWJCdUx0SHh2RlRBS0d5NzRXMmZZOXBja2pNUlVFSllSa1ZQWndidkJ0VEt5MkY4VUxBc2V1bnpXajdHNWQ2UTNFeGZtTkNYcWc0TURhaEg5Y1NydVh5V3hMTkFzR1FUUFNGaEQ5SFlWekIzTWVnMnJrNUN2YWNmcThtZFo2dFVFNEtScHdibjdqSkFldjdXUmJWZ1BkbmNNTEt4d204eVQ5cFFaNEROVWthWWo2enRYZmhzQkhxRjVFckN1MzJHY2piM0g2S3h2V3N1OXA1ZmtQcmg3WkFSWE1MRGRTODJlbkZVNENxd3p0VGF5RW1HTlZnWUpC'
|
||||
|
||||
*/
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import {
|
|||
} from "~/server/controllers/user.controller";
|
||||
import { db } from "~/server/db";
|
||||
import { getClientProfilo } from "~/server/services/user.service";
|
||||
import { checkCtxSession } from "~/server/utils/ctxChecker";
|
||||
import { zUserId } from "~/server/utils/zod_types";
|
||||
|
||||
export const usersRouter = createTRPCRouter({
|
||||
|
|
@ -27,8 +26,8 @@ export const usersRouter = createTRPCRouter({
|
|||
id: zUserId,
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return await blockUserHandler({ ctx, id: input.id });
|
||||
.mutation(async ({ input }) => {
|
||||
return await blockUserHandler(input.id);
|
||||
}),
|
||||
deleteUser: adminProcedure
|
||||
.input(
|
||||
|
|
@ -36,8 +35,8 @@ export const usersRouter = createTRPCRouter({
|
|||
id: zUserId,
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return await deleteUserHandler({ ctx, id: input.id });
|
||||
.mutation(async ({ input }) => {
|
||||
return await deleteUserHandler(input.id);
|
||||
}),
|
||||
editUser: protectedProcedure
|
||||
.input(
|
||||
|
|
@ -46,8 +45,7 @@ export const usersRouter = createTRPCRouter({
|
|||
data: z.custom<UsersUpdate>(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
checkCtxSession({ ctx });
|
||||
.mutation(async ({ input }) => {
|
||||
return await editAccountHandler({ ...input });
|
||||
}),
|
||||
editUserAnagrafica: protectedProcedure
|
||||
|
|
|
|||
|
|
@ -141,6 +141,33 @@ const enforceUserIsAuthed = t.middleware(({ ctx, next, path }) => {
|
|||
message: `User is not logged, error in path: ${path}`,
|
||||
});
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
// infers the `session` as non-nullable
|
||||
session: { ...ctx.session },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const enforceUserIsAuthedOrOverride = t.middleware(({ ctx, next, path }) => {
|
||||
const overrideToken =
|
||||
ctx.req?.cookies[TOKEN_CONFIG.OVERRIDE_TOKEN_COOKIE_NAME];
|
||||
if (overrideToken && overrideToken === env.EXP_API_PASS) {
|
||||
return next({
|
||||
ctx: {
|
||||
// infers the `session` as non-nullable
|
||||
session: { ...ctx.session },
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!ctx.session) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: `User is not logged, error in path: ${path}`,
|
||||
});
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
// infers the `session` as non-nullable
|
||||
|
|
@ -158,6 +185,7 @@ const enforceUserIsAuthed = t.middleware(({ ctx, next, path }) => {
|
|||
* @see https://trpc.io/docs/procedures
|
||||
*/
|
||||
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
|
||||
export const overridableProcedure = t.procedure.use(enforceUserIsAuthedOrOverride);
|
||||
|
||||
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
||||
const enforceUserIsAdmin = t.middleware(({ ctx, next, path }) => {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type {
|
|||
OrdiniOrdineId,
|
||||
OrdiniUpdate,
|
||||
} from "~/schemas/public/Ordini";
|
||||
import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum";
|
||||
import type { UsersId } from "~/schemas/public/Users";
|
||||
import { db, type Querier } from "~/server/db";
|
||||
|
||||
|
|
@ -67,11 +68,36 @@ export const getOrdineById = async (ordineId: OrdiniOrdineId) => {
|
|||
|
||||
export const createOrdine = async (data: NewOrdini) => {
|
||||
try {
|
||||
return await db
|
||||
return await db.transaction().execute(async (tx) => {
|
||||
const prezzo = await tx
|
||||
.selectFrom("prezziario")
|
||||
.select(["prezzo_cent", "nome_it"])
|
||||
.where("prezziario.idprezziario", "=", data.packid)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
const order = await tx
|
||||
.insertInto("ordini")
|
||||
.values(data)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
await tx
|
||||
.insertInto("payments")
|
||||
.values({
|
||||
ordine_id: order.ordine_id,
|
||||
servizio_id: order.servizio_id,
|
||||
userid: order.userid,
|
||||
amount_cent: prezzo.prezzo_cent,
|
||||
paymentname: prezzo.nome_it,
|
||||
paymentmethod: "manual",
|
||||
paymentstatus: data.isActive
|
||||
? PaymentStatusEnum.success
|
||||
: PaymentStatusEnum.processing,
|
||||
paid_at: data.isActive ? new Date() : null,
|
||||
})
|
||||
.execute();
|
||||
|
||||
return order;
|
||||
});
|
||||
} catch (e) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
|||
import type { Users, UsersId } from "~/schemas/public/Users";
|
||||
import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica";
|
||||
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
|
||||
import type { Context } from "~/server/api/trpc";
|
||||
import {
|
||||
addEventToQueue,
|
||||
canSendNotification,
|
||||
|
|
@ -882,8 +881,8 @@ export const getAllServizioAnnunci = async (
|
|||
withVideos(),
|
||||
])
|
||||
//ignora annunci che non sono disponibili
|
||||
.where("annunci.web", "=", true)
|
||||
.where("annunci.stato", "!=", "Sospeso")
|
||||
//.where("annunci.web", "=", true)
|
||||
//.where("annunci.stato", "!=", "Sospeso")
|
||||
.orderBy("servizio_annunci.user_confirmed_at", (ob) =>
|
||||
ob.asc().nullsLast(),
|
||||
)
|
||||
|
|
@ -1035,8 +1034,8 @@ export const getServizioDataById = async (
|
|||
withVideos(),
|
||||
])
|
||||
//ignora annunci che non sono disponibili
|
||||
.where("annunci.web", "=", true)
|
||||
.where("annunci.stato", "!=", "Sospeso")
|
||||
//.where("annunci.web", "=", true)
|
||||
//.where("annunci.stato", "!=", "Sospeso")
|
||||
.orderBy("servizio_annunci.user_confirmed_at", (ob) =>
|
||||
ob.asc().nullsLast(),
|
||||
)
|
||||
|
|
@ -2025,10 +2024,8 @@ export const getCompatibileAnnunci = async ({
|
|||
|
||||
export const getOrdineRicevutaData = async ({
|
||||
ordineId,
|
||||
ctx,
|
||||
}: {
|
||||
ordineId: OrdiniOrdineId;
|
||||
ctx: Context;
|
||||
}) => {
|
||||
try {
|
||||
const data = await db
|
||||
|
|
@ -2070,19 +2067,6 @@ export const getOrdineRicevutaData = async ({
|
|||
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
if (!ctx.session) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "User not authenticated",
|
||||
});
|
||||
}
|
||||
if (!ctx.session.isAdmin && data.userid !== ctx.session.id) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You do not have permission to access this ordine",
|
||||
});
|
||||
}
|
||||
|
||||
if (!data || !data.user || !data.anagrafica) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
|
|
|
|||
|
|
@ -4,10 +4,15 @@ import stripe from "~/lib/stripe";
|
|||
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
|
||||
import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum";
|
||||
import type { Payments, PaymentsId } from "~/schemas/public/Payments";
|
||||
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
||||
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
||||
import { db } from "~/server/db";
|
||||
import { NewMail } from "~/server/services/mailer";
|
||||
import { getUser } from "~/server/services/user.service";
|
||||
import {
|
||||
genPdfCondizioniBase64,
|
||||
genPdfRicevutaCortesiaBase64,
|
||||
} from "../services/puppeteer.service";
|
||||
|
||||
export const stripeHealthCheck = async () => {
|
||||
try {
|
||||
|
|
@ -162,23 +167,31 @@ export const whIntentSucceededHandler = async ({
|
|||
})
|
||||
.execute();
|
||||
|
||||
//SERVIZIO ATTIVATO
|
||||
await NewMail({
|
||||
template: {
|
||||
mailType: "generic",
|
||||
|
||||
props: {
|
||||
link: {
|
||||
href: `${env.BASE_URL}/area-riservata/payment-recap/${ordine.ordine_id}`,
|
||||
label: "Vai alla ricevuta",
|
||||
},
|
||||
noreply: true,
|
||||
testo: `Il pagamento dell'acconto per il servizio ${ordine.packid} è stato confermato.`,
|
||||
title: "Pagamento Confermato ",
|
||||
},
|
||||
mailType: "servizioAttivato",
|
||||
},
|
||||
mail: {
|
||||
subject: "Pagamento Confermato - Acconto Infoalloggi.it",
|
||||
to: utente.email,
|
||||
attachments: [
|
||||
{
|
||||
filename: `condizioni_contrattuali_${ordine.created_at.toISOString()}.pdf`,
|
||||
//ATTENTION: hardcoded condizioni key
|
||||
content: await genPdfCondizioniBase64(
|
||||
"CONDIZIONI_CERCHI" as TestiEStringheStingaId,
|
||||
),
|
||||
encoding: "base64",
|
||||
contentType: "application/pdf",
|
||||
},
|
||||
{
|
||||
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
|
||||
content: await genPdfRicevutaCortesiaBase64(ordine.ordine_id),
|
||||
encoding: "base64",
|
||||
contentType: "application/pdf",
|
||||
},
|
||||
],
|
||||
},
|
||||
userId: utente.id,
|
||||
});
|
||||
|
|
@ -196,20 +209,19 @@ export const whIntentSucceededHandler = async ({
|
|||
.execute();
|
||||
await NewMail({
|
||||
template: {
|
||||
mailType: "generic",
|
||||
props: {
|
||||
link: {
|
||||
href: `${env.BASE_URL}/area-riservata/payment-recap/${ordine.ordine_id}`,
|
||||
label: "Vai alla ricevuta",
|
||||
},
|
||||
noreply: true,
|
||||
testo: `Il pagamento del saldo per il servizio ${ordine.packid} è stato confermato.`,
|
||||
title: "Pagamento Confermato ",
|
||||
},
|
||||
mailType: "pagamentoConferma",
|
||||
},
|
||||
mail: {
|
||||
subject: "Pagamento Confermato - Saldo Infoalloggi.it",
|
||||
to: utente.email,
|
||||
attachments: [
|
||||
{
|
||||
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
|
||||
content: await genPdfRicevutaCortesiaBase64(ordine.ordine_id),
|
||||
encoding: "base64",
|
||||
contentType: "application/pdf",
|
||||
},
|
||||
],
|
||||
},
|
||||
userId: utente.id,
|
||||
});
|
||||
|
|
@ -224,20 +236,19 @@ export const whIntentSucceededHandler = async ({
|
|||
.execute();
|
||||
await NewMail({
|
||||
template: {
|
||||
mailType: "generic",
|
||||
props: {
|
||||
link: {
|
||||
href: `${env.BASE_URL}/area-riservata/payment-recap/${ordine.ordine_id}`,
|
||||
label: "Vai alla ricevuta",
|
||||
},
|
||||
noreply: true,
|
||||
testo: `Il pagamento per la consulenza è stato confermato.`,
|
||||
title: "Pagamento Confermato ",
|
||||
},
|
||||
mailType: "pagamentoConferma",
|
||||
},
|
||||
mail: {
|
||||
subject: "Pagamento Confermato - Consulenza Infoalloggi.it",
|
||||
to: utente.email,
|
||||
attachments: [
|
||||
{
|
||||
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
|
||||
content: await genPdfRicevutaCortesiaBase64(ordine.ordine_id),
|
||||
encoding: "base64",
|
||||
contentType: "application/pdf",
|
||||
},
|
||||
],
|
||||
},
|
||||
userId: utente.id,
|
||||
});
|
||||
|
|
@ -247,13 +258,18 @@ export const whIntentSucceededHandler = async ({
|
|||
await NewMail({
|
||||
template: {
|
||||
mailType: "pagamentoConferma",
|
||||
props: {
|
||||
receiptHref: `${env.BASE_URL}/area-riservata/payment-recap/${ordine.ordine_id}`,
|
||||
},
|
||||
},
|
||||
mail: {
|
||||
subject: "Pagamento Confermato",
|
||||
subject: "Pagamento Confermato - Infoalloggi.it",
|
||||
to: utente.email,
|
||||
attachments: [
|
||||
{
|
||||
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
|
||||
content: await genPdfRicevutaCortesiaBase64(ordine.ordine_id),
|
||||
encoding: "base64",
|
||||
contentType: "application/pdf",
|
||||
},
|
||||
],
|
||||
},
|
||||
userId: utente.id,
|
||||
});
|
||||
|
|
@ -294,7 +310,7 @@ export const whIntentFailedHandler = async ({
|
|||
mailType: "pagamentoErrore",
|
||||
},
|
||||
mail: {
|
||||
subject: "Pagamento Fallito",
|
||||
subject: "Pagamento Fallito - Infoalloggi.it",
|
||||
to: utente.email,
|
||||
},
|
||||
userId: utente.id,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { TRPCError } from "@trpc/server";
|
||||
import type { UsersId, UsersUpdate } from "~/schemas/public/Users";
|
||||
import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica";
|
||||
import type { Context } from "~/server/api/trpc";
|
||||
import { db, type Querier } from "~/server/db";
|
||||
import { checkCtxAdmin } from "~/server/utils/ctxChecker";
|
||||
|
||||
export const editAccountHandler = async ({
|
||||
id,
|
||||
|
|
@ -92,15 +90,7 @@ export const editAnagraficaHandler = async ({
|
|||
}
|
||||
};
|
||||
|
||||
export const deleteUserHandler = async ({
|
||||
ctx,
|
||||
id,
|
||||
}: {
|
||||
ctx: Context;
|
||||
id: UsersId;
|
||||
}) => {
|
||||
checkCtxAdmin({ ctx });
|
||||
|
||||
export const deleteUserHandler = async (id: UsersId) => {
|
||||
try {
|
||||
await db.deleteFrom("users").where("id", "=", id).execute();
|
||||
return { success: true };
|
||||
|
|
@ -112,14 +102,7 @@ export const deleteUserHandler = async ({
|
|||
}
|
||||
};
|
||||
|
||||
export const blockUserHandler = async ({
|
||||
ctx,
|
||||
id,
|
||||
}: {
|
||||
ctx: Context;
|
||||
id: UsersId;
|
||||
}) => {
|
||||
checkCtxAdmin({ ctx });
|
||||
export const blockUserHandler = async (id: UsersId) => {
|
||||
return await db.transaction().execute(async (trx) => {
|
||||
return await trx
|
||||
.updateTable("users")
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import EmailSchedaContatto, {
|
|||
} from "emails/scheda-annuncio";
|
||||
import type { ContattiConScheda_NewMail } from "emails/scheda-contatti";
|
||||
import EmailContattiConScheda from "emails/scheda-contatti";
|
||||
import type { ServizioAttivato_NewMail } from "emails/servizio-attivato";
|
||||
import ServizioAttivato from "emails/servizio-attivato";
|
||||
import ShareAnnunci, { type ShareAnnunci_NewMail } from "emails/share-annunci";
|
||||
import VerificaEmail, {
|
||||
type VerificaEmail_NewMail,
|
||||
|
|
@ -93,7 +95,8 @@ export type MailsTemplates =
|
|||
| Generic_NewMail
|
||||
| SchedaContatto_NewMail
|
||||
| Invito_NewMail
|
||||
| ContattiConScheda_NewMail;
|
||||
| ContattiConScheda_NewMail
|
||||
| ServizioAttivato_NewMail;
|
||||
|
||||
export const genMail = async ({ ...mail }: MailsTemplates) => {
|
||||
let mailComponent: JSX.Element | null = null;
|
||||
|
|
@ -121,9 +124,7 @@ export const genMail = async ({ ...mail }: MailsTemplates) => {
|
|||
break;
|
||||
|
||||
case "pagamentoConferma":
|
||||
mailComponent = PagamentoConferma({
|
||||
...mail.props,
|
||||
});
|
||||
mailComponent = PagamentoConferma();
|
||||
title = "Conferma pagamento";
|
||||
break;
|
||||
|
||||
|
|
@ -191,6 +192,10 @@ export const genMail = async ({ ...mail }: MailsTemplates) => {
|
|||
});
|
||||
title = "Contatti Con Scheda";
|
||||
break;
|
||||
case "servizioAttivato":
|
||||
mailComponent = ServizioAttivato();
|
||||
title = "Servizio Attivato";
|
||||
break;
|
||||
default:
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { TRPCError } from "@trpc/server";
|
||||
import type Redis from "ioredis";
|
||||
import puppeteer from "puppeteer-core";
|
||||
import puppeteer, { type PDFOptions } from "puppeteer-core";
|
||||
import { env } from "~/env";
|
||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
||||
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
||||
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
||||
import { getKeydbClient } from "~/utils/keydb";
|
||||
import { TOKEN_CONFIG } from "../auth/configs";
|
||||
|
||||
|
|
@ -82,13 +84,17 @@ export const genPdfSchedaAnnuncioBase64 = async (annuncioId: AnnunciId) => {
|
|||
export const genPdfSchedaAnnuncio = async (annuncioId: AnnunciId) => {
|
||||
const keybd = getKeydbClient();
|
||||
const url = `/area-riservata/admin/scheda-annuncio-stampa/${annuncioId}?raw=true`;
|
||||
const options: PDFOptions = { printBackground: true };
|
||||
if (keybd) {
|
||||
const cacheKey = `${SCHEDA_ANNUNCIO_CACHE_PREFIX}${annuncioId}`;
|
||||
const cached = await keybd.get(cacheKey);
|
||||
if (cached) {
|
||||
return new Uint8Array(Buffer.from(cached, "base64"));
|
||||
}
|
||||
const scheda = await puppeteerGenPdf(url);
|
||||
const scheda = await puppeteerGenPdf({
|
||||
url,
|
||||
options,
|
||||
});
|
||||
await keybd.set(
|
||||
cacheKey,
|
||||
Buffer.from(scheda).toString("base64"),
|
||||
|
|
@ -97,10 +103,26 @@ export const genPdfSchedaAnnuncio = async (annuncioId: AnnunciId) => {
|
|||
);
|
||||
return scheda;
|
||||
}
|
||||
return await puppeteerGenPdf(url);
|
||||
return await puppeteerGenPdf({ url, options });
|
||||
};
|
||||
|
||||
export const genPdfCondizioni = async (condizioniId: AnnunciId) => {
|
||||
export const genPdfCondizioniBase64 = async (
|
||||
condizioniId: TestiEStringheStingaId,
|
||||
) => {
|
||||
const pdf = await genPdfCondizioni(condizioniId);
|
||||
const pdfBase64 = Buffer.from(pdf).toString("base64");
|
||||
const base64Content = pdfBase64.includes("base64,")
|
||||
? pdfBase64.split("base64,")[1]
|
||||
: pdfBase64;
|
||||
if (!base64Content) {
|
||||
throw new Error("Failed to convert PDF to base64");
|
||||
}
|
||||
return base64Content;
|
||||
};
|
||||
|
||||
export const genPdfCondizioni = async (
|
||||
condizioniId: TestiEStringheStingaId,
|
||||
) => {
|
||||
const keybd = getKeydbClient();
|
||||
const url = `/servizio/condizioni/${condizioniId}?raw=true`;
|
||||
if (keybd) {
|
||||
|
|
@ -109,7 +131,7 @@ export const genPdfCondizioni = async (condizioniId: AnnunciId) => {
|
|||
if (cached) {
|
||||
return new Uint8Array(Buffer.from(cached, "base64"));
|
||||
}
|
||||
const scheda = await puppeteerGenPdf(url);
|
||||
const scheda = await puppeteerGenPdf({ url });
|
||||
await keybd.set(
|
||||
cacheKey,
|
||||
Buffer.from(scheda).toString("base64"),
|
||||
|
|
@ -118,10 +140,31 @@ export const genPdfCondizioni = async (condizioniId: AnnunciId) => {
|
|||
);
|
||||
return scheda;
|
||||
}
|
||||
return await puppeteerGenPdf(url);
|
||||
return await puppeteerGenPdf({ url });
|
||||
};
|
||||
|
||||
const puppeteerGenPdf = async (url: string) => {
|
||||
export const genPdfRicevutaCortesiaBase64 = async (
|
||||
ordineId: OrdiniOrdineId,
|
||||
) => {
|
||||
const url = `/servizio/ricevuta-cortesia/${ordineId}?raw=true`;
|
||||
const pdf = await puppeteerGenPdf({ url });
|
||||
const pdfBase64 = Buffer.from(pdf).toString("base64");
|
||||
const base64Content = pdfBase64.includes("base64,")
|
||||
? pdfBase64.split("base64,")[1]
|
||||
: pdfBase64;
|
||||
if (!base64Content) {
|
||||
throw new Error("Failed to convert PDF to base64");
|
||||
}
|
||||
return base64Content;
|
||||
};
|
||||
|
||||
const puppeteerGenPdf = async ({
|
||||
url,
|
||||
options,
|
||||
}: {
|
||||
url: string;
|
||||
options?: PDFOptions;
|
||||
}) => {
|
||||
let browser = null;
|
||||
|
||||
try {
|
||||
|
|
@ -155,7 +198,7 @@ const puppeteerGenPdf = async (url: string) => {
|
|||
|
||||
const pdf = await page.pdf({
|
||||
format: "A4",
|
||||
printBackground: true,
|
||||
...options,
|
||||
});
|
||||
return pdf;
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import type { Prettify } from "TypeHelpers.type";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import type { Context, Session } from "~/server/api/trpc";
|
||||
|
||||
export const checkCtx = (props: {
|
||||
res: NextApiResponse | undefined;
|
||||
|
|
@ -11,29 +9,3 @@ export const checkCtx = (props: {
|
|||
if (!req || !res)
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "res o req mancanti" });
|
||||
};
|
||||
type CtxWSession = Prettify<Context & { session: Session }>;
|
||||
|
||||
export const checkCtxSession = ({ ctx }: { ctx: Context }) => {
|
||||
if (!ctx.session) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Utente non autorizzato",
|
||||
});
|
||||
}
|
||||
return ctx as CtxWSession;
|
||||
};
|
||||
|
||||
export const checkCtxAdmin = ({ ctx }: { ctx: Context }) => {
|
||||
const ckd_ctx = checkCtxSession({ ctx });
|
||||
if (!ckd_ctx.session.isAdmin) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Utente non autorizzato",
|
||||
});
|
||||
}
|
||||
return ckd_ctx as Context & {
|
||||
session: Session & {
|
||||
isAdmin: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue