fix: adminnotify observability — money-critical rows sort first, flood-cap suppression surfaced to operator, stale coordination doc fixed

- notifications priority ordering: money-critical reasons (webhooks, sweeps, refunds, gift-card, manual-refund failures) above routine
- admin notifications page exposes the flood-cap suppressed count
- adminnotify.go contract doc: removed stale 2FA reissue-fail site, current insert-site list

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
This commit is contained in:
2026-08-22 00:34:51 +01:00
co-authored by Sisyphus
parent e9b34d0ad6
commit 049c361e16
16 changed files with 1048 additions and 314 deletions
+95
View File
@@ -31,6 +31,60 @@ export const NONCE_STALENESS_MS = 240_000;
// 'saved_card' across the booking, tip, account, till and admin surfaces.
export const PAYMENT_METHOD_SAVED_CARD = 'saved_card';
/**
* Cash till-sale request body shared by every cash payment surface. The
* backend's CreateTerminalPaymentRequest derives the tip from
* `amount - remaining` when `tip_enabled && remaining < amount` — it has NO
* `tip_amount` field — so the tip must be FOLDED INTO the amount (a separate
* `tip_amount` key is dead: it is never parsed and the tip would be silently
* dropped). Single source of truth so the admin and customer cash flows can't
* drift. `tip_enabled` is set only when there actually IS a tip, exactly like
* the old inline bodies.
*/
export type CashTillPaymentBody = {
amount: number;
payment_type: 'full';
payment_method: 'cash';
tip_enabled?: true;
};
export function buildCashTillPaymentBody(
cashDuePence: number,
tipPence: number
): CashTillPaymentBody {
const body: CashTillPaymentBody = {
amount: cashDuePence + tipPence,
payment_type: 'full',
payment_method: 'cash'
};
if (tipPence > 0) body.tip_enabled = true;
return body;
}
/**
* The cash till-sale charge base in pence, aligned with the backend's
* CreateTerminalPayment tip carve (backend/handlers/payments/handlers.go
* ~577-630). The backend derives the recorded tip from `amount remaining`,
* where `remaining` comes from GetBookingRemainingBalancePence (service.go) —
* which does NOT subtract the pending campaign discount: campaigns auto-apply
* AFTER the remaining is read, minting `payment_method='discount'` rows that
* never reduce the balance. If the frontend charges the campaign-reduced total
* (`totalDuePence`), the sent amount is smaller than the backend's remaining,
* so `amount remaining` is absorbed (the tip is under-recorded or the whole
* payment lands as booking credit). Restoring the campaign credit into the
* charge base keeps the sent amount on the same basis the backend carves
* against. `totalDuePence` is the net total (campaign already subtracted,
* pre-loyalty), `campaignPence` the eligible preview credit and `loyaltyPence`
* the redemption being applied on top.
*/
export function cashChargeBasePence(
totalDuePence: number,
campaignPence: number,
loyaltyPence: number
): number {
return Math.max(0, totalDuePence - loyaltyPence + campaignPence);
}
/**
* True when a user's role is allowed to save cards for reuse. Only VERIFIED
* accounts (verified_email, admin) may save cards — guests, unverified accounts
@@ -588,6 +642,47 @@ export async function adminRequestNewTwoFactorCode(
return requestTwoFactorCode(`/api/admin/users/${encodeURIComponent(userID)}/2fa/code`);
}
/**
* Re-sends the account email-verification code via POST /api/verify/generate
* (backend/handlers/auth/local.go GenerateVerificationCodeHandler), minting a
* fresh code for the given email with purpose `email_verify`. Response shape:
* `{ success, message }`.
*
* The legacy `/api/verify-email` endpoint does NOT exist in the backend; its
* ONLY caller was +layout.svelte's verify_email() (frontend/src/routes/+layout.svelte
* lines ~45-61), which still POSTs to the dead URL with no body. +layout.svelte
* is OUTSIDE this fix's ownership, so this helper is the fix vehicle: the
* round-2 agent must switch verify_email() to call
* `resendEmailVerification(authStore.currentUser?.email)` — the endpoint keys
* on the logged-in user's email, so it must be passed in, never read from the
* store inside this helper (keeps it import-free for the vitest suite). No
* other call site references the dead endpoint.
*/
export async function resendEmailVerification(email: string): Promise<Response> {
return fetch('/api/verify/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, purpose: 'email_verify' })
});
}
/**
* Submits the emailed verification code via POST /api/verify/check
* (backend/handlers/auth/local.go VerifyCodeHandler), escalating the account
* from unverified_email to verified_email. The handler keys on the submitted
* `code` alone (the remaining fields are ignored server-side but kept for
* forward-compatibility with a purpose-keyed check). Response shape:
* `{ success, message }` — a success clears the +layout.svelte banner via a
* reload.
*/
export async function verifyEmailCode(email: string, code: string): Promise<Response> {
return fetch('/api/verify/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, code, purpose: 'email_verify' })
});
}
/** Minimal `{"error"|"message": "..."}` extractor for the 2FA code-request
* endpoint bodies (429/503), kept inline so square.ts stays import-free for
* the vitest suite. */