diff --git a/cloud/app/src/component/workspace/payment-section.tsx b/cloud/app/src/component/workspace/payment-section.tsx
index a346cb55..d45b01c0 100644
--- a/cloud/app/src/component/workspace/payment-section.tsx
+++ b/cloud/app/src/component/workspace/payment-section.tsx
@@ -1,5 +1,5 @@
import { Billing } from "@opencode/cloud-core/billing.js"
-import { query, useParams, createAsync } from "@solidjs/router"
+import { query, action, useParams, createAsync, useAction } from "@solidjs/router"
import { For } from "solid-js"
import { withActor } from "~/context/auth.withActor"
import { formatDateUTC, formatDateForTable } from "./common"
@@ -12,9 +12,15 @@ const getPaymentsInfo = query(async (workspaceID: string) => {
}, workspaceID)
}, "payment.list")
+const downloadReceipt = action(async (workspaceID: string, paymentID: string) => {
+ "use server"
+ return withActor(() => Billing.generateReceiptUrl({ paymentID }), workspaceID)
+}, "receipt.download")
+
export function PaymentSection() {
const params = useParams()
const payments = createAsync(() => getPaymentsInfo(params.id))
+ const downloadReceiptAction = useAction(downloadReceipt)
return (
payments() &&
@@ -31,6 +37,7 @@ export function PaymentSection() {
Date |
Payment ID |
Amount |
+ Receipt |
@@ -44,6 +51,20 @@ export function PaymentSection() {
{payment.id} |
${((payment.amount ?? 0) / 100000000).toFixed(2)} |
+
+
+ |
)
}}
diff --git a/cloud/app/src/routes/zen/handler.ts b/cloud/app/src/routes/zen/handler.ts
index bfa4d077..c2c5c45a 100644
--- a/cloud/app/src/routes/zen/handler.ts
+++ b/cloud/app/src/routes/zen/handler.ts
@@ -538,7 +538,6 @@ export async function handler(
async function reload() {
if (!apiKey) return
- // acquire reload lock
const lock = await Database.use((tx) =>
tx
.update(BillingTable)
diff --git a/cloud/core/src/billing.ts b/cloud/core/src/billing.ts
index cbf064bf..2254adc7 100644
--- a/cloud/core/src/billing.ts
+++ b/cloud/core/src/billing.ts
@@ -224,4 +224,21 @@ export namespace Billing {
return session.url
},
)
+
+ export const generateReceiptUrl = fn(
+ z.object({
+ paymentID: z.string(),
+ }),
+ async (input) => {
+ const { paymentID } = input
+
+ const intent = await Billing.stripe().paymentIntents.retrieve(paymentID)
+ if (!intent.latest_charge) throw new Error("No charge found")
+
+ const charge = await Billing.stripe().charges.retrieve(intent.latest_charge as string)
+ if (!charge.receipt_url) throw new Error("No receipt URL found")
+
+ return charge.receipt_url
+ },
+ )
}