Frank 1 месяц назад
Родитель
Сommit
cf069dd046

+ 23 - 3
packages/console/app/src/routes/workspace/[id]/billing/payment-section.module.css

@@ -45,6 +45,19 @@
           text-decoration: line-through;
         }
       }
+
+      &[data-slot="payment-receipt"] {
+        span {
+          display: inline-block;
+          padding: var(--space-3) var(--space-4);
+          font-size: var(--font-size-sm);
+          line-height: 1.5;
+        }
+
+        button {
+          font-size: var(--font-size-sm);
+        }
+      }
     }
 
     tbody tr {
@@ -54,6 +67,7 @@
     }
 
     @media (max-width: 40rem) {
+
       th,
       td {
         padding: var(--space-2) var(--space-3);
@@ -61,16 +75,22 @@
       }
 
       th {
-        &:nth-child(2) /* Payment ID */ {
+        &:nth-child(2)
+
+        /* Payment ID */
+          {
           display: none;
         }
       }
 
       td {
-        &:nth-child(2) /* Payment ID */ {
+        &:nth-child(2)
+
+        /* Payment ID */
+          {
           display: none;
         }
       }
     }
   }
-}
+}

+ 17 - 11
packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx

@@ -77,6 +77,7 @@ export function PaymentSection() {
               <For each={payments()!}>
                 {(payment) => {
                   const date = new Date(payment.timeCreated)
+                  const isCredit = !payment.paymentID
                   return (
                     <tr>
                       <td data-slot="payment-date" title={formatDateUTC(date)}>
@@ -85,19 +86,24 @@ export function PaymentSection() {
                       <td data-slot="payment-id">{payment.id}</td>
                       <td data-slot="payment-amount" data-refunded={!!payment.timeRefunded}>
                         ${((payment.amount ?? 0) / 100000000).toFixed(2)}
+                        {isCredit ? " (credit)" : ""}
                       </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"
-                        >
-                          View
-                        </button>
+                        {isCredit ? (
+                          <span>-</span>
+                        ) : (
+                          <button
+                            onClick={async () => {
+                              const receiptUrl = await downloadReceiptAction(params.id!, payment.paymentID!)
+                              if (receiptUrl) {
+                                window.open(receiptUrl, "_blank")
+                              }
+                            }}
+                            data-slot="receipt-button"
+                          >
+                            View
+                          </button>
+                        )}
                       </td>
                     </tr>
                   )

+ 20 - 0
packages/console/core/script/credit-workspace.ts

@@ -0,0 +1,20 @@
+import { Billing } from "../src/billing.js"
+
+// get input from command line
+const workspaceID = process.argv[2]
+const dollarAmount = process.argv[3]
+
+if (!workspaceID || !dollarAmount) {
+  console.error("Usage: bun credit-workspace.ts <workspaceID> <dollarAmount>")
+  process.exit(1)
+}
+
+const amountInDollars = parseFloat(dollarAmount)
+if (isNaN(amountInDollars) || amountInDollars <= 0) {
+  console.error("Error: dollarAmount must be a positive number")
+  process.exit(1)
+}
+
+await Billing.grantCredit(workspaceID, amountInDollars)
+
+console.log(`Added payment of $${amountInDollars.toFixed(2)} to workspace ${workspaceID}`)

+ 27 - 1
packages/console/core/script/lookup-user.ts

@@ -1,7 +1,7 @@
 import { Database, eq, sql, inArray } from "../src/drizzle/index.js"
 import { AuthTable } from "../src/schema/auth.sql.js"
 import { UserTable } from "../src/schema/user.sql.js"
-import { BillingTable, PaymentTable } from "../src/schema/billing.sql.js"
+import { BillingTable, PaymentTable, UsageTable } from "../src/schema/billing.sql.js"
 import { WorkspaceTable } from "../src/schema/workspace.sql.js"
 
 // get input from command line
@@ -95,6 +95,32 @@ async function printWorkspace(workspaceID: string) {
         })),
       ),
   )
+
+  await printTable("Usage", (tx) =>
+    tx
+      .select({
+        model: UsageTable.model,
+        provider: UsageTable.provider,
+        inputTokens: UsageTable.inputTokens,
+        outputTokens: UsageTable.outputTokens,
+        reasoningTokens: UsageTable.reasoningTokens,
+        cacheReadTokens: UsageTable.cacheReadTokens,
+        cacheWrite5mTokens: UsageTable.cacheWrite5mTokens,
+        cacheWrite1hTokens: UsageTable.cacheWrite1hTokens,
+        cost: UsageTable.cost,
+        timeCreated: UsageTable.timeCreated,
+      })
+      .from(UsageTable)
+      .where(eq(UsageTable.workspaceID, workspace.id))
+      .orderBy(sql`${UsageTable.timeCreated} DESC`)
+      .limit(1000)
+      .then((rows) =>
+        rows.map((row) => ({
+          ...row,
+          cost: `$${(row.cost / 100000000).toFixed(2)}`,
+        })),
+      ),
+  )
 }
 
 function printHeader(title: string) {

+ 18 - 0
packages/console/core/src/billing.ts

@@ -157,6 +157,24 @@ export namespace Billing {
     })
   }
 
+  export const grantCredit = async (workspaceID: string, dollarAmount: number) => {
+    const amountInMicroCents = centsToMicroCents(dollarAmount * 100)
+    await Database.transaction(async (tx) => {
+      await tx
+        .update(BillingTable)
+        .set({
+          balance: sql`${BillingTable.balance} + ${amountInMicroCents}`,
+        })
+        .where(eq(BillingTable.workspaceID, workspaceID))
+      await tx.insert(PaymentTable).values({
+        workspaceID,
+        id: Identifier.create("payment"),
+        amount: amountInMicroCents,
+      })
+    })
+    return amountInMicroCents
+  }
+
   export const setMonthlyLimit = fn(z.number(), async (input) => {
     return await Database.use((tx) =>
       tx