Added adoption report and transfer code
This commit is contained in:
+121
-3
@@ -35,6 +35,7 @@ import {
|
||||
completePendingBirdTransfersForOwner,
|
||||
createBird,
|
||||
createBirdMilestoneReminderDelivery,
|
||||
createBirdTransferCode,
|
||||
createMedicationForBird,
|
||||
createPendingBirdTransfer,
|
||||
findBirdsByBandId,
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
deleteVetVisitForBird,
|
||||
getBirdById,
|
||||
getBirdByPublicProfileCode,
|
||||
getOpenBirdTransferCode,
|
||||
listBirds,
|
||||
listDueBirdMilestoneReminders,
|
||||
listMemorializedBirds,
|
||||
@@ -53,6 +55,7 @@ import {
|
||||
listVetVisitsForBird,
|
||||
listWeightsForBird,
|
||||
memorializeBird,
|
||||
markBirdTransferCodeCompleted,
|
||||
transferBirdToWorkspace,
|
||||
updateBird,
|
||||
updateMemorialReminderPreference,
|
||||
@@ -258,6 +261,7 @@ const lostBirdReportSchema = z.object({
|
||||
});
|
||||
|
||||
const publicProfileCodeSchema = z.string().trim().regex(/^[A-Za-z0-9_-]{8,32}$/);
|
||||
const birdTransferCodeSchema = z.string().trim().regex(/^[A-Za-z0-9_-]{12,32}$/);
|
||||
const birdProfileListSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -700,6 +704,8 @@ const normalizeBird = (row: BirdRow) => ({
|
||||
latestRecordedOn: row.latest_recorded_on,
|
||||
});
|
||||
|
||||
const createBirdTransferCodeValue = () => crypto.randomBytes(12).toString('base64url');
|
||||
|
||||
const normalizePublicBirdProfile = (row: BirdRow) => ({
|
||||
id: row.id,
|
||||
workspaceId: row.workspace_id,
|
||||
@@ -3314,7 +3320,7 @@ app.post('/api/birds', requireAuth, requireWriteAccess, requireWorkspaceRole(['o
|
||||
await deleteBirdPhotoObjectIfNeeded(uploadedObjectKeyToCleanup);
|
||||
|
||||
if (typeof error === 'object' && error && 'code' in error && error.code === '23505') {
|
||||
res.status(409).json({ error: 'That band/tag ID is already in use in this flock.' });
|
||||
res.status(409).json({ error: 'That band/tag ID is already in use in FlockPal.' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3396,7 +3402,7 @@ app.post('/api/birds/:birdId/transfer', requireAuth, requireWriteAccess, require
|
||||
res.json({ bird: normalizeBird(bird), destinationOwnerEmail, destinationWorkspace: normalizeWorkspace(targetWorkspace) });
|
||||
} catch (error) {
|
||||
if (typeof error === 'object' && error && 'code' in error && error.code === '23505') {
|
||||
res.status(409).json({ error: 'That band/tag ID is already in use in the destination flock.' });
|
||||
res.status(409).json({ error: 'That band/tag ID is already in use in FlockPal.' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3404,6 +3410,118 @@ app.post('/api/birds/:birdId/transfer', requireAuth, requireWriteAccess, require
|
||||
}
|
||||
});
|
||||
|
||||
app.post(
|
||||
'/api/birds/:birdId/transfer-code',
|
||||
requireAuth,
|
||||
requireWriteAccess,
|
||||
requireSessionAuth,
|
||||
requireWorkspaceRole(['owner', 'assistant']),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const sourceBird = await getBirdById(req.params.birdId, req.auth!.workspace.id);
|
||||
|
||||
if (!sourceBird) {
|
||||
res.status(404).json({ error: 'Bird not found.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensureBirdWritable(sourceBird, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let transferCode = null;
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
try {
|
||||
transferCode = await createBirdTransferCode({
|
||||
code: createBirdTransferCodeValue(),
|
||||
birdId: sourceBird.id,
|
||||
sourceWorkspaceId: req.auth!.workspace.id,
|
||||
requestedByUserId: req.auth!.user.id,
|
||||
});
|
||||
break;
|
||||
} catch (error) {
|
||||
if (typeof error === 'object' && error && 'code' in error && error.code === '23505' && attempt < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!transferCode) {
|
||||
throw new Error('Unable to create bird transfer code.');
|
||||
}
|
||||
|
||||
await writeAuditLog(req.auth!, 'bird.transfer_code_created', 'bird', sourceBird.id, sourceBird.name, {
|
||||
transferCodeId: transferCode.id,
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
transferCode: {
|
||||
code: transferCode.code,
|
||||
bird: normalizeBird(sourceBird),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.post(
|
||||
'/api/bird-transfer-codes/:code/accept',
|
||||
requireAuth,
|
||||
requireWriteAccess,
|
||||
requireSessionAuth,
|
||||
requireWorkspaceRole(['owner', 'assistant']),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const parsed = birdTransferCodeSchema.safeParse(req.params.code);
|
||||
|
||||
if (!parsed.success) {
|
||||
res.status(404).json({ error: 'Bird transfer code not found.' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const transferCode = await getOpenBirdTransferCode(parsed.data);
|
||||
|
||||
if (!transferCode) {
|
||||
res.status(404).json({ error: 'Bird transfer code not found or already used.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (transferCode.source_workspace_id === req.auth!.workspace.id) {
|
||||
res.status(409).json({ error: 'This bird is already in your active flock.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const bird = await transferBirdToWorkspace(transferCode.id, transferCode.source_workspace_id, req.auth!.workspace.id);
|
||||
|
||||
if (!bird) {
|
||||
res.status(404).json({ error: 'Bird is no longer available for transfer.' });
|
||||
return;
|
||||
}
|
||||
|
||||
await markBirdTransferCodeCompleted(transferCode.transfer_code_id, req.auth!.workspace.id);
|
||||
await writeAuditLog(req.auth!, 'bird.transfer_code_accepted', 'bird', bird.id, bird.name, {
|
||||
sourceWorkspaceId: transferCode.source_workspace_id,
|
||||
sourceWorkspaceName: transferCode.workspace_name,
|
||||
transferCodeId: transferCode.transfer_code_id,
|
||||
});
|
||||
|
||||
res.json({ bird: normalizeBird(bird), sourceWorkspaceName: transferCode.workspace_name, workspace: normalizeWorkspace(req.auth!.workspace) });
|
||||
} catch (error) {
|
||||
if (typeof error === 'object' && error && 'code' in error && error.code === '23505') {
|
||||
res.status(409).json({ error: 'That band/tag ID is already in use in FlockPal.' });
|
||||
return;
|
||||
}
|
||||
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceRole(['owner', 'assistant', 'caregiver']), async (req: Request, res: Response, next: NextFunction) => {
|
||||
const parsed = birdSchema.safeParse(req.body);
|
||||
|
||||
@@ -3477,7 +3595,7 @@ app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceR
|
||||
await deleteBirdPhotoObjectIfNeeded(uploadedObjectKeyToCleanup);
|
||||
|
||||
if (typeof error === 'object' && error && 'code' in error && error.code === '23505') {
|
||||
res.status(409).json({ error: 'That band/tag ID is already in use in this flock.' });
|
||||
res.status(409).json({ error: 'That band/tag ID is already in use in FlockPal.' });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user