@@ -19,21 +19,29 @@ module.exports = async function handler(req, res) {
1919 const claimId = typeof req . body ?. claimId === 'string' ? req . body . claimId . trim ( ) : '' ;
2020 if ( ! claimId ) return res . status ( 400 ) . json ( { ok : false , status : 'INVALID_CLAIM_ID' } ) ;
2121
22+ let transactionOpen = false ;
2223 try {
2324 const claimRows = db . normalizeRows ( await db . query ( 'select * from claim_requests where claim_id = $1 limit 1' , [ claimId ] ) ) ;
2425 if ( ! claimRows . length ) return res . status ( 404 ) . json ( { ok : false , status : 'CLAIM_NOT_FOUND' } ) ;
25- if ( claimRows [ 0 ] . status !== 'approved' && claimRows [ 0 ] . status !== 'cards_published' ) {
26- return res . status ( 409 ) . json ( { ok : false , status : 'INVALID_STATUS' , error : 'Claim must be approved before publishing cards.' } ) ;
26+
27+ const claim = claimRows [ 0 ] ;
28+ if ( claim . status !== 'approved' && claim . status !== 'cards_published' ) {
29+ return res . status ( 409 ) . json ( { ok : false , status : 'CLAIM_NOT_APPROVED' , error : 'Claim must be approved before publishing cards.' } ) ;
2730 }
2831
2932 const agents = db . normalizeRows ( await db . query ( 'select * from claim_agents where claim_id = $1 order by capability asc' , [ claimId ] ) ) ;
30- if ( ! agents . length ) return res . status ( 409 ) . json ( { ok : false , status : 'NO_AGENTS_FOR_CLAIM ' } ) ;
33+ if ( ! agents . length ) return res . status ( 409 ) . json ( { ok : false , status : 'AGENTS_NOT_FOUND ' } ) ;
3134
3235 const existing = db . normalizeRows ( await db . query ( 'select * from agent_cards where claim_id = $1 order by ens asc' , [ claimId ] ) ) ;
33- if ( existing . length ) {
34- return res . status ( 200 ) . json ( { ok : true , claimId, status : 'CARDS_ALREADY_PUBLISHED' , cards : existing . map ( ( r ) => ( { ens : r . ens , cardUrl : r . card_url } ) ) } ) ;
36+ const existingByEns = new Map ( existing . map ( ( row ) => [ String ( row . ens || '' ) . trim ( ) , row ] ) ) ;
37+
38+ if ( claim . status === 'cards_published' && isComplete ( agents , existingByEns ) ) {
39+ return res . status ( 200 ) . json ( { ok : true , claimId, status : 'CARDS_ALREADY_PUBLISHED' , cards : formatCards ( existing ) } ) ;
3540 }
3641
42+ await db . query ( 'begin' ) ;
43+ transactionOpen = true ;
44+
3745 const cards = [ ] ;
3846 for ( const agent of agents ) {
3947 const ens = String ( agent . ens || '' ) . trim ( ) ;
@@ -60,34 +68,84 @@ module.exports = async function handler(req, res) {
6068 `update claim_agents
6169 set card_url = $3,
6270 card_status = 'published',
63- card_published_at = now()
71+ card_published_at = coalesce(card_published_at, now() )
6472 where claim_id = $1 and id = $2` ,
6573 [ claimId , agent . id , cardUrl ]
6674 ) ;
6775 cards . push ( { ens, cardUrl } ) ;
6876 }
6977
70- await db . query ( "update claim_requests set status = 'cards_published' where claim_id = $1" , [ claimId ] ) ;
71- await db . query (
72- `insert into claim_events (claim_id, event_type, actor, message, event_json)
73- values ($1, 'agent_cards.published', 'admin', 'Agent cards published', $2::jsonb)` ,
74- [ claimId , JSON . stringify ( { count : cards . length , cards } ) ]
78+ const alreadyPublishedEvent = db . normalizeRows (
79+ await db . query ( "select id from claim_events where claim_id = $1 and event_type = 'agent_cards.published' limit 1" , [ claimId ] )
7580 ) ;
76- await db . query (
77- `insert into claim_status_transitions (claim_id, from_status, to_status, action, actor, reason, metadata_json)
78- values ($1, 'approved', 'cards_published', 'publish_agent_cards', 'admin', null, $2::jsonb)` ,
79- [ claimId , JSON . stringify ( { cardCount : cards . length } ) ]
81+ if ( ! alreadyPublishedEvent . length ) {
82+ await db . query (
83+ `insert into claim_events (claim_id, event_type, actor, message, event_json)
84+ values ($1, 'agent_cards.published', 'admin', 'Agent cards published', $2::jsonb)` ,
85+ [ claimId , JSON . stringify ( { count : cards . length , cards } ) ]
86+ ) ;
87+ }
88+
89+ const publishedTransition = db . normalizeRows (
90+ await db . query (
91+ `select id from claim_status_transitions
92+ where claim_id = $1 and from_status = 'approved' and to_status = 'cards_published' and action = 'publish_agent_cards'
93+ limit 1` ,
94+ [ claimId ]
95+ )
8096 ) ;
97+ if ( ! publishedTransition . length ) {
98+ await db . query (
99+ `insert into claim_status_transitions (claim_id, from_status, to_status, action, actor, reason, metadata_json)
100+ values ($1, 'approved', 'cards_published', 'publish_agent_cards', 'admin', null, $2::jsonb)` ,
101+ [ claimId , JSON . stringify ( { cardCount : cards . length } ) ]
102+ ) ;
103+ }
81104
82- return res . status ( 200 ) . json ( { ok : true , claimId, status : 'CARDS_PUBLISHED' , cards } ) ;
105+ if ( claim . status === 'approved' ) {
106+ await db . query ( "update claim_requests set status = 'cards_published' where claim_id = $1" , [ claimId ] ) ;
107+ }
108+
109+ await db . query ( 'commit' ) ;
110+ transactionOpen = false ;
111+
112+ return res . status ( 200 ) . json ( {
113+ ok : true ,
114+ claimId,
115+ status : claim . status === 'approved' ? 'CARDS_PUBLISHED' : 'AGENT_CARDS_REPAIRED' ,
116+ cards
117+ } ) ;
83118 } catch ( error ) {
119+ if ( transactionOpen ) {
120+ try { await db . query ( 'rollback' ) ; } catch ( _ ) { }
121+ }
122+
123+ const statusCode = error && error . statusCode ? error . statusCode : 500 ;
124+ const status = error && error . status ? error . status : ( statusCode === 500 ? 'ADMIN_PUBLISH_AGENT_CARDS_FAILED' : 'AGENT_CARD_PUBLISH_FAILED' ) ;
125+ const message = error && error . error ? error . error : 'Failed to publish agent cards.' ;
126+
84127 console . error ( 'ADMIN_PUBLISH_AGENT_CARDS_FAILED' , { message : error . message , code : error . code , claimId } ) ;
85- const payload = { ok : false , status : 'ADMIN_PUBLISH_AGENT_CARDS_FAILED' , error : 'Failed to publish agent cards.' } ;
128+
129+ const payload = { ok : false , status, error : message } ;
86130 if ( process . env . NODE_ENV !== 'production' ) payload . debug = { message : error . message , code : error . code } ;
87- return res . status ( 500 ) . json ( payload ) ;
131+ return res . status ( statusCode ) . json ( payload ) ;
88132 }
89133} ;
90134
135+ function isComplete ( agents , existingByEns ) {
136+ for ( const agent of agents ) {
137+ const ens = String ( agent . ens || '' ) . trim ( ) ;
138+ const row = existingByEns . get ( ens ) ;
139+ if ( ! row ) return false ;
140+ if ( ! agent . card_url || ! agent . card_status ) return false ;
141+ }
142+ return true ;
143+ }
144+
145+ function formatCards ( rows ) {
146+ return rows . map ( ( row ) => ( { ens : row . ens , cardUrl : row . card_url } ) ) ;
147+ }
148+
91149function buildCardJson ( agent , ens , capability ) {
92150 return {
93151 type : 'erc8004/registration/v1' ,
0 commit comments