File size: 4,161 Bytes
c09f67c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | import { updateTransaction } from "@midday/db/queries";
import { DocumentClient } from "@midday/documents";
import { triggerJob } from "@midday/job-client";
import { createClient } from "@midday/supabase/job";
import type { Job } from "bullmq";
import type { ProcessTransactionAttachmentPayload } from "../../schemas/transactions";
import { getDb } from "../../utils/db";
import { convertHeicToJpeg } from "../../utils/image-processing";
import { BaseProcessor } from "../base";
/**
* Process transaction attachments (receipts/invoices)
* Extracts tax information and updates the transaction
*/
export class ProcessTransactionAttachmentProcessor extends BaseProcessor<ProcessTransactionAttachmentPayload> {
async process(job: Job<ProcessTransactionAttachmentPayload>): Promise<void> {
const { transactionId, mimetype, filePath, teamId } = job.data;
const supabase = createClient();
this.logger.info("Processing transaction attachment", {
transactionId,
filePath: filePath.join("/"),
mimetype,
teamId,
});
// If the file is a HEIC we need to convert it to a JPG
if (mimetype === "image/heic") {
this.logger.info("Converting HEIC to JPG", {
filePath: filePath.join("/"),
});
const { data } = await supabase.storage
.from("vault")
.download(filePath.join("/"));
if (!data) {
throw new Error("File not found");
}
const buffer = await data.arrayBuffer();
// Use shared HEIC conversion utility (resizes to 2048px)
const { buffer: image } = await convertHeicToJpeg(buffer, this.logger);
// Upload the converted image
const { data: uploadedData } = await supabase.storage
.from("vault")
.upload(filePath.join("/"), image, {
contentType: "image/jpeg",
upsert: true,
});
if (!uploadedData) {
throw new Error("Failed to upload converted image");
}
}
const filename = filePath.at(-1);
// Use 10 minutes expiration to ensure URL doesn't expire during processing
// (document processing can take up to 120s, plus buffer for retries)
const { data: signedUrlData } = await supabase.storage
.from("vault")
.createSignedUrl(filePath.join("/"), 600);
if (!signedUrlData) {
throw new Error("File not found");
}
const document = new DocumentClient();
this.logger.info("Extracting tax information from document", {
transactionId,
filename,
mimetype,
});
const result = await document.getInvoiceOrReceipt({
documentUrl: signedUrlData.signedUrl,
mimetype,
});
// Update the transaction with the tax information
if (result.tax_rate && result.tax_type) {
this.logger.info("Updating transaction with tax information", {
transactionId,
taxRate: result.tax_rate,
taxType: result.tax_type,
});
const db = getDb();
await updateTransaction(db, {
id: transactionId,
teamId,
taxRate: result.tax_rate ?? undefined,
taxType: result.tax_type ?? undefined,
});
this.logger.info("Transaction updated with tax information", {
transactionId,
taxRate: result.tax_rate,
taxType: result.tax_type,
});
} else {
this.logger.info("No tax information found in document", {
transactionId,
});
}
// NOTE: Process documents and images for classification
// This is non-blocking, classification happens separately
try {
await triggerJob(
"process-document",
{
mimetype,
filePath,
teamId,
},
"documents",
);
this.logger.info("Triggered document processing for classification", {
transactionId,
filePath: filePath.join("/"),
});
} catch (error) {
this.logger.warn("Failed to trigger document processing (non-critical)", {
transactionId,
error: error instanceof Error ? error.message : "Unknown error",
});
// Don't fail the entire process if document processing fails
}
}
}
|