@@ -130,6 +130,15 @@ export const isValidIdempotencyKey = (k: unknown): k is string =>
130130
131131export type Payment = typeof payments . $inferSelect
132132
133+ /** Detecta violación de UNIQUE recorriendo la cadena de causas del error. */
134+ function isUniqueViolation ( e : unknown ) : boolean {
135+ for ( let err = e , depth = 0 ; err && depth < 5 ; err = ( err as { cause ?: unknown } ) . cause , depth ++ ) {
136+ const { message, code } = err as { message ?: string ; code ?: string }
137+ if ( / u n i q u e | c o n s t r a i n t / i. test ( message ?? '' ) || / C O N S T R A I N T / i. test ( code ?? '' ) ) return true
138+ }
139+ return false
140+ }
141+
133142/**
134143 * Crea el pago o devuelve el existente si la clave de idempotencia ya se usó.
135144 * A prueba de carreras: si dos requests simultáneos pasan el SELECT previo,
@@ -161,10 +170,9 @@ export async function createPaymentIdempotent(
161170 return { payment : row , replayed : false }
162171 } catch ( e ) {
163172 // Carrera perdida: otro request insertó la misma clave entre el SELECT y el INSERT.
164- // libsql reporta el UNIQUE en e.code (SQLITE_CONSTRAINT_UNIQUE), no siempre en el mensaje.
165- const msg = e instanceof Error ? e . message : ''
166- const code = ( e as { code ?: string } ) ?. code ?? ''
167- if ( / u n i q u e | c o n s t r a i n t / i. test ( msg ) || / C O N S T R A I N T / i. test ( code ) ) {
173+ // Drizzle envuelve el error de libsql (código SQLITE_CONSTRAINT_UNIQUE) en
174+ // DrizzleQueryError con la causa original en `cause`: hay que mirar toda la cadena.
175+ if ( isUniqueViolation ( e ) ) {
168176 const [ row ] = await db . select ( ) . from ( payments ) . where ( eq ( payments . idempotencyKey , input . idempotencyKey ) )
169177 if ( row ) return { payment : row , replayed : true }
170178 }
0 commit comments