Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions src/CouponHubBot/Services/CouponOcrEngine.fs
Original file line number Diff line number Diff line change
Expand Up @@ -455,14 +455,9 @@ type CouponOcrEngine(azureTextOcr: IBotOcr, logger: ILogger<CouponOcrEngine>, ti
if not (isNull activity) then
%activity.SetTag("image.size_bytes", imageBytes.Length)
let nowUtc = time.GetUtcNow().UtcDateTime
let barcode = tryDecodeBarcode imageBytes

// The Azure OCR SDK already retried transient failures; if one still surfaces (network,
// timeout, or a non-2xx response after retries) AnalyzeImageBytes re-throws it. Catch it
// here (don't crash) and record that the OCR *backend* failed, so callers can tell "Azure
// was down/timed out/errored" apart from "Azure answered 200 but found no usable text".
// The ZXing barcode above is independent of Azure and may still be set.
let! ocrAnalysis, backendFailed =
// OCR runs while the local barcode decoder works; backend failure stays separate from empty text.
let ocrTask =
task {
use azure = botActivity.StartActivity("couponOcr.azure")
try
Expand All @@ -476,6 +471,8 @@ type CouponOcrEngine(azureTextOcr: IBotOcr, logger: ILogger<CouponOcrEngine>, ti
return (null: OcrAnalysis | null), true
}

let barcode = tryDecodeBarcode imageBytes
let! ocrAnalysis, backendFailed = ocrTask
let ocrText = if isNull ocrAnalysis then null else ocrAnalysis.Text
use parse = botActivity.StartActivity("couponOcr.parse")

Expand Down
60 changes: 60 additions & 0 deletions tests/CouponHubBot.Ocr.Tests/OcrTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,66 @@ type OcrTests(output: ITestOutputHelper) =
do! Task.WhenAll jobs
}

[<Theory>]
[<InlineData("success")>]
[<InlineData("empty")>]
[<InlineData("failure")>]
[<InlineData("canceled")>]
member _.``Recognition awaits pending OCR and preserves the local barcode``(outcome: string) =
task {
let bytes = File.ReadAllBytes(Path.Combine(AppContext.BaseDirectory, "Images", "5_25_2026-02-08_2026-02-14_2706726228947.jpg"))
let completion = TaskCompletionSource<OcrAnalysis | null>(TaskCreationOptions.RunContinuationsAsynchronously)
let mutable requests = 0
let azure =
{ new IBotOcr with
member _.AnalyzeImageBytes _ =
requests <- requests + 1
completion.Task }
let time = BotInfra.Time.FixedTimeProvider(DateTimeOffset(2026, 2, 8, 0, 0, 0, TimeSpan.Zero))
let engine = CouponOcrEngine(azure, Microsoft.Extensions.Logging.Abstractions.NullLogger<CouponOcrEngine>.Instance, time)
let recognition = engine.Recognize(ReadOnlyMemory<byte>(bytes))
Assert.Equal(1, requests)
Assert.False(recognition.IsCompleted)

match outcome with
| "success" ->
completion.SetResult(
{ RawJson = "{}"
Text = "€5 OFF €25\nValid 8 Feb - 14 Feb\n2706602781191" })
| "empty" -> completion.SetResult(null: OcrAnalysis | null)
| "failure" -> completion.SetException(InvalidOperationException("OCR unavailable"))
| "canceled" -> completion.SetCanceled()
| _ -> failwith "Unexpected OCR outcome"

let! result = recognition
Assert.Equal("2706726228947", result.barcode)
Assert.Equal(outcome = "failure" || outcome = "canceled", result.backendFailed)
match outcome with
| "success" ->
Assert.Equal(Nullable 5m, result.couponValue)
Assert.Equal(Nullable 25m, result.minCheck)
Assert.Equal(Nullable(DateTime(2026, 2, 8)), result.validFrom)
Assert.Equal(Nullable(DateTime(2026, 2, 14)), result.validTo)
| _ ->
Assert.False(result.couponValue.HasValue)
Assert.False(result.minCheck.HasValue)
Assert.False(result.validFrom.HasValue)
Assert.False(result.validTo.HasValue)
}

[<Fact>]
member _.``Immediate OCR failure preserves the local barcode``() =
task {
let bytes = File.ReadAllBytes(Path.Combine(AppContext.BaseDirectory, "Images", "5_25_2026-02-08_2026-02-14_2706726228947.jpg"))
let azure =
{ new IBotOcr with
member _.AnalyzeImageBytes _ = raise (InvalidOperationException("OCR unavailable")) }
let engine = CouponOcrEngine(azure, Microsoft.Extensions.Logging.Abstractions.NullLogger<CouponOcrEngine>.Instance, TimeProvider.System)
let! result = engine.Recognize(ReadOnlyMemory<byte>(bytes))
Assert.Equal("2706726228947", result.barcode)
Assert.True(result.backendFailed)
}

[<Theory>]
[<InlineData(17)>]
[<InlineData(33)>]
Expand Down
Loading