// api.jsx — OttoKlar Partner API client (v1)
// Base URL resolves in three steps: window.OK_ENV.API_BASE (set by the optional,
// gitignored env.js loaded before this file — see env.example.js), then the
// production hostname map, then the dev environment. env.js is never deployed, so
// in practice it is the local override and the map decides deployed environments.
// CORS allows the deployed Vercel origin; localhost works once the backend
// allowlists it.

// A prod allowlist with a dev default: only these hostnames reach the prod API.
// Previews, localhost, and any unrecognised origin fall through to dev, so
// production traffic is never reachable by omission.
const OK_API_HOSTS = {
  "ottoklar.com": "https://api.prod.ottoklar.com",
  "www.ottoklar.com": "https://api.prod.ottoklar.com",
};

const OK_API_BASE = (window.OK_ENV && window.OK_ENV.API_BASE)
  || OK_API_HOSTS[location.hostname]
  || "https://api.dev.ottoklar.com";

// ── Auth token storage ────────────────────────────────────────────────────────

const OK_AUTH_KEY = "ok_partner_auth";

// The stored auth is the whole PartnerLoginResponse — { accessToken, refreshToken,
// partnerId }. The accessToken (a short-lived JWT) goes on auth-gated requests; the
// refreshToken renews it (see okRefreshAuth) so the partner isn't logged out when the
// access token expires.
function okGetAuth() {
  try { return JSON.parse(localStorage.getItem(OK_AUTH_KEY)) || null; }
  catch { return null; }
}
function okSetAuth(auth) {
  if (auth) localStorage.setItem(OK_AUTH_KEY, JSON.stringify(auth));
  else localStorage.removeItem(OK_AUTH_KEY);
}

// Single-flight refresh. A dashboard mount fires several auth-gated requests at once;
// if the access token has expired they all 401 together. The refresh token is rotated
// (single-use) by /v1/partner/refresh, so concurrent refreshes would each revoke the
// other's token and bounce the partner to the login screen. Funnelling every 401 through
// one shared in-flight promise means a single refresh call, a single rotation.
let okRefreshInFlight = null;
function okRefreshAuth() {
  if (okRefreshInFlight) return okRefreshInFlight;
  const a = okGetAuth();
  if (!a || !a.refreshToken) return Promise.reject(new Error("No refresh token"));
  okRefreshInFlight = okFetch("/v1/partner/refresh", { method: "POST", json: { refreshToken: a.refreshToken } })
    .then((data) => { okSetAuth(data); return data; })
    .finally(() => { okRefreshInFlight = null; });
  return okRefreshInFlight;
}

// Consumer report access tokens, one per inspection. The email-verify endpoint
// returns an accessToken on success; persisting it (keyed by inspection id) keeps a
// returning visitor's report unlocked without re-verifying their email.
const OK_INSPECTION_TOKEN_PREFIX = "ok_inspection_token:";
function okGetInspectionToken(id) {
  if (!id) return null;
  try { return localStorage.getItem(OK_INSPECTION_TOKEN_PREFIX + id) || null; }
  catch { return null; }
}
function okSetInspectionToken(id, token) {
  if (!id) return;
  try {
    if (token) localStorage.setItem(OK_INSPECTION_TOKEN_PREFIX + id, token);
    else localStorage.removeItem(OK_INSPECTION_TOKEN_PREFIX + id);
  } catch { /* ignore storage failures */ }
}
// Bearer header carrying the consumer's per-inspection access token (set on email
// verification) for the auth-gated inspection endpoints (partners, vin). okFetch's
// `auth` flag attaches the *partner* token, which is the wrong credential here.
function okInspectionAuthHeaders(id) {
  const token = okGetInspectionToken(id);
  return token ? { Authorization: `Bearer ${token}` } : {};
}

// ── Helpers ───────────────────────────────────────────────────────────────────

function okIsAbsoluteUrl(v) {
  return typeof v === "string" && /^https?:\/\//i.test(v);
}

// ── Error helper ──────────────────────────────────────────────────────────────

async function okParseError(res) {
  let msg = `Request failed (${res.status})`;
  try {
    const body = await res.json();
    if (typeof body === "string") msg = body;
    else if (body.detail) msg = body.detail;
    else if (body.title) msg = body.title;
    else if (body.errors) {
      const parts = Object.values(body.errors).flat();
      if (parts.length) msg = parts.join(" ");
    }
  } catch { /* keep default */ }
  const err = new Error(msg);
  err.status = res.status;
  return err;
}

async function okFetch(path, { method = "GET", json, formData, auth = false, headers = {}, blob = false, _retried = false } = {}) {
  const opts = { method, headers: { ...headers } };
  if (json !== undefined) {
    opts.headers["Content-Type"] = "application/json";
    opts.body = JSON.stringify(json);
  }
  if (formData) opts.body = formData;
  if (auth) {
    const a = okGetAuth();
    if (a && a.accessToken) opts.headers["Authorization"] = `Bearer ${a.accessToken}`;
  }
  const res = await fetch(`${OK_API_BASE}${path}`, opts);
  if (!res.ok) {
    // Expired access token: refresh once and retry transparently so the partner stays
    // signed in. Only for auth-gated calls, only on the first try (guard against loops),
    // and only when a refresh token is on hand. If the refresh itself fails — refresh
    // token expired or revoked — drop the stored auth and surface the original 401, which
    // sends the partner back to login on the next gated navigation.
    if (res.status === 401 && auth && !_retried && okGetAuth() && okGetAuth().refreshToken) {
      try {
        await okRefreshAuth();
      } catch {
        okSetAuth(null);
        throw await okParseError(res);
      }
      return okFetch(path, { method, json, formData, auth, headers, blob, _retried: true });
    }
    throw await okParseError(res);
  }
  if (blob) return res.blob();
  const ct = res.headers.get("content-type") || "";
  // Parse defensively: several 2xx endpoints (attach email / attach VIN) return an empty
  // body — sometimes with a JSON content-type. res.json() throws on empty input, which would
  // mis-report a success as a failure and, in the email gate, silently strand the user on the
  // email step (the "code window won't show" bug). Read once, treat empty as null.
  const text = await res.text();
  if (!text) return null;
  return ct.includes("json") ? JSON.parse(text) : text;
}

// ── API surface ───────────────────────────────────────────────────────────────

const OK_API = {
  base: OK_API_BASE,

  // Vehicle lookups
  getMakes: () => okFetch("/v1/makes"),
  getModels: (makeId) => okFetch(`/v1/models?makeId=${encodeURIComponent(makeId)}`),
  getModelYears: (modelId) => okFetch(`/v1/models/${encodeURIComponent(modelId)}/years`),

  // Inspections
  // fields: { makeId, modelId, year, zipCode, countryCode, photos: File[], latitude?, longitude? }
  submitEstimate: ({ makeId, modelId, year, zipCode, countryCode = "DE", photos = [], latitude, longitude }) => {
    const fd = new FormData();
    fd.append("makeId", makeId);
    fd.append("modelId", modelId);
    fd.append("year", year);
    fd.append("zipCode", zipCode);
    fd.append("countryCode", countryCode);
    if (photos[0]) fd.append("photos", photos[0]);
    if (latitude != null) fd.append("latitude", String(latitude));
    if (longitude != null) fd.append("longitude", String(longitude));
    return okFetch("/v1/inspections/estimate", { method: "POST", formData: fd });
  },
  getProcessStatus: (id) => okFetch(`/v1/inspections/${encodeURIComponent(id)}/process-status`),
  // Full inspection report (vehicle, damaged parts, damages, diagram, images). Publicly accessible by id.
  getInspection: (id) => okFetch(`/v1/inspections/${encodeURIComponent(id)}`),
  // Partners near an inspection. `type` is required — "Appraiser" (KFZ Gutachter) or
  // "Workshop". `postcode` is optional and defaults server-side to the inspection's own
  // zipCode; pass it to search around a different area. Returns an array of
  // NearbyPartnerResponse { id, rank, name, street, houseNumber, postcode, city,
  // countryCode, distanceKm, rating, reviewCount, specialty, latitude, longitude }.
  getInspectionPartners: (id, { type, postcode, limit = 10 } = {}) => {
    const p = new URLSearchParams({ type, limit: String(limit) });
    if (postcode) p.set("postcode", postcode);
    // Auth-gated (401 without it) — carries the consumer's per-inspection access token.
    return okFetch(`/v1/inspections/${encodeURIComponent(id)}/partners?${p.toString()}`, { headers: okInspectionAuthHeaders(id) });
  },
  // Independent (non-partner) workshops near an inspection — live Google Places results,
  // ranked by distance, shown as the fallback when no OttoKlar partner covers the region.
  // `postcode` is optional and defaults server-side to the inspection's own zipCode; pass
  // it to search around a different area. `limit` caps how many are returned (default 5).
  // Returns an array of NearbyWorkshopResponse { rank, name, address, distanceKm, rating,
  // reviewCount, phoneNumber, latitude, longitude, googleMapsUri, websiteUri }. Auth-gated
  // like getInspectionPartners (401 without the consumer's per-inspection access token).
  getNearbyWorkshops: (id, { postcode, limit = 5 } = {}) => {
    const p = new URLSearchParams({ limit: String(limit) });
    if (postcode) p.set("postcode", postcode);
    return okFetch(`/v1/inspections/${encodeURIComponent(id)}/nearby-workshops?${p.toString()}`, { headers: okInspectionAuthHeaders(id) });
  },
  // Independent (non-partner) appraisers near an inspection — live Google Places results,
  // ranked by distance, the fallback shown when no OttoKlar partner appraiser is in range.
  // Same shape and auth as getNearbyWorkshops. `postcode` is optional (defaults server-side
  // to the inspection's own zipCode); `limit` caps the count (default 5). Returns an array
  // of NearbyAppraiserResponse { rank, name, address, distanceKm, rating, reviewCount,
  // phoneNumber, latitude, longitude, googleMapsUri, websiteUri }.
  getNearbyAppraisers: (id, { postcode, limit = 5 } = {}) => {
    const p = new URLSearchParams({ limit: String(limit) });
    if (postcode) p.set("postcode", postcode);
    return okFetch(`/v1/inspections/${encodeURIComponent(id)}/nearby-appraisers?${p.toString()}`, { headers: okInspectionAuthHeaders(id) });
  },
  // Attach a VIN to an inspection's vehicle (auth-gated, same per-inspection token as
  // partners). Request { vin }; 200 with no body on success, 400 if the VIN is rejected.
  attachInspectionVin: (id, vin) =>
    okFetch(`/v1/inspections/${encodeURIComponent(id)}/vin`, { method: "POST", json: { vin }, headers: okInspectionAuthHeaders(id) }),
  // Refer an inspection to a nearby partner (auth-gated, same per-inspection token) —
  // backs the "Book appointment" / "Request quote" buttons. No body; returns
  // { referralId, status }. 409 means a referral to this partner already exists.
  createPartnerReferral: (id, partnerId) =>
    okFetch(`/v1/inspections/${encodeURIComponent(id)}/partners/${encodeURIComponent(partnerId)}/referral`, { method: "POST", headers: okInspectionAuthHeaders(id) }),
  // Detailed cost-breakdown PDF — a one-time Stripe purchase (auth-gated, same per-inspection
  // token as the partner endpoints). All three back the "Download detailed cost breakdown" card:
  //  • status   → { paid, available, priceCents, currency }; polled to toggle buy↔download and
  //    to restore access after a page refresh.
  //  • checkout → creates a one-time Stripe Checkout Session and returns its hosted URL (shape
  //    undocumented, so callers extract it defensively); 409 if already paid, 400 if no estimate yet.
  //  • pdf      → streams the PDF as a blob; 403 until paid, re-downloadable thereafter.
  getCostBreakdownStatus: (id) =>
    okFetch(`/v1/inspections/${encodeURIComponent(id)}/cost-breakdown`, { headers: okInspectionAuthHeaders(id) }),
  createCostBreakdownCheckout: (id) =>
    okFetch(`/v1/inspections/${encodeURIComponent(id)}/cost-breakdown/checkout`, { method: "POST", headers: okInspectionAuthHeaders(id) }),
  // `lang` (en|de) is a pure render parameter — it selects the PDF's language and does NOT
  // affect the payment entitlement (that stays keyed on inspectionId only). Defaults to the
  // app's active language via getLang().
  getCostBreakdownPdf: (id) =>
    okFetch(`/v1/inspections/${encodeURIComponent(id)}/cost-breakdown/pdf?lang=${encodeURIComponent(getLang())}`, { blob: true, headers: okInspectionAuthHeaders(id) }),
  // Consumer email capture on the results page (publicly accessible by inspection id, no auth):
  // attach stores the email and emails a 6-digit code; verify confirms that code and returns
  // an accessToken ({ accessToken }) which we persist client-side to keep the report unlocked.
  attachInspectionEmail: (id, email) =>
    okFetch(`/v1/inspections/${encodeURIComponent(id)}/email`, { method: "POST", json: { email } }),
  verifyInspectionEmail: async (id, code) => {
    const data = await okFetch(`/v1/inspections/${encodeURIComponent(id)}/email/verify`, { method: "POST", json: { code } });
    if (data && data.accessToken) okSetInspectionToken(id, data.accessToken);
    return data;
  },
  // Stored consumer report token for an inspection (null until the email is verified).
  getInspectionToken: (id) => okGetInspectionToken(id),
  // SSE stream of processing progress. The server emits NAMED `progress` events, which the
  // browser delivers only to an explicit listener — `onmessage` alone never fires for them.
  // Reconnection is owned here (like okFetch owns the 401→refresh→retry): a native EventSource
  // auto-reconnects while CONNECTING, but a CLOSED stream is permanently dead and dies silently,
  // so it is recreated after a short delay. Returns a { close } handle — the underlying
  // EventSource is replaced across reconnects, so the raw instance is never exposed.
  openInspectionEvents: (id, { onStatus, onError } = {}) => {
    const url = `${OK_API_BASE}/v1/inspections/${encodeURIComponent(id)}/events`;
    let es = null;
    let retryId = null;
    let closed = false;
    const handle = (e) => {
      if (!e.data) return;
      try {
        const data = JSON.parse(e.data);
        onStatus && onStatus(data);
      } catch { /* ignore non-JSON frames (heartbeats) */ }
    };
    const connect = () => {
      es = new EventSource(url);
      es.addEventListener("progress", handle); // server sends `event: progress`
      es.onmessage = handle;                   // fallback for unnamed/default frames
      es.onerror = (e) => {
        onError && onError(e);
        if (closed || es.readyState !== EventSource.CLOSED) return;
        es.close();
        retryId = setTimeout(connect, 5000);
      };
    };
    connect();
    return {
      close: () => {
        closed = true;
        if (retryId) clearTimeout(retryId);
        if (es) es.close();
      },
    };
  },
  // The inspection API returns these as absolute URLs already; pass them through
  // untouched. The filename-wrapping form is a fallback for bare filenames.
  inspectionPhotoUrl: (filename, size) =>
    okIsAbsoluteUrl(filename)
      ? (size ? `${filename}${filename.includes("?") ? "&" : "?"}size=${size}` : filename)
      : `${OK_API_BASE}/v1/inspection-photos/${encodeURIComponent(filename)}${size ? `?size=${size}` : ""}`,
  damagePhotoUrl: (filename) =>
    okIsAbsoluteUrl(filename) ? filename : `${OK_API_BASE}/v1/damage-photos/${encodeURIComponent(filename)}`,
  diagramUrl: (filename) =>
    okIsAbsoluteUrl(filename) ? filename : `${OK_API_BASE}/v1/inspections/diagram/${encodeURIComponent(filename)}`,

  // Partner auth
  partnerRegister: ({ partnerType, fullName, businessName, email, password }) =>
    okFetch("/v1/partner/register", { method: "POST", json: { partnerType, fullName, businessName, email, password } }),
  partnerActivate: async (email, code) => {
    const data = await okFetch("/v1/partner/activate", { method: "POST", json: { email, code } });
    okSetAuth(data);
    return data;
  },
  partnerResendActivation: (email) =>
    okFetch("/v1/partner/resend-activation", { method: "POST", json: { email } }),
  // Forgot/reset password (pre-login, no auth). forgot-password emails a reset link
  // (…/reset-password?token=<token>); reset-password completes it with the new password.
  partnerForgotPassword: (email) =>
    okFetch("/v1/partner/forgot-password", { method: "POST", json: { email } }),
  partnerResetPassword: (token, newPassword) =>
    okFetch("/v1/partner/reset-password", { method: "POST", json: { token, newPassword } }),
  partnerLogin: async (email, password) => {
    const data = await okFetch("/v1/partner/login", { method: "POST", json: { email, password } });
    okSetAuth(data);
    return data;
  },
  // Exchange the stored refresh token for a fresh access + rotated refresh token and
  // persist them. okFetch calls this automatically on a 401, so the dashboard rarely
  // needs it directly; exposed for explicit/manual renewal. Single-flighted internally.
  partnerRefresh: () => okRefreshAuth(),
  signOut: () => okSetAuth(null),
  isSignedIn: () => !!okGetAuth(),

  // Partner profile (authenticated) — data the partner gave at sign-up
  getPartnerMe: () => okFetch("/v1/partner/me", { auth: true }),
  updatePartnerMe: (data) => okFetch("/v1/partner/me", { method: "PUT", json: data, auth: true }),
  changePassword: (currentPassword, newPassword) =>
    okFetch("/v1/partner/change-password", { method: "POST", json: { currentPassword, newPassword }, auth: true }),

  // Partner program stats for the referrals dashboard (authenticated). Note the
  // plural "partners" path — unlike the singular /v1/partner/* endpoints above.
  // Returns { newReferrals, openReferrals, averageJobValue, currency, totalReferrals }.
  getPartnerStats: () => okFetch("/v1/partners/stats", { auth: true }),

  // Partner billing — Stripe invoices (authenticated). Returns an array of
  // InvoiceSummary { id, number, status, total (cents), currency, created (date-time),
  // hostedInvoiceUrl, invoicePdf }. Throws a 400 ("Billing has not been set up for this
  // partner yet.") when the partner has no Stripe billing configured. `StartingAfter` is
  // a Stripe cursor (an invoice id) for paging.
  getInvoices: ({ limit = 20, startingAfter } = {}) => {
    const p = new URLSearchParams({ Limit: String(limit) });
    if (startingAfter) p.set("StartingAfter", startingAfter);
    return okFetch(`/v1/partner/billing/invoices?${p.toString()}`, { auth: true });
  },

  // Partner billing — default (primary) card (authenticated). Returns a
  // PaymentMethodSummary { brand, last4, expMonth, expYear } (no card-holder name —
  // the dashboard sources that from the partner profile). Throws a 400 when the
  // partner has no card / billing isn't set up; treat that as "no card on file".
  getPaymentMethod: () => okFetch("/v1/partner/billing/payment-method", { auth: true }),

  // Partner billing — upcoming (next) auto-charge (authenticated). Returns an
  // UpcomingChargeSummary { amountCents, currency, nextBillingDate }. Like the other
  // billing reads, throws a 400 when billing isn't set up for the partner yet; callers
  // treat that as "nothing scheduled".
  getUpcomingCharge: () => okFetch("/v1/partner/billing/upcoming-charge", { auth: true }),

  // Create a Stripe Customer Portal session (authenticated) so the partner can
  // manage/edit their card. The response carries the hosted-portal URL to redirect
  // to (Stripe returns the partner here when they're done). Returned shape isn't
  // documented, so callers extract the URL defensively (string or { url }).
  createBillingPortalSession: () => okFetch("/v1/partner/billing/portal-session", { method: "POST", auth: true }),

  // Create a Stripe Checkout session for card setup (authenticated) — used during
  // onboarding to add the first payment method. Like the portal session, the response
  // carries the hosted Checkout URL to redirect to; Stripe sends the partner back when
  // done. Throws 409 when the partner already has card setup in progress / on file.
  createSetupSession: () => okFetch("/v1/partner/billing/setup-session", { method: "POST", auth: true }),

  // The Stripe session-creating endpoints (portal / setup / checkout) return the hosted
  // URL to redirect to, but the response shape isn't documented — it may be a bare string
  // or an object under any of several keys. Extract it defensively in one place.
  sessionUrl: (r) => (typeof r === "string" ? r : (r && (r.url || r.checkoutUrl || r.portalUrl || r.sessionUrl || r.redirectUrl)) || null),

  // Partner documents (authenticated). The upload endpoints return the updated
  // PartnerProfileResponse (lift it into state); delete returns 204 with no body
  // (re-fetch /me to refresh). Downloads are auth-gated binary — fetch as a blob.
  // businessRegistration is a single slot (PUT replaces); certifications is a
  // collection (POST appends). Each doc summary: { id, fileName, contentType, sizeBytes, uploadedAt }.
  uploadBusinessRegistration: (file) => {
    const fd = new FormData();
    fd.append("file", file);
    return okFetch("/v1/partner/me/business-registration", { method: "PUT", formData: fd, auth: true });
  },
  deleteBusinessRegistration: () =>
    okFetch("/v1/partner/me/business-registration", { method: "DELETE", auth: true }),
  downloadBusinessRegistration: () =>
    okFetch("/v1/partner/me/business-registration", { auth: true, blob: true }),

  uploadCertifications: (files) => {
    const fd = new FormData();
    (Array.isArray(files) ? files : [files]).forEach((f) => fd.append("files", f));
    return okFetch("/v1/partner/me/certifications", { method: "POST", formData: fd, auth: true });
  },
  deleteCertification: (documentId) =>
    okFetch(`/v1/partner/me/certifications/${encodeURIComponent(documentId)}`, { method: "DELETE", auth: true }),
  downloadCertification: (documentId) =>
    okFetch(`/v1/partner/me/certifications/${encodeURIComponent(documentId)}`, { auth: true, blob: true }),

  // Referrals (authenticated) — one page per call.
  // The endpoint returns a PagedResultOfReferralListItemResponse envelope
  // ({ items, pageNumber, pageSize, totalCount, totalPages, … }). We normalize and
  // return the whole envelope so the dashboard can render a single page and drive
  // its pager from totalCount/totalPages. `status` is the API-form value
  // ("New", "OfferSent", …) or omitted for all. Tolerates a bare array (old shape).
  getReferrals: async ({ page = 1, pageSize = 20, status } = {}) => {
    const p = new URLSearchParams({ PageNumber: String(page), PageSize: String(pageSize) });
    if (status) p.set("Status", status);
    const data = await okFetch(`/v1/partner/referrals?${p.toString()}`, { auth: true });
    if (Array.isArray(data)) {                               // legacy: bare array, no page metadata
      return { items: data, pageNumber: 1, pageSize: data.length, totalCount: data.length, totalPages: 1 };
    }
    return {
      items:      Array.isArray(data?.items) ? data.items : [],
      pageNumber: Number(data?.pageNumber) || page,
      pageSize:   Number(data?.pageSize)   || pageSize,
      totalCount: Number(data?.totalCount) || 0,
      totalPages: Number(data?.totalPages) || 1,
    };
  },
  getReferral: (referralId) =>
    okFetch(`/v1/partner/referrals/${encodeURIComponent(referralId)}`, { auth: true }),
  // Submit a workshop offer for an assigned referral (multipart/form-data).
  // fields: { minValue, maxValue, requiresInPersonInspection, estimatedWorkingDays, earliestDropOffDate, quote? }
  submitOffer: (referralId, { minValue, maxValue, requiresInPersonInspection, estimatedWorkingDays, earliestDropOffDate, quote } = {}) => {
    const fd = new FormData();
    fd.append("minValue", minValue);
    fd.append("maxValue", maxValue);
    fd.append("requiresInPersonInspection", String(!!requiresInPersonInspection));
    fd.append("estimatedWorkingDays", estimatedWorkingDays);
    fd.append("earliestDropOffDate", earliestDropOffDate);
    if (quote) fd.append("quote", quote);
    return okFetch(`/v1/partner/referrals/${encodeURIComponent(referralId)}/offer`, { method: "POST", formData: fd, auth: true });
  },
  // Accept an assessment (Gutachten) case for an assigned referral (application/json).
  // fields: { estimatedDays: int, notes?: string, slots: [{ date, fromTime, toTime }] }
  acceptReferral: (referralId, { estimatedDays, notes, slots } = {}) =>
    okFetch(`/v1/partner/referrals/${encodeURIComponent(referralId)}/accept`, {
      method: "POST",
      json: { estimatedDays, notes: notes || null, slots },
      auth: true,
    }),
  // Decline an assigned referral (workshop or Gutachten). No request body.
  // Fails with 409 if the referral has already been responded to.
  declineReferral: (referralId) =>
    okFetch(`/v1/partner/referrals/${encodeURIComponent(referralId)}/decline`, { method: "POST", auth: true }),
  // Record that the partner opened a new referral (New → Reviewed). No request body.
  // Idempotent on already-reviewed referrals; 409 if already responded to.
  markReferralReviewed: (referralId) =>
    okFetch(`/v1/partner/referrals/${encodeURIComponent(referralId)}/mark-referral-as-reviewed`, { method: "POST", auth: true }),
};

Object.assign(window, { OK_API });
