CRM · Receipt verification queue
Moderation workspace for customer-uploaded purchase receipts: review the image and OCR’d data, validate (which also redeems the entitlement), reject with a reason, un-reject, and navigate the queue with previous/next.
| Area | CRM |
| Routes | crm.receipts · crm.receipts.index |
| Centre modules | tickets |
| Staff levels | root · admin · manager · crmmanager · crmoperator · weboperator |
| Models | campaign · commerce · receipt |
| UI label | title.header.crm.receipts / menu.navbar.receipt.show |
| Coverage | 5 use cases · 4 happy · 7 failure |
Known issues (1)
How it works
The richest state machine in the app. Seven receipt states (draft, pending, not-redeemable, redeemable, not-yet-redeemable, redeemed, rejected) each with a colour and a tooltip; eight reject reasons (duplicated, illegible, modified, other centre, out of period, out of store, over max amount, other) and seven automatic pending reasons explaining why the system could not decide (no code, no store, over 250 amount, not this centre, out of date, no amount, invalid date). isValidating/isSavingReject/isUnrejecting guard concurrent mutations, and previousReceipt/nextReceipt with isFirstSelectedReceipt/isLastSelectedReceipt make high-volume moderation keyboard-fast. canSeeModerator reveals who moderated a receipt. reprocessPendingReceipts re-runs automatic processing over the pending backlog.
Use cases
Why someone opens this (5)
- A moderator working through the overnight backlog of uploaded receipts.
- Rejecting a duplicate receipt with an auditable reason the customer can be told.
- Reversing a wrong rejection (
unrejectre-runs validation).- Re-processing the pending queue after fixing a store’s configuration or campaign dates.
- Finding out which moderator handled a disputed receipt.
Path map
flowchart LR F(["Receipt verification queue"]):::feat F --> H1["Receipt validated"]:::ok F --> H2["Receipt rejected with a reason"]:::ok F --> H3["Rejection reversed"]:::ok F --> H4["Pending backlog reprocessed"]:::ok F -.-> X1["Validated but not redeemable"]:::bad F -.-> X2["Validation request fails"]:::bad F -.-> X3["Reject without a reason"]:::bad F -.-> X4["Unknown reject reason"]:::bad F -.-> X5["Reject save fails"]:::bad F -.-> X6["Embedded API errors"]:::bad F -.-> X7["Reprocess not permitted"]:::bad classDef feat fill:#157068,stroke:#157068,color:#ffffff,font-weight:bold classDef ok fill:#e3f1ea,stroke:#0d6b42,color:#0d4a2e classDef bad fill:#f7e6e2,stroke:#9a2d1c,color:#6e1f13
Happy paths
Receipt validated
- Moderator opens the queue filtered to pending and selects a receipt.
- Reviews the image against the parsed amount, date, code and store.
- Clicks validate;
isValidatingblocks double submission.- The state moves to a validated state,
validatedAtis stamped and the entitlement is redeemed.
Receipt rejected with a reason
- Moderator picks a reject reason (required) and saves.
- State becomes
rejected,validatedAtis stamped and the reason is stored for the customer-facing explanation.
Rejection reversed
- Moderator opens a rejected receipt and clicks un-reject.
unreject()delegates straight tovalidate(), so the receipt is re-validated in one step.
Pending backlog reprocessed
- Moderator triggers reprocess on the pending set.
crm.receipts.reprocess-receipts-successconfirms the job was accepted.
Failure paths
Validated but not redeemable
Cause — The receipt validates but the entitlement cannot be redeemed (campaign closed, limits reached).
What happens —
receipt.validate.error-message-unable-to-redeem— explicitly states the receipt was validated but not redeemed, so the operator knows the two outcomes diverged.
Validation request fails
Cause — API error.
What happens —
catchresetsselectedReceipt.validatedAtto null and clearsisValidating, rolling the optimistic update back.
Reject without a reason
Cause —
rejectReasonnot set.What happens —
reject()only proceeds when a reason exists; the field is markedrequired.
Unknown reject reason
Cause — A stored
rejectReasonis not in the enum.What happens —
rejectReasonsgetter does.find(...).labelwithout a guard, so this throws a TypeError and breaks the panel.
Reject save fails
Cause — API error.
What happens —
catchresetsvalidatedAtand leaves the previous state.
Embedded API errors
Cause — A 200 response that still carries
data.errors.What happens —
throw data.errors.firstObjectsurfaces it into the catch chain.
Reprocess not permitted
Cause —
canReprocessReceiptsfalse for the role.What happens — The action is hidden.
QA checklist
Tick these off while regression-testing this feature. The boxes are plain Markdown, so progress is visible in git diffs.
- ✅ Receipt validated
- ✅ Receipt rejected with a reason
- ✅ Rejection reversed
- ✅ Pending backlog reprocessed
- ⛔ Validated but not redeemable
- ⛔ Validation request fails
- ⛔ Reject without a reason
- ⛔ Unknown reject reason
- ⛔ Reject save fails
- ⛔ Embedded API errors
- ⛔ Reprocess not permitted
Implementation
app/controllers/crm/receipts/index.jsapp/routes/crm/receipts/index.jsapp/enums/receipt-status.jsapp/enums/reject-reasons-receipts.jsapp/enums/pending-reasons-receipts.jsapp/abilities/receipt.js
Translation keys (27)
Key English title.header.crm.receiptsReceipts menu.navbar.receipt.showVerify receipts receipt.status.draftDraft receipt.status.pendingPending receipt.status.redeemableRedeemable receipt.status.not-redeemableNot redeemable receipt.status.not-yet-redeemableNot yet redeemable receipt.status.redeemedRedeemed receipt.status.rejectedRejected receipt.validate.error-message-unable-to-redeemThe tickets has been successfully validated but could not be redeemed. crm.receipts.reprocess-receipts-successThe tickets have been successfully reprocessed receipts.reject-reasonRejection reason receipts.index.moderator-nameModerator receipt.state.duplicatedDuplicated receipt.state.illegibleIllegible receipt.state.modifiedThe receipt and the taken photo have not the same information receipt.state.other-centerThe receipt does not belong to this center receipt.state.out-of-periodThe receipt date is out of the activity period receipt.state.out-of-commerceCommerce does not belong to the campaign receipt.state.over-max-amountMaximum amount exceeded receipt.pending.not_codeThe ticket code could not be read receipt.pending.not_storeThe store name could not be read receipt.pending.over_250_amountThe amount is greater than €250 receipt.pending.not_centerThe center could not be read from the receipt receipt.pending.out_of_dateThe date could not be read correctly receipt.pending.not_amountThe amount or the date could not be read correctly receipt.pending.invalid_dateInvalid date
Related
- Same area — Customer list & filtering · Customer create & edit · Customer deletion with unsubscribe notice · Customer CSV export with column picker · Customer CSV import · Children CSV import · AI natural-language customer filter · Save current filters as a segment · Segment list, refresh & export · Segment builder with automatic tag actions · Segment categories · User tags (user types) · Customer receipts tab & ticket-stamp generation · Customer loyalty — raffle tickets · Customer loyalty — cards & stamps · Customer loyalty — rewards · Customer loyalty — coupon exchanges · Customer children (dependants) · Store list, filters & exports · Store create & edit · Store CSV import · Store labels (store types) · Customer action log (dormant)
- Shares data with — Activity editor (loyalty mechanics, rules & targeting) · Proposal editor (content, coupon, push, AI images) · Activity list & duplication · Activity report & PDF · Activity campaigns (programme grouping) · Push notification editor (scheduling, geofencing, deep links) · App button manager (MOFs) · Multicentre proposals
- Behind the same module — Customer loyalty — raffle tickets · Customer receipts tab & ticket-stamp generation · Activity editor (loyalty mechanics, rules & targeting)
Generated from doc/feature-inventory.json · id crm-receipt-moderation · commit 276788ca6