Objective
- Let staff return only the pass-through card processing / convenience fee to the payer’s card without having to refund a slice of the inspection payment that forces a proportional fee refund today.
- Remove the gap where finance or support must use processor or manual work outside Attik when the goal is simply to reverse or waive the fee after capture (goodwill, mistaken pass-through, policy).
Background
- Guardian (CardPointe) capture, when pass-through is enabled and fees are not disabled on the job, creates a linked Fee record and stores fee metadata on Payment on the
guardianPaymentscapture path. POST /guardian-payments/refundinattik-backend/src/routes/guardianPayments.tsrequires a positiverefundAmount, caps settled refunds against remaining refundable payment, and computesproportionalFee = (refundAmount / payment.amount) * feeAmountso the amount sent to the processor isrefundAmount + proportionalFee. A fee-only refund would implyrefundAmount === 0, which the route rejects today.- Void reverses the full capture (payment + fee), not the fee alone.
- Queued refunds and follow-up processing live in
attik-backend/src/util/functions/guardian/checkRefundStatus.ts(batched Guardian refunds); parity with synchronous refund ledger behavior needs to stay consistent when semantics change. - Spectora-synced payments remain blocked from in-app Guardian refund in the same route—treat that as unchanged unless explicitly expanded later.
Scope
Backend
- Refund semantics for Guardian:
attik-backend/src/routes/guardianPayments.ts(router.post('/refund', …)), including validation (refundAmount > 0), ACH partial-refund rules, inquiry-driven void vs refund vs queued paths, andrefundOrVoidTransaction/refundTransactionusage fromattik-backend/src/util/functions/guardian/cardpointeApi.ts. - Ledger consistency:
Refundcreation (includingfeeRefundAmount),Feeupdates (including negative fee / refunded flags on settled refunds), and how partial refunds compose with any new fee-only path. checkRefundStatus.ts: ensure queued / batched refund processing still matches synchronous expectations when fee-only or split semantics exist.- Finix parity is a product decision: fee representation touches
attik-backend/src/routes/webhooks/finix/transfers.ts(e.g.feeless_totalon transfer tags) and may need aligned behavior if Guardian gains fee-only refunds.
Frontend
- Staff refund UX and server actions:
attik-frontend/src/app/tools/inspections/[id]/components/RefundModal.tsx(Guardian vs Finix branching,refundGuardianPaymentfromrefundPaymentactions,getPaymentFeeAmount, max amount / schema validation) so users can choose fee-only vs payment refund safely and copy prevents double-refunding fees.
Out of scope unless explicitly expanded
- Spectora-synced payments in-app refund (still rejected by backend today).
Acceptance criteria (draft)
- [ ] Staff can trigger a refund that returns only the fee (or an agreed subset) to the payer’s card while payment / invoice application matches the chosen product rule (e.g. payment unchanged vs a recorded adjustment—decision needed where product has not locked one rule).
- [ ] Ledger: Fee, Refund, and batch views stay internally consistent; no duplicate fee refunds.
- [ ] Guardrails: permissions, idempotency, clear errors when no fee, fee already refunded, or processor rejects the operation.
- [ ] Document CardPointe constraints (minimum amounts, ACH partial rules) wherever staff-facing errors or internal runbooks are updated.
References
- Guardian refund + fee math:
attik-backend/src/routes/guardianPayments.ts(refund handler:proportionalFee,amountToSendToProcessor,Refund/feeRefundAmount, fee marking on void vs refund). - CardPointe helpers:
attik-backend/src/util/functions/guardian/cardpointeApi.ts(refundOrVoidTransaction,refundTransaction). - Queued refunds:
attik-backend/src/util/functions/guardian/checkRefundStatus.ts. - Refund UI:
attik-frontend/src/app/tools/inspections/[id]/components/RefundModal.tsx. - Finix fee tagging (parity research):
attik-backend/src/routes/webhooks/finix/transfers.ts(feeless_total).