Skip to content

Commit 0dbcf23

Browse files
committed
Adds OriginalFailureReasons/CurrentFailureReasons columns,
refresh_failedmeasurements_current procedure, validation endpoint, auto-reingest for ready rows, and coordinate null/zero handling fixes.Merge branch 'dev-consolidate-failures' into forestgeo-app-development Adds OriginalFailureReasons/CurrentFailureReasons columns, refresh_failedmeasurements_current procedure, validation endpoint, auto-reingest for ready rows, and coordinate null/zero handling fixes.# the commit.
2 parents 8a768c3 + 9eecf2a commit 0dbcf23

27 files changed

Lines changed: 1957 additions & 451 deletions

File tree

frontend/app/api/batchedupload/[schema]/[[...slugs]]/route.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ describe('batchedupload POST route', () => {
9191

9292
// ⟵ FIX: default import already *is* the mocked module
9393
const exec = connectionmanager.getInstance().executeQuery as ReturnType<typeof vi.fn>;
94-
// Route calls executeQuery twice: INSERT + CALL reviewfailed()
94+
// Route calls executeQuery twice: INSERT + CALL refresh_failedmeasurements_current()
9595
expect(exec).toHaveBeenCalledTimes(2);
9696

9797
// With mocked mysql2.format we can sanity-check the SQL + params
@@ -110,9 +110,9 @@ describe('batchedupload POST route', () => {
110110
expect(sqlArg).toContain('42'); // plotID
111111
expect(sqlArg).toContain('7'); // censusID
112112

113-
// Verify the second call is the reviewfailed procedure
114-
const reviewFailedCall = exec.mock.calls[1][0] as string;
115-
expect(reviewFailedCall).toMatch(/CALL myschema\.reviewfailed\(\)/i);
113+
// Verify the second call is the refresh_failedmeasurements_current procedure
114+
const refreshCall = exec.mock.calls[1][0] as string;
115+
expect(refreshCall).toMatch(/CALL myschema\.refresh_failedmeasurements_current/i);
116116
});
117117

118118
it('500s and logs on DB error', async () => {

frontend/app/api/batchedupload/[schema]/[[...slugs]]/route.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ export async function POST(request: NextRequest, props: { params: Promise<{ sche
5555
errorRows = errorRows.map(row => ({
5656
...row,
5757
plotID,
58-
censusID
58+
censusID,
59+
originalFailureReasons: row.originalFailureReasons ?? row.failureReasons ?? null,
60+
currentFailureReasons: row.currentFailureReasons ?? row.failureReasons ?? null
5961
}));
6062

6163
const connectionManager = connectionmanager.getInstance();
@@ -67,9 +69,9 @@ export async function POST(request: NextRequest, props: { params: Promise<{ sche
6769
try {
6870
await connectionManager.executeQuery(insertQuery);
6971

70-
// Populate failure reasons for the newly inserted failed measurements
71-
const reviewFailedQuery = `CALL ${schema}.reviewfailed()`;
72-
await connectionManager.executeQuery(reviewFailedQuery);
72+
// Refresh current failure reasons for the affected plot/census
73+
const refreshFailedQuery = `CALL ${schema}.refresh_failedmeasurements_current(?, ?)`;
74+
await connectionManager.executeQuery(refreshFailedQuery, [plotID, censusID]);
7375

7476
return new NextResponse(JSON.stringify({ message: 'Insert to SQL successful' }), { status: HTTPResponses.OK });
7577
} catch (error: any) {

frontend/app/api/reingest/[schema]/[plotID]/[censusID]/route.test.ts

Lines changed: 90 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,10 @@ import ConnectionManager from '@/config/connectionmanager';
66
// Mock ConnectionManager
77
vi.mock('@/config/connectionmanager', () => {
88
const executeQuery = vi.fn();
9-
const beginTransaction = vi.fn();
10-
const commitTransaction = vi.fn();
11-
const rollbackTransaction = vi.fn();
129
const closeConnection = vi.fn();
1310
const cleanupStaleTransactions = vi.fn();
1411
const instance = {
1512
executeQuery,
16-
beginTransaction,
17-
commitTransaction,
18-
rollbackTransaction,
1913
closeConnection,
2014
cleanupStaleTransactions
2115
};
@@ -61,6 +55,11 @@ function makeParams() {
6155
} as any;
6256
}
6357

58+
/** Helper: generate mock FailedMeasurementID rows */
59+
function mockFailedIds(count: number) {
60+
return Array.from({ length: count }, (_, i) => ({ FailedMeasurementID: i + 1 }));
61+
}
62+
6463
describe('reingest API routes', () => {
6564
let mockConnectionManager: any;
6665
let mockValidateContextualValues: any;
@@ -69,9 +68,6 @@ describe('reingest API routes', () => {
6968
vi.clearAllMocks();
7069
mockConnectionManager = ConnectionManager.getInstance();
7170
mockConnectionManager.cleanupStaleTransactions.mockResolvedValue(undefined);
72-
mockConnectionManager.beginTransaction.mockResolvedValue('test-transaction-id');
73-
mockConnectionManager.commitTransaction.mockResolvedValue(undefined);
74-
mockConnectionManager.rollbackTransaction.mockResolvedValue(undefined);
7571
mockConnectionManager.closeConnection.mockResolvedValue(undefined);
7672

7773
// Get the mocked function
@@ -91,12 +87,12 @@ describe('reingest API routes', () => {
9187

9288
describe('POST route (move rows only)', () => {
9389
it('moves rows from failedmeasurements to temporarymeasurements', async () => {
94-
// Mock count query
9590
mockConnectionManager.executeQuery
96-
.mockResolvedValueOnce([{ total: 5 }]) // Count query
97-
.mockResolvedValueOnce(undefined) // DELETE temporarymeasurements
98-
.mockResolvedValueOnce(undefined) // INSERT INTO temporarymeasurements
99-
.mockResolvedValueOnce(undefined); // DELETE failedmeasurements
91+
.mockResolvedValueOnce([{ total: 5 }]) // 1. COUNT(*)
92+
.mockResolvedValueOnce(mockFailedIds(5)) // 2. SELECT FailedMeasurementID
93+
.mockResolvedValueOnce(undefined) // 3. DELETE temporarymeasurements
94+
.mockResolvedValueOnce(undefined) // 4. INSERT INTO temporarymeasurements
95+
.mockResolvedValueOnce(undefined); // 5. DELETE failedmeasurements by IDs
10096

10197
const req = makeRequest('POST');
10298
const res = await POST(req, makeParams());
@@ -107,13 +103,8 @@ describe('reingest API routes', () => {
107103
expect(body.fileID).toBe('reingestion.csv');
108104
expect(body.batchID).toBe('test-batch-id-12345');
109105

110-
// Verify transaction management
111-
expect(mockConnectionManager.beginTransaction).toHaveBeenCalledTimes(1);
112-
expect(mockConnectionManager.commitTransaction).toHaveBeenCalledTimes(1);
113106
expect(mockConnectionManager.closeConnection).toHaveBeenCalledTimes(1);
114-
115-
// Verify queries were called
116-
expect(mockConnectionManager.executeQuery).toHaveBeenCalledTimes(4);
107+
expect(mockConnectionManager.executeQuery).toHaveBeenCalledTimes(5);
117108
});
118109

119110
it('returns 200 with rowsMoved=0 when no failed measurements exist', async () => {
@@ -128,7 +119,7 @@ describe('reingest API routes', () => {
128119
expect(body.responseMessage).toMatch(/No failed measurements found/i);
129120
});
130121

131-
it('rolls back transaction on error', async () => {
122+
it('returns 500 on error', async () => {
132123
mockConnectionManager.executeQuery.mockRejectedValueOnce(new Error('Database error'));
133124

134125
const req = makeRequest('POST');
@@ -137,21 +128,22 @@ describe('reingest API routes', () => {
137128
expect(res.status).toBe(500);
138129
const body = await res.json();
139130
expect(body.error).toBe('Database error');
140-
expect(mockConnectionManager.rollbackTransaction).toHaveBeenCalledTimes(1);
131+
expect(mockConnectionManager.closeConnection).toHaveBeenCalledTimes(1);
141132
});
142133
});
143134

144135
describe('GET route (full reingestion)', () => {
145136
it('moves rows and runs batch ingestion process', async () => {
146-
// Mock all queries for full reingestion
147137
mockConnectionManager.executeQuery
148-
.mockResolvedValueOnce([{ total: 10 }]) // Count query
149-
.mockResolvedValueOnce(undefined) // DELETE temporarymeasurements
150-
.mockResolvedValueOnce(undefined) // INSERT INTO temporarymeasurements
151-
.mockResolvedValueOnce(undefined) // DELETE failedmeasurements
152-
.mockResolvedValueOnce(undefined) // CALL bulkingestionprocess
153-
.mockResolvedValueOnce([{ remaining: 2 }]) // Count remaining failures
154-
.mockResolvedValueOnce(undefined); // CALL reviewfailed
138+
.mockResolvedValueOnce([{ total: 10 }]) // 1. COUNT(*)
139+
.mockResolvedValueOnce(mockFailedIds(10)) // 2. SELECT FailedMeasurementID
140+
.mockResolvedValueOnce(undefined) // 3. DELETE temporarymeasurements
141+
.mockResolvedValueOnce(undefined) // 4. INSERT INTO temporarymeasurements
142+
.mockResolvedValueOnce(undefined) // 5. CALL bulkingestionprocess
143+
.mockResolvedValueOnce(undefined) // 6. DELETE failedmeasurements by IDs
144+
.mockResolvedValueOnce(undefined) // 7. CALL refresh_failedmeasurements_current
145+
.mockResolvedValueOnce([{ cnt: 0 }]) // 8. SELECT COUNT(*) ready for reingestion
146+
.mockResolvedValueOnce([{ remaining: 2 }]); // 9. COUNT(*) remaining failures
155147

156148
const req = makeRequest('GET');
157149
const res = await GET(req, makeParams());
@@ -167,9 +159,9 @@ describe('reingest API routes', () => {
167159
const bulkIngestionCall = calls.find((call: any) => call[0]?.includes('bulkingestionprocess'));
168160
expect(bulkIngestionCall).toBeDefined();
169161

170-
// Verify reviewfailed was called
171-
const reviewFailedCall = calls.find((call: any) => call[0]?.includes('reviewfailed'));
172-
expect(reviewFailedCall).toBeDefined();
162+
// Verify refresh was called
163+
const refreshCall = calls.find((call: any) => call[0]?.includes('refresh_failedmeasurements_current'));
164+
expect(refreshCall).toBeDefined();
173165
});
174166

175167
it('returns 200 with 0 processed when no failed measurements exist', async () => {
@@ -187,13 +179,15 @@ describe('reingest API routes', () => {
187179

188180
it('handles all rows successfully reingested', async () => {
189181
mockConnectionManager.executeQuery
190-
.mockResolvedValueOnce([{ total: 5 }])
191-
.mockResolvedValueOnce(undefined) // DELETE temporarymeasurements
192-
.mockResolvedValueOnce(undefined) // INSERT INTO temporarymeasurements
193-
.mockResolvedValueOnce(undefined) // DELETE failedmeasurements
194-
.mockResolvedValueOnce(undefined) // CALL bulkingestionprocess
195-
.mockResolvedValueOnce([{ remaining: 0 }]) // All successful
196-
.mockResolvedValueOnce(undefined); // CALL reviewfailed
182+
.mockResolvedValueOnce([{ total: 5 }]) // 1. COUNT(*)
183+
.mockResolvedValueOnce(mockFailedIds(5)) // 2. SELECT FailedMeasurementID
184+
.mockResolvedValueOnce(undefined) // 3. DELETE temporarymeasurements
185+
.mockResolvedValueOnce(undefined) // 4. INSERT INTO temporarymeasurements
186+
.mockResolvedValueOnce(undefined) // 5. CALL bulkingestionprocess
187+
.mockResolvedValueOnce(undefined) // 6. DELETE failedmeasurements by IDs
188+
.mockResolvedValueOnce(undefined) // 7. CALL refresh_failedmeasurements_current
189+
.mockResolvedValueOnce([{ cnt: 0 }]) // 8. SELECT COUNT(*) ready
190+
.mockResolvedValueOnce([{ remaining: 0 }]); // 9. All successful
197191

198192
const req = makeRequest('GET');
199193
const res = await GET(req, makeParams());
@@ -205,23 +199,47 @@ describe('reingest API routes', () => {
205199
expect(body.remainingFailures).toBe(0);
206200
});
207201

208-
it('rolls back and tries to run reviewfailed on error', async () => {
202+
it('returns 500 on error', async () => {
209203
mockConnectionManager.executeQuery
210-
.mockResolvedValueOnce([{ total: 5 }])
211-
.mockResolvedValueOnce(undefined)
212-
.mockRejectedValueOnce(new Error('Bulk ingestion failed'));
204+
.mockResolvedValueOnce([{ total: 5 }]) // 1. COUNT(*)
205+
.mockResolvedValueOnce(mockFailedIds(5)) // 2. SELECT FailedMeasurementID
206+
.mockResolvedValueOnce(undefined) // 3. DELETE temporarymeasurements
207+
.mockRejectedValueOnce(new Error('Bulk ingestion failed')); // 4. INSERT fails
213208

214209
const req = makeRequest('GET');
215210
const res = await GET(req, makeParams());
216211

217212
expect(res.status).toBe(500);
218213
const body = await res.json();
219214
expect(body.error).toBe('Bulk ingestion failed');
220-
expect(mockConnectionManager.rollbackTransaction).toHaveBeenCalledTimes(1);
215+
expect(mockConnectionManager.closeConnection).toHaveBeenCalledTimes(1);
216+
});
217+
218+
it('auto-reingests rows marked Ready for reingestion', async () => {
219+
mockConnectionManager.executeQuery
220+
.mockResolvedValueOnce([{ total: 5 }]) // 1. COUNT(*)
221+
.mockResolvedValueOnce(mockFailedIds(5)) // 2. SELECT FailedMeasurementID
222+
.mockResolvedValueOnce(undefined) // 3. DELETE temporarymeasurements
223+
.mockResolvedValueOnce(undefined) // 4. INSERT INTO temporarymeasurements
224+
.mockResolvedValueOnce(undefined) // 5. CALL bulkingestionprocess
225+
.mockResolvedValueOnce(undefined) // 6. DELETE failedmeasurements by IDs
226+
.mockResolvedValueOnce(undefined) // 7. CALL refresh_failedmeasurements_current
227+
.mockResolvedValueOnce([{ cnt: 2 }]) // 8. 2 rows ready for reingestion
228+
.mockResolvedValueOnce(undefined) // 9. INSERT ready rows into temporarymeasurements
229+
.mockResolvedValueOnce(undefined) // 10. CALL bulkingestionprocess (auto-reingest)
230+
.mockResolvedValueOnce(undefined) // 11. DELETE ready rows from failedmeasurements
231+
.mockResolvedValueOnce([{ remaining: 1 }]); // 12. COUNT(*) remaining
221232

222-
// Should attempt to run reviewfailed even after error
223-
const reviewFailedCall = mockConnectionManager.executeQuery.mock.calls.find((call: any) => call[0]?.includes('reviewfailed'));
224-
expect(reviewFailedCall).toBeDefined();
233+
const req = makeRequest('GET');
234+
const res = await GET(req, makeParams());
235+
236+
expect(res.status).toBe(200);
237+
const body = await res.json();
238+
expect(body.totalProcessed).toBe(5);
239+
expect(body.successfulReingestions).toBe(4);
240+
expect(body.remainingFailures).toBe(1);
241+
242+
expect(mockConnectionManager.executeQuery).toHaveBeenCalledTimes(12);
225243
});
226244
});
227245

@@ -250,12 +268,12 @@ describe('reingest API routes', () => {
250268

251269
describe('Attribute persistence regression tests', () => {
252270
it('should preserve Codes field when moving to temporarymeasurements', async () => {
253-
// Mock count query
254271
mockConnectionManager.executeQuery
255-
.mockResolvedValueOnce([{ total: 1 }]) // Count query
256-
.mockResolvedValueOnce(undefined) // DELETE temporarymeasurements
257-
.mockResolvedValueOnce({ insertId: 1, affectedRows: 1 }) // INSERT INTO temporarymeasurements
258-
.mockResolvedValueOnce(undefined); // DELETE failedmeasurements
272+
.mockResolvedValueOnce([{ total: 1 }]) // 1. COUNT(*)
273+
.mockResolvedValueOnce(mockFailedIds(1)) // 2. SELECT FailedMeasurementID
274+
.mockResolvedValueOnce(undefined) // 3. DELETE temporarymeasurements
275+
.mockResolvedValueOnce({ insertId: 1, affectedRows: 1 }) // 4. INSERT INTO temporarymeasurements
276+
.mockResolvedValueOnce(undefined); // 5. DELETE failedmeasurements by IDs
259277

260278
const req = makeRequest('POST');
261279
const res = await POST(req, makeParams());
@@ -272,39 +290,38 @@ describe('reingest API routes', () => {
272290
expect(insertCall[0]).toContain('fm.Codes'); // Maps from failedmeasurements
273291
});
274292

275-
it('should call reviewfailed after successful GET reingestion', async () => {
293+
it('should complete GET reingestion without extra validation calls', async () => {
276294
mockConnectionManager.executeQuery
277-
.mockResolvedValueOnce([{ total: 5 }])
278-
.mockResolvedValueOnce(undefined) // DELETE temporarymeasurements
279-
.mockResolvedValueOnce(undefined) // INSERT INTO temporarymeasurements
280-
.mockResolvedValueOnce(undefined) // DELETE failedmeasurements
281-
.mockResolvedValueOnce(undefined) // CALL bulkingestionprocess
282-
.mockResolvedValueOnce([{ remaining: 0 }]) // Count remaining
283-
.mockResolvedValueOnce(undefined); // CALL reviewfailed
295+
.mockResolvedValueOnce([{ total: 5 }]) // 1. COUNT(*)
296+
.mockResolvedValueOnce(mockFailedIds(5)) // 2. SELECT FailedMeasurementID
297+
.mockResolvedValueOnce(undefined) // 3. DELETE temporarymeasurements
298+
.mockResolvedValueOnce(undefined) // 4. INSERT INTO temporarymeasurements
299+
.mockResolvedValueOnce(undefined) // 5. CALL bulkingestionprocess
300+
.mockResolvedValueOnce(undefined) // 6. DELETE failedmeasurements by IDs
301+
.mockResolvedValueOnce(undefined) // 7. CALL refresh_failedmeasurements_current
302+
.mockResolvedValueOnce([{ cnt: 0 }]) // 8. SELECT COUNT(*) ready
303+
.mockResolvedValueOnce([{ remaining: 0 }]); // 9. Count remaining
284304

285305
const req = makeRequest('GET');
286306
const res = await GET(req, makeParams());
287307

288308
expect(res.status).toBe(200);
289-
290-
// Verify reviewfailed was called to update failure reasons
291-
const reviewFailedCall = mockConnectionManager.executeQuery.mock.calls.find((call: any) => call[0]?.includes('reviewfailed'));
292-
293-
expect(reviewFailedCall).toBeDefined();
294309
});
295310

296311
it('should handle rows with codes correctly in bulk ingestion', async () => {
297312
// This test verifies the complete flow:
298313
// failedmeasurements (with Codes) → temporarymeasurements → bulkingestionprocess → cmattributes
299314

300315
mockConnectionManager.executeQuery
301-
.mockResolvedValueOnce([{ total: 1 }]) // Count
302-
.mockResolvedValueOnce(undefined) // DELETE temp
303-
.mockResolvedValueOnce(undefined) // INSERT temp
304-
.mockResolvedValueOnce(undefined) // DELETE failed
305-
.mockResolvedValueOnce(undefined) // bulkingestionprocess
306-
.mockResolvedValueOnce([{ remaining: 0 }]) // Count remaining - all succeeded
307-
.mockResolvedValueOnce(undefined); // reviewfailed
316+
.mockResolvedValueOnce([{ total: 1 }]) // 1. COUNT(*)
317+
.mockResolvedValueOnce(mockFailedIds(1)) // 2. SELECT FailedMeasurementID
318+
.mockResolvedValueOnce(undefined) // 3. DELETE temporarymeasurements
319+
.mockResolvedValueOnce(undefined) // 4. INSERT INTO temporarymeasurements
320+
.mockResolvedValueOnce(undefined) // 5. CALL bulkingestionprocess
321+
.mockResolvedValueOnce(undefined) // 6. DELETE failedmeasurements by IDs
322+
.mockResolvedValueOnce(undefined) // 7. CALL refresh_failedmeasurements_current
323+
.mockResolvedValueOnce([{ cnt: 0 }]) // 8. SELECT COUNT(*) ready
324+
.mockResolvedValueOnce([{ remaining: 0 }]); // 9. All succeeded
308325

309326
const req = makeRequest('GET');
310327
const res = await GET(req, makeParams());

0 commit comments

Comments
 (0)