From f45d748c887cea226cf38c696742a68bdf4c16bc Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Thu, 21 Aug 2025 16:05:59 +0400 Subject: [PATCH 1/6] feat: set invoice to "processing" status after being paid. --- src/app/api/webhook/route.ts | 8 ++++++++ src/components/payment-section.tsx | 9 ++++++++- src/server/routers/invoice.ts | 31 ++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/app/api/webhook/route.ts b/src/app/api/webhook/route.ts index d85a340..21460f0 100644 --- a/src/app/api/webhook/route.ts +++ b/src/app/api/webhook/route.ts @@ -167,6 +167,14 @@ export async function POST(req: Request) { } } break; + case "payment.failed": + console.debug("Handling failed"); + if (!isCryptoToFiat) { + await updateRequestStatus(requestId, "pending"); + } else { + await updateRequestStatus(requestId, "offramp_failed"); + } + break; case "request.recurring": await db.transaction(async (tx) => { const originalRequests = await tx diff --git a/src/components/payment-section.tsx b/src/components/payment-section.tsx index 1eafd99..a0e238a 100644 --- a/src/components/payment-section.tsx +++ b/src/components/payment-section.tsx @@ -142,6 +142,9 @@ export function PaymentSection({ serverInvoice }: PaymentSectionProps) { const { mutateAsync: payRequest } = api.invoice.payRequest.useMutation(); const { mutateAsync: sendPaymentIntent } = api.invoice.sendPaymentIntent.useMutation(); + const { mutateAsync: setInvoiceAsProcessing } = + api.invoice.setInvoiceAsProcessing.useMutation(); + const { data: paymentRoutesData, refetch, @@ -371,6 +374,10 @@ export function PaymentSection({ serverInvoice }: PaymentSectionProps) { } else { await handleDirectPayments(paymentData, signer); } + + await setInvoiceAsProcessing({ + id: invoice.id, + }); } catch (error) { console.error("Error : ", error); toast("Payment Failed", { @@ -464,7 +471,7 @@ export function PaymentSection({ serverInvoice }: PaymentSectionProps) { {/* Payment Steps */} - {paymentStatus !== "paid" && ( + {paymentStatus === "pending" && (
{/* Step indicators */}
diff --git a/src/server/routers/invoice.ts b/src/server/routers/invoice.ts index a646fb4..4c66dbf 100644 --- a/src/server/routers/invoice.ts +++ b/src/server/routers/invoice.ts @@ -462,4 +462,35 @@ export const invoiceRouter = router({ return response.data; }), + setInvoiceAsProcessing: publicProcedure + .input( + z.object({ + id: z.string().ulid(), + }), + ) + .mutation(async ({ ctx, input }) => { + const { db } = ctx; + + try { + const invoice = await db.query.requestTable.findFirst({ + where: (requestTable, { eq }) => eq(requestTable.id, input.id), + }); + + if (!invoice) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Invoice not found", + }); + } + + await db + .update(requestTable) + .set({ + status: "processing", + }) + .where(eq(requestTable.id, input.id)); + } catch (error) { + throw toTRPCError(error); + } + }), }); From fd7deba522e9e54de3a5777bb62850fad9ba8cb1 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Mon, 25 Aug 2025 13:36:53 +0400 Subject: [PATCH 2/6] refactor: remove the database update and failed webhook --- src/app/api/webhook/route.ts | 8 -------- src/server/routers/invoice.ts | 7 +------ 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/app/api/webhook/route.ts b/src/app/api/webhook/route.ts index 21460f0..d85a340 100644 --- a/src/app/api/webhook/route.ts +++ b/src/app/api/webhook/route.ts @@ -167,14 +167,6 @@ export async function POST(req: Request) { } } break; - case "payment.failed": - console.debug("Handling failed"); - if (!isCryptoToFiat) { - await updateRequestStatus(requestId, "pending"); - } else { - await updateRequestStatus(requestId, "offramp_failed"); - } - break; case "request.recurring": await db.transaction(async (tx) => { const originalRequests = await tx diff --git a/src/server/routers/invoice.ts b/src/server/routers/invoice.ts index 4c66dbf..ad7e01f 100644 --- a/src/server/routers/invoice.ts +++ b/src/server/routers/invoice.ts @@ -483,12 +483,7 @@ export const invoiceRouter = router({ }); } - await db - .update(requestTable) - .set({ - status: "processing", - }) - .where(eq(requestTable.id, input.id)); + // TODO: set a timebound cache in redis } catch (error) { throw toTRPCError(error); } From fc0c72a28cdf3e48e17b5ee111333e95fc34e477 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Mon, 25 Aug 2025 13:56:27 +0400 Subject: [PATCH 3/6] feat: implement redis based double payment prevention system --- .env.example | 3 ++- src/lib/redis/index.ts | 17 +++++++++++++++++ src/server/routers/invoice.ts | 20 ++++++++++++++++++-- 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 src/lib/redis/index.ts diff --git a/.env.example b/.env.example index 9c6c8f2..a1fc990 100644 --- a/.env.example +++ b/.env.example @@ -12,8 +12,9 @@ NEXT_PUBLIC_API_TERMS_CONDITIONS="https://request.network/api-terms" FEE_PERCENTAGE_FOR_PAYMENT="" FEE_ADDRESS_FOR_PAYMENT="" +REDIS_URL=redis://localhost:7379 # Optional # NEXT_PUBLIC_GTM_ID="" # NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS="" - +INVOICE_PROCESSING_TTL="" diff --git a/src/lib/redis/index.ts b/src/lib/redis/index.ts new file mode 100644 index 0000000..0670c70 --- /dev/null +++ b/src/lib/redis/index.ts @@ -0,0 +1,17 @@ +import Redis from "ioredis"; + +let redis: Redis | null = null; + +export function getRedis(): Redis { + if (!redis) { + redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379", { + lazyConnect: true, + }); + + redis.on("error", (err) => { + console.error("Redis connection error:", err); + }); + } + + return redis; +} diff --git a/src/server/routers/invoice.ts b/src/server/routers/invoice.ts index ad7e01f..1298181 100644 --- a/src/server/routers/invoice.ts +++ b/src/server/routers/invoice.ts @@ -1,5 +1,6 @@ import { apiClient } from "@/lib/axios"; import { toTRPCError } from "@/lib/errors"; +import { getRedis } from "@/lib/redis"; import { invoiceFormSchema } from "@/lib/schemas/invoice"; import { type PaymentDetailsPayers, @@ -280,7 +281,16 @@ export const invoiceRouter = router({ }); } - return invoice; + const redis = getRedis(); + const isProcessing = await redis.get(`processing:${invoice.id}`); + + return { + ...invoice, + status: + isProcessing && invoice.status === "pending" + ? "processing" + : invoice.status, + }; }), payRequest: publicProcedure .input( @@ -483,7 +493,13 @@ export const invoiceRouter = router({ }); } - // TODO: set a timebound cache in redis + const redis = getRedis(); + + await redis.setex( + `processing:${invoice.id}`, + Number(process.env.INVOICE_PROCESSING_TTL) || 60, + "true", + ); } catch (error) { throw toTRPCError(error); } From 82c25f2a064c8bfa89ebbea92ff779e574f9ae88 Mon Sep 17 00:00:00 2001 From: Aimen Sahnoun Date: Mon, 25 Aug 2025 14:00:00 +0400 Subject: [PATCH 4/6] refactor: add error handling for status update API call --- src/components/payment-section.tsx | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/components/payment-section.tsx b/src/components/payment-section.tsx index a0e238a..9025ab7 100644 --- a/src/components/payment-section.tsx +++ b/src/components/payment-section.tsx @@ -375,9 +375,18 @@ export function PaymentSection({ serverInvoice }: PaymentSectionProps) { await handleDirectPayments(paymentData, signer); } - await setInvoiceAsProcessing({ - id: invoice.id, - }); + try { + await setInvoiceAsProcessing({ + id: invoice.id, + }); + } catch (statusError) { + console.error("Status update failed:", statusError); + toast("Payment Successful", { + description: + "Payment confirmed but status update failed. Please refresh.", + }); + return; // Don't reset to idle + } } catch (error) { console.error("Error : ", error); toast("Payment Failed", { @@ -655,11 +664,7 @@ export function PaymentSection({ serverInvoice }: PaymentSectionProps) {