| 1 |
/** |
| 2 |
* Admin invoice download helper. |
| 3 |
* |
| 4 |
* Calls the public invoice REST endpoint with the WordPress admin nonce + cookie, |
| 5 |
* so administrators can trigger an invoice PDF download from the admin UI without |
| 6 |
* any extra permission/login round-trip. The same endpoint is used on the |
| 7 |
* customer account page (with the same auth pattern), and supports an HMAC |
| 8 |
* `invoice_token` for guests on the booking confirmation page. |
| 9 |
*/ |
| 10 |
|
| 11 |
interface AdminRestConfig { |
| 12 |
base: string; |
| 13 |
nonce: string; |
| 14 |
} |
| 15 |
|
| 16 |
function getAdminRestConfig(): AdminRestConfig { |
| 17 |
const w = window as unknown as { |
| 18 |
yatraAdmin?: { restUrl?: string; nonce?: string }; |
| 19 |
}; |
| 20 |
const raw = w?.yatraAdmin?.restUrl || "/wp-json"; |
| 21 |
const base = raw.endsWith("/") ? raw.slice(0, -1) : raw; |
| 22 |
const nonce = w?.yatraAdmin?.nonce || ""; |
| 23 |
return { base, nonce }; |
| 24 |
} |
| 25 |
|
| 26 |
function buildInvoiceUrl(base: string, suffix: string): string { |
| 27 |
const origin = |
| 28 |
typeof window !== "undefined" ? window.location.origin : "http://localhost"; |
| 29 |
|
| 30 |
let u: URL; |
| 31 |
try { |
| 32 |
u = new URL(base, origin); |
| 33 |
} catch { |
| 34 |
return `${base.replace(/\/$/, "")}${suffix}?download=1`; |
| 35 |
} |
| 36 |
|
| 37 |
// Plain permalinks: /wp-json is exposed as ?rest_route= |
| 38 |
if (u.searchParams.has("rest_route")) { |
| 39 |
const route = (u.searchParams.get("rest_route") || "").replace(/\/$/, ""); |
| 40 |
u.searchParams.set("rest_route", `${route}${suffix}`); |
| 41 |
u.searchParams.set("download", "1"); |
| 42 |
return u.toString(); |
| 43 |
} |
| 44 |
|
| 45 |
u.pathname = (u.pathname || "").replace(/\/$/, "") + suffix; |
| 46 |
u.searchParams.set("download", "1"); |
| 47 |
return u.toString(); |
| 48 |
} |
| 49 |
|
| 50 |
async function readError(res: Response): Promise<string> { |
| 51 |
let msg = res.statusText || "Request failed"; |
| 52 |
try { |
| 53 |
const j = await res.json(); |
| 54 |
if (j?.message && typeof j.message === "string") { |
| 55 |
msg = j.message; |
| 56 |
} |
| 57 |
} catch { |
| 58 |
const t = await res.text().catch(() => ""); |
| 59 |
if (t && t.length < 300) { |
| 60 |
msg = t; |
| 61 |
} |
| 62 |
} |
| 63 |
return msg; |
| 64 |
} |
| 65 |
|
| 66 |
/** |
| 67 |
* Fetch an invoice PDF from `suffix` and save it, falling back to |
| 68 |
* `fallbackName` when the response carries no Content-Disposition. |
| 69 |
* |
| 70 |
* Authentication is handled server-side via the admin REST nonce + cookie |
| 71 |
* (administrators bypass the ownership check inside the endpoint). |
| 72 |
*/ |
| 73 |
async function downloadInvoicePdf( |
| 74 |
suffix: string, |
| 75 |
fallbackName: string, |
| 76 |
): Promise<void> { |
| 77 |
const { base, nonce } = getAdminRestConfig(); |
| 78 |
if (!nonce) { |
| 79 |
throw new Error("Missing REST nonce; please reload the page."); |
| 80 |
} |
| 81 |
|
| 82 |
const url = buildInvoiceUrl(base, suffix); |
| 83 |
const res = await fetch(url, { |
| 84 |
method: "GET", |
| 85 |
credentials: "include", |
| 86 |
headers: { |
| 87 |
"X-WP-Nonce": nonce, |
| 88 |
Accept: "application/pdf, application/octet-stream, */*", |
| 89 |
}, |
| 90 |
}); |
| 91 |
|
| 92 |
if (!res.ok) { |
| 93 |
throw new Error(await readError(res)); |
| 94 |
} |
| 95 |
|
| 96 |
const blob = await res.blob(); |
| 97 |
const dispo = res.headers.get("Content-Disposition"); |
| 98 |
let filename = fallbackName; |
| 99 |
if (dispo) { |
| 100 |
const m = /filename\*?=(?:UTF-8'')?["']?([^";\n]+)/i.exec(dispo); |
| 101 |
if (m?.[1]) { |
| 102 |
try { |
| 103 |
filename = decodeURIComponent(m[1].replace(/['"]/g, "").trim()); |
| 104 |
} catch { |
| 105 |
filename = m[1].replace(/['"]/g, "").trim(); |
| 106 |
} |
| 107 |
} |
| 108 |
} |
| 109 |
|
| 110 |
const objectUrl = URL.createObjectURL(blob); |
| 111 |
const link = document.createElement("a"); |
| 112 |
link.href = objectUrl; |
| 113 |
link.download = filename; |
| 114 |
document.body.appendChild(link); |
| 115 |
link.click(); |
| 116 |
document.body.removeChild(link); |
| 117 |
URL.revokeObjectURL(objectUrl); |
| 118 |
} |
| 119 |
|
| 120 |
/** |
| 121 |
* Download a payment invoice (receipt for one recorded payment). |
| 122 |
*/ |
| 123 |
export async function downloadAdminInvoice(paymentId: number): Promise<void> { |
| 124 |
if (!paymentId || paymentId <= 0) { |
| 125 |
throw new Error("Invalid payment ID"); |
| 126 |
} |
| 127 |
|
| 128 |
await downloadInvoicePdf( |
| 129 |
`/yatra/v1/payment/${paymentId}/invoice`, |
| 130 |
`invoice-${paymentId}.pdf`, |
| 131 |
); |
| 132 |
} |
| 133 |
|
| 134 |
/** |
| 135 |
* Download a booking invoice — the whole booking rather than a single |
| 136 |
* payment, so it also works for bookings with no payment recorded yet |
| 137 |
* (bank transfer, pay later), where it renders as a pro-forma invoice with |
| 138 |
* the gateway's payment instructions. |
| 139 |
*/ |
| 140 |
export async function downloadAdminBookingInvoice( |
| 141 |
bookingId: number, |
| 142 |
): Promise<void> { |
| 143 |
if (!bookingId || bookingId <= 0) { |
| 144 |
throw new Error("Invalid booking ID"); |
| 145 |
} |
| 146 |
|
| 147 |
await downloadInvoicePdf( |
| 148 |
`/yatra/v1/booking/${bookingId}/invoice`, |
| 149 |
`invoice-booking-${bookingId}.pdf`, |
| 150 |
); |
| 151 |
} |
| 152 |
|