Skip to content

← Back to DWS Receipts

Receipt management is the core feature. Employees upload receipt images, OCR extracts data, and admins review for reimbursement.

1. User selects/captures image
2. POST /api/receipts/upload → Temp storage
3. POST /api/receipts/ocr → Extract date, amount, category
4. Auto-submit OR manual confirmation
5. POST /api/receipts → Create record, move image to final path
// Temp path format
{userId}/temp_{uuid8}_{timestamp}.jpg
// POST /api/receipts/upload
const formData = new FormData();
formData.append('file', file);
const { tempFilePath } = await fetch('/api/receipts/upload', {
method: 'POST',
body: formData
});

When receipt is created, file moves to permanent path:

// Final path format
{userId}/{receiptId}.jpg
// Inside POST /api/receipts
await supabase.storage.from('receipt-images').move(tempFilePath, finalPath);

Images are optimized on upload using Sharp:

api/receipts/upload/route.ts
sharp(buffer)
.resize(1200, 1200, { fit: 'inside', withoutEnlargement: true })
.jpeg({ quality: 80, progressive: true })
SettingValue
Max dimensions1200x1200
FormatJPEG
Quality80%
PDFsUploaded as-is

OCR uses BAML to call GPT-4.1-nano via OpenRouter.

baml_src/receipts.baml
class ExtractedReceipt {
date string? @description("YYYY-MM-DD format")
amount float? @description("Total amount paid")
category string? @description("Parking, Gas, Meals & Entertainment, Office Supplies, Other")
}
function ExtractReceiptFromImage(img: image) -> ExtractedReceipt {
client GPT4oMini
prompt #"Extract receipt data..."#
}
// POST /api/receipts/ocr response
{
success: true,
data: {
date: "2025-01-15",
amount: 42.99,
category: "Office Supplies",
category_id: "uuid-or-null"
},
duplicate: {
isDuplicate: false,
existingReceipts: []
},
canAutoSubmit: true // All fields extracted + no duplicates
}

Receipts auto-submit when ALL conditions are met:

  1. Date was extracted
  2. Amount was extracted
  3. Category was mapped to ID
  4. No duplicate receipts found

Otherwise, user sees confirmation form.

Before creating a receipt, the system checks for existing receipts with same:

  • user_id
  • receipt_date
  • amount

User is warned and must add a unique description.

New Receipt → Pending
(Admin Review)
↙ ↘
Approved Rejected
Reimbursed

Database stores capitalized: "Pending", "Approved", "Rejected", "Reimbursed"

Frontend normalizes to lowercase for display.

ActionEmployeeAdmin
Edit Pending✅ Own only✅ Any
Edit Other Status
Delete Pending✅ Own only✅ Any
Delete Other Status
FilePurpose
components/receipt-uploader.tsxUpload UI + OCR flow
components/receipt-details-card.tsxEdit/confirmation form
api/receipts/upload/route.tsImage upload + processing
api/receipts/ocr/route.tsOCR extraction
api/receipts/route.tsCRUD operations
TypeExtensionNotes
JPEG.jpg, .jpegConverted to optimized JPEG
PNG.pngConverted to JPEG
WebP.webpConverted to JPEG
HEIC.heic, .heifiOS photos, converted
PDF.pdfUploaded as-is
PlatformLimit
Mobile50 MB
Desktop10 MB