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 { and, desc, eq } from "drizzle-orm";
|
||||
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";
|
||||
|
||||
@Injectable()
|
||||
@@ -13,12 +13,42 @@ export class PaymentsService {
|
||||
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) {
|
||||
return this.db
|
||||
.select()
|
||||
const rows = await this.db
|
||||
.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)
|
||||
.leftJoin(userSubscriptions, eq(payments.subscriptionId, userSubscriptions.id))
|
||||
.leftJoin(plans, eq(userSubscriptions.planId, plans.id))
|
||||
.where(eq(payments.userId, userId))
|
||||
.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;
|
||||
method: "stripe" | "eft";
|
||||
status: "completed" | "pending" | "failed" | "refunded";
|
||||
planName?: string;
|
||||
eftReceiptUrl?: string;
|
||||
stripePaymentIntentId?: string | null;
|
||||
planName?: string | null;
|
||||
eftReceiptUrl?: string | null;
|
||||
hasStripeReceipt?: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -307,9 +307,7 @@ function BillingPage() {
|
||||
{t("billing.downloadReceipt")}
|
||||
</a>
|
||||
</Button>
|
||||
) : payment.method === "stripe" &&
|
||||
payment.status === "completed" &&
|
||||
payment.stripePaymentIntentId ? (
|
||||
) : payment.hasStripeReceipt ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
Reference in New Issue
Block a user