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,