fix(payments): curate /payments/me response — stop leaking internal columns, join plan name
getMyPayments returned the raw payment row, exposing internal fields (adminNote, iyzicoPaymentId, bankAccountId, session/intent ids) to the end user. Replace with an explicit projection that returns only what the billing UI needs, joins planName from the subscription's plan (was always "-"), and surfaces Stripe receipt availability as a hasStripeReceipt boolean instead of the raw payment intent id. Frontend reads the boolean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
import { Inject, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||||
import { and, desc, eq } from "drizzle-orm";
|
import { and, desc, eq } from "drizzle-orm";
|
||||||
import { DATABASE, type Database } from "../database/database.provider";
|
import { DATABASE, type Database } from "../database/database.provider";
|
||||||
import { payments } from "../database/schema/core";
|
import { payments, plans, userSubscriptions } from "../database/schema/core";
|
||||||
import { StripeService } from "./stripe/stripe.service";
|
import { StripeService } from "./stripe/stripe.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -13,12 +13,42 @@ export class PaymentsService {
|
|||||||
private stripeService: StripeService,
|
private stripeService: StripeService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payment history for the billing page. Returns a curated projection — never
|
||||||
|
* the raw row — so internal columns (adminNote, iyzicoPaymentId,
|
||||||
|
* bankAccountId, session/intent ids) are not leaked to the client. planName
|
||||||
|
* is joined from the subscription's plan; Stripe receipt availability is
|
||||||
|
* surfaced as a boolean rather than exposing the payment intent id.
|
||||||
|
*/
|
||||||
async getMyPayments(userId: string) {
|
async getMyPayments(userId: string) {
|
||||||
return this.db
|
const rows = await this.db
|
||||||
.select()
|
.select({
|
||||||
|
id: payments.id,
|
||||||
|
amount: payments.amount,
|
||||||
|
method: payments.method,
|
||||||
|
status: payments.status,
|
||||||
|
createdAt: payments.createdAt,
|
||||||
|
planName: plans.name,
|
||||||
|
eftReceiptUrl: payments.eftReceiptUrl,
|
||||||
|
stripePaymentIntentId: payments.stripePaymentIntentId,
|
||||||
|
})
|
||||||
.from(payments)
|
.from(payments)
|
||||||
|
.leftJoin(userSubscriptions, eq(payments.subscriptionId, userSubscriptions.id))
|
||||||
|
.leftJoin(plans, eq(userSubscriptions.planId, plans.id))
|
||||||
.where(eq(payments.userId, userId))
|
.where(eq(payments.userId, userId))
|
||||||
.orderBy(desc(payments.createdAt));
|
.orderBy(desc(payments.createdAt));
|
||||||
|
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
amount: row.amount,
|
||||||
|
method: row.method,
|
||||||
|
status: row.status,
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
planName: row.planName ?? null,
|
||||||
|
eftReceiptUrl: row.eftReceiptUrl ?? null,
|
||||||
|
hasStripeReceipt:
|
||||||
|
row.method === "stripe" && row.status === "completed" && !!row.stripePaymentIntentId,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ interface Payment {
|
|||||||
amount: number;
|
amount: number;
|
||||||
method: "stripe" | "eft";
|
method: "stripe" | "eft";
|
||||||
status: "completed" | "pending" | "failed" | "refunded";
|
status: "completed" | "pending" | "failed" | "refunded";
|
||||||
planName?: string;
|
planName?: string | null;
|
||||||
eftReceiptUrl?: string;
|
eftReceiptUrl?: string | null;
|
||||||
stripePaymentIntentId?: string | null;
|
hasStripeReceipt?: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,9 +307,7 @@ function BillingPage() {
|
|||||||
{t("billing.downloadReceipt")}
|
{t("billing.downloadReceipt")}
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
) : payment.method === "stripe" &&
|
) : payment.hasStripeReceipt ? (
|
||||||
payment.status === "completed" &&
|
|
||||||
payment.stripePaymentIntentId ? (
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
Reference in New Issue
Block a user