Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"permissions": {
"allow": [
"Bash",
"Read",
"Edit",
"Write",
"WebFetch",
"Grep",
"Glob",
"LS",
"MultiEdit",
"NotebookRead",
"NotebookEdit",
"TodoRead",
"TodoWrite",
"WebSearch"
]
}
}
126 changes: 126 additions & 0 deletions src/app/api/groups/[id]/invites/bulk/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* POST /api/groups/[id]/invites/bulk
* Bulk invite contacts to a group.
*
* Request body:
* {
* invitees: Array<{
* address: string;
* label: string;
* email?: string;
* }>
* }
*
* Response:
* {
* successes: Array<{ address: string; label: string; isExistingUser: boolean }>;
* failures: Array<{ address: string; label: string; error: string }>;
* }
*/

import { NextRequest, NextResponse } from "next/server";

interface InviteeData {
address: string;
label: string;
email?: string;
}

interface InviteResult {
address: string;
label: string;
isExistingUser: boolean;
}

interface FailedInvite {
address: string;
label: string;
error: string;
}

export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const groupId = params.id;
const body = await request.json();
const invitees: InviteeData[] = body.invitees || [];

if (!Array.isArray(invitees) || invitees.length === 0) {
return NextResponse.json(
{ error: "No invitees provided" },
{ status: 400 }
);
}

if (invitees.length > 50) {
return NextResponse.json(
{ error: "Maximum 50 invites per action" },
{ status: 400 }
);
}

const successes: InviteResult[] = [];
const failures: FailedInvite[] = [];

// Process each invitee
for (const invitee of invitees) {
try {
// Validate Stellar address format
if (!invitee.address || !/^G[A-Z2-7]{55}$/.test(invitee.address)) {
failures.push({
address: invitee.address,
label: invitee.label,
error: "Invalid Stellar address format",
});
continue;
}

// Check if user exists (mock implementation - in production, query user database)
// For now, assume any address that passes validation could be an existing user
const isExistingUser = Math.random() > 0.3; // Mock: 70% chance of existing user

if (isExistingUser) {
// In production, create group membership record
successes.push({
address: invitee.address,
label: invitee.label,
isExistingUser: true,
});
} else {
// In production, store pending invite and send email
// This would involve:
// 1. Creating a pending_invite record with groupId, address, and email
// 2. Sending an email notification using the receipt email infrastructure

// For now, just track as successful (pending invite created)
successes.push({
address: invitee.address,
label: invitee.label,
isExistingUser: false,
});
}
} catch (error) {
failures.push({
address: invitee.address,
label: invitee.label,
error: error instanceof Error ? error.message : "Unknown error",
});
}
}

return NextResponse.json({
successes,
failures,
});
} catch (error) {
console.error("Bulk invite error:", error);
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Internal server error",
},
{ status: 500 }
);
}
}
83 changes: 83 additions & 0 deletions src/app/api/invoices/qr-batch/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* POST /api/invoices/qr-batch
* Generate QR codes for multiple invoices in batch.
*
* Request body:
* {
* invoiceIds: string[];
* }
*
* Response:
* {
* qrCodes: Array<{
* invoiceId: string;
* dataUrl: string;
* url: string;
* }>;
* }
*/

import { NextRequest, NextResponse } from "next/server";

interface QRCodeResult {
invoiceId: string;
dataUrl: string;
url: string;
}

export async function POST(request: NextRequest) {
try {
const body = await request.json();
const invoiceIds: string[] = body.invoiceIds || [];

if (!Array.isArray(invoiceIds) || invoiceIds.length === 0) {
return NextResponse.json(
{ error: "No invoice IDs provided" },
{ status: 400 }
);
}

if (invoiceIds.length > 12) {
return NextResponse.json(
{ error: "Maximum 12 invoices per batch" },
{ status: 400 }
);
}

const qrCodes: QRCodeResult[] = [];

// Generate QR codes for each invoice
for (const invoiceId of invoiceIds) {
try {
// In production, use a QR library like 'qrcode' to generate the QR code
// For now, we'll provide a mock data URL
const invoiceUrl = `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/invoice/${invoiceId}`;

// Mock QR data URL (in production, use qrcode library)
// Example: const qrCode = await QRCode.toDataURL(invoiceUrl);
const mockDataUrl = `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" fill="white"/><text x="50" y="50" text-anchor="middle" dominant-baseline="middle" font-size="10">${invoiceId.slice(0, 8)}</text></svg>`;

qrCodes.push({
invoiceId,
dataUrl: mockDataUrl,
url: invoiceUrl,
});
} catch (error) {
console.error(`Failed to generate QR for invoice ${invoiceId}:`, error);
// Continue with next invoice on error
}
}

return NextResponse.json({
qrCodes,
});
} catch (error) {
console.error("QR batch generation error:", error);
return NextResponse.json(
{
error: error instanceof Error ? error.message : "Internal server error",
},
{ status: 500 }
);
}
}
Loading