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
53 changes: 52 additions & 1 deletion backend/controllers/verificationController.js
Original file line number Diff line number Diff line change
Expand Up @@ -628,20 +628,23 @@ const bulkIssueCredentials = async (req, res) => {
}

const adminId = req.user.id;
const dryRun = req.query.dryRun === 'true' || req.body?.dryRun === true;
const job = await BulkVerificationJob.create({
status: 'pending',
mode: dryRun ? 'dry-run' : 'bulk',
totalRows: normalizedRecords.length,
actorId: adminId,
});

// Background processing (service re-validates again)
bulkVerificationService.processJob(job._id, normalizedRecords, adminId).catch((err) => {
bulkVerificationService.processJob(job._id, normalizedRecords, adminId, { dryRun }).catch((err) => {
console.error(`Error processing bulk verification job ${job._id}:`, err);
});

res.status(202).json(apiResponse.successResponse({
jobId: job._id,
status: job.status,
mode: job.mode,
totalRows: job.totalRows,
}, 'Bulk verification job initiated successfully'));
return;
Expand Down Expand Up @@ -677,7 +680,52 @@ const getBulkJobStatus = async (req, res) => {
}
};

const retryBulkFailedRows = async (req, res) => {
try {
const { jobId } = req.params;
if (!mongoose.Types.ObjectId.isValid(jobId)) {
return res.status(400).json(apiResponse.validationErrorResponse(['Invalid job ID format']));
}

const job = await BulkVerificationJob.findById(jobId);
if (!job) {
return res.status(404).json(apiResponse.errorResponse('Bulk verification job not found', 404));
}

if (job.status !== 'completed') {
return res.status(400).json(apiResponse.errorResponse('Bulk verification job must be completed before retry', 400));
}

const adminId = req.user.id;
const failedRows = Array.isArray(job.results) ? job.results.filter((r) => r?.status === 'failure') : [];

if (!failedRows.length) {
return res.status(400).json(apiResponse.errorResponse('No failed rows found for this job', 400));
}

const retryJob = await bulkVerificationService.retryJob(jobId, failedRows, adminId);
if (!retryJob) {
return res.status(400).json(apiResponse.errorResponse('Retry could not be created (missing originalInput)', 400));
}

return res.status(202).json(apiResponse.successResponse({
jobId: retryJob._id,
status: retryJob.status,
mode: retryJob.mode,
totalRows: retryJob.totalRows,
retriedFromJobId: job._id,
failedRowsCount: failedRows.length,
}, 'Bulk retry job initiated successfully'));
} catch (error) {
return handleServerError(res, error, {
code: 'BULK_RETRY_FAILED_ROWS_ERROR',
message: 'Failed to initiate bulk retry of failed rows',
});
}
};

module.exports = {

generateLinkWalletChallenge,
generateIssueCredentialChallenge,
linkWallet,
Expand All @@ -691,4 +739,7 @@ module.exports = {
exportVerifiedUsers,
bulkIssueCredentials,
getBulkJobStatus,
retryBulkFailedRows,
};


18 changes: 17 additions & 1 deletion backend/models/BulkVerificationJob.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,32 @@ const bulkVerificationJobSchema = new mongoose.Schema({
type: Number,
default: 0,
},
mode: {
type: String,
enum: ['bulk', 'dry-run'],
default: 'bulk',
},
results: [{
rowNumber: { type: Number, required: true },
userId: { type: String, trim: true },
walletAddress: { type: String, lowercase: true, trim: true },
action: { type: String, trim: true },
idempotencyKey: { type: String, trim: true },
status: { type: String, enum: ['success', 'failure', 'skipped'], required: true },
status: {
type: String,
enum: ['success', 'failure', 'skipped', 'dry-run-success', 'dry-run-failure', 'dry-run-skipped'],
required: true,
},
error: { type: String },
details: { type: mongoose.Schema.Types.Mixed, default: {} },
// Minimal normalized record-like input captured during initial processing.
// Used for admin retry of failed rows without requiring CSV re-upload.
originalInput: {
type: mongoose.Schema.Types.Mixed,
default: {},
},
}],

actorId: {
type: String,
required: true,
Expand Down
4 changes: 4 additions & 0 deletions backend/routes/verification.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const {
exportVerifiedUsers,
bulkIssueCredentials,
getBulkJobStatus,
retryBulkFailedRows,
} = require('../controllers/verificationController');

const { protect, adminOnly, authorizeRoles } = require('../middleware/auth');
const {
challengeLinkWalletLimiter,
Expand Down Expand Up @@ -131,5 +133,7 @@ router.post(
);
router.get('/bulk/:jobId', protect, adminOnly, getBulkJobStatus);

router.post('/bulk/:jobId/retry-failed', protect, adminOnly, retryFailedBulkRows);

module.exports = router;

Loading
Loading