Skip to content

Commit 7da6ec8

Browse files
authored
fix for counts (#147)
1 parent e09a39f commit 7da6ec8

13 files changed

Lines changed: 473 additions & 50 deletions

File tree

OpenAlprWebhookProcessor.Server/Data/Repositories/PlateGroupRepository.cs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,22 +138,11 @@ public async Task<PlateStatisticsAggregation> GetPlateStatisticsAggregationAsync
138138
long last90DaysEpoch,
139139
CancellationToken cancellationToken = default)
140140
{
141-
var bestNumberQuery = _dbSet
141+
var allEpochsQuery = _dbSet
142142
.AsNoTracking()
143-
.Where(pg => pg.BestNumber == plateNumber)
143+
.Where(pg => pg.BestNumber == plateNumber || pg.PossibleNumbers.Any(pn => pn.Number == plateNumber))
144144
.Select(pg => pg.ReceivedOnEpoch);
145145

146-
var possibleNumberQuery = _dbSet
147-
.AsNoTracking()
148-
.Join(_context.PlateGroupPossibleNumbers,
149-
pg => pg.Id,
150-
pn => pn.PlateGroupId,
151-
(pg, pn) => new { pg, pn })
152-
.Where(x => x.pn.Number == plateNumber)
153-
.Select(x => x.pg.ReceivedOnEpoch);
154-
155-
var allEpochsQuery = bestNumberQuery.Concat(possibleNumberQuery);
156-
157146
var result = await allEpochsQuery
158147
.GroupBy(e => 1)
159148
.Select(g => new PlateStatisticsAggregation

OpenAlprWebhookProcessor.Server/Features/LicensePlates/Commands/UpdatePlateNumber/UpdatePlateNumberCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ public class UpdatePlateNumberCommand : ICommand
2828

2929

3030

31+

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
# OpenALPR Webhook Processor
2+
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=mlapaglia_OpenAlprWebhookProcessor&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=mlapaglia_OpenAlprWebhookProcessor)
3+
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=mlapaglia_OpenAlprWebhookProcessor&metric=bugs)](https://sonarcloud.io/summary/new_code?id=mlapaglia_OpenAlprWebhookProcessor)
4+
[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=mlapaglia_OpenAlprWebhookProcessor&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=mlapaglia_OpenAlprWebhookProcessor)
5+
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=mlapaglia_OpenAlprWebhookProcessor&metric=coverage)](https://sonarcloud.io/summary/new_code?id=mlapaglia_OpenAlprWebhookProcessor)
6+
[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=mlapaglia_OpenAlprWebhookProcessor&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=mlapaglia_OpenAlprWebhookProcessor)
7+
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=mlapaglia_OpenAlprWebhookProcessor&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=mlapaglia_OpenAlprWebhookProcessor)
8+
[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=mlapaglia_OpenAlprWebhookProcessor&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=mlapaglia_OpenAlprWebhookProcessor)
9+
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=mlapaglia_OpenAlprWebhookProcessor&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=mlapaglia_OpenAlprWebhookProcessor)
10+
[![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=mlapaglia_OpenAlprWebhookProcessor&metric=sqale_index)](https://sonarcloud.io/summary/new_code?id=mlapaglia_OpenAlprWebhookProcessor)
11+
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=mlapaglia_OpenAlprWebhookProcessor&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=mlapaglia_OpenAlprWebhookProcessor)
12+
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=mlapaglia_OpenAlprWebhookProcessor&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=mlapaglia_OpenAlprWebhookProcessor)
213

314
A comprehensive license plate recognition management system that processes webhooks from OpenALPR web servers, manages IP cameras, and provides intelligent alerting capabilities.
415
<img width="1672" height="1061" alt="Untitled" src="https://github.com/user-attachments/assets/073f4ac9-d63d-452d-a80c-b9dcbd12bb29" />
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
using AwesomeAssertions;
2+
using NUnit.Framework;
3+
using OpenAlprWebhookProcessor.Data;
4+
using OpenAlprWebhookProcessor.Features.LicensePlates.Queries.GetStatistics;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Threading.Tasks;
8+
using Tests.TestHelpers;
9+
10+
namespace Tests.Data.Repositories
11+
{
12+
[Parallelizable(ParallelScope.Self)]
13+
[TestFixture]
14+
public class PlateGroupRepositoryTests : TestBase
15+
{
16+
[Test]
17+
public async Task GetPlateStatisticsAggregationAsync_PlateNumberInBestNumberOnly_ReturnsCorrectCount()
18+
{
19+
// Arrange
20+
var plateNumber = "ABC123";
21+
var now = DateTimeOffset.UtcNow;
22+
var last90DaysEpoch = now.AddDays(-90).ToUnixTimeMilliseconds();
23+
24+
// Create plate groups where the plate number is only in BestNumber
25+
var plateGroup1 = TestDataFactory.CreateTestPlateGroup(plateNumber, now.AddDays(-30).ToUnixTimeMilliseconds());
26+
var plateGroup2 = TestDataFactory.CreateTestPlateGroup(plateNumber, now.AddDays(-60).ToUnixTimeMilliseconds());
27+
var plateGroup3 = TestDataFactory.CreateTestPlateGroup("DIFFERENT", now.AddDays(-10).ToUnixTimeMilliseconds());
28+
29+
await UnitOfWork.PlateGroups.AddAsync(plateGroup1);
30+
await UnitOfWork.PlateGroups.AddAsync(plateGroup2);
31+
await UnitOfWork.PlateGroups.AddAsync(plateGroup3);
32+
await UnitOfWork.SaveChangesAsync();
33+
34+
// Act
35+
var result = await UnitOfWork.PlateGroups.GetPlateStatisticsAggregationAsync(
36+
plateNumber, last90DaysEpoch, GetCancellationToken());
37+
38+
// Assert
39+
result.Should().NotBeNull();
40+
result.TotalCount.Should().Be(2);
41+
result.Last90DaysCount.Should().Be(2);
42+
result.MinEpoch.Should().Be(plateGroup2.ReceivedOnEpoch);
43+
result.MaxEpoch.Should().Be(plateGroup1.ReceivedOnEpoch);
44+
}
45+
46+
[Test]
47+
public async Task GetPlateStatisticsAggregationAsync_PlateNumberInPossibleNumbersOnly_ReturnsCorrectCount()
48+
{
49+
// Arrange
50+
var plateNumber = "XYZ789";
51+
var now = DateTimeOffset.UtcNow;
52+
var last90DaysEpoch = now.AddDays(-90).ToUnixTimeMilliseconds();
53+
54+
// Create plate groups where the plate number is only in PossibleNumbers
55+
var plateGroup1 = TestDataFactory.CreateTestPlateGroup("BEST123", now.AddDays(-20).ToUnixTimeMilliseconds());
56+
plateGroup1.PossibleNumbers = new List<PlateGroupPossibleNumbers>
57+
{
58+
new PlateGroupPossibleNumbers { Id = Guid.NewGuid(), PlateGroupId = plateGroup1.Id, Number = plateNumber }
59+
};
60+
61+
var plateGroup2 = TestDataFactory.CreateTestPlateGroup("BEST456", now.AddDays(-50).ToUnixTimeMilliseconds());
62+
plateGroup2.PossibleNumbers = new List<PlateGroupPossibleNumbers>
63+
{
64+
new PlateGroupPossibleNumbers { Id = Guid.NewGuid(), PlateGroupId = plateGroup2.Id, Number = plateNumber }
65+
};
66+
67+
var plateGroup3 = TestDataFactory.CreateTestPlateGroup("DIFFERENT", now.AddDays(-10).ToUnixTimeMilliseconds());
68+
69+
await UnitOfWork.PlateGroups.AddAsync(plateGroup1);
70+
await UnitOfWork.PlateGroups.AddAsync(plateGroup2);
71+
await UnitOfWork.PlateGroups.AddAsync(plateGroup3);
72+
await UnitOfWork.SaveChangesAsync();
73+
74+
// Act
75+
var result = await UnitOfWork.PlateGroups.GetPlateStatisticsAggregationAsync(
76+
plateNumber, last90DaysEpoch, GetCancellationToken());
77+
78+
// Assert
79+
result.Should().NotBeNull();
80+
result.TotalCount.Should().Be(2);
81+
result.Last90DaysCount.Should().Be(2);
82+
result.MinEpoch.Should().Be(plateGroup2.ReceivedOnEpoch);
83+
result.MaxEpoch.Should().Be(plateGroup1.ReceivedOnEpoch);
84+
}
85+
86+
[Test]
87+
public async Task GetPlateStatisticsAggregationAsync_PlateNumberInBothBestAndPossible_CountsOnlyOnce()
88+
{
89+
// Arrange
90+
var plateNumber = "DUPLICATE123";
91+
var now = DateTimeOffset.UtcNow;
92+
var last90DaysEpoch = now.AddDays(-90).ToUnixTimeMilliseconds();
93+
94+
// Create a plate group where the plate number is in BOTH BestNumber AND PossibleNumbers
95+
var plateGroup1 = TestDataFactory.CreateTestPlateGroup(plateNumber, now.AddDays(-30).ToUnixTimeMilliseconds());
96+
plateGroup1.PossibleNumbers = new List<PlateGroupPossibleNumbers>
97+
{
98+
new PlateGroupPossibleNumbers { Id = Guid.NewGuid(), PlateGroupId = plateGroup1.Id, Number = plateNumber },
99+
new PlateGroupPossibleNumbers { Id = Guid.NewGuid(), PlateGroupId = plateGroup1.Id, Number = "OTHER123" }
100+
};
101+
102+
// Create another plate group with different plate number
103+
var plateGroup2 = TestDataFactory.CreateTestPlateGroup("DIFFERENT456", now.AddDays(-60).ToUnixTimeMilliseconds());
104+
105+
await UnitOfWork.PlateGroups.AddAsync(plateGroup1);
106+
await UnitOfWork.PlateGroups.AddAsync(plateGroup2);
107+
await UnitOfWork.SaveChangesAsync();
108+
109+
// Act
110+
var result = await UnitOfWork.PlateGroups.GetPlateStatisticsAggregationAsync(
111+
plateNumber, last90DaysEpoch, GetCancellationToken());
112+
113+
// Assert
114+
result.Should().NotBeNull();
115+
result.TotalCount.Should().Be(1); // Should count only once, not twice
116+
result.Last90DaysCount.Should().Be(1);
117+
result.MinEpoch.Should().Be(plateGroup1.ReceivedOnEpoch);
118+
result.MaxEpoch.Should().Be(plateGroup1.ReceivedOnEpoch);
119+
}
120+
121+
[Test]
122+
public async Task GetPlateStatisticsAggregationAsync_MultiplePlateGroupsWithMixedMatches_ReturnsCorrectAggregation()
123+
{
124+
// Arrange
125+
var plateNumber = "MIXED123";
126+
var now = DateTimeOffset.UtcNow;
127+
var last90DaysEpoch = now.AddDays(-90).ToUnixTimeMilliseconds();
128+
129+
// Plate group 1: BestNumber matches
130+
var plateGroup1 = TestDataFactory.CreateTestPlateGroup(plateNumber, now.AddDays(-10).ToUnixTimeMilliseconds());
131+
132+
// Plate group 2: PossibleNumbers matches
133+
var plateGroup2 = TestDataFactory.CreateTestPlateGroup("BEST456", now.AddDays(-40).ToUnixTimeMilliseconds());
134+
plateGroup2.PossibleNumbers = new List<PlateGroupPossibleNumbers>
135+
{
136+
new PlateGroupPossibleNumbers { Id = Guid.NewGuid(), PlateGroupId = plateGroup2.Id, Number = plateNumber }
137+
};
138+
139+
// Plate group 3: Both BestNumber and PossibleNumbers match (should count only once)
140+
var plateGroup3 = TestDataFactory.CreateTestPlateGroup(plateNumber, now.AddDays(-70).ToUnixTimeMilliseconds());
141+
plateGroup3.PossibleNumbers = new List<PlateGroupPossibleNumbers>
142+
{
143+
new PlateGroupPossibleNumbers { Id = Guid.NewGuid(), PlateGroupId = plateGroup3.Id, Number = plateNumber }
144+
};
145+
146+
// Plate group 4: No match
147+
var plateGroup4 = TestDataFactory.CreateTestPlateGroup("NOMATCH789", now.AddDays(-20).ToUnixTimeMilliseconds());
148+
149+
await UnitOfWork.PlateGroups.AddAsync(plateGroup1);
150+
await UnitOfWork.PlateGroups.AddAsync(plateGroup2);
151+
await UnitOfWork.PlateGroups.AddAsync(plateGroup3);
152+
await UnitOfWork.PlateGroups.AddAsync(plateGroup4);
153+
await UnitOfWork.SaveChangesAsync();
154+
155+
// Act
156+
var result = await UnitOfWork.PlateGroups.GetPlateStatisticsAggregationAsync(
157+
plateNumber, last90DaysEpoch, GetCancellationToken());
158+
159+
// Assert
160+
result.Should().NotBeNull();
161+
result.TotalCount.Should().Be(3); // plateGroup1, plateGroup2, plateGroup3 (counted once)
162+
result.Last90DaysCount.Should().Be(3); // All are within 90 days
163+
result.MinEpoch.Should().Be(plateGroup3.ReceivedOnEpoch); // Oldest matching
164+
result.MaxEpoch.Should().Be(plateGroup1.ReceivedOnEpoch); // Newest matching
165+
}
166+
167+
[Test]
168+
public async Task GetPlateStatisticsAggregationAsync_WithLast90DaysFiltering_ReturnsCorrectCounts()
169+
{
170+
// Arrange
171+
var plateNumber = "TIMETEST123";
172+
var now = DateTimeOffset.UtcNow;
173+
var last90DaysEpoch = now.AddDays(-90).ToUnixTimeMilliseconds();
174+
175+
// Plate group 1: Within 90 days
176+
var plateGroup1 = TestDataFactory.CreateTestPlateGroup(plateNumber, now.AddDays(-30).ToUnixTimeMilliseconds());
177+
178+
// Plate group 2: Within 90 days
179+
var plateGroup2 = TestDataFactory.CreateTestPlateGroup(plateNumber, now.AddDays(-60).ToUnixTimeMilliseconds());
180+
181+
// Plate group 3: Outside 90 days (older)
182+
var plateGroup3 = TestDataFactory.CreateTestPlateGroup(plateNumber, now.AddDays(-120).ToUnixTimeMilliseconds());
183+
184+
// Plate group 4: Outside 90 days (much older)
185+
var plateGroup4 = TestDataFactory.CreateTestPlateGroup(plateNumber, now.AddDays(-200).ToUnixTimeMilliseconds());
186+
187+
await UnitOfWork.PlateGroups.AddAsync(plateGroup1);
188+
await UnitOfWork.PlateGroups.AddAsync(plateGroup2);
189+
await UnitOfWork.PlateGroups.AddAsync(plateGroup3);
190+
await UnitOfWork.PlateGroups.AddAsync(plateGroup4);
191+
await UnitOfWork.SaveChangesAsync();
192+
193+
// Act
194+
var result = await UnitOfWork.PlateGroups.GetPlateStatisticsAggregationAsync(
195+
plateNumber, last90DaysEpoch, GetCancellationToken());
196+
197+
// Assert
198+
result.Should().NotBeNull();
199+
result.TotalCount.Should().Be(4); // All plate groups
200+
result.Last90DaysCount.Should().Be(2); // Only plateGroup1 and plateGroup2
201+
result.MinEpoch.Should().Be(plateGroup4.ReceivedOnEpoch); // Oldest overall
202+
result.MaxEpoch.Should().Be(plateGroup1.ReceivedOnEpoch); // Newest overall
203+
}
204+
205+
[Test]
206+
public async Task GetPlateStatisticsAggregationAsync_NoMatchingPlates_ReturnsZeroAggregation()
207+
{
208+
// Arrange
209+
var plateNumber = "NOTFOUND123";
210+
var now = DateTimeOffset.UtcNow;
211+
var last90DaysEpoch = now.AddDays(-90).ToUnixTimeMilliseconds();
212+
213+
// Create some plate groups that don't match
214+
var plateGroup1 = TestDataFactory.CreateTestPlateGroup("DIFFERENT1", now.AddDays(-30).ToUnixTimeMilliseconds());
215+
var plateGroup2 = TestDataFactory.CreateTestPlateGroup("DIFFERENT2", now.AddDays(-60).ToUnixTimeMilliseconds());
216+
217+
await UnitOfWork.PlateGroups.AddAsync(plateGroup1);
218+
await UnitOfWork.PlateGroups.AddAsync(plateGroup2);
219+
await UnitOfWork.SaveChangesAsync();
220+
221+
// Act
222+
var result = await UnitOfWork.PlateGroups.GetPlateStatisticsAggregationAsync(
223+
plateNumber, last90DaysEpoch, GetCancellationToken());
224+
225+
// Assert
226+
result.Should().NotBeNull();
227+
result.TotalCount.Should().Be(0);
228+
result.Last90DaysCount.Should().Be(0);
229+
result.MinEpoch.Should().Be(0);
230+
result.MaxEpoch.Should().Be(0);
231+
}
232+
233+
[Test]
234+
public async Task GetPlateStatisticsAggregationAsync_EmptyDatabase_ReturnsZeroAggregation()
235+
{
236+
// Arrange
237+
var plateNumber = "EMPTY123";
238+
var now = DateTimeOffset.UtcNow;
239+
var last90DaysEpoch = now.AddDays(-90).ToUnixTimeMilliseconds();
240+
241+
// Act (no data in database)
242+
var result = await UnitOfWork.PlateGroups.GetPlateStatisticsAggregationAsync(
243+
plateNumber, last90DaysEpoch, GetCancellationToken());
244+
245+
// Assert
246+
result.Should().NotBeNull();
247+
result.TotalCount.Should().Be(0);
248+
result.Last90DaysCount.Should().Be(0);
249+
result.MinEpoch.Should().Be(0);
250+
result.MaxEpoch.Should().Be(0);
251+
}
252+
}
253+
}

openalprwebhookprocessor.client/src/app/plates/plate-item/plate-item.component.html

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,16 @@
2929
</ng-template>
3030
<mat-action-row class="plate-actions">
3131
<div class="primary-actions">
32-
<button mat-raised-button
33-
[disabled]="plate().canBeEnriched"
34-
type="button"
35-
color="accent"
36-
(click)="onEnrichPlate()"
37-
i18n>
38-
<mat-icon>auto_fix_high</mat-icon>
39-
<span>Enrich plate</span>
40-
</button>
41-
32+
@if (plate().canBeEnriched) {
33+
<button mat-raised-button
34+
type="button"
35+
color="accent"
36+
(click)="onEnrichPlate()"
37+
i18n>
38+
<mat-icon>auto_fix_high</mat-icon>
39+
<span>Enrich plate</span>
40+
</button>
41+
}
4242
<button mat-raised-button
4343
type="button"
4444
color="primary"
@@ -49,23 +49,35 @@
4949
</button>
5050

5151
<app-refresh-button
52+
style="margin:0px;"
5253
[disabled]="plate().isIgnore"
5354
(refreshStarted)="onIgnorePlate()"
5455
buttonText="Add to ignore list"
5556
buttonRefreshingText="Adding..."
57+
buttonType="raised"
5658
icon="visibility_off"
5759
color="accent"
5860
i18n />
5961

60-
6162
<app-refresh-button
63+
style="margin:0px;"
6264
[disabled]="plate().isAlert"
6365
(refreshStarted)="onAlertPlate()"
6466
buttonText="Add to alert list"
6567
buttonRefreshingText="Adding..."
68+
buttonType="raised"
6669
icon="notifications_active"
6770
color="primary"
6871
i18n />
72+
73+
<button mat-raised-button
74+
type="button"
75+
color="primary"
76+
(click)="onSearchForPlate()"
77+
i18n>
78+
<mat-icon>search</mat-icon>
79+
<span>Search for plate</span>
80+
</button>
6981
</div>
7082
</mat-action-row>
7183
</mat-expansion-panel>

0 commit comments

Comments
 (0)