diff --git a/apps/api/src/routes/invoices.ts b/apps/api/src/routes/invoices.ts index a5c0d95..91ac4ee 100644 --- a/apps/api/src/routes/invoices.ts +++ b/apps/api/src/routes/invoices.ts @@ -130,7 +130,17 @@ invoicesRouter.get("/:id", async (c) => { db.select().from(invoiceTipSplits).where(eq(invoiceTipSplits.invoiceId, id)), ]); - return c.json({ ...invoice, lineItems, tipSplits }); + let cardLast4: string | null = null; + let paymentStatus: string | null = null; + if (invoice.stripePaymentIntentId) { + const details = await getPaymentIntentDetails(invoice.stripePaymentIntentId); + if (details) { + cardLast4 = details.cardLast4; + paymentStatus = details.paymentStatus; + } + } + + return c.json({ ...invoice, lineItems, tipSplits, cardLast4, paymentStatus }); }); // Save tip splits for an invoice (replaces existing splits) @@ -450,9 +460,6 @@ invoicesRouter.post( if (invoice.status !== "paid") { return c.json({ error: "Refund only allowed on paid invoices" }, 422); } - if (!invoice.stripePaymentIntentId) { - return c.json({ error: "No Stripe payment intent found for this invoice" }, 422); - } return await db.transaction(async (tx) => { if (body.idempotencyKey) { @@ -465,17 +472,25 @@ invoicesRouter.post( } } - const result = await processRefund(id, body.amountCents); - if (!result) return c.json({ error: "Refund failed" }, 500); + let refundId: string; + + if (invoice.stripePaymentIntentId) { + const result = await processRefund(id, body.amountCents); + if (!result) return c.json({ error: "Refund failed" }, 500); + refundId = result.refundId; + } else { + // Manual refund — no Stripe call needed + refundId = `manual_${id}_${Date.now()}`; + } await tx.insert(refunds).values({ invoiceId: id, - stripeRefundId: result.refundId, + stripeRefundId: refundId, idempotencyKey: body.idempotencyKey ?? null, amountCents: body.amountCents ?? null, }); - return c.json({ refundId: result.refundId }); + return c.json({ refundId }); }); } ); diff --git a/apps/web/src/pages/Invoices.tsx b/apps/web/src/pages/Invoices.tsx index 246a9be..0eceb4c 100644 --- a/apps/web/src/pages/Invoices.tsx +++ b/apps/web/src/pages/Invoices.tsx @@ -487,7 +487,7 @@ const [showRefundDialog, setShowRefundDialog] = useState(false); )}
- {invoice.status === "paid" && invoice.stripePaymentIntentId && !invoice.stripeRefundId && isManager && ( + {invoice.status === "paid" && !invoice.stripeRefundId && isManager && ( diff --git a/charts/groombook/templates/_helpers.tpl b/charts/groombook/templates/_helpers.tpl index 9c97648..93f19ad 100644 --- a/charts/groombook/templates/_helpers.tpl +++ b/charts/groombook/templates/_helpers.tpl @@ -119,3 +119,10 @@ uri database-url {{- end -}} {{- end }} + +{{/* +Auth secret name — always use groombook-auth (sealed secret name) +*/}} +{{- define "groombook.authSecretName" -}} +{{- printf "%s" "groombook-auth" }} +{{- end }} diff --git a/charts/groombook/templates/api-deployment.yaml b/charts/groombook/templates/api-deployment.yaml index aaee7b0..6283210 100644 --- a/charts/groombook/templates/api-deployment.yaml +++ b/charts/groombook/templates/api-deployment.yaml @@ -50,6 +50,27 @@ spec: - name: OIDC_AUDIENCE value: {{ .Values.api.env.oidcAudience | quote }} {{- end }} + {{- if .Values.api.env.internalBaseUrl }} + - name: OIDC_INTERNAL_BASE + value: {{ .Values.api.env.internalBaseUrl | quote }} + {{- end }} + - name: BETTER_AUTH_URL + value: {{ .Values.api.env.betterAuthUrl | quote }} + - name: OIDC_CLIENT_ID + valueFrom: + secretKeyRef: + name: {{ include "groombook.authSecretName" . }} + key: OIDC_CLIENT_ID + - name: OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "groombook.authSecretName" . }} + key: OIDC_CLIENT_SECRET + - name: BETTER_AUTH_SECRET + valueFrom: + secretKeyRef: + name: {{ include "groombook.authSecretName" . }} + key: BETTER_AUTH_SECRET - name: DATABASE_URL valueFrom: secretKeyRef: diff --git a/charts/groombook/values.yaml b/charts/groombook/values.yaml index 5f888a5..0e85682 100644 --- a/charts/groombook/values.yaml +++ b/charts/groombook/values.yaml @@ -18,6 +18,8 @@ api: corsOrigin: "" oidcIssuer: "" oidcAudience: groombook + betterAuthUrl: "" + internalBaseUrl: "" port: "3000" service: type: ClusterIP diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index dca21d4..4563ce7 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -978,6 +978,7 @@ async function seed() { const invoiceStatus = rand() < 0.95 ? "paid" as const : "pending" as const; const paidAt = invoiceStatus === "paid" ? new Date(endTime.getTime() + randInt(5, 30) * 60 * 1000) : null; + const stripePaymentIntentId = invoiceStatus === "paid" && rand() < 0.2 ? `pi_test_${uuid().replace(/-/g, "").slice(0, 24)}` : null; invoiceBatch.push({ id: invoiceId, appointmentId: apptId, @@ -989,6 +990,7 @@ async function seed() { status: invoiceStatus, paymentMethod: invoiceStatus === "paid" ? pick(["cash", "card", "card", "card", "check"]) as "cash" | "card" | "check" : null, paidAt, + stripePaymentIntentId, notes: rand() < 0.05 ? "Added extra service at checkout" : null, }); @@ -1092,13 +1094,14 @@ async function seed() { const taxCents = Math.round(effectivePrice * 0.08); const totalCents = effectivePrice + taxCents + tipCents; const paidAt = new Date(endTime.getTime() + randInt(5, 30) * 60 * 1000); + const stripePaymentIntentId = rand() < 0.2 ? `pi_test_${uuid().replace(/-/g, "").slice(0, 24)}` : null; invoiceBatch.push({ id: invoiceId, appointmentId: apptId, clientId, subtotalCents: effectivePrice, taxCents, tipCents, totalCents, status: "paid" as const, paymentMethod: pick(["cash", "card", "card", "card", "check"]) as "cash" | "card" | "check", - paidAt, notes: null, + paidAt, stripePaymentIntentId, notes: null, }); lineItemBatch.push({ id: uuid(), invoiceId, description: svc.name, quantity: 1,