feat: add Google Sheets registration pipeline
- Add googleapis for Google Sheets integration - Create /api/register endpoint to record registrations with "Pending" status - Update webhook to mark registrations as "Paid" when payment completes - Add lib/google-sheets.ts with addRegistration and updatePaymentStatus functions - Update docker-compose.yml with Google Sheets env vars - Add .env.example documenting required environment variables Flow: Form submit → Sheet (Pending) → Payment → Webhook → Sheet (Paid) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f2736fc3d3
commit
3091c01819
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Stripe Payment Integration
|
||||||
|
STRIPE_SECRET_KEY=sk_live_xxx
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxx
|
||||||
|
|
||||||
|
# Public URL (used for Stripe redirect URLs)
|
||||||
|
NEXT_PUBLIC_BASE_URL=https://cryptocommonsgather.ing
|
||||||
|
|
||||||
|
# Google Sheets Integration
|
||||||
|
# Service account key JSON (paste as single line, or use escaped JSON)
|
||||||
|
GOOGLE_SERVICE_ACCOUNT_KEY={"type":"service_account","project_id":"...","private_key_id":"...","private_key":"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n","client_email":"...@....iam.gserviceaccount.com","client_id":"...","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_x509_cert_url":"..."}
|
||||||
|
|
||||||
|
# Google Sheet ID (from the URL: https://docs.google.com/spreadsheets/d/SHEET_ID/edit)
|
||||||
|
GOOGLE_SHEET_ID=your-google-sheet-id-here
|
||||||
|
|
||||||
|
# Optional: Sheet tab name (defaults to "Registrations")
|
||||||
|
GOOGLE_SHEET_NAME=Registrations
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
import { type NextRequest, NextResponse } from "next/server"
|
import { type NextRequest, NextResponse } from "next/server"
|
||||||
import Stripe from "stripe"
|
import Stripe from "stripe"
|
||||||
|
|
||||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
// Lazy initialization to avoid build-time errors
|
||||||
apiVersion: "2024-12-18.acacia",
|
let stripe: Stripe | null = null
|
||||||
})
|
|
||||||
|
function getStripe() {
|
||||||
|
if (!stripe) {
|
||||||
|
stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||||||
|
apiVersion: "2024-12-18.acacia",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return stripe
|
||||||
|
}
|
||||||
|
|
||||||
// Dynamic pricing configuration (in EUR cents)
|
// Dynamic pricing configuration (in EUR cents)
|
||||||
const TICKET_PRICE_CENTS = 8000 // €80 early bird
|
const TICKET_PRICE_CENTS = 8000 // €80 early bird
|
||||||
|
|
@ -42,7 +50,7 @@ export async function POST(request: NextRequest) {
|
||||||
paymentMethodTypes = ["customer_balance"]
|
paymentMethodTypes = ["customer_balance"]
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await stripe.checkout.sessions.create({
|
const session = await getStripe().checkout.sessions.create({
|
||||||
payment_method_types: paymentMethodTypes,
|
payment_method_types: paymentMethodTypes,
|
||||||
line_items: lineItems,
|
line_items: lineItems,
|
||||||
mode: "payment",
|
mode: "payment",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { type NextRequest, NextResponse } from "next/server"
|
||||||
|
import { addRegistration, initializeSheetHeaders } from "@/lib/google-sheets"
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
|
||||||
|
const { name, contact, contributions, expectations, howHeard, dietary, crewConsent } = body
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!name || !contact || !contributions || !expectations || !crewConsent) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Missing required fields" },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize headers if needed (first registration)
|
||||||
|
await initializeSheetHeaders()
|
||||||
|
|
||||||
|
// Add registration to Google Sheet
|
||||||
|
const rowNumber = await addRegistration({
|
||||||
|
name,
|
||||||
|
contact,
|
||||||
|
contributions,
|
||||||
|
expectations,
|
||||||
|
howHeard: howHeard || "",
|
||||||
|
dietary: Array.isArray(dietary) ? dietary.join(", ") : dietary || "",
|
||||||
|
crewConsent,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`[Register API] Registration added for ${name} at row ${rowNumber}`)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: "Registration recorded",
|
||||||
|
rowNumber,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Register API] Error:", error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to record registration" },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Use POST to submit registration" },
|
||||||
|
{ status: 405 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,22 @@
|
||||||
import { type NextRequest, NextResponse } from "next/server"
|
import { type NextRequest, NextResponse } from "next/server"
|
||||||
import Stripe from "stripe"
|
import Stripe from "stripe"
|
||||||
|
import { updatePaymentStatus } from "@/lib/google-sheets"
|
||||||
|
|
||||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
// Lazy initialization to avoid build-time errors
|
||||||
apiVersion: "2024-12-18.acacia",
|
let stripe: Stripe | null = null
|
||||||
})
|
|
||||||
|
|
||||||
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
|
function getStripe() {
|
||||||
|
if (!stripe) {
|
||||||
|
stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||||||
|
apiVersion: "2024-12-18.acacia",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return stripe
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWebhookSecret() {
|
||||||
|
return process.env.STRIPE_WEBHOOK_SECRET!
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -15,9 +26,9 @@ export async function POST(request: NextRequest) {
|
||||||
let event: Stripe.Event
|
let event: Stripe.Event
|
||||||
|
|
||||||
try {
|
try {
|
||||||
event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
|
event = getStripe().webhooks.constructEvent(body, signature, getWebhookSecret())
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[v0] Webhook signature verification failed:", err)
|
console.error("[Webhook] Signature verification failed:", err)
|
||||||
return NextResponse.json({ error: "Invalid signature" }, { status: 400 })
|
return NextResponse.json({ error: "Invalid signature" }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,32 +36,64 @@ export async function POST(request: NextRequest) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "checkout.session.completed": {
|
case "checkout.session.completed": {
|
||||||
const session = event.data.object as Stripe.Checkout.Session
|
const session = event.data.object as Stripe.Checkout.Session
|
||||||
console.log("[v0] Payment successful:", session.id)
|
console.log("[Webhook] Payment successful:", session.id)
|
||||||
|
|
||||||
// Here you would:
|
// Extract registration data from metadata
|
||||||
// 1. Store the registration in your database
|
const metadata = session.metadata || {}
|
||||||
// 2. Send confirmation email
|
const customerEmail = session.customer_details?.email || ""
|
||||||
// 3. Add to attendee list
|
|
||||||
|
// Update Google Sheet with payment confirmation
|
||||||
|
const updated = await updatePaymentStatus({
|
||||||
|
name: metadata.name || "",
|
||||||
|
email: customerEmail,
|
||||||
|
stripeSessionId: session.id,
|
||||||
|
paymentStatus: "Paid",
|
||||||
|
paymentMethod: session.payment_method_types?.[0] || "unknown",
|
||||||
|
amountPaid: session.amount_total
|
||||||
|
? `€${(session.amount_total / 100).toFixed(2)}`
|
||||||
|
: "",
|
||||||
|
paymentDate: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
console.log(`[Webhook] Google Sheet updated for ${metadata.name}`)
|
||||||
|
} else {
|
||||||
|
console.error(`[Webhook] Failed to update Google Sheet for ${metadata.name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Send confirmation email
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "payment_intent.succeeded": {
|
case "payment_intent.succeeded": {
|
||||||
const paymentIntent = event.data.object as Stripe.PaymentIntent
|
const paymentIntent = event.data.object as Stripe.PaymentIntent
|
||||||
console.log("[v0] PaymentIntent successful:", paymentIntent.id)
|
console.log("[Webhook] PaymentIntent successful:", paymentIntent.id)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "payment_intent.payment_failed": {
|
case "payment_intent.payment_failed": {
|
||||||
const paymentIntent = event.data.object as Stripe.PaymentIntent
|
const paymentIntent = event.data.object as Stripe.PaymentIntent
|
||||||
console.error("[v0] Payment failed:", paymentIntent.id)
|
console.error("[Webhook] Payment failed:", paymentIntent.id)
|
||||||
|
|
||||||
|
// Optionally update sheet to mark as failed
|
||||||
|
const failedMetadata = paymentIntent.metadata || {}
|
||||||
|
if (failedMetadata.name) {
|
||||||
|
await updatePaymentStatus({
|
||||||
|
name: failedMetadata.name,
|
||||||
|
stripeSessionId: paymentIntent.id,
|
||||||
|
paymentStatus: "Failed",
|
||||||
|
paymentDate: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
console.log("[v0] Unhandled event type:", event.type)
|
console.log("[Webhook] Unhandled event type:", event.type)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ received: true })
|
return NextResponse.json({ received: true })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[v0] Webhook error:", err)
|
console.error("[Webhook] Error:", err)
|
||||||
return NextResponse.json({ error: "Webhook error" }, { status: 500 })
|
return NextResponse.json({ error: "Webhook error" }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { useState } from "react"
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const [step, setStep] = useState<"form" | "payment">("form")
|
const [step, setStep] = useState<"form" | "payment">("form")
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
contact: "",
|
contact: "",
|
||||||
|
|
@ -26,7 +27,7 @@ export default function RegisterPage() {
|
||||||
})
|
})
|
||||||
const baseTicketPrice = 80 // Early bird price €80
|
const baseTicketPrice = 80 // Early bird price €80
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (
|
if (
|
||||||
|
|
@ -39,7 +40,43 @@ export default function RegisterPage() {
|
||||||
alert("Please fill in all required fields")
|
alert("Please fill in all required fields")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStep("payment")
|
|
||||||
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Submit registration to Google Sheet first
|
||||||
|
const dietaryString =
|
||||||
|
formData.dietary.join(", ") +
|
||||||
|
(formData.dietaryOther ? `, ${formData.dietaryOther}` : "")
|
||||||
|
|
||||||
|
const response = await fetch("/api/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: formData.name,
|
||||||
|
contact: formData.contact,
|
||||||
|
contributions: formData.contributions,
|
||||||
|
expectations: formData.expectations,
|
||||||
|
howHeard: formData.howHeard,
|
||||||
|
dietary: dietaryString,
|
||||||
|
crewConsent: formData.crewConsent,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to record registration")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proceed to payment step
|
||||||
|
setStep("payment")
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Registration error:", error)
|
||||||
|
alert("There was an error recording your registration. Please try again.")
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDietaryChange = (value: string, checked: boolean) => {
|
const handleDietaryChange = (value: string, checked: boolean) => {
|
||||||
|
|
@ -455,8 +492,8 @@ export default function RegisterPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<Button type="submit" className="w-full" size="lg">
|
<Button type="submit" className="w-full" size="lg" disabled={isSubmitting}>
|
||||||
Continue to Payment
|
{isSubmitting ? "Recording registration..." : "Continue to Payment"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ services:
|
||||||
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
|
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
|
||||||
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
|
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
|
||||||
- NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}
|
- NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}
|
||||||
|
- NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL:-https://cryptocommonsgather.ing}
|
||||||
|
- GOOGLE_SERVICE_ACCOUNT_KEY=${GOOGLE_SERVICE_ACCOUNT_KEY}
|
||||||
|
- GOOGLE_SHEET_ID=${GOOGLE_SHEET_ID}
|
||||||
|
- GOOGLE_SHEET_NAME=${GOOGLE_SHEET_NAME:-Registrations}
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.ccg.rule=Host(`cryptocommonsgather.ing`) || Host(`www.cryptocommonsgather.ing`)"
|
- "traefik.http.routers.ccg.rule=Host(`cryptocommonsgather.ing`) || Host(`www.cryptocommonsgather.ing`)"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
import { google } from "googleapis"
|
||||||
|
|
||||||
|
// Initialize Google Sheets API
|
||||||
|
function getGoogleSheetsClient() {
|
||||||
|
const credentials = JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_KEY || "{}")
|
||||||
|
|
||||||
|
const auth = new google.auth.GoogleAuth({
|
||||||
|
credentials,
|
||||||
|
scopes: ["https://www.googleapis.com/auth/spreadsheets"],
|
||||||
|
})
|
||||||
|
|
||||||
|
return google.sheets({ version: "v4", auth })
|
||||||
|
}
|
||||||
|
|
||||||
|
const SPREADSHEET_ID = process.env.GOOGLE_SHEET_ID!
|
||||||
|
const SHEET_NAME = process.env.GOOGLE_SHEET_NAME || "Registrations"
|
||||||
|
|
||||||
|
export interface RegistrationData {
|
||||||
|
name: string
|
||||||
|
email?: string
|
||||||
|
contact: string
|
||||||
|
contributions: string
|
||||||
|
expectations: string
|
||||||
|
howHeard: string
|
||||||
|
dietary: string
|
||||||
|
crewConsent: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentUpdateData {
|
||||||
|
email?: string
|
||||||
|
name?: string
|
||||||
|
stripeSessionId: string
|
||||||
|
paymentStatus: "Paid" | "Failed"
|
||||||
|
paymentMethod?: string
|
||||||
|
amountPaid?: string
|
||||||
|
paymentDate?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new registration to the Google Sheet with "Pending" status
|
||||||
|
* Returns the row number where the registration was added
|
||||||
|
*/
|
||||||
|
export async function addRegistration(data: RegistrationData): Promise<number> {
|
||||||
|
const sheets = getGoogleSheetsClient()
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString()
|
||||||
|
|
||||||
|
const values = [
|
||||||
|
[
|
||||||
|
timestamp, // A: Timestamp
|
||||||
|
data.name, // B: Name
|
||||||
|
data.email || "", // C: Email
|
||||||
|
data.contact, // D: Contact
|
||||||
|
data.contributions, // E: Contributions
|
||||||
|
data.expectations, // F: Expectations
|
||||||
|
data.howHeard || "", // G: How Heard
|
||||||
|
data.dietary, // H: Dietary
|
||||||
|
data.crewConsent, // I: Crew Consent
|
||||||
|
"Pending", // J: Payment Status
|
||||||
|
"", // K: Payment Method
|
||||||
|
"", // L: Stripe Session ID
|
||||||
|
"", // M: Amount Paid
|
||||||
|
"", // N: Payment Date
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
const response = await sheets.spreadsheets.values.append({
|
||||||
|
spreadsheetId: SPREADSHEET_ID,
|
||||||
|
range: `${SHEET_NAME}!A:N`,
|
||||||
|
valueInputOption: "USER_ENTERED",
|
||||||
|
insertDataOption: "INSERT_ROWS",
|
||||||
|
requestBody: { values },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Extract row number from the updated range (e.g., "Registrations!A5:N5" -> 5)
|
||||||
|
const updatedRange = response.data.updates?.updatedRange || ""
|
||||||
|
const match = updatedRange.match(/!A(\d+):/)
|
||||||
|
const rowNumber = match ? parseInt(match[1], 10) : -1
|
||||||
|
|
||||||
|
console.log(`[Google Sheets] Added registration for ${data.name} at row ${rowNumber}`)
|
||||||
|
|
||||||
|
return rowNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update payment status for a registration
|
||||||
|
* Finds the row by name (since email might not be available until payment)
|
||||||
|
*/
|
||||||
|
export async function updatePaymentStatus(data: PaymentUpdateData): Promise<boolean> {
|
||||||
|
const sheets = getGoogleSheetsClient()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First, get all rows to find the matching registration
|
||||||
|
const response = await sheets.spreadsheets.values.get({
|
||||||
|
spreadsheetId: SPREADSHEET_ID,
|
||||||
|
range: `${SHEET_NAME}!A:N`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const rows = response.data.values || []
|
||||||
|
|
||||||
|
// Find the row that matches (by name and has "Pending" status)
|
||||||
|
// Start from index 1 to skip header row
|
||||||
|
let targetRowIndex = -1
|
||||||
|
for (let i = rows.length - 1; i >= 1; i--) {
|
||||||
|
const row = rows[i]
|
||||||
|
const rowName = row[1] // Column B: Name
|
||||||
|
const rowStatus = row[9] // Column J: Payment Status
|
||||||
|
|
||||||
|
// Match by name and pending status (most recent first)
|
||||||
|
if (rowName === data.name && rowStatus === "Pending") {
|
||||||
|
targetRowIndex = i + 1 // Convert to 1-indexed row number
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetRowIndex === -1) {
|
||||||
|
console.error(`[Google Sheets] Could not find pending registration for ${data.name}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the row with payment information
|
||||||
|
const updateRange = `${SHEET_NAME}!C${targetRowIndex}:N${targetRowIndex}`
|
||||||
|
const updateValues = [
|
||||||
|
[
|
||||||
|
data.email || rows[targetRowIndex - 1][2] || "", // C: Email (from Stripe or existing)
|
||||||
|
rows[targetRowIndex - 1][3], // D: Contact (preserve)
|
||||||
|
rows[targetRowIndex - 1][4], // E: Contributions (preserve)
|
||||||
|
rows[targetRowIndex - 1][5], // F: Expectations (preserve)
|
||||||
|
rows[targetRowIndex - 1][6], // G: How Heard (preserve)
|
||||||
|
rows[targetRowIndex - 1][7], // H: Dietary (preserve)
|
||||||
|
rows[targetRowIndex - 1][8], // I: Crew Consent (preserve)
|
||||||
|
data.paymentStatus, // J: Payment Status
|
||||||
|
data.paymentMethod || "", // K: Payment Method
|
||||||
|
data.stripeSessionId, // L: Stripe Session ID
|
||||||
|
data.amountPaid || "", // M: Amount Paid
|
||||||
|
data.paymentDate || new Date().toISOString(), // N: Payment Date
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
await sheets.spreadsheets.values.update({
|
||||||
|
spreadsheetId: SPREADSHEET_ID,
|
||||||
|
range: updateRange,
|
||||||
|
valueInputOption: "USER_ENTERED",
|
||||||
|
requestBody: { values: updateValues },
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`[Google Sheets] Updated payment status to ${data.paymentStatus} for ${data.name} at row ${targetRowIndex}`)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Google Sheets] Error updating payment status:", error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the sheet with headers if it's empty
|
||||||
|
*/
|
||||||
|
export async function initializeSheetHeaders(): Promise<void> {
|
||||||
|
const sheets = getGoogleSheetsClient()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if first row has data
|
||||||
|
const response = await sheets.spreadsheets.values.get({
|
||||||
|
spreadsheetId: SPREADSHEET_ID,
|
||||||
|
range: `${SHEET_NAME}!A1:N1`,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.data.values || response.data.values.length === 0) {
|
||||||
|
// Add headers
|
||||||
|
const headers = [
|
||||||
|
[
|
||||||
|
"Timestamp",
|
||||||
|
"Name",
|
||||||
|
"Email",
|
||||||
|
"Contact",
|
||||||
|
"Contributions",
|
||||||
|
"Expectations",
|
||||||
|
"How Heard",
|
||||||
|
"Dietary",
|
||||||
|
"Crew Consent",
|
||||||
|
"Payment Status",
|
||||||
|
"Payment Method",
|
||||||
|
"Stripe Session ID",
|
||||||
|
"Amount Paid",
|
||||||
|
"Payment Date",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
await sheets.spreadsheets.values.update({
|
||||||
|
spreadsheetId: SPREADSHEET_ID,
|
||||||
|
range: `${SHEET_NAME}!A1:N1`,
|
||||||
|
valueInputOption: "USER_ENTERED",
|
||||||
|
requestBody: { values: headers },
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("[Google Sheets] Initialized sheet with headers")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Google Sheets] Error initializing headers:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -44,6 +44,7 @@
|
||||||
"cmdk": "1.0.4",
|
"cmdk": "1.0.4",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
"embla-carousel-react": "8.5.1",
|
"embla-carousel-react": "8.5.1",
|
||||||
|
"googleapis": "^170.1.0",
|
||||||
"input-otp": "1.4.1",
|
"input-otp": "1.4.1",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
"next": "16.0.10",
|
"next": "16.0.10",
|
||||||
|
|
@ -71,4 +72,4 @@
|
||||||
"tw-animate-css": "1.3.3",
|
"tw-animate-css": "1.3.3",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue