import { db } from '../db/client.js'; import type { BirdGender, BirdRow, PendingBirdTransferRow, VetVisitRow, WeightRow } from '../types.js'; const birdSelectFields = ` birds.id, birds.workspace_id, birds.name, birds.tag_id, birds.species, birds.gender, birds.date_of_birth::text, birds.gotcha_day::text, birds.chart_color, birds.photo_data_url, birds.notify_on_dob, birds.notify_on_gotcha_day, birds.created_at, latest.weight_grams AS latest_weight_grams, latest.recorded_on::text AS latest_recorded_on `; export const getBirdById = async (birdId: string, workspaceId: number) => { const result = await db.query( `SELECT ${birdSelectFields} FROM birds LEFT JOIN LATERAL ( SELECT weight_grams, recorded_on FROM weight_records WHERE weight_records.bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) latest ON TRUE WHERE birds.id = $1 AND birds.workspace_id = $2`, [birdId, workspaceId], ); return result.rows[0] ?? null; }; export const listBirds = async (workspaceId: number) => { const result = await db.query( `SELECT ${birdSelectFields} FROM birds LEFT JOIN LATERAL ( SELECT weight_grams, recorded_on FROM weight_records WHERE weight_records.bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) latest ON TRUE WHERE birds.workspace_id = $1 ORDER BY birds.name ASC`, [workspaceId], ); return result.rows; }; export const createBird = async ({ workspaceId, name, tagId, species, gender, dateOfBirth, gotchaDay, chartColor, photoDataUrl, notifyOnDob, notifyOnGotchaDay, }: { workspaceId: number; name: string; tagId: string; species: string; gender: BirdGender; dateOfBirth: string | null; gotchaDay: string | null; chartColor: string; photoDataUrl: string | null; notifyOnDob: boolean; notifyOnGotchaDay: boolean; }) => { const result = await db.query( `INSERT INTO birds (workspace_id, name, tag_id, species, gender, date_of_birth, gotcha_day, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`, [workspaceId, name, tagId, species, gender, dateOfBirth, gotchaDay, chartColor, photoDataUrl, notifyOnDob, notifyOnGotchaDay], ); return result.rows[0] ?? null; }; export const updateBird = async ({ birdId, workspaceId, name, tagId, species, gender, dateOfBirth, gotchaDay, chartColor, photoDataUrl, notifyOnDob, notifyOnGotchaDay, }: { birdId: string; workspaceId: number; name: string; tagId: string; species: string; gender: BirdGender; dateOfBirth: string | null; gotchaDay: string | null; chartColor: string; photoDataUrl: string | null; notifyOnDob: boolean; notifyOnGotchaDay: boolean; }) => { const result = await db.query( `UPDATE birds SET name = $2, tag_id = $3, species = $4, gender = $5, date_of_birth = $6, gotcha_day = $7, chart_color = $8, photo_data_url = $9, notify_on_dob = $10, notify_on_gotcha_day = $11 WHERE id = $1 AND workspace_id = $12 RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, created_at, ( SELECT weight_grams::text FROM weight_records WHERE bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) AS latest_weight_grams, ( SELECT recorded_on::text FROM weight_records WHERE bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) AS latest_recorded_on`, [birdId, name, tagId, species, gender, dateOfBirth, gotchaDay, chartColor, photoDataUrl, notifyOnDob, notifyOnGotchaDay, workspaceId], ); return result.rows[0] ?? null; }; export const deleteBird = async (birdId: string, workspaceId: number) => { const result = await db.query<{ id: string }>( `DELETE FROM birds WHERE id = $1 AND workspace_id = $2 RETURNING id`, [birdId, workspaceId], ); return Boolean(result.rowCount); }; export const transferBirdToWorkspace = async (birdId: string, sourceWorkspaceId: number, targetWorkspaceId: number) => { const result = await db.query( `UPDATE birds SET workspace_id = $3 WHERE id = $1 AND workspace_id = $2 RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, created_at, ( SELECT weight_grams::text FROM weight_records WHERE bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) AS latest_weight_grams, ( SELECT recorded_on::text FROM weight_records WHERE bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) AS latest_recorded_on`, [birdId, sourceWorkspaceId, targetWorkspaceId], ); return result.rows[0] ?? null; }; export const createPendingBirdTransfer = async ({ birdId, sourceWorkspaceId, destinationOwnerEmail, requestedByUserId, }: { birdId: string; sourceWorkspaceId: number; destinationOwnerEmail: string; requestedByUserId: string; }) => { const result = await db.query( `INSERT INTO pending_bird_transfers (bird_id, source_workspace_id, destination_owner_email, requested_by_user_id) VALUES ($1, $2, $3, $4) ON CONFLICT (bird_id) WHERE completed_at IS NULL DO UPDATE SET destination_owner_email = EXCLUDED.destination_owner_email, requested_by_user_id = EXCLUDED.requested_by_user_id, last_error = NULL, created_at = CURRENT_TIMESTAMP RETURNING id, bird_id, source_workspace_id, destination_owner_email, requested_by_user_id, completed_at::text, completed_workspace_id, last_error, created_at`, [birdId, sourceWorkspaceId, destinationOwnerEmail, requestedByUserId], ); return result.rows[0] ?? null; }; export const listPendingBirdTransfersForOwnerEmail = async (ownerEmail: string) => { const result = await db.query( `SELECT id, bird_id, source_workspace_id, destination_owner_email, requested_by_user_id, completed_at::text, completed_workspace_id, last_error, created_at FROM pending_bird_transfers WHERE LOWER(destination_owner_email) = LOWER($1) AND completed_at IS NULL ORDER BY created_at ASC`, [ownerEmail], ); return result.rows; }; export const markPendingBirdTransferCompleted = async (transferId: string, completedWorkspaceId: number) => { await db.query( `UPDATE pending_bird_transfers SET completed_at = CURRENT_TIMESTAMP, completed_workspace_id = $2, last_error = NULL WHERE id = $1`, [transferId, completedWorkspaceId], ); }; export const markPendingBirdTransferFailed = async (transferId: string, lastError: string) => { await db.query( `UPDATE pending_bird_transfers SET last_error = $2 WHERE id = $1`, [transferId, lastError], ); }; export const completePendingBirdTransfersForOwner = async (ownerEmail: string, targetWorkspaceId: number) => { const transfers = await listPendingBirdTransfersForOwnerEmail(ownerEmail); let completed = 0; let failed = 0; for (const transfer of transfers) { try { const bird = await transferBirdToWorkspace(transfer.bird_id, transfer.source_workspace_id, targetWorkspaceId); if (!bird) { failed += 1; await markPendingBirdTransferFailed(transfer.id, 'Bird is no longer available in the source flock.'); continue; } await markPendingBirdTransferCompleted(transfer.id, targetWorkspaceId); completed += 1; } catch (error) { failed += 1; const message = typeof error === 'object' && error && 'code' in error && error.code === '23505' ? 'The receiving flock already has a bird using the same band/tag ID.' : error instanceof Error ? error.message : 'Unable to complete pending bird transfer.'; await markPendingBirdTransferFailed(transfer.id, message); } } return { completed, failed }; }; export const listWeightsForBird = async (birdId: string, workspaceId: number, days: number) => { const result = await db.query( `SELECT id, bird_id, weight_grams, recorded_on::text, notes FROM weight_records WHERE bird_id = $1 AND EXISTS ( SELECT 1 FROM birds WHERE birds.id = weight_records.bird_id AND birds.workspace_id = $3 ) AND recorded_on >= CURRENT_DATE - (($2::int - 1) * INTERVAL '1 day') ORDER BY recorded_on ASC`, [birdId, days, workspaceId], ); return result.rows; }; export const createWeightForBird = async (birdId: string, weightGrams: number, recordedOn: string, notes: string | null) => { const result = await db.query( `INSERT INTO weight_records (bird_id, weight_grams, recorded_on, notes) VALUES ($1, $2, $3, $4) RETURNING id, bird_id, weight_grams, recorded_on::text, notes`, [birdId, weightGrams, recordedOn, notes], ); return result.rows[0] ?? null; }; export const listVetVisitsForBird = async (birdId: string, workspaceId: number) => { const result = await db.query( `SELECT id, bird_id, visited_on::text, clinic_name, reason, notes FROM vet_visits WHERE bird_id = $1 AND EXISTS ( SELECT 1 FROM birds WHERE birds.id = vet_visits.bird_id AND birds.workspace_id = $2 ) ORDER BY visited_on DESC, created_at DESC`, [birdId, workspaceId], ); return result.rows; }; export const createVetVisitForBird = async (birdId: string, visitedOn: string, clinicName: string, reason: string, notes: string | null) => { const result = await db.query( `INSERT INTO vet_visits (bird_id, visited_on, clinic_name, reason, notes) VALUES ($1, $2, $3, $4, $5) RETURNING id, bird_id, visited_on::text, clinic_name, reason, notes`, [birdId, visitedOn, clinicName, reason, notes], ); return result.rows[0] ?? null; }; export const updateVetVisitForBird = async ( visitId: string, birdId: string, visitedOn: string, clinicName: string, reason: string, notes: string | null, ) => { const result = await db.query( `UPDATE vet_visits SET visited_on = $3, clinic_name = $4, reason = $5, notes = $6 WHERE id = $1 AND bird_id = $2 RETURNING id, bird_id, visited_on::text, clinic_name, reason, notes`, [visitId, birdId, visitedOn, clinicName, reason, notes], ); return result.rows[0] ?? null; }; export const deleteVetVisitForBird = async (visitId: string, birdId: string) => { const result = await db.query<{ id: string }>( `DELETE FROM vet_visits WHERE id = $1 AND bird_id = $2 RETURNING id`, [visitId, birdId], ); return (result.rowCount ?? 0) > 0; };