11import {
2- JsonEncoder ,
2+ Hasher ,
33 JsonTransformer ,
44 Jwt ,
55 MdocDeviceResponse ,
@@ -20,6 +20,11 @@ import { agent } from './agent.js'
2020import { funkeDeployedAccessCertificate , funkeDeployedRegistrationCertificate } from './eudiTrust.js'
2121import { getIssuerIdForCredentialConfigurationId , type IssuanceMetadata } from './issuer.js'
2222import { issuers } from './issuers/index.js'
23+ import {
24+ updatePaymentStatusForWeroCredential ,
25+ weroScaConfiguration ,
26+ weroScaThirdPartyConfiguration ,
27+ } from './issuers/openHorizonBank.js'
2328import { getX509DcsCertificate , getX509RootCertificate } from './keyMethods/index.js'
2429import { oidcUrl } from './oidcProvider/provider.js'
2530import { LimitedSizeCollection } from './utils/LimitedSizeCollection.js'
@@ -52,6 +57,28 @@ const deferIntervalMapping: Record<z.infer<typeof zCreateOfferRequest>['deferBy'
5257
5358apiRouter . use ( express . json ( ) )
5459apiRouter . use ( express . text ( ) )
60+
61+ // Extracts paymentTransactionId (hash of the raw transaction_data entry, matching
62+ // what the wallet computes per the transaction_data spec) and the amount from the
63+ // authorization request JWT. Returns undefined if no transaction_data is present.
64+ function extractPaymentTransactionData (
65+ authorizationRequestJwt : string | undefined
66+ ) : { paymentTransactionId : string ; amount : number } | undefined {
67+ if ( ! authorizationRequestJwt ) return undefined
68+
69+ const transactionData = Jwt . fromSerializedJwt ( authorizationRequestJwt ) . payload . additionalClaims . transaction_data
70+ if ( ! Array . isArray ( transactionData ) || typeof transactionData [ 0 ] !== 'string' ) return undefined
71+
72+ const rawEntry = transactionData [ 0 ]
73+ const paymentTransactionId = TypedArrayEncoder . toBase64Url (
74+ Hasher . hash ( TypedArrayEncoder . fromBase64Url ( rawEntry ) , 'sha-256' )
75+ )
76+ const decoded = JSON . parse ( Buffer . from ( rawEntry , 'base64url' ) . toString ( ) ) as { payload ?: { amount ?: string } }
77+ const amount = parseFloat ( decoded . payload ?. amount ?? '0' )
78+
79+ return { paymentTransactionId, amount }
80+ }
81+
5582apiRouter . post ( '/offers/create' , async ( request : Request , response : Response ) => {
5683 const createOfferRequest = zCreateOfferRequest . parse ( request . body )
5784
@@ -174,6 +201,38 @@ apiRouter.get('/verifier', async (_, response: Response) => {
174201 } )
175202} )
176203
204+ apiRouter . post ( '/transaction-status' , async ( request : Request , response : Response ) => {
205+ const authHeader = request . headers . authorization
206+ const token = authHeader ?. startsWith ( 'Bearer ' ) ? authHeader . slice ( 7 ) : undefined
207+ if ( ! token ) {
208+ return response . sendStatus ( 401 )
209+ }
210+
211+ const parseResult = await z . object ( { transaction : z . string ( ) } ) . safeParseAsync ( request . body )
212+ if ( ! parseResult . success ) {
213+ agent . config . logger . warn ( 'transaction-status: missing transactionId in request body' )
214+ return response . sendStatus ( 401 )
215+ }
216+
217+ const { transaction } = parseResult . data
218+ agent . config . logger . info ( `transaction-status: looking up record for transaction ${ transaction } ` )
219+ const record = await agent . genericRecords . findById ( `transaction-status-${ transaction } ` )
220+
221+ if ( ! record || record . content . transaction_status_token !== token || ! ( 'statusCode' in record . content ) ) {
222+ agent . config . logger . warn (
223+ `transaction-status: unauthorized - record ${ record ? 'found but token mismatch or no statusCode' : 'not found' } `
224+ )
225+ return response . sendStatus ( 401 )
226+ }
227+
228+ agent . config . logger . info (
229+ `transaction-status: returning status ${ record . content . statusCode } for transaction ${ transaction } `
230+ )
231+ return response . json ( {
232+ status_code : record . content . statusCode ,
233+ } )
234+ } )
235+
177236// apiRouter.post('/trust-chains', async (request: Request, response: Response) => {
178237// const parseResult = await z
179238// .object({
@@ -228,7 +287,6 @@ apiRouter.post('/requests/create', async (request: Request, response: Response)
228287 redirectUriBase,
229288 } = await zCreatePresentationRequestBody . parseAsync ( request . body )
230289
231- const _x509RootCertificate = getX509RootCertificate ( )
232290 const x509DcsCertificate = getX509DcsCertificate ( )
233291
234292 // Funke access certificate uses same key as the dcs certificate
@@ -262,13 +320,32 @@ apiRouter.post('/requests/create', async (request: Request, response: Response)
262320 }
263321 }
264322
265- console . log ( ' Requesting definition' , JSON . stringify ( definition , null , 2 ) )
323+ agent . config . logger . debug ( ` Requesting definition ${ JSON . stringify ( definition , null , 2 ) } ` )
266324
267325 const queryLanguageDefinition = dcqlQueryFromRequest ( definition , purpose )
268326 const credentialIds = queryLanguageDefinition . credentials . map ( ( query ) => query . id )
269327
270328 const responseCode = randomUUID ( )
271329 const redirectUri = redirectUriBase ? `${ redirectUriBase } ?response_code=${ responseCode } ` : undefined
330+ const paymentTransactionEntry =
331+ transactionAuthorizationType === 'payment'
332+ ? {
333+ type : 'urn:eudi:sca:eu.europa.ec:payment:single:1' ,
334+ credential_ids : [ credentialIds [ credentialIds . length - 1 ] ] as [ string , ...string [ ] ] ,
335+ transaction_data_hashes_alg : [ 'sha-256' ] as [ string , ...string [ ] ] ,
336+ payload : {
337+ transaction_id : randomUUID ( ) ,
338+ amount : `${ paymentAmount } EUR` ,
339+ date_time : new Date ( ) . toISOString ( ) ,
340+ payee : {
341+ name : verifier . clientMetadata ?. client_name ?? 'TODO: NAME' ,
342+ id : verifierId ,
343+ logo : verifier . clientMetadata ?. logo_uri ?? 'TODO: logo' ,
344+ website : 'https://playground.animo.id' ,
345+ } ,
346+ } ,
347+ }
348+ : undefined
272349
273350 // When credential_sets is used we need to add the wero card to each group so that it will always be requested when also requesting a payment
274351 if ( transactionAuthorizationType === 'payment' ) {
@@ -322,26 +399,8 @@ apiRouter.post('/requests/create', async (request: Request, response: Response)
322399 ] ,
323400 } ,
324401 ]
325- : transactionAuthorizationType === 'payment'
326- ? [
327- {
328- type : 'urn:eudi:sca:eu.europa.ec:payment:single:1' ,
329- // We pick the last credential id as we push the wero card
330- credential_ids : [ credentialIds [ credentialIds . length - 1 ] ] as [ string , ...string [ ] ] ,
331- transaction_data_hashes_alg : [ 'sha-256' ] ,
332- payload : {
333- transaction_id : randomUUID ( ) ,
334- amount : `${ paymentAmount } EUR` ,
335- date_time : new Date ( ) . toISOString ( ) ,
336- payee : {
337- name : verifier . clientMetadata ?. client_name ?? 'TODO: NAME' ,
338- id : verifierId ,
339- logo : verifier . clientMetadata ?. logo_uri ?? 'TODO: logo' ,
340- website : 'https://playground.animo.id' ,
341- } ,
342- } ,
343- } ,
344- ]
402+ : transactionAuthorizationType === 'payment' && paymentTransactionEntry
403+ ? [ paymentTransactionEntry ]
345404 : undefined ,
346405 dcql : {
347406 query : queryLanguageDefinition ,
@@ -358,24 +417,30 @@ apiRouter.post('/requests/create', async (request: Request, response: Response)
358417 responseCodeMap . set ( responseCode , verificationSession . id )
359418 }
360419
420+ const paymentTransaction = extractPaymentTransactionData ( verificationSession . authorizationRequestJwt )
421+ if ( transactionAuthorizationType === 'payment' && paymentTransaction ) {
422+ agent . config . logger . info (
423+ `requests/create: saving PDNG payment record for paymentTransactionId ${ paymentTransaction . paymentTransactionId } `
424+ )
425+ await agent . genericRecords . save ( {
426+ id : `transaction-status-${ paymentTransaction . paymentTransactionId } ` ,
427+ content : { statusCode : 'PDNG' } ,
428+ } )
429+ }
430+
361431 const authorizationRequestJwt = verificationSession . authorizationRequestJwt
362432 ? Jwt . fromSerializedJwt ( verificationSession . authorizationRequestJwt )
363433 : undefined
364434 const authorizationRequestPayload = verificationSession . requestPayload
365435 const dcqlQuery = authorizationRequestPayload . dcql_query
366- const transactionData = authorizationRequestPayload . verifier_info ?. map ( ( e ) => ( {
367- ...e ,
368- data : typeof e . data === 'string' ? JsonEncoder . fromBase64 ( e . data ) : e . data ,
369- } ) )
370436
371- console . log ( JSON . stringify ( authorizationRequestObject , null , 2 ) )
437+ agent . config . logger . debug ( JSON . stringify ( authorizationRequestObject , null , 2 ) )
372438 return response . json ( {
373439 authorizationRequestObject,
374440 authorizationRequestUri : authorizationRequest . replace ( 'openid4vp://' , requestScheme ) ,
375441 verificationSessionId : verificationSession . id ,
376442 responseStatus : verificationSession . state ,
377443 dcqlQuery,
378- transactionData,
379444 authorizationRequest : authorizationRequestJwt
380445 ? {
381446 payload : authorizationRequestJwt . payload . toJson ( ) ,
@@ -402,14 +467,32 @@ async function getVerificationStatus(verificationSession: OpenId4VcVerificationS
402467 }
403468 const dcqlQuery = authorizationRequestPayload . dcql_query
404469
405- const transactionData = authorizationRequestPayload . verifier_info ?. map ( ( e ) => ( {
406- ...e ,
407- data : typeof e . data === 'string' ? JsonEncoder . fromBase64 ( e . data ) : e . data ,
408- } ) )
409-
410470 if ( verificationSession . state === OpenId4VcVerificationSessionState . ResponseVerified ) {
411471 const verified = await agent . openid4vc . verifier . getVerifiedAuthorizationResponse ( verificationSession . id )
412- console . log ( verified . dcql ?. presentationResult )
472+ agent . config . logger . debug ( JSON . stringify ( verified . dcql ?. presentationResult ) )
473+
474+ // Find the Wero SCA presentation to extract the credential jti, then signal the issuer to update payment status.
475+ // The issuer owns the transaction_status_token — the verifier only passes the jti and payment details.
476+ const weroVcts : string [ ] = [ weroScaConfiguration . vct , weroScaThirdPartyConfiguration . vct ]
477+ const presentationsList = Object . values ( verified . dcql ?. presentations ?? { } ) . flat ( ) as Array < {
478+ prettyClaims ?: { vct ?: string ; jti ?: string }
479+ } >
480+ const weroJti = presentationsList . find ( ( p ) => weroVcts . includes ( p . prettyClaims ?. vct ?? '' ) ) ?. prettyClaims ?. jti
481+
482+ const paymentTransaction = extractPaymentTransactionData ( verificationSession . authorizationRequestJwt )
483+ if ( weroJti && paymentTransaction ) {
484+ const { paymentTransactionId, amount } = paymentTransaction
485+ agent . config . logger . info (
486+ `getVerificationStatus: Wero credential jti ${ weroJti } , paymentTransactionId ${ paymentTransactionId } , amount ${ amount } `
487+ )
488+ await updatePaymentStatusForWeroCredential ( weroJti , paymentTransactionId , amount ) . catch ( ( error ) => {
489+ agent . config . logger . error ( `getVerificationStatus: payment update failed for ${ paymentTransactionId } ` , error )
490+ } )
491+ } else {
492+ agent . config . logger . debug (
493+ `getVerificationStatus: no payment update - weroJti ${ weroJti ?? 'not found' } , paymentTransaction ${ paymentTransaction ? 'found' : 'not found' } `
494+ )
495+ }
413496
414497 const presentations = await Promise . all (
415498 Object . values ( verified . dcql ?. presentations ?? { } )
@@ -497,7 +580,7 @@ async function getVerificationStatus(verificationSession: OpenId4VcVerificationS
497580 } ) )
498581 : undefined
499582
500- console . log ( ' presentations' , presentations )
583+ agent . config . logger . debug ( ` presentations ${ JSON . stringify ( presentations ) } ` )
501584
502585 return {
503586 verificationSessionId : verificationSession . id ,
@@ -520,7 +603,6 @@ async function getVerificationStatus(verificationSession: OpenId4VcVerificationS
520603 responseStatus : verificationSession . state ,
521604 error : verificationSession . errorMessage ,
522605 authorizationRequest,
523- transactionData,
524606 dcqlQuery,
525607 }
526608}
@@ -559,7 +641,7 @@ apiRouter.get('/requests/:verificationSessionId', async (request, response) => {
559641} )
560642
561643apiRouter . use ( ( error : Error , _request : Request , response : Response , _next : NextFunction ) => {
562- console . error ( ' Unhandled error' , error )
644+ agent . config . logger . error ( ` Unhandled error ${ error } ` )
563645 return response . status ( 500 ) . json ( {
564646 error : error . message ,
565647 } )
0 commit comments