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:
@@ -10,8 +10,10 @@
|
||||
import { apiFetch } from '$lib/utils/api';
|
||||
import { formatCurrency } from '$lib/utils/format';
|
||||
import {
|
||||
buildCashTillPaymentBody,
|
||||
CARD_VERIFICATION_RETRY_MESSAGE,
|
||||
campaignDiscountPence,
|
||||
cashChargeBasePence,
|
||||
isOverflowTipConfirmationRequired,
|
||||
isVerificationRequiredSignal,
|
||||
PAYMENT_METHOD_SAVED_CARD,
|
||||
@@ -558,7 +560,20 @@
|
||||
|
||||
let cashAmount = $state<string>('');
|
||||
const cashAmountNum = $derived(cashAmount === '' ? 0 : parseFloat(cashAmount));
|
||||
const changeDue = $derived(cashAmountNum > totalDue ? cashAmountNum - totalDue : 0);
|
||||
// Cash charge base in pence. The backend's CreateTerminalPayment derives the
|
||||
// tip from `amount − remaining`, and `remaining` (GetBookingRemainingBalancePence)
|
||||
// does NOT subtract the pending campaign discount — so the campaign preview
|
||||
// must be restored into the charge base or the tip is absorbed into booking
|
||||
// credit. Shared with the change/tip display so every cash figure agrees.
|
||||
const cashDuePence = $derived(
|
||||
cashChargeBasePence(
|
||||
Math.round(totalDue * 100),
|
||||
campaignDiscountPence(discountPreview),
|
||||
loyaltyDiscount
|
||||
)
|
||||
);
|
||||
const cashDue = $derived(cashDuePence / 100);
|
||||
const changeDue = $derived(cashAmountNum > cashDue ? cashAmountNum - cashDue : 0);
|
||||
let extraAsTip = $state(false);
|
||||
|
||||
function handleCashInput(e: Event) {
|
||||
@@ -571,7 +586,6 @@
|
||||
|
||||
async function handleCashPayment() {
|
||||
if (isProcessingPaymentSync) return;
|
||||
const cashDue = totalDue - loyaltyDiscount / 100;
|
||||
|
||||
if (cashDue <= 0) {
|
||||
toast.error('Nothing to charge — the booking is fully covered by discounts');
|
||||
@@ -591,15 +605,11 @@
|
||||
try {
|
||||
await applyLoyaltyRedemption();
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
amount: Math.round(cashDue * 100),
|
||||
payment_type: 'full',
|
||||
payment_method: 'cash'
|
||||
};
|
||||
if (tipAmount > 0) {
|
||||
body.tip_enabled = true;
|
||||
body.tip_amount = Math.round(tipAmount * 100);
|
||||
}
|
||||
// Cash till-sale body: the tip is FOLDED into the amount (the backend's
|
||||
// CreateTerminalPaymentRequest derives the tip from `amount - remaining`
|
||||
// when tip_enabled; it has no tip_amount field, so sending one would
|
||||
// silently drop the tip).
|
||||
const body = buildCashTillPaymentBody(Math.round(cashDue * 100), Math.round(tipAmount * 100));
|
||||
|
||||
const response = await apiFetch(`/api/admin/bookings/${booking.id}/payment`, {
|
||||
method: 'POST',
|
||||
@@ -890,22 +900,21 @@
|
||||
status = 'saved-card-processing';
|
||||
}
|
||||
|
||||
const response = await submitPaymentWithRetry(
|
||||
() =>
|
||||
apiFetch(`/api/admin/bookings/${booking.id}/payment`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
amount: chargeAmount,
|
||||
payment_type: 'full',
|
||||
payment_method: 'saved_card',
|
||||
saved_card_id: selectedSavedCardId,
|
||||
// C1: the SCA tokenize-result token is the charge SOURCE
|
||||
// (new_card_token) alongside the saved-card ref — never
|
||||
// the legacy verification_token.
|
||||
...(verificationToken ? { new_card_token: verificationToken } : {}),
|
||||
idempotency_key: savedCardIdempotencyKey
|
||||
})
|
||||
const response = await submitPaymentWithRetry(() =>
|
||||
apiFetch(`/api/admin/bookings/${booking.id}/payment`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
amount: chargeAmount,
|
||||
payment_type: 'full',
|
||||
payment_method: 'saved_card',
|
||||
saved_card_id: selectedSavedCardId,
|
||||
// C1: the SCA tokenize-result token is the charge SOURCE
|
||||
// (new_card_token) alongside the saved-card ref — never
|
||||
// the legacy verification_token.
|
||||
...(verificationToken ? { new_card_token: verificationToken } : {}),
|
||||
idempotency_key: savedCardIdempotencyKey
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
@@ -996,7 +1005,7 @@
|
||||
|
||||
$effect(() => {
|
||||
if (selectedMethod === 'cash') {
|
||||
cashAmount = totalDue.toFixed(2);
|
||||
cashAmount = cashDue.toFixed(2);
|
||||
extraAsTip = false;
|
||||
}
|
||||
if (selectedMethod === 'giftcard') {
|
||||
@@ -1387,7 +1396,7 @@
|
||||
{#each tipPercentages as tip (tip.pct)}
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg border py-3 text-center text-sm font-semibold transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none hover:bg-fuchsia-50 {selectedTipPercent ===
|
||||
class="rounded-lg border py-3 text-center text-sm font-semibold transition-colors hover:bg-fuchsia-50 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none {selectedTipPercent ===
|
||||
tip.pct
|
||||
? 'border-input bg-fuchsia-100 text-foreground'
|
||||
: 'border-input'}"
|
||||
@@ -1447,7 +1456,7 @@
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between rounded-md border border-gray-200 bg-white p-4">
|
||||
<span class="text-base font-semibold text-gray-700">Total Due</span>
|
||||
<span class="text-xl font-bold text-gray-900">{formatCurrency(totalDue)}</span>
|
||||
<span class="text-xl font-bold text-gray-900">{formatCurrency(cashDue)}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -1465,7 +1474,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if cashAmountNum >= totalDue}
|
||||
{#if cashAmountNum >= cashDue}
|
||||
<div class="rounded-md border border-green-200 bg-green-50 p-4">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm font-medium text-green-800">Change Due</span>
|
||||
@@ -1487,7 +1496,7 @@
|
||||
<Button
|
||||
onclick={handleCashPayment}
|
||||
class="min-h-11 flex-1"
|
||||
disabled={cashAmountNum < totalDue || nothingToCharge}
|
||||
disabled={cashAmountNum < cashDue || nothingToCharge}
|
||||
>
|
||||
Confirm Cash
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user