Repository navigation
The DoorDash skin: rendi's first non-analytics capability #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
11cc676
8803b81
d242e75
0bd4a35
c8c0e0a
e6b8aac
6ab35a4
21acccb
d918ca0
0a7e38f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { eq } from "drizzle-orm"; | ||
| import { getDb } from "@/lib/db/index"; | ||
| import { ddApprovals } from "@/lib/db/schema"; | ||
| import { voidApproval } from "@/lib/rendi/doordash-approval"; | ||
| import { setCartStatus } from "@/lib/rendi/doordash-db"; | ||
|
|
||
| // The card's cancel button: voids an unconsumed approval and reopens | ||
| // the cart for editing. A consumed approval already became an order; | ||
| // cancelling here must never rewind that cart. | ||
| export async function POST( | ||
| _request: Request, | ||
| { params }: { params: Promise<{ id: string }> }, | ||
| ) { | ||
| const { id } = await params; | ||
| const approvalId = Number(id); | ||
| if (!Number.isInteger(approvalId)) { | ||
| return Response.json({ error: "bad approval" }, { status: 400 }); | ||
| } | ||
| const [row] = await getDb() | ||
| .select({ | ||
| cartUuid: ddApprovals.cartUuid, | ||
| consumedAt: ddApprovals.consumedAt, | ||
| }) | ||
| .from(ddApprovals) | ||
| .where(eq(ddApprovals.id, approvalId)); | ||
| if (!row) | ||
| return Response.json({ error: "no such approval" }, { status: 404 }); | ||
| if (row.consumedAt) { | ||
| return Response.json({ error: "already used" }, { status: 409 }); | ||
| } | ||
| await voidApproval(approvalId); | ||
| await setCartStatus(row.cartUuid, "open"); | ||
|
Comment on lines
+31
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If submission consumes the approval after this initial read but before Useful? React with 👍 / 👎. |
||
| return Response.json({ ok: true }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { eq } from "drizzle-orm"; | ||
| import { getDb } from "@/lib/db/index"; | ||
| import { ddApprovals } from "@/lib/db/schema"; | ||
|
|
||
| export type ApprovalStatus = | ||
| | "waiting" | ||
| | "verified" | ||
| | "consumed" | ||
| | "voided" | ||
| | "expired"; | ||
|
|
||
| // The card re-renders from the transcript on every reload, so it asks | ||
| // the row where things actually stand before offering a code input. | ||
| export async function GET( | ||
| _request: Request, | ||
| { params }: { params: Promise<{ id: string }> }, | ||
| ) { | ||
| const { id } = await params; | ||
| const approvalId = Number(id); | ||
| if (!Number.isInteger(approvalId)) { | ||
| return Response.json({ error: "bad approval" }, { status: 400 }); | ||
| } | ||
| const [row] = await getDb() | ||
| .select({ | ||
| consumedAt: ddApprovals.consumedAt, | ||
| voidedAt: ddApprovals.voidedAt, | ||
| verifiedAt: ddApprovals.verifiedAt, | ||
| expiresAt: ddApprovals.expiresAt, | ||
| }) | ||
| .from(ddApprovals) | ||
| .where(eq(ddApprovals.id, approvalId)); | ||
| if (!row) | ||
| return Response.json({ error: "no such approval" }, { status: 404 }); | ||
| const status: ApprovalStatus = row.consumedAt | ||
| ? "consumed" | ||
| : row.voidedAt | ||
| ? "voided" | ||
| : row.verifiedAt | ||
| ? "verified" | ||
| : row.expiresAt.getTime() < Date.now() | ||
| ? "expired" | ||
| : "waiting"; | ||
| return Response.json({ status, expiresAt: row.expiresAt.toISOString() }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { eq } from "drizzle-orm"; | ||
| import { z } from "zod"; | ||
| import { getDb } from "@/lib/db/index"; | ||
| import { ddApprovals } from "@/lib/db/schema"; | ||
| import { verifyApproval } from "@/lib/rendi/doordash-approval"; | ||
| import { sendSessionText } from "@/lib/rendi/nudge"; | ||
|
|
||
| // The code's only door. Deterministic verification, no model anywhere | ||
| // in the path; on success the session inbox wakes the agent, the | ||
| // three-writers pattern doing what it was born for. | ||
|
|
||
| const bodySchema = z.object({ code: z.string().min(1).max(12) }); | ||
|
|
||
| export async function POST( | ||
| request: Request, | ||
| { params }: { params: Promise<{ id: string }> }, | ||
| ) { | ||
| const { id } = await params; | ||
| const approvalId = Number(id); | ||
| if (!Number.isInteger(approvalId)) { | ||
| return Response.json({ error: "bad approval" }, { status: 400 }); | ||
| } | ||
| const parsed = bodySchema.safeParse(await request.json().catch(() => null)); | ||
| if (!parsed.success) { | ||
| return Response.json({ error: "bad code" }, { status: 400 }); | ||
| } | ||
| const result = await verifyApproval(approvalId, parsed.data.code); | ||
| if (!result.ok) { | ||
| return Response.json(result, { status: 400 }); | ||
| } | ||
| const [row] = await getDb() | ||
| .select({ conversationId: ddApprovals.conversationId }) | ||
| .from(ddApprovals) | ||
| .where(eq(ddApprovals.id, approvalId)); | ||
| if (row) { | ||
| await sendSessionText( | ||
| row.conversationId, | ||
| `[order approved, approval ${approvalId}] The owner entered the code. Place the order with doordash-submit.`, | ||
| ); | ||
|
Comment on lines
+36
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The code is marked verified before this external session send. If Trigger is temporarily unavailable, the route returns an error but the durable row remains verified; on reload the card sees AGENTS.md reference: AGENTS.md:L17-L18 Useful? React with 👍 / 👎. |
||
| } | ||
| return Response.json({ ok: true }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import { runs, tasks } from "@trigger.dev/sdk"; | ||
| import { z } from "zod"; | ||
| import { getCartSnapshot } from "@/lib/rendi/doordash-db"; | ||
| import type { ddCartExec } from "@/trigger/dd-cart-exec"; | ||
|
|
||
| // The card's touch edits. The dd-cli binary lives in the worker, so the | ||
| // route triggers the exec task and waits the run out; the fresh snapshot | ||
| // comes back for the card to adopt. Actor is the user by construction: | ||
| // this route only ever runs from the browser's hand. | ||
|
|
||
| const opSchema = z.discriminatedUnion("kind", [ | ||
| z.object({ | ||
| kind: z.literal("set-quantity"), | ||
| lineId: z.string(), | ||
| quantity: z.number().int().min(0).max(50), | ||
| }), | ||
| z.object({ kind: z.literal("remove-line"), lineId: z.string() }), | ||
| z.object({ | ||
| kind: z.literal("set-tip"), | ||
| tipCents: z.number().int().min(0).max(50_000), | ||
| }), | ||
| z.object({ | ||
| kind: z.literal("set-fulfillment"), | ||
| fulfillment: z.enum(["delivery", "pickup"]), | ||
| }), | ||
| z.object({ kind: z.literal("preview") }), | ||
| ]); | ||
|
|
||
| const POLL_MS = 800; | ||
| const DEADLINE_MS = 60_000; | ||
|
|
||
| export async function POST( | ||
| request: Request, | ||
| { params }: { params: Promise<{ uuid: string }> }, | ||
| ) { | ||
| const { uuid } = await params; | ||
| const snapshot = await getCartSnapshot(uuid); | ||
| if (!snapshot) { | ||
| return Response.json({ error: "unknown cart" }, { status: 404 }); | ||
| } | ||
| const parsed = opSchema.safeParse(await request.json()); | ||
| if (!parsed.success) { | ||
| return Response.json({ error: "malformed op" }, { status: 400 }); | ||
| } | ||
| const handle = await tasks.trigger<typeof ddCartExec>("rendi-dd-cart-exec", { | ||
| cartUuid: uuid, | ||
| conversationId: snapshot.conversationId, | ||
| op: parsed.data, | ||
| }); | ||
| const startedAt = Date.now(); | ||
| while (Date.now() - startedAt < DEADLINE_MS) { | ||
| const run = await runs.retrieve(handle.id); | ||
| if (run.status === "COMPLETED") { | ||
| const fresh = await getCartSnapshot(uuid); | ||
| return Response.json({ ok: true, cart: fresh ?? null }); | ||
|
Comment on lines
+53
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
After a browser edit completes, the fresh cart is returned only to that HTTP caller. The task writes no append-only operation or unseen marker, while AGENTS.md reference: AGENTS.md:L17-L18 Useful? React with 👍 / 👎. |
||
| } | ||
| if ( | ||
| run.status === "FAILED" || | ||
| run.status === "CRASHED" || | ||
| run.status === "CANCELED" || | ||
| run.status === "SYSTEM_FAILURE" || | ||
| run.status === "TIMED_OUT" | ||
| ) { | ||
| return Response.json( | ||
| { error: "the edit did not go through" }, | ||
| { status: 502 }, | ||
| ); | ||
| } | ||
| await new Promise((resolve) => setTimeout(resolve, POLL_MS)); | ||
| } | ||
| return Response.json({ error: "edit timed out" }, { status: 504 }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When this 60-second deadline expires, the route returns a definitive failure but leaves the Trigger task running for up to 120 seconds, including while it waits in the global single-concurrency queue. The task can therefore mutate DoorDash and persist the cart after the UI has reported that the edit failed, causing an unexpected quantity, tip, or fulfillment change. Cancel the run on timeout or return a durable pending state that the card continues to reconcile. AGENTS.md reference: AGENTS.md:L17-L18 Useful? React with 👍 / 👎. |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the documented shared-gate deployment, every visitor has the same middleware authorization, while approval IDs are sequential integers and this route cancels solely by that ID. Any gate holder can enumerate IDs and void another conversation's live approval without possessing its code or cart UUID. Put an unguessable cancellation capability in the card output and require it here, or bind cancellation to an equivalent per-conversation authorization.
Useful? React with 👍 / 👎.