Phase3: Billing Customerpage/Mailings
Some checks failed
Build and Push / build (push) Failing after 46s
Some checks failed
Build and Push / build (push) Failing after 46s
This commit is contained in:
@@ -2407,6 +2407,38 @@ export async function getInvoiceDetail(
|
||||
return { invoice, lines: lines.rows.map(rowToInvoiceLine) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 3 — customer-scoped lookup by human-readable invoice
|
||||
* number with ownership enforcement in a single query. The org
|
||||
* filter is part of the WHERE clause so a customer can't probe
|
||||
* another org's invoice numbers (which are sequential and easy
|
||||
* to guess) and get a different status code (404 vs 403) than
|
||||
* for their own — both miss-and-not-yours return null.
|
||||
*
|
||||
* Used by /api/billing/invoices/[invoiceNumber] and the
|
||||
* /billing/[invoiceNumber] customer page.
|
||||
*/
|
||||
export async function getInvoiceByNumberForOrg(
|
||||
invoiceNumber: string,
|
||||
zitadelOrgId: string
|
||||
): Promise<InvoiceDetail | null> {
|
||||
await ensureSchema();
|
||||
const head = await getPool().query(
|
||||
`SELECT ${INVOICE_LIST_COLUMNS} FROM invoices
|
||||
WHERE invoice_number = $1 AND zitadel_org_id = $2
|
||||
LIMIT 1`,
|
||||
[invoiceNumber, zitadelOrgId]
|
||||
);
|
||||
if (head.rows.length === 0) return null;
|
||||
const invoice = rowToInvoice(head.rows[0]);
|
||||
const lines = await getPool().query(
|
||||
`SELECT * FROM invoice_lines WHERE invoice_id = $1
|
||||
ORDER BY display_order, id`,
|
||||
[invoice.id]
|
||||
);
|
||||
return { invoice, lines: lines.rows.map(rowToInvoiceLine) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the PDF bytes for an invoice. Returns null if no PDF was
|
||||
* stored (shouldn't happen in v1; defensive against partial state).
|
||||
|
||||
Reference in New Issue
Block a user