diff --git a/src/CouponHubBot/Services/CouponOcrEngine.fs b/src/CouponHubBot/Services/CouponOcrEngine.fs index cbc1ab68..23654774 100644 --- a/src/CouponHubBot/Services/CouponOcrEngine.fs +++ b/src/CouponHubBot/Services/CouponOcrEngine.fs @@ -112,12 +112,23 @@ module private CouponOcrParsing = let m2 = Regex.Match( text, - @"(?i)\bvalid\s+from\s+(?\d{1,4}[./-]\d{1,2}[./-]\d{1,4})\s+to\s+(?\d{1,4}[./-]\d{1,2}[./-]\d{1,4})\b" + @"(?i)\bvalid\s+from\s+(?\d{1,4}[./-]\d{1,2}[./-]\d{1,4})\s+to\s+(?\d{1,4}[./-]\d{1,2}(?:[./-]\d{1,4})?)\b(?!\d|[./-]\d)" ) if m2.Success then - match tryParseDateAny nowUtc m2.Groups.["a"].Value, tryParseDateAny nowUtc m2.Groups.["b"].Value with - | Some a, Some b -> Some(a.Date, b.Date) - | _ -> None + match tryParseDateAny nowUtc m2.Groups.["a"].Value with + | Some a -> + let endText = m2.Groups.["b"].Value + let parts = endText.Split([| '/'; '.'; '-' |]) + let endDate = + match parts with + | [| day; month |] -> + // Receipt ranges use day/month; a missing end year follows the start date. + match tryParseDateAny nowUtc $"{day}/{month}/{a.Year}" with + | Some b when b < a -> tryParseDateAny nowUtc $"{day}/{month}/{a.Year + 1}" + | result -> result + | _ -> tryParseDateAny nowUtc endText + endDate |> Option.map (fun b -> a.Date, b.Date) + | None -> None else None diff --git a/tests/CouponHubBot.Ocr.Tests/AzureCache/10_50_2026-09-24_2026-10-03_2707322589159.jpg.azure.json b/tests/CouponHubBot.Ocr.Tests/AzureCache/10_50_2026-09-24_2026-10-03_2707322589159.jpg.azure.json new file mode 100644 index 00000000..cda7b250 --- /dev/null +++ b/tests/CouponHubBot.Ocr.Tests/AzureCache/10_50_2026-09-24_2026-10-03_2707322589159.jpg.azure.json @@ -0,0 +1 @@ +{"modelVersion":"2023-10-01","metadata":{"width":960,"height":1280},"readResult":{"blocks":[{"lines":[{"text":"**","boundingPolygon":[{"x":160,"y":8},{"x":203,"y":11},{"x":202,"y":33},{"x":159,"y":28}],"words":[{"text":"**","boundingPolygon":[{"x":164,"y":8},{"x":193,"y":10},{"x":191,"y":32},{"x":162,"y":29}],"confidence":0.78}]},{"text":"SEE END OF","boundingPolygon":[{"x":209,"y":15},{"x":379,"y":16},{"x":378,"y":52},{"x":209,"y":49}],"words":[{"text":"SEE","boundingPolygon":[{"x":213,"y":16},{"x":261,"y":19},{"x":260,"y":49},{"x":211,"y":44}],"confidence":0.993},{"text":"END","boundingPolygon":[{"x":278,"y":20},{"x":328,"y":19},{"x":327,"y":52},{"x":277,"y":50}],"confidence":0.993},{"text":"OF","boundingPolygon":[{"x":345,"y":19},{"x":377,"y":16},{"x":376,"y":50},{"x":344,"y":51}],"confidence":0.991}]},{"text":"RECEIPT FOR DETAILS ** ub","boundingPolygon":[{"x":392,"y":9},{"x":780,"y":23},{"x":779,"y":53},{"x":391,"y":42}],"words":[{"text":"RECEIPT","boundingPolygon":[{"x":392,"y":9},{"x":505,"y":13},{"x":504,"y":45},{"x":391,"y":43}],"confidence":0.852},{"text":"FOR","boundingPolygon":[{"x":519,"y":14},{"x":568,"y":16},{"x":567,"y":47},{"x":518,"y":46}],"confidence":0.993},{"text":"DETAILS","boundingPolygon":[{"x":585,"y":17},{"x":697,"y":23},{"x":696,"y":51},{"x":584,"y":47}],"confidence":0.99},{"text":"**","boundingPolygon":[{"x":714,"y":24},{"x":741,"y":25},{"x":740,"y":52},{"x":712,"y":52}],"confidence":0.761},{"text":"ub","boundingPolygon":[{"x":747,"y":26},{"x":779,"y":28},{"x":777,"y":53},{"x":746,"y":53}],"confidence":0.806}]},{"text":"Qualifying items can be returned up to","boundingPolygon":[{"x":137,"y":92},{"x":771,"y":96},{"x":770,"y":135},{"x":137,"y":129}],"words":[{"text":"Qualifying","boundingPolygon":[{"x":139,"y":97},{"x":309,"y":92},{"x":308,"y":126},{"x":137,"y":130}],"confidence":0.676},{"text":"items","boundingPolygon":[{"x":324,"y":92},{"x":407,"y":92},{"x":405,"y":125},{"x":323,"y":125}],"confidence":0.959},{"text":"can","boundingPolygon":[{"x":422,"y":92},{"x":472,"y":93},{"x":470,"y":125},{"x":421,"y":125}],"confidence":0.994},{"text":"be","boundingPolygon":[{"x":489,"y":93},{"x":524,"y":94},{"x":522,"y":126},{"x":487,"y":126}],"confidence":0.993},{"text":"returned","boundingPolygon":[{"x":539,"y":95},{"x":671,"y":100},{"x":670,"y":130},{"x":537,"y":126}],"confidence":0.983},{"text":"up","boundingPolygon":[{"x":686,"y":101},{"x":719,"y":103},{"x":718,"y":132},{"x":685,"y":131}],"confidence":0.995},{"text":"to","boundingPolygon":[{"x":738,"y":104},{"x":769,"y":106},{"x":767,"y":135},{"x":737,"y":133}],"confidence":0.997}]},{"text":"21/10/26","boundingPolygon":[{"x":358,"y":118},{"x":511,"y":115},{"x":512,"y":145},{"x":358,"y":150}],"words":[{"text":"21/10/26","boundingPolygon":[{"x":375,"y":119},{"x":505,"y":116},{"x":505,"y":146},{"x":375,"y":150}],"confidence":0.993}]},{"text":"Thank you for shopping with usgute","boundingPolygon":[{"x":206,"y":151},{"x":767,"y":148},{"x":767,"y":183},{"x":206,"y":188}],"words":[{"text":"Thank","boundingPolygon":[{"x":210,"y":157},{"x":288,"y":154},{"x":287,"y":186},{"x":209,"y":188}],"confidence":0.994},{"text":"you","boundingPolygon":[{"x":303,"y":153},{"x":354,"y":151},{"x":353,"y":184},{"x":303,"y":185}],"confidence":0.998},{"text":"for","boundingPolygon":[{"x":374,"y":151},{"x":420,"y":150},{"x":420,"y":183},{"x":373,"y":184}],"confidence":0.997},{"text":"shopping","boundingPolygon":[{"x":440,"y":149},{"x":576,"y":148},{"x":575,"y":182},{"x":439,"y":182}],"confidence":0.994},{"text":"with","boundingPolygon":[{"x":592,"y":148},{"x":657,"y":149},{"x":657,"y":182},{"x":591,"y":182}],"confidence":0.993},{"text":"usgute","boundingPolygon":[{"x":675,"y":149},{"x":767,"y":151},{"x":766,"y":184},{"x":674,"y":182}],"confidence":0.315}]},{"text":"For details of our refund & exchange","boundingPolygon":[{"x":150,"y":179},{"x":762,"y":177},{"x":762,"y":214},{"x":150,"y":219}],"words":[{"text":"For","boundingPolygon":[{"x":152,"y":189},{"x":203,"y":186},{"x":203,"y":217},{"x":151,"y":219}],"confidence":0.997},{"text":"details","boundingPolygon":[{"x":225,"y":184},{"x":340,"y":180},{"x":339,"y":212},{"x":224,"y":216}],"confidence":0.989},{"text":"of","boundingPolygon":[{"x":357,"y":179},{"x":392,"y":178},{"x":391,"y":211},{"x":356,"y":212}],"confidence":0.996},{"text":"our","boundingPolygon":[{"x":405,"y":178},{"x":455,"y":178},{"x":454,"y":210},{"x":404,"y":211}],"confidence":0.996},{"text":"refund","boundingPolygon":[{"x":470,"y":177},{"x":577,"y":178},{"x":575,"y":211},{"x":469,"y":210}],"confidence":0.987},{"text":"&","boundingPolygon":[{"x":594,"y":178},{"x":612,"y":179},{"x":611,"y":211},{"x":593,"y":211}],"confidence":0.994},{"text":"exchange","boundingPolygon":[{"x":628,"y":179},{"x":761,"y":184},{"x":760,"y":215},{"x":626,"y":212}],"confidence":0.991}]},{"text":"policy see the back of this receipt","boundingPolygon":[{"x":168,"y":212},{"x":765,"y":206},{"x":766,"y":242},{"x":168,"y":251}],"words":[{"text":"policy","boundingPolygon":[{"x":169,"y":222},{"x":269,"y":215},{"x":269,"y":247},{"x":169,"y":251}],"confidence":0.983},{"text":"see","boundingPolygon":[{"x":287,"y":214},{"x":338,"y":211},{"x":338,"y":245},{"x":287,"y":246}],"confidence":0.995},{"text":"the","boundingPolygon":[{"x":358,"y":211},{"x":407,"y":209},{"x":406,"y":243},{"x":357,"y":244}],"confidence":0.998},{"text":"back","boundingPolygon":[{"x":420,"y":208},{"x":489,"y":207},{"x":488,"y":242},{"x":420,"y":243}],"confidence":0.989},{"text":"of","boundingPolygon":[{"x":509,"y":207},{"x":545,"y":207},{"x":544,"y":241},{"x":508,"y":242}],"confidence":0.999},{"text":"this","boundingPolygon":[{"x":565,"y":207},{"x":631,"y":207},{"x":630,"y":241},{"x":564,"y":241}],"confidence":0.993},{"text":"receipt","boundingPolygon":[{"x":647,"y":208},{"x":765,"y":211},{"x":764,"y":243},{"x":645,"y":242}],"confidence":0.993}]},{"text":"or go to dunnesstores.com/store","boundingPolygon":[{"x":201,"y":245},{"x":743,"y":237},{"x":743,"y":268},{"x":201,"y":281}],"words":[{"text":"or","boundingPolygon":[{"x":201,"y":251},{"x":233,"y":249},{"x":233,"y":281},{"x":201,"y":282}],"confidence":0.993},{"text":"go","boundingPolygon":[{"x":250,"y":248},{"x":286,"y":246},{"x":286,"y":279},{"x":250,"y":280}],"confidence":0.956},{"text":"to","boundingPolygon":[{"x":306,"y":245},{"x":337,"y":244},{"x":337,"y":277},{"x":306,"y":278}],"confidence":0.997},{"text":"dunnesstores.com/store","boundingPolygon":[{"x":357,"y":243},{"x":734,"y":238},{"x":734,"y":269},{"x":357,"y":276}],"confidence":0.956}]},{"text":"Price of electrical items includes","boundingPolygon":[{"x":164,"y":275},{"x":756,"y":267},{"x":757,"y":300},{"x":164,"y":312}],"words":[{"text":"Price","boundingPolygon":[{"x":165,"y":279},{"x":251,"y":275},{"x":250,"y":311},{"x":164,"y":313}],"confidence":0.992},{"text":"of","boundingPolygon":[{"x":267,"y":275},{"x":306,"y":274},{"x":305,"y":310},{"x":267,"y":311}],"confidence":0.997},{"text":"electrical","boundingPolygon":[{"x":322,"y":273},{"x":495,"y":269},{"x":494,"y":305},{"x":321,"y":309}],"confidence":0.986},{"text":"items","boundingPolygon":[{"x":511,"y":269},{"x":599,"y":268},{"x":599,"y":303},{"x":510,"y":305}],"confidence":0.982},{"text":"includes","boundingPolygon":[{"x":618,"y":268},{"x":756,"y":268},{"x":756,"y":299},{"x":617,"y":302}],"confidence":0.989}]},{"text":"a contribution to recycling costs","boundingPolygon":[{"x":181,"y":308},{"x":757,"y":301},{"x":758,"y":333},{"x":181,"y":344}],"words":[{"text":"a","boundingPolygon":[{"x":182,"y":311},{"x":199,"y":311},{"x":199,"y":344},{"x":182,"y":344}],"confidence":0.986},{"text":"contribution","boundingPolygon":[{"x":213,"y":310},{"x":421,"y":304},{"x":420,"y":341},{"x":213,"y":344}],"confidence":0.976},{"text":"to","boundingPolygon":[{"x":441,"y":304},{"x":474,"y":303},{"x":474,"y":340},{"x":441,"y":340}],"confidence":0.991},{"text":"recycling","boundingPolygon":[{"x":490,"y":303},{"x":652,"y":302},{"x":651,"y":334},{"x":490,"y":339}],"confidence":0.955},{"text":"costs","boundingPolygon":[{"x":668,"y":302},{"x":756,"y":302},{"x":755,"y":330},{"x":667,"y":334}],"confidence":0.994}]},{"text":"DUNNES","boundingPolygon":[{"x":190,"y":332},{"x":724,"y":332},{"x":725,"y":431},{"x":190,"y":439}],"words":[{"text":"DUNNES","boundingPolygon":[{"x":190,"y":332},{"x":700,"y":335},{"x":700,"y":425},{"x":190,"y":440}],"confidence":0.995}]},{"text":"e brebhal","boundingPolygon":[{"x":222,"y":441},{"x":373,"y":442},{"x":373,"y":480},{"x":222,"y":479}],"words":[{"text":"e","boundingPolygon":[{"x":228,"y":444},{"x":244,"y":443},{"x":245,"y":479},{"x":229,"y":480}],"confidence":0.133},{"text":"brebhal","boundingPolygon":[{"x":252,"y":443},{"x":372,"y":444},{"x":372,"y":480},{"x":252,"y":479}],"confidence":0.359}]},{"text":"STORES","boundingPolygon":[{"x":488,"y":439},{"x":727,"y":435},{"x":728,"y":487},{"x":488,"y":493}],"words":[{"text":"STORES","boundingPolygon":[{"x":492,"y":440},{"x":718,"y":435},{"x":718,"y":489},{"x":492,"y":494}],"confidence":0.996}]},{"text":"Always better value","boundingPolygon":[{"x":247,"y":512},{"x":727,"y":497},{"x":728,"y":590},{"x":251,"y":608}],"words":[{"text":"Always","boundingPolygon":[{"x":263,"y":512},{"x":431,"y":507},{"x":434,"y":604},{"x":267,"y":608}],"confidence":0.667},{"text":"better","boundingPolygon":[{"x":450,"y":506},{"x":593,"y":502},{"x":595,"y":596},{"x":452,"y":603}],"confidence":0.792},{"text":"value","boundingPolygon":[{"x":612,"y":501},{"x":721,"y":498},{"x":721,"y":587},{"x":613,"y":595}],"confidence":0.828}]},{"text":"bibbie","boundingPolygon":[{"x":150,"y":656},{"x":251,"y":655},{"x":251,"y":687},{"x":150,"y":688}],"words":[{"text":"bibbie","boundingPolygon":[{"x":153,"y":657},{"x":252,"y":655},{"x":251,"y":688},{"x":152,"y":688}],"confidence":0.54}]},{"text":"SAVE €10 !","boundingPolygon":[{"x":295,"y":635},{"x":634,"y":635},{"x":634,"y":700},{"x":295,"y":700}],"words":[{"text":"SAVE","boundingPolygon":[{"x":296,"y":639},{"x":425,"y":636},{"x":425,"y":701},{"x":296,"y":701}],"confidence":0.992},{"text":"€10","boundingPolygon":[{"x":459,"y":636},{"x":559,"y":636},{"x":559,"y":701},{"x":459,"y":701}],"confidence":0.998},{"text":"!","boundingPolygon":[{"x":602,"y":637},{"x":632,"y":638},{"x":632,"y":701},{"x":602,"y":701}],"confidence":0.994}]},{"text":"When you spend €50 or more","boundingPolygon":[{"x":244,"y":734},{"x":697,"y":735},{"x":697,"y":774},{"x":244,"y":773}],"words":[{"text":"When","boundingPolygon":[{"x":244,"y":740},{"x":311,"y":737},{"x":311,"y":773},{"x":244,"y":772}],"confidence":0.986},{"text":"you","boundingPolygon":[{"x":328,"y":737},{"x":380,"y":736},{"x":381,"y":774},{"x":328,"y":773}],"confidence":0.996},{"text":"spend","boundingPolygon":[{"x":398,"y":735},{"x":484,"y":735},{"x":484,"y":775},{"x":398,"y":774}],"confidence":0.994},{"text":"€50","boundingPolygon":[{"x":496,"y":735},{"x":551,"y":736},{"x":551,"y":775},{"x":497,"y":775}],"confidence":0.845},{"text":"or","boundingPolygon":[{"x":566,"y":736},{"x":601,"y":737},{"x":602,"y":775},{"x":566,"y":775}],"confidence":0.992},{"text":"more","boundingPolygon":[{"x":616,"y":738},{"x":692,"y":741},{"x":693,"y":774},{"x":617,"y":774}],"confidence":0.985}]},{"text":"on Groceries.","boundingPolygon":[{"x":342,"y":769},{"x":575,"y":770},{"x":575,"y":806},{"x":342,"y":806}],"words":[{"text":"on","boundingPolygon":[{"x":346,"y":770},{"x":381,"y":770},{"x":380,"y":806},{"x":345,"y":805}],"confidence":0.995},{"text":"Groceries.","boundingPolygon":[{"x":395,"y":770},{"x":570,"y":770},{"x":569,"y":806},{"x":395,"y":806}],"confidence":0.984}]},{"text":"10","boundingPolygon":[{"x":797,"y":762},{"x":827,"y":763},{"x":826,"y":791},{"x":797,"y":791}],"words":[{"text":"10","boundingPolygon":[{"x":797,"y":762},{"x":822,"y":762},{"x":822,"y":791},{"x":797,"y":790}],"confidence":0.397}]},{"text":"no apcsi","boundingPolygon":[{"x":163,"y":829},{"x":286,"y":829},{"x":286,"y":854},{"x":163,"y":854}],"words":[{"text":"no","boundingPolygon":[{"x":167,"y":831},{"x":197,"y":830},{"x":196,"y":854},{"x":167,"y":854}],"confidence":0.903},{"text":"apcsi","boundingPolygon":[{"x":208,"y":830},{"x":287,"y":830},{"x":286,"y":855},{"x":208,"y":854}],"confidence":0.341}]},{"text":"2707322589159","boundingPolygon":[{"x":343,"y":928},{"x":584,"y":935},{"x":583,"y":976},{"x":342,"y":969}],"words":[{"text":"2707322589159","boundingPolygon":[{"x":351,"y":928},{"x":582,"y":936},{"x":580,"y":977},{"x":348,"y":970}],"confidence":0.993}]},{"text":"Coupon valid from 24/09/26 to 03/10","boundingPolygon":[{"x":128,"y":994},{"x":757,"y":1019},{"x":755,"y":1061},{"x":126,"y":1034}],"words":[{"text":"Coupon","boundingPolygon":[{"x":128,"y":994},{"x":227,"y":1000},{"x":225,"y":1038},{"x":127,"y":1033}],"confidence":0.994},{"text":"valid","boundingPolygon":[{"x":240,"y":1000},{"x":331,"y":1004},{"x":329,"y":1044},{"x":239,"y":1039}],"confidence":0.994},{"text":"from","boundingPolygon":[{"x":350,"y":1005},{"x":419,"y":1008},{"x":418,"y":1048},{"x":348,"y":1044}],"confidence":0.988},{"text":"24/09/26","boundingPolygon":[{"x":438,"y":1009},{"x":586,"y":1015},{"x":585,"y":1055},{"x":437,"y":1049}],"confidence":0.983},{"text":"to","boundingPolygon":[{"x":605,"y":1015},{"x":640,"y":1016},{"x":640,"y":1057},{"x":604,"y":1056}],"confidence":0.997},{"text":"03/10","boundingPolygon":[{"x":657,"y":1017},{"x":752,"y":1019},{"x":752,"y":1061},{"x":656,"y":1058}],"confidence":0.993}]},{"text":"Offer excludes clothing, footwear, homevares,","boundingPolygon":[{"x":88,"y":1064},{"x":722,"y":1103},{"x":719,"y":1145},{"x":86,"y":1105}],"words":[{"text":"Offer","boundingPolygon":[{"x":88,"y":1065},{"x":157,"y":1070},{"x":154,"y":1108},{"x":86,"y":1103}],"confidence":0.975},{"text":"excludes","boundingPolygon":[{"x":165,"y":1070},{"x":278,"y":1078},{"x":275,"y":1117},{"x":162,"y":1109}],"confidence":0.958},{"text":"clothing,","boundingPolygon":[{"x":286,"y":1078},{"x":419,"y":1087},{"x":417,"y":1127},{"x":283,"y":1118}],"confidence":0.897},{"text":"footwear,","boundingPolygon":[{"x":428,"y":1087},{"x":560,"y":1095},{"x":557,"y":1136},{"x":425,"y":1127}],"confidence":0.6},{"text":"homevares,","boundingPolygon":[{"x":567,"y":1095},{"x":723,"y":1103},{"x":720,"y":1146},{"x":565,"y":1136}],"confidence":0.616}]},{"text":"alcohol, tobacco, medicines, infant wilk formulago","boundingPolygon":[{"x":92,"y":1110},{"x":838,"y":1156},{"x":835,"y":1202},{"x":89,"y":1152}],"words":[{"text":"alcohol,","boundingPolygon":[{"x":92,"y":1110},{"x":205,"y":1118},{"x":203,"y":1157},{"x":90,"y":1149}],"confidence":0.783},{"text":"tobacco,","boundingPolygon":[{"x":214,"y":1118},{"x":327,"y":1125},{"x":325,"y":1166},{"x":211,"y":1157}],"confidence":0.627},{"text":"medicines,","boundingPolygon":[{"x":335,"y":1126},{"x":487,"y":1135},{"x":485,"y":1178},{"x":333,"y":1166}],"confidence":0.738},{"text":"infant","boundingPolygon":[{"x":496,"y":1136},{"x":590,"y":1142},{"x":588,"y":1185},{"x":494,"y":1178}],"confidence":0.882},{"text":"wilk","boundingPolygon":[{"x":598,"y":1142},{"x":661,"y":1146},{"x":659,"y":1190},{"x":596,"y":1186}],"confidence":0.653},{"text":"formulago","boundingPolygon":[{"x":673,"y":1147},{"x":824,"y":1156},{"x":823,"y":1203},{"x":671,"y":1191}],"confidence":0.405}]},{"text":"cafe,","boundingPolygon":[{"x":738,"y":1103},{"x":822,"y":1107},{"x":819,"y":1152},{"x":736,"y":1147}],"words":[{"text":"cafe,","boundingPolygon":[{"x":738,"y":1103},{"x":822,"y":1107},{"x":819,"y":1152},{"x":736,"y":1147}],"confidence":0.383}]},{"text":"lottery products, checkout bags, saving: \"staré+4","boundingPolygon":[{"x":94,"y":1153},{"x":855,"y":1207},{"x":853,"y":1250},{"x":91,"y":1196}],"words":[{"text":"lottery","boundingPolygon":[{"x":99,"y":1153},{"x":197,"y":1161},{"x":192,"y":1201},{"x":94,"y":1191}],"confidence":0.81},{"text":"products,","boundingPolygon":[{"x":205,"y":1162},{"x":336,"y":1172},{"x":333,"y":1214},{"x":201,"y":1202}],"confidence":0.659},{"text":"checkout","boundingPolygon":[{"x":345,"y":1172},{"x":468,"y":1181},{"x":465,"y":1225},{"x":341,"y":1215}],"confidence":0.92},{"text":"bags,","boundingPolygon":[{"x":476,"y":1182},{"x":559,"y":1188},{"x":557,"y":1232},{"x":473,"y":1226}],"confidence":0.787},{"text":"saving:","boundingPolygon":[{"x":568,"y":1188},{"x":666,"y":1195},{"x":664,"y":1240},{"x":566,"y":1233}],"confidence":0.165},{"text":"\"staré+4","boundingPolygon":[{"x":675,"y":1196},{"x":850,"y":1207},{"x":849,"y":1250},{"x":673,"y":1240}],"confidence":0.127}]},{"text":"Deposit Return Scheme deposit fees offt es punta,","boundingPolygon":[{"x":29,"y":1189},{"x":797,"y":1249},{"x":793,"y":1279},{"x":26,"y":1236}],"words":[{"text":"Deposit","boundingPolygon":[{"x":34,"y":1190},{"x":138,"y":1200},{"x":135,"y":1242},{"x":31,"y":1230}],"confidence":0.998},{"text":"Return","boundingPolygon":[{"x":145,"y":1201},{"x":234,"y":1209},{"x":232,"y":1252},{"x":142,"y":1243}],"confidence":0.999},{"text":"Scheme","boundingPolygon":[{"x":242,"y":1210},{"x":333,"y":1218},{"x":331,"y":1261},{"x":239,"y":1253}],"confidence":0.991},{"text":"deposit","boundingPolygon":[{"x":344,"y":1219},{"x":452,"y":1229},{"x":450,"y":1269},{"x":341,"y":1262}],"confidence":0.999},{"text":"fees","boundingPolygon":[{"x":462,"y":1230},{"x":527,"y":1235},{"x":525,"y":1273},{"x":460,"y":1270}],"confidence":0.991},{"text":"offt","boundingPolygon":[{"x":552,"y":1237},{"x":619,"y":1243},{"x":617,"y":1277},{"x":550,"y":1274}],"confidence":0.403},{"text":"es","boundingPolygon":[{"x":631,"y":1244},{"x":670,"y":1247},{"x":668,"y":1279},{"x":630,"y":1277}],"confidence":0.853},{"text":"punta,","boundingPolygon":[{"x":685,"y":1248},{"x":791,"y":1257},{"x":790,"y":1279},{"x":684,"y":1279}],"confidence":0.384}]}]}]}} \ No newline at end of file diff --git a/tests/CouponHubBot.Ocr.Tests/AzureCache/5_25_2026-09-07_2026-09-13_2707288849571.jpg.azure.json b/tests/CouponHubBot.Ocr.Tests/AzureCache/5_25_2026-09-07_2026-09-13_2707288849571.jpg.azure.json new file mode 100644 index 00000000..d67dbf8c --- /dev/null +++ b/tests/CouponHubBot.Ocr.Tests/AzureCache/5_25_2026-09-07_2026-09-13_2707288849571.jpg.azure.json @@ -0,0 +1 @@ +{"metadata": {"width": 960, "height": 1280}, "readResult": {"blocks": [{"lines": [{"text": "Always better value", "words": [{"text": "Always", "confidence": 0.652, "boundingPolygon": [{"x": 111, "y": 89}, {"x": 410, "y": 62}, {"x": 414, "y": 206}, {"x": 115, "y": 237}]}, {"text": "better", "confidence": 0.684, "boundingPolygon": [{"x": 437, "y": 60}, {"x": 646, "y": 47}, {"x": 650, "y": 182}, {"x": 441, "y": 203}]}, {"text": "value", "confidence": 0.842, "boundingPolygon": [{"x": 673, "y": 45}, {"x": 869, "y": 38}, {"x": 873, "y": 159}, {"x": 677, "y": 179}]}], "boundingPolygon": [{"x": 70, "y": 91}, {"x": 874, "y": 37}, {"x": 881, "y": 166}, {"x": 73, "y": 238}]}, {"text": "b", "words": [{"text": "b", "confidence": 0.866, "boundingPolygon": [{"x": 850, "y": 149}, {"x": 876, "y": 149}, {"x": 877, "y": 194}, {"x": 851, "y": 194}]}], "boundingPolygon": [{"x": 832, "y": 150}, {"x": 885, "y": 149}, {"x": 885, "y": 193}, {"x": 833, "y": 195}]}, {"text": "SAVE €5 !", "words": [{"text": "SAVE", "confidence": 0.988, "boundingPolygon": [{"x": 186, "y": 288}, {"x": 401, "y": 282}, {"x": 402, "y": 377}, {"x": 185, "y": 381}]}, {"text": "€5", "confidence": 0.993, "boundingPolygon": [{"x": 465, "y": 280}, {"x": 569, "y": 276}, {"x": 570, "y": 371}, {"x": 466, "y": 375}]}, {"text": "!", "confidence": 0.99, "boundingPolygon": [{"x": 645, "y": 274}, {"x": 682, "y": 273}, {"x": 684, "y": 366}, {"x": 647, "y": 368}]}], "boundingPolygon": [{"x": 184, "y": 288}, {"x": 685, "y": 272}, {"x": 688, "y": 365}, {"x": 184, "y": 381}]}, {"text": "When you spend €25 or more", "words": [{"text": "When", "confidence": 0.987, "boundingPolygon": [{"x": 79, "y": 454}, {"x": 188, "y": 450}, {"x": 188, "y": 502}, {"x": 78, "y": 504}]}, {"text": "you", "confidence": 0.998, "boundingPolygon": [{"x": 217, "y": 449}, {"x": 302, "y": 446}, {"x": 302, "y": 499}, {"x": 216, "y": 501}]}, {"text": "spend", "confidence": 0.989, "boundingPolygon": [{"x": 334, "y": 445}, {"x": 474, "y": 441}, {"x": 474, "y": 494}, {"x": 334, "y": 498}]}, {"text": "€25", "confidence": 0.959, "boundingPolygon": [{"x": 499, "y": 440}, {"x": 584, "y": 437}, {"x": 585, "y": 491}, {"x": 499, "y": 493}]}, {"text": "or", "confidence": 0.997, "boundingPolygon": [{"x": 612, "y": 436}, {"x": 666, "y": 435}, {"x": 668, "y": 488}, {"x": 614, "y": 490}]}, {"text": "more", "confidence": 0.989, "boundingPolygon": [{"x": 695, "y": 434}, {"x": 814, "y": 430}, {"x": 817, "y": 482}, {"x": 696, "y": 487}]}], "boundingPolygon": [{"x": 77, "y": 452}, {"x": 820, "y": 429}, {"x": 821, "y": 482}, {"x": 77, "y": 504}]}, {"text": "on Groceries.", "words": [{"text": "on", "confidence": 0.998, "boundingPolygon": [{"x": 251, "y": 507}, {"x": 300, "y": 505}, {"x": 301, "y": 555}, {"x": 252, "y": 557}]}, {"text": "Groceries.", "confidence": 0.983, "boundingPolygon": [{"x": 334, "y": 503}, {"x": 605, "y": 496}, {"x": 606, "y": 546}, {"x": 335, "y": 554}]}], "boundingPolygon": [{"x": 251, "y": 505}, {"x": 605, "y": 495}, {"x": 606, "y": 545}, {"x": 252, "y": 556}]}, {"text": "Voucher valid for 7 days !", "words": [{"text": "Voucher", "confidence": 0.989, "boundingPolygon": [{"x": 87, "y": 563}, {"x": 277, "y": 558}, {"x": 277, "y": 608}, {"x": 87, "y": 613}]}, {"text": "valid", "confidence": 0.994, "boundingPolygon": [{"x": 308, "y": 557}, {"x": 447, "y": 553}, {"x": 447, "y": 605}, {"x": 308, "y": 608}]}, {"text": "for", "confidence": 0.997, "boundingPolygon": [{"x": 482, "y": 552}, {"x": 556, "y": 550}, {"x": 556, "y": 602}, {"x": 482, "y": 604}]}, {"text": "7", "confidence": 0.993, "boundingPolygon": [{"x": 591, "y": 549}, {"x": 621, "y": 548}, {"x": 621, "y": 601}, {"x": 591, "y": 602}]}, {"text": "days", "confidence": 0.989, "boundingPolygon": [{"x": 642, "y": 548}, {"x": 761, "y": 545}, {"x": 761, "y": 599}, {"x": 642, "y": 601}]}, {"text": "!", "confidence": 0.989, "boundingPolygon": [{"x": 792, "y": 544}, {"x": 813, "y": 544}, {"x": 813, "y": 598}, {"x": 792, "y": 598}]}], "boundingPolygon": [{"x": 86, "y": 561}, {"x": 814, "y": 543}, {"x": 815, "y": 597}, {"x": 86, "y": 613}]}, {"text": "2707288849571", "words": [{"text": "2707288849571", "confidence": 0.985, "boundingPolygon": [{"x": 287, "y": 822}, {"x": 646, "y": 813}, {"x": 645, "y": 863}, {"x": 287, "y": 870}]}], "boundingPolygon": [{"x": 283, "y": 820}, {"x": 646, "y": 812}, {"x": 647, "y": 862}, {"x": 283, "y": 870}]}, {"text": "upon valid from 07/09/26 to 13/09/", "words": [{"text": "upon", "confidence": 0.992, "boundingPolygon": [{"x": 0, "y": 936}, {"x": 100, "y": 935}, {"x": 100, "y": 988}, {"x": 0, "y": 991}]}, {"text": "valid", "confidence": 0.995, "boundingPolygon": [{"x": 129, "y": 934}, {"x": 268, "y": 932}, {"x": 268, "y": 983}, {"x": 129, "y": 987}]}, {"text": "from", "confidence": 0.993, "boundingPolygon": [{"x": 300, "y": 931}, {"x": 404, "y": 929}, {"x": 404, "y": 980}, {"x": 300, "y": 982}]}, {"text": "07/09/26", "confidence": 0.993, "boundingPolygon": [{"x": 433, "y": 929}, {"x": 655, "y": 924}, {"x": 656, "y": 976}, {"x": 433, "y": 979}]}, {"text": "to", "confidence": 0.997, "boundingPolygon": [{"x": 687, "y": 923}, {"x": 739, "y": 922}, {"x": 740, "y": 975}, {"x": 688, "y": 976}]}, {"text": "13/09/", "confidence": 0.959, "boundingPolygon": [{"x": 775, "y": 921}, {"x": 941, "y": 916}, {"x": 942, "y": 974}, {"x": 775, "y": 975}]}], "boundingPolygon": [{"x": 0, "y": 935}, {"x": 949, "y": 916}, {"x": 951, "y": 973}, {"x": 0, "y": 991}]}, {"text": "excludes clothing, footwear, homewares, caf", "words": [{"text": "excludes", "confidence": 0.871, "boundingPolygon": [{"x": 32, "y": 1042}, {"x": 203, "y": 1040}, {"x": 204, "y": 1091}, {"x": 32, "y": 1092}]}, {"text": "clothing,", "confidence": 0.993, "boundingPolygon": [{"x": 218, "y": 1040}, {"x": 413, "y": 1038}, {"x": 414, "y": 1090}, {"x": 218, "y": 1091}]}, {"text": "footwear,", "confidence": 0.99, "boundingPolygon": [{"x": 431, "y": 1038}, {"x": 626, "y": 1036}, {"x": 627, "y": 1088}, {"x": 431, "y": 1090}]}, {"text": "homewares,", "confidence": 0.989, "boundingPolygon": [{"x": 641, "y": 1036}, {"x": 863, "y": 1033}, {"x": 864, "y": 1086}, {"x": 641, "y": 1088}]}, {"text": "caf", "confidence": 0.976, "boundingPolygon": [{"x": 881, "y": 1033}, {"x": 955, "y": 1032}, {"x": 956, "y": 1085}, {"x": 882, "y": 1086}]}], "boundingPolygon": [{"x": 1, "y": 1040}, {"x": 958, "y": 1032}, {"x": 958, "y": 1084}, {"x": 1, "y": 1092}]}, {"text": "ohol, tobacco, medicines, infant milk formula", "words": [{"text": "ohol,", "confidence": 0.932, "boundingPolygon": [{"x": 0, "y": 1102}, {"x": 101, "y": 1101}, {"x": 101, "y": 1152}, {"x": 0, "y": 1153}]}, {"text": "tobacco,", "confidence": 0.994, "boundingPolygon": [{"x": 119, "y": 1101}, {"x": 292, "y": 1100}, {"x": 292, "y": 1151}, {"x": 119, "y": 1152}]}, {"text": "medicines,", "confidence": 0.868, "boundingPolygon": [{"x": 303, "y": 1100}, {"x": 523, "y": 1098}, {"x": 523, "y": 1150}, {"x": 303, "y": 1151}]}, {"text": "infant", "confidence": 0.993, "boundingPolygon": [{"x": 541, "y": 1098}, {"x": 676, "y": 1097}, {"x": 676, "y": 1149}, {"x": 541, "y": 1150}]}, {"text": "milk", "confidence": 0.99, "boundingPolygon": [{"x": 687, "y": 1097}, {"x": 778, "y": 1096}, {"x": 778, "y": 1149}, {"x": 687, "y": 1149}]}, {"text": "formula", "confidence": 0.993, "boundingPolygon": [{"x": 800, "y": 1096}, {"x": 956, "y": 1095}, {"x": 956, "y": 1149}, {"x": 800, "y": 1149}]}], "boundingPolygon": [{"x": 0, "y": 1100}, {"x": 956, "y": 1094}, {"x": 956, "y": 1147}, {"x": 0, "y": 1153}]}, {"text": "ttery products, checkout bags, savings stamps", "words": [{"text": "ttery", "confidence": 0.993, "boundingPolygon": [{"x": 3, "y": 1161}, {"x": 105, "y": 1160}, {"x": 105, "y": 1209}, {"x": 3, "y": 1209}]}, {"text": "products,", "confidence": 0.993, "boundingPolygon": [{"x": 119, "y": 1160}, {"x": 314, "y": 1159}, {"x": 314, "y": 1210}, {"x": 119, "y": 1209}]}, {"text": "checkout", "confidence": 0.993, "boundingPolygon": [{"x": 329, "y": 1159}, {"x": 507, "y": 1159}, {"x": 507, "y": 1211}, {"x": 328, "y": 1210}]}, {"text": "bags,", "confidence": 0.993, "boundingPolygon": [{"x": 518, "y": 1159}, {"x": 633, "y": 1159}, {"x": 633, "y": 1211}, {"x": 518, "y": 1211}]}, {"text": "savings", "confidence": 0.994, "boundingPolygon": [{"x": 650, "y": 1159}, {"x": 805, "y": 1160}, {"x": 805, "y": 1212}, {"x": 650, "y": 1211}]}, {"text": "stamps", "confidence": 0.993, "boundingPolygon": [{"x": 823, "y": 1160}, {"x": 956, "y": 1161}, {"x": 956, "y": 1212}, {"x": 823, "y": 1212}]}], "boundingPolygon": [{"x": 0, "y": 1158}, {"x": 955, "y": 1159}, {"x": 955, "y": 1211}, {"x": 0, "y": 1209}]}, {"text": "t Return Scheme deposit fees, gift vouchers/c", "words": [{"text": "t", "confidence": 0.993, "boundingPolygon": [{"x": 7, "y": 1214}, {"x": 32, "y": 1214}, {"x": 32, "y": 1261}, {"x": 7, "y": 1261}]}, {"text": "Return", "confidence": 0.993, "boundingPolygon": [{"x": 43, "y": 1214}, {"x": 167, "y": 1215}, {"x": 167, "y": 1265}, {"x": 43, "y": 1262}]}, {"text": "Scheme", "confidence": 0.994, "boundingPolygon": [{"x": 188, "y": 1215}, {"x": 316, "y": 1216}, {"x": 316, "y": 1267}, {"x": 188, "y": 1265}]}, {"text": "deposit", "confidence": 0.994, "boundingPolygon": [{"x": 330, "y": 1216}, {"x": 484, "y": 1216}, {"x": 484, "y": 1269}, {"x": 330, "y": 1267}]}, {"text": "fees,", "confidence": 0.994, "boundingPolygon": [{"x": 501, "y": 1216}, {"x": 609, "y": 1217}, {"x": 609, "y": 1269}, {"x": 501, "y": 1269}]}, {"text": "gift", "confidence": 0.992, "boundingPolygon": [{"x": 627, "y": 1217}, {"x": 722, "y": 1217}, {"x": 722, "y": 1269}, {"x": 627, "y": 1269}]}, {"text": "vouchers/c", "confidence": 0.983, "boundingPolygon": [{"x": 736, "y": 1217}, {"x": 956, "y": 1218}, {"x": 956, "y": 1266}, {"x": 736, "y": 1269}]}], "boundingPolygon": [{"x": 0, "y": 1214}, {"x": 956, "y": 1217}, {"x": 956, "y": 1269}, {"x": 0, "y": 1265}]}]}]}, "modelVersion": "2023-10-01"} diff --git a/tests/CouponHubBot.Ocr.Tests/Images/10_50_2026-09-24_2026-10-03_2707322589159.jpg b/tests/CouponHubBot.Ocr.Tests/Images/10_50_2026-09-24_2026-10-03_2707322589159.jpg new file mode 100644 index 00000000..ffb6c4d0 Binary files /dev/null and b/tests/CouponHubBot.Ocr.Tests/Images/10_50_2026-09-24_2026-10-03_2707322589159.jpg differ diff --git a/tests/CouponHubBot.Ocr.Tests/Images/5_25_2026-09-07_2026-09-13_2707288849571.jpg b/tests/CouponHubBot.Ocr.Tests/Images/5_25_2026-09-07_2026-09-13_2707288849571.jpg new file mode 100644 index 00000000..e5a00292 Binary files /dev/null and b/tests/CouponHubBot.Ocr.Tests/Images/5_25_2026-09-07_2026-09-13_2707288849571.jpg differ diff --git a/tests/CouponHubBot.Ocr.Tests/OcrTests.fs b/tests/CouponHubBot.Ocr.Tests/OcrTests.fs index 187c8c25..b3b405a7 100644 --- a/tests/CouponHubBot.Ocr.Tests/OcrTests.fs +++ b/tests/CouponHubBot.Ocr.Tests/OcrTests.fs @@ -410,6 +410,8 @@ type OcrTests(output: ITestOutputHelper) = [] [] [] + [] + [] member _.``OCR engine recognizes coupon from file``(fileName: string) = task { let imagesDir = Path.Combine(AppContext.BaseDirectory, "Images") @@ -452,6 +454,43 @@ type OcrTests(output: ITestOutputHelper) = Assert.Equal(expected.Barcode, res.barcode) } + [] + [] + [] + [] + [] + [] + [] + [] + [] + [] + [] + [] + [] + [] + [] + member _.``Labelled validity range takes precedence over receipt return deadline``(range: string, startDate: string, endDate: string) = + task { + let azure = + { new IBotOcr with + member _.AnalyzeImageBytes _ = + task { + return + { RawJson = "{}" + Text = $"Qualifying items can be returned up to\n21/10/26\nSAVE €10\nWhen you spend €50\n{range}" } + } } + let time = BotInfra.Time.FixedTimeProvider(DateTimeOffset(2030, 1, 15, 0, 0, 0, TimeSpan.Zero)) + let engine = CouponOcrEngine(azure, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance, time) + use image = new Image(1, 1, L8(255uy)) + use stream = new MemoryStream() + image.SaveAsPng(stream) + let! result = engine.Recognize(ReadOnlyMemory(stream.ToArray())) + Assert.Equal(Nullable(Parsing.parseDate startDate), result.validFrom) + Assert.Equal(Nullable(Parsing.parseDate endDate), result.validTo) + Assert.Equal(Nullable 10m, result.couponValue) + Assert.Equal(Nullable 50m, result.minCheck) + } + [] // Low-quality images: assert only fields that are reliable for this file. // Pass null for fields we intentionally do not assert.