Frank 5 месяцев назад
Родитель
Сommit
8c71107a93

+ 22 - 1
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() {
                 <th>Date</th>
                 <th>Payment ID</th>
                 <th>Amount</th>
+                <th>Receipt</th>
               </tr>
             </thead>
             <tbody>
@@ -44,6 +51,20 @@ export function PaymentSection() {
                       </td>
                       <td data-slot="payment-id">{payment.id}</td>
                       <td data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</td>
+                      <td data-slot="payment-receipt">
+                        <button
+                          onClick={async () => {
+                            const receiptUrl = await downloadReceiptAction(params.id, payment.paymentID!)
+                            if (receiptUrl) {
+                              window.open(receiptUrl, "_blank")
+                            }
+                          }}
+                          data-slot="receipt-button"
+                          style="cursor: pointer;"
+                        >
+                          download
+                        </button>
+                      </td>
                     </tr>
                   )
                 }}

+ 0 - 1
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)

+ 17 - 0
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
+    },
+  )
 }