Skip to content

Commit e251ce9

Browse files
Cover escrow webhook state guards
1 parent 0684fc8 commit e251ce9

1 file changed

Lines changed: 131 additions & 0 deletions

File tree

src/app/api/payments/coinpayportal/webhook/route.test.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,137 @@ describe("POST /api/payments/coinpayportal/webhook", () => {
547547
expect(notifChain.insert).not.toHaveBeenCalled();
548548
});
549549

550+
it("escrow.released does not release a pending payment escrow", async () => {
551+
const payload = makeWebhookPayload("escrow.released", {
552+
payment_id: "cp-escrow-pending",
553+
status: "released",
554+
});
555+
556+
const escrowFetchChain = chainResult({
557+
data: {
558+
id: "escrow-pending",
559+
coinpay_escrow_id: "cp-escrow-pending",
560+
status: "pending_payment",
561+
application_id: "app-escrow",
562+
gig_id: "gig-escrow",
563+
worker_id: "worker-escrow",
564+
poster_id: "poster-escrow",
565+
amount_usd: 20,
566+
},
567+
error: null,
568+
});
569+
const escrowUpdateChain = chainResult({ data: null, error: null });
570+
const appChain = chainResult({ data: null, error: null });
571+
let escrowCalls = 0;
572+
573+
mockFrom.mockImplementation((table: string) => {
574+
if (table === "gig_escrows") {
575+
escrowCalls += 1;
576+
return escrowCalls === 1 ? escrowFetchChain : escrowUpdateChain;
577+
}
578+
if (table === "applications") return appChain;
579+
return chainResult({ data: null, error: null });
580+
});
581+
582+
const res = await POST(makeRequest(payload, WEBHOOK_SECRET));
583+
expect(res.status).toBe(200);
584+
expect(escrowUpdateChain.update).not.toHaveBeenCalled();
585+
expect(appChain.update).not.toHaveBeenCalled();
586+
});
587+
588+
it("escrow.released transitions a funded escrow and completes the application", async () => {
589+
const payload = makeWebhookPayload("escrow.released", {
590+
payment_id: "cp-escrow-funded",
591+
status: "released",
592+
});
593+
594+
const escrowFetchChain = chainResult({
595+
data: {
596+
id: "escrow-funded",
597+
coinpay_escrow_id: "cp-escrow-funded",
598+
status: "funded",
599+
application_id: "app-escrow",
600+
gig_id: "gig-escrow",
601+
worker_id: "worker-escrow",
602+
poster_id: "poster-escrow",
603+
amount_usd: 20,
604+
},
605+
error: null,
606+
});
607+
const escrowUpdateChain = chainResult({ data: { id: "escrow-funded" }, error: null });
608+
const appChain = chainResult({ data: null, error: null });
609+
let escrowCalls = 0;
610+
611+
mockFrom.mockImplementation((table: string) => {
612+
if (table === "gig_escrows") {
613+
escrowCalls += 1;
614+
return escrowCalls === 1 ? escrowFetchChain : escrowUpdateChain;
615+
}
616+
if (table === "applications") return appChain;
617+
return chainResult({ data: null, error: null });
618+
});
619+
620+
const res = await POST(makeRequest(payload, WEBHOOK_SECRET));
621+
expect(res.status).toBe(200);
622+
expect(escrowUpdateChain.update).toHaveBeenCalledWith(
623+
expect.objectContaining({ status: "released" })
624+
);
625+
expect(escrowUpdateChain.eq).toHaveBeenCalledWith("status", "funded");
626+
expect(appChain.update).toHaveBeenCalledWith(
627+
expect.objectContaining({ status: "completed" })
628+
);
629+
});
630+
631+
it("escrow.refunded transitions a funded escrow and notifies the poster", async () => {
632+
const payload = makeWebhookPayload("escrow.refunded", {
633+
payment_id: "cp-escrow-funded",
634+
status: "refunded",
635+
});
636+
637+
const escrowFetchChain = chainResult({
638+
data: {
639+
id: "escrow-funded",
640+
coinpay_escrow_id: "cp-escrow-funded",
641+
status: "funded",
642+
application_id: "app-escrow",
643+
gig_id: "gig-escrow",
644+
worker_id: "worker-escrow",
645+
poster_id: "poster-escrow",
646+
amount_usd: 20,
647+
},
648+
error: null,
649+
});
650+
const escrowUpdateChain = chainResult({ data: { id: "escrow-funded" }, error: null });
651+
const notifChain = chainResult({ data: null, error: null });
652+
let escrowCalls = 0;
653+
654+
mockFrom.mockImplementation((table: string) => {
655+
if (table === "gig_escrows") {
656+
escrowCalls += 1;
657+
return escrowCalls === 1 ? escrowFetchChain : escrowUpdateChain;
658+
}
659+
if (table === "notifications") return notifChain;
660+
return chainResult({ data: null, error: null });
661+
});
662+
663+
const res = await POST(makeRequest(payload, WEBHOOK_SECRET));
664+
expect(res.status).toBe(200);
665+
expect(escrowUpdateChain.update).toHaveBeenCalledWith(
666+
expect.objectContaining({ status: "refunded" })
667+
);
668+
expect(escrowUpdateChain.in).toHaveBeenCalledWith("status", [
669+
"pending_payment",
670+
"funded",
671+
"disputed",
672+
]);
673+
expect(notifChain.insert).toHaveBeenCalledWith(
674+
expect.objectContaining({
675+
user_id: "poster-escrow",
676+
title: "Escrow refunded",
677+
})
678+
);
679+
});
680+
550681
it("escrow.refunded does not override an already released escrow", async () => {
551682
const payload = makeWebhookPayload("escrow.refunded", {
552683
payment_id: "cp-escrow-released",

0 commit comments

Comments
 (0)