fix(security+trips): auth/ownership on all API routes; trip analytics in AUD
Ten routes accepted requests with no getCurrentUser check (transactions/[id], bulk, splits, tags-on-tx, splits/settle, statements/[id], tags, tags/[id], merchants, participants/[id]/balance), and by-id routes did no ownership check at all — any participant could read or modify another's data. Adds canAccessTransactions() (owner via statement/direct, or split participant), applies it to every transaction-scoped route, owner-scopes statements/[id], and rescopes splits/settle in raw SQL so settlement only touches splits the caller is party to. Also: all trip analytics now sum COALESCE(amount_aud, amount) instead of raw amount, matching every other analytics query — trip totals previously added foreign-currency amounts to AUD ones unit-less. And rules apply_split no longer delete+reinserts splits (which reset settled flags on every run) — it upserts share_percent and removes only participants no longer in the rule.
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getMerchantSuggestions, getBankNames } from "@/lib/queries";
|
import { getMerchantSuggestions, getBankNames } from "@/lib/queries";
|
||||||
|
import { getCurrentUser } from "@/lib/auth";
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
|
const user = await getCurrentUser(req);
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||||
const search = req.nextUrl.searchParams.get("search");
|
const search = req.nextUrl.searchParams.get("search");
|
||||||
const type = req.nextUrl.searchParams.get("type");
|
const type = req.nextUrl.searchParams.get("type");
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { queryRaw } from "@/lib/db";
|
import { queryRaw } from "@/lib/db";
|
||||||
|
import { getCurrentUser } from "@/lib/auth";
|
||||||
|
|
||||||
interface BalanceRow {
|
interface BalanceRow {
|
||||||
participant_id: number;
|
participant_id: number;
|
||||||
@@ -9,9 +10,11 @@ interface BalanceRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
_req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
|
const user = await getCurrentUser(req);
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
|
||||||
const rows = await queryRaw<BalanceRow>(
|
const rows = await queryRaw<BalanceRow>(
|
||||||
|
|||||||
@@ -151,11 +151,18 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
if (actions.apply_split?.length) {
|
if (actions.apply_split?.length) {
|
||||||
if (splitFrom && tx.transaction_date < splitFrom) continue;
|
if (splitFrom && tx.transaction_date < splitFrom) continue;
|
||||||
await queryRaw(`DELETE FROM transaction_splits WHERE transaction_id = $1`, [tx.id]);
|
// Remove only participants no longer in the rule's split, and upsert the
|
||||||
|
// rest — a plain delete+reinsert would reset settled flags on every run.
|
||||||
|
await queryRaw(
|
||||||
|
`DELETE FROM transaction_splits WHERE transaction_id = $1 AND participant_id != ALL($2::int[])`,
|
||||||
|
[tx.id, actions.apply_split.map((s) => s.participant_id)]
|
||||||
|
);
|
||||||
for (const s of actions.apply_split) {
|
for (const s of actions.apply_split) {
|
||||||
await queryRaw(
|
await queryRaw(
|
||||||
`INSERT INTO transaction_splits (transaction_id, participant_id, share_percent)
|
`INSERT INTO transaction_splits (transaction_id, participant_id, share_percent)
|
||||||
VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`,
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (transaction_id, participant_id)
|
||||||
|
DO UPDATE SET share_percent = EXCLUDED.share_percent`,
|
||||||
[tx.id, s.participant_id, s.share_percent]
|
[tx.id, s.participant_id, s.share_percent]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,45 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/db";
|
import { queryRaw } from "@/lib/db";
|
||||||
|
import { getCurrentUser } from "@/lib/auth";
|
||||||
|
|
||||||
|
// A split may be settled by the transaction's effective owner or by the
|
||||||
|
// participant the split belongs to.
|
||||||
|
const SCOPE = `
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM transactions t
|
||||||
|
LEFT JOIN statements s ON s.id = t.statement_id
|
||||||
|
WHERE t.id = transaction_splits.transaction_id
|
||||||
|
AND (COALESCE(t.owner_id, s.owner_id) = $2 OR transaction_splits.participant_id = $2)
|
||||||
|
)`;
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
|
const user = await getCurrentUser(req);
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||||
|
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const { participant_id, split_ids } = body as {
|
const { participant_id, split_ids } = body as {
|
||||||
participant_id?: number;
|
participant_id?: number;
|
||||||
split_ids?: number[];
|
split_ids?: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
if (participant_id) {
|
if (participant_id) {
|
||||||
const result = await prisma.transaction_splits.updateMany({
|
const rows = await queryRaw<{ id: number }>(
|
||||||
where: { participant_id, settled: false },
|
`UPDATE transaction_splits SET settled = true, settled_at = NOW()
|
||||||
data: { settled: true, settled_at: now },
|
WHERE participant_id = $1 AND settled = false ${SCOPE}
|
||||||
});
|
RETURNING id`,
|
||||||
return NextResponse.json({ settled: result.count });
|
[participant_id, user.id]
|
||||||
|
);
|
||||||
|
return NextResponse.json({ settled: rows.length });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (split_ids?.length) {
|
if (split_ids?.length) {
|
||||||
const result = await prisma.transaction_splits.updateMany({
|
const rows = await queryRaw<{ id: number }>(
|
||||||
where: { id: { in: split_ids }, settled: false },
|
`UPDATE transaction_splits SET settled = true, settled_at = NOW()
|
||||||
data: { settled: true, settled_at: now },
|
WHERE id = ANY($1::int[]) AND settled = false ${SCOPE}
|
||||||
});
|
RETURNING id`,
|
||||||
return NextResponse.json({ settled: result.count });
|
[split_ids, user.id]
|
||||||
|
);
|
||||||
|
return NextResponse.json({ settled: rows.length });
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ error: "participant_id or split_ids required" }, { status: 400 });
|
return NextResponse.json({ error: "participant_id or split_ids required" }, { status: 400 });
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getStatementById } from "@/lib/queries";
|
import { getStatementById } from "@/lib/queries";
|
||||||
|
import { getCurrentUser } from "@/lib/auth";
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
_req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
|
const user = await getCurrentUser(req);
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const stmt = await getStatementById(Number(id));
|
const stmt = await getStatementById(Number(id));
|
||||||
if (!stmt) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
if (!stmt || stmt.owner_id !== user.id) {
|
||||||
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
return NextResponse.json(stmt);
|
return NextResponse.json(stmt);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { queryRaw } from "@/lib/db";
|
import { queryRaw } from "@/lib/db";
|
||||||
|
import { getCurrentUser } from "@/lib/auth";
|
||||||
|
|
||||||
export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const user = await getCurrentUser(req);
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
await queryRaw(`DELETE FROM tags WHERE id = $1`, [Number(id)]);
|
await queryRaw(`DELETE FROM tags WHERE id = $1`, [Number(id)]);
|
||||||
return NextResponse.json({ ok: true });
|
return NextResponse.json({ ok: true });
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getTags } from "@/lib/queries";
|
import { getTags } from "@/lib/queries";
|
||||||
import { queryRaw } from "@/lib/db";
|
import { queryRaw } from "@/lib/db";
|
||||||
|
import { getCurrentUser } from "@/lib/auth";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(req: NextRequest) {
|
||||||
|
const user = await getCurrentUser(req);
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||||
const tags = await getTags();
|
const tags = await getTags();
|
||||||
return NextResponse.json(tags);
|
return NextResponse.json(tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
|
const user = await getCurrentUser(req);
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||||
const { name, color } = await req.json();
|
const { name, color } = await req.json();
|
||||||
if (!name?.trim()) {
|
if (!name?.trim()) {
|
||||||
return NextResponse.json({ error: "name required" }, { status: 400 });
|
return NextResponse.json({ error: "name required" }, { status: 400 });
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getTransactionById } from "@/lib/queries";
|
import { getTransactionById, canAccessTransactions } from "@/lib/queries";
|
||||||
|
import { getCurrentUser } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/db";
|
import { prisma } from "@/lib/db";
|
||||||
import { queryRaw } from "@/lib/db";
|
import { queryRaw } from "@/lib/db";
|
||||||
|
|
||||||
const VALID_TYPES = ["debit", "credit", "payment", "refund", "fee", "interest", "transfer"];
|
const VALID_TYPES = ["debit", "credit", "payment", "refund", "fee", "interest", "transfer"];
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
_req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
|
const user = await getCurrentUser(req);
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
if (!(await canAccessTransactions(user.id, [Number(id)]))) {
|
||||||
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
const txn = await getTransactionById(Number(id));
|
const txn = await getTransactionById(Number(id));
|
||||||
if (!txn) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
if (!txn) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
return NextResponse.json(txn);
|
return NextResponse.json(txn);
|
||||||
@@ -19,8 +25,13 @@ export async function PATCH(
|
|||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
|
const user = await getCurrentUser(req);
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const transactionId = Number(id);
|
const transactionId = Number(id);
|
||||||
|
if (!(await canAccessTransactions(user.id, [transactionId]))) {
|
||||||
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
|
|
||||||
const { category, merchant_normalized, notes, transaction_type, my_share_percent, description, amount, transaction_date, trip_id } = body as {
|
const { category, merchant_normalized, notes, transaction_type, my_share_percent, description, amount, transaction_date, trip_id } = body as {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/db";
|
import { prisma } from "@/lib/db";
|
||||||
import { queryRaw } from "@/lib/db";
|
import { queryRaw } from "@/lib/db";
|
||||||
|
import { getCurrentUser } from "@/lib/auth";
|
||||||
|
import { canAccessTransactions } from "@/lib/queries";
|
||||||
|
|
||||||
interface SplitInput {
|
interface SplitInput {
|
||||||
participant_id: number;
|
participant_id: number;
|
||||||
@@ -19,10 +21,15 @@ interface SplitRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
_req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
|
const user = await getCurrentUser(req);
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
if (!(await canAccessTransactions(user.id, [Number(id)]))) {
|
||||||
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
const splits = await queryRaw<SplitRow>(
|
const splits = await queryRaw<SplitRow>(
|
||||||
`SELECT ts.*, p.name
|
`SELECT ts.*, p.name
|
||||||
FROM transaction_splits ts
|
FROM transaction_splits ts
|
||||||
@@ -38,8 +45,13 @@ export async function POST(
|
|||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
|
const user = await getCurrentUser(req);
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const transactionId = Number(id);
|
const transactionId = Number(id);
|
||||||
|
if (!(await canAccessTransactions(user.id, [transactionId]))) {
|
||||||
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
const { splits } = (await req.json()) as { splits: SplitInput[] };
|
const { splits } = (await req.json()) as { splits: SplitInput[] };
|
||||||
|
|
||||||
if (!splits || !Array.isArray(splits) || splits.length === 0) {
|
if (!splits || !Array.isArray(splits) || splits.length === 0) {
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { queryRaw } from "@/lib/db";
|
import { queryRaw } from "@/lib/db";
|
||||||
|
import { getCurrentUser } from "@/lib/auth";
|
||||||
|
import { canAccessTransactions } from "@/lib/queries";
|
||||||
|
|
||||||
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const user = await getCurrentUser(req);
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
if (!(await canAccessTransactions(user.id, [Number(id)]))) {
|
||||||
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
const { tag_id } = await req.json();
|
const { tag_id } = await req.json();
|
||||||
if (!tag_id) return NextResponse.json({ error: "tag_id required" }, { status: 400 });
|
if (!tag_id) return NextResponse.json({ error: "tag_id required" }, { status: 400 });
|
||||||
await queryRaw(
|
await queryRaw(
|
||||||
@@ -13,7 +20,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const user = await getCurrentUser(req);
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
if (!(await canAccessTransactions(user.id, [Number(id)]))) {
|
||||||
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
const { tag_id } = await req.json();
|
const { tag_id } = await req.json();
|
||||||
if (!tag_id) return NextResponse.json({ error: "tag_id required" }, { status: 400 });
|
if (!tag_id) return NextResponse.json({ error: "tag_id required" }, { status: 400 });
|
||||||
await queryRaw(
|
await queryRaw(
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { prisma, queryRaw } from "@/lib/db";
|
import { prisma, queryRaw } from "@/lib/db";
|
||||||
import { assignTransactionsToTrip } from "@/lib/queries";
|
import { assignTransactionsToTrip, canAccessTransactions } from "@/lib/queries";
|
||||||
|
import { getCurrentUser } from "@/lib/auth";
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
|
const user = await getCurrentUser(req);
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const { action, ids, category, merchant_normalized, splits, tag_id } = body as {
|
const { action, ids, category, merchant_normalized, splits, tag_id } = body as {
|
||||||
action: string;
|
action: string;
|
||||||
@@ -17,6 +20,10 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({ error: "ids required" }, { status: 400 });
|
return NextResponse.json({ error: "ids required" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(await canAccessTransactions(user.id, ids.map(Number)))) {
|
||||||
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
if (action === "categorize" && category) {
|
if (action === "categorize" && category) {
|
||||||
const ops = ids.map((id) =>
|
const ops = ids.map((id) =>
|
||||||
prisma.transaction_overrides.upsert({
|
prisma.transaction_overrides.upsert({
|
||||||
|
|||||||
+25
-9
@@ -231,6 +231,22 @@ export async function getTransactions(ownerId: number, filters: TransactionFilte
|
|||||||
return { data, total, limit, offset };
|
return { data, total, limit, offset };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A user may act on a transaction they own (directly or via the parent
|
||||||
|
// statement) or one they participate in via a split.
|
||||||
|
export async function canAccessTransactions(ownerId: number, transactionIds: number[]): Promise<boolean> {
|
||||||
|
if (!transactionIds.length) return false;
|
||||||
|
const rows = await queryRaw<{ n: number }>(
|
||||||
|
`SELECT COUNT(*)::int AS n
|
||||||
|
FROM transactions t
|
||||||
|
LEFT JOIN statements s ON s.id = t.statement_id
|
||||||
|
WHERE t.id = ANY($2::int[])
|
||||||
|
AND (COALESCE(t.owner_id, s.owner_id) = $1
|
||||||
|
OR EXISTS (SELECT 1 FROM transaction_splits ts WHERE ts.transaction_id = t.id AND ts.participant_id = $1))`,
|
||||||
|
[ownerId, transactionIds]
|
||||||
|
);
|
||||||
|
return rows[0]?.n === transactionIds.length;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getTransactionById(id: number) {
|
export async function getTransactionById(id: number) {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT t.*,
|
SELECT t.*,
|
||||||
@@ -654,7 +670,7 @@ export async function getTrips(ownerId: number): Promise<TripRow[]> {
|
|||||||
SELECT
|
SELECT
|
||||||
t.*,
|
t.*,
|
||||||
COALESCE(SUM(
|
COALESCE(SUM(
|
||||||
CASE WHEN tx.transaction_type IN ('debit','fee','interest') THEN tx.amount ELSE 0 END
|
CASE WHEN tx.transaction_type IN ('debit','fee','interest') THEN COALESCE(tx.amount_aud, tx.amount) ELSE 0 END
|
||||||
), 0)::float AS total_spend,
|
), 0)::float AS total_spend,
|
||||||
COUNT(o.transaction_id)::int AS transaction_count
|
COUNT(o.transaction_id)::int AS transaction_count
|
||||||
FROM trips t
|
FROM trips t
|
||||||
@@ -671,7 +687,7 @@ export async function getTripById(id: number, ownerId: number): Promise<TripRow
|
|||||||
SELECT
|
SELECT
|
||||||
t.*,
|
t.*,
|
||||||
COALESCE(SUM(
|
COALESCE(SUM(
|
||||||
CASE WHEN tx.transaction_type IN ('debit','fee','interest') THEN tx.amount ELSE 0 END
|
CASE WHEN tx.transaction_type IN ('debit','fee','interest') THEN COALESCE(tx.amount_aud, tx.amount) ELSE 0 END
|
||||||
), 0)::float AS total_spend,
|
), 0)::float AS total_spend,
|
||||||
COUNT(o.transaction_id)::int AS transaction_count
|
COUNT(o.transaction_id)::int AS transaction_count
|
||||||
FROM trips t
|
FROM trips t
|
||||||
@@ -691,7 +707,7 @@ export async function getTripAnalytics(tripId: number, ownerId: number): Promise
|
|||||||
queryRaw<{ category: string; amount: number; count: number }>(`
|
queryRaw<{ category: string; amount: number; count: number }>(`
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(o.category_override, tx.category, 'other') AS category,
|
COALESCE(o.category_override, tx.category, 'other') AS category,
|
||||||
SUM(tx.amount)::float AS amount,
|
SUM(COALESCE(tx.amount_aud, tx.amount))::float AS amount,
|
||||||
COUNT(*)::int AS count
|
COUNT(*)::int AS count
|
||||||
FROM transaction_overrides o
|
FROM transaction_overrides o
|
||||||
JOIN transactions tx ON tx.id = o.transaction_id
|
JOIN transactions tx ON tx.id = o.transaction_id
|
||||||
@@ -704,7 +720,7 @@ export async function getTripAnalytics(tripId: number, ownerId: number): Promise
|
|||||||
queryRaw<{ date: string; amount: number }>(`
|
queryRaw<{ date: string; amount: number }>(`
|
||||||
SELECT
|
SELECT
|
||||||
tx.transaction_date::text AS date,
|
tx.transaction_date::text AS date,
|
||||||
SUM(tx.amount)::float AS amount
|
SUM(COALESCE(tx.amount_aud, tx.amount))::float AS amount
|
||||||
FROM transaction_overrides o
|
FROM transaction_overrides o
|
||||||
JOIN transactions tx ON tx.id = o.transaction_id
|
JOIN transactions tx ON tx.id = o.transaction_id
|
||||||
WHERE o.trip_id = $1
|
WHERE o.trip_id = $1
|
||||||
@@ -716,7 +732,7 @@ export async function getTripAnalytics(tripId: number, ownerId: number): Promise
|
|||||||
queryRaw<{ merchant: string; amount: number; count: number }>(`
|
queryRaw<{ merchant: string; amount: number; count: number }>(`
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(o.merchant_normalized, tx.merchant_normalized, tx.merchant_name, tx.description) AS merchant,
|
COALESCE(o.merchant_normalized, tx.merchant_normalized, tx.merchant_name, tx.description) AS merchant,
|
||||||
SUM(tx.amount)::float AS amount,
|
SUM(COALESCE(tx.amount_aud, tx.amount))::float AS amount,
|
||||||
COUNT(*)::int AS count
|
COUNT(*)::int AS count
|
||||||
FROM transaction_overrides o
|
FROM transaction_overrides o
|
||||||
JOIN transactions tx ON tx.id = o.transaction_id
|
JOIN transactions tx ON tx.id = o.transaction_id
|
||||||
@@ -730,7 +746,7 @@ export async function getTripAnalytics(tripId: number, ownerId: number): Promise
|
|||||||
queryRaw<{ tag_id: number; name: string; color: string; amount: number; count: number }>(`
|
queryRaw<{ tag_id: number; name: string; color: string; amount: number; count: number }>(`
|
||||||
SELECT
|
SELECT
|
||||||
tg.id AS tag_id, tg.name, tg.color,
|
tg.id AS tag_id, tg.name, tg.color,
|
||||||
SUM(tx.amount)::float AS amount,
|
SUM(COALESCE(tx.amount_aud, tx.amount))::float AS amount,
|
||||||
COUNT(DISTINCT tx.id)::int AS count
|
COUNT(DISTINCT tx.id)::int AS count
|
||||||
FROM transaction_overrides o
|
FROM transaction_overrides o
|
||||||
JOIN transactions tx ON tx.id = o.transaction_id
|
JOIN transactions tx ON tx.id = o.transaction_id
|
||||||
@@ -746,9 +762,9 @@ export async function getTripAnalytics(tripId: number, ownerId: number): Promise
|
|||||||
SELECT
|
SELECT
|
||||||
p.id AS participant_id,
|
p.id AS participant_id,
|
||||||
p.name,
|
p.name,
|
||||||
SUM(ts.share_percent / 100.0 * tx.amount)::float AS owed,
|
SUM(ts.share_percent / 100.0 * COALESCE(tx.amount_aud, tx.amount))::float AS owed,
|
||||||
SUM(CASE WHEN ts.settled THEN ts.share_percent / 100.0 * tx.amount ELSE 0 END)::float AS settled,
|
SUM(CASE WHEN ts.settled THEN ts.share_percent / 100.0 * COALESCE(tx.amount_aud, tx.amount) ELSE 0 END)::float AS settled,
|
||||||
SUM(CASE WHEN NOT ts.settled THEN ts.share_percent / 100.0 * tx.amount ELSE 0 END)::float AS unsettled
|
SUM(CASE WHEN NOT ts.settled THEN ts.share_percent / 100.0 * COALESCE(tx.amount_aud, tx.amount) ELSE 0 END)::float AS unsettled
|
||||||
FROM transaction_overrides o
|
FROM transaction_overrides o
|
||||||
JOIN transactions tx ON tx.id = o.transaction_id
|
JOIN transactions tx ON tx.id = o.transaction_id
|
||||||
JOIN transaction_splits ts ON ts.transaction_id = tx.id
|
JOIN transaction_splits ts ON ts.transaction_id = tx.id
|
||||||
|
|||||||
Reference in New Issue
Block a user