Midday / packages /supabase /src /utils /account-matching.ts
Jules
Final deployment with all fixes and verified content
c09f67c
export type DbAccount = {
id: string;
account_reference: string | null;
type: string | null;
currency: string | null;
name: string | null;
};
export type ApiAccount = {
id: string;
resource_id: string | null;
type: string;
currency: string;
name: string;
};
export type MatchingResult = {
matched: number;
unmatched: number;
errors: number;
};
/**
* Finds the best matching DB account for an API account.
* Returns null if no match is found.
*
* The matching algorithm:
* 1. Filter by resource_id (account_reference) - must match exactly
* 2. Filter by type if available on both sides
* 3. Filter by currency if available on both sides
* 4. If multiple candidates remain, prefer exact name match
* 5. Each DB account can only be matched once (tracked via matchedDbIds)
*
* This prevents the issue where accounts with the same last_four digits
* would all get updated to the same account_id.
*/
export function findMatchingAccount(
apiAccount: ApiAccount,
existingAccounts: DbAccount[],
matchedDbIds: Set<string>,
): DbAccount | null {
if (!apiAccount.resource_id) {
return null;
}
// Find candidates that match on resource_id and haven't been matched yet
const candidates = existingAccounts.filter((dbAccount) => {
if (matchedDbIds.has(dbAccount.id)) return false;
if (dbAccount.account_reference !== apiAccount.resource_id) return false;
if (dbAccount.type && dbAccount.type !== apiAccount.type) return false;
if (dbAccount.currency && dbAccount.currency !== apiAccount.currency)
return false;
return true;
});
// If multiple candidates, prefer exact name match
const match = candidates.find(
(c) => c.name?.toLowerCase() === apiAccount.name?.toLowerCase(),
);
// Otherwise take the first candidate
return match ?? candidates[0] ?? null;
}