11import { renderToBuffer } from "@react-pdf/renderer" ;
22
33import { captureException , setUser } from "@sentry/node" ;
4- import { and , arrayOverlaps , eq , inArray } from "drizzle-orm" ;
4+ import { arrayOverlaps , eq } from "drizzle-orm" ;
55import { Hono } from "hono" ;
66import { accepts } from "hono/accepts" ;
77import { validator as vValidator } from "hono-openapi/valibot" ;
@@ -90,7 +90,7 @@ export default new Hono().get(
9090 columns : { account : true } ,
9191 with : {
9292 cards : {
93- columns : { } ,
93+ columns : { id : true , lastFour : true } ,
9494 with : { transactions : { columns : { hashes : true , payload : true } } } ,
9595 limit : ignore ( "card" ) || maturity !== undefined ? 0 : undefined ,
9696 } ,
@@ -262,29 +262,26 @@ export default new Hono().get(
262262 ] . map ( ( blockNumber ) => publicClient . getBlock ( { blockNumber } ) ) ,
263263 ) ;
264264 const timestamps = new Map ( blocks . map ( ( { number : block , timestamp } ) => [ block , timestamp ] ) ) ;
265- let statementCards : string [ ] = [ ] ;
266- let cardPurchases : typeof credential . cards ;
267- if ( ! ignore ( "card" ) && maturity !== undefined && borrows ) {
268- const hashes = borrows
269- . entries ( )
270- . filter ( ( [ _ , { events } ] ) => events . some ( ( { maturity : m } ) => Number ( m ) === maturity ) )
271- . map ( ( [ hash ] ) => hash )
272- . toArray ( ) ;
273- const userCards = await database . query . cards
274- . findMany ( { columns : { id : true } , where : eq ( cards . credentialId , credentialId ) } )
275- . then ( ( rows ) => rows . map ( ( { id } ) => id ) ) ;
276- const statementTransactions =
277- hashes . length === 0 || userCards . length === 0
278- ? [ ]
279- : await database . query . transactions . findMany ( {
280- where : and ( arrayOverlaps ( transactions . hashes , hashes ) , inArray ( transactions . cardId , userCards ) ) ,
281- columns : { cardId : true , hashes : true , payload : true } ,
265+ const purchases =
266+ ! ignore ( "card" ) && borrows && maturity !== undefined
267+ ? await ( ( ) => {
268+ const hashes = borrows
269+ . entries ( )
270+ . filter ( ( [ _ , { events } ] ) => events . some ( ( { maturity : m } ) => Number ( m ) === maturity ) )
271+ . map ( ( [ hash ] ) => hash )
272+ . toArray ( ) ;
273+ return database . query . cards . findMany ( {
274+ where : eq ( cards . credentialId , credentialId ) ,
275+ columns : { id : true , lastFour : true } ,
276+ with : {
277+ transactions : {
278+ columns : { hashes : true , payload : true } ,
279+ where : arrayOverlaps ( transactions . hashes , hashes ) ,
280+ } ,
281+ } ,
282282 } ) ;
283- statementCards = [ ...new Set ( statementTransactions . map ( ( { cardId } ) => cardId ) ) ] ;
284- cardPurchases = [ { transactions : statementTransactions } ] ;
285- } else {
286- cardPurchases = credential . cards ;
287- }
283+ } ) ( )
284+ : credential . cards ;
288285
289286 const accept = accepts ( c , {
290287 header : "Accept" ,
@@ -294,7 +291,7 @@ export default new Hono().get(
294291 const pdf = accept === "application/pdf" ;
295292
296293 const response = [
297- ...cardPurchases . flatMap ( ( { transactions : txs } ) =>
294+ ...purchases . flatMap ( ( { transactions : txs } ) =>
298295 txs . map ( ( { hashes, payload } ) => {
299296 const panda = safeParse ( PandaActivity , {
300297 ...( payload as object ) ,
@@ -423,20 +420,16 @@ export default new Hono().get(
423420 . toSorted ( ( a , b ) => b . timestamp . localeCompare ( a . timestamp ) || b . id . localeCompare ( a . id ) ) ;
424421
425422 if ( maturity !== undefined && pdf ) {
426- if ( statementCards . length > 1 ) return c . json ( { code : "multiple cards" } , 400 ) ;
427- const statementCurrency = market ( marketUSDCAddress ) . symbol ;
428- const card =
429- statementCards . length === 0
430- ? undefined
431- : await database . query . cards . findFirst ( {
432- columns : { lastFour : true } ,
433- where : and ( eq ( cards . credentialId , credentialId ) , inArray ( cards . id , statementCards ) ) ,
434- } ) ;
435- const statement = {
436- maturity,
437- lastFour : card ?. lastFour ?? "" ,
438- data : response . flatMap ( ( item ) : Parameters < typeof Statement > [ 0 ] [ "data" ] => {
423+ const cardLookup = new Map (
424+ purchases . flatMap ( ( { id, transactions : txs } ) =>
425+ txs . flatMap ( ( { hashes } ) => hashes . map ( ( hash ) => [ hash , id ] as const ) ) ,
426+ ) ,
427+ ) ;
428+ const purchasesByCard = Map . groupBy (
429+ response . flatMap ( ( item ) => {
439430 if ( item . type === "panda" ) {
431+ const cardId = item . operations [ 0 ] && cardLookup . get ( item . operations [ 0 ] . transactionHash ) ;
432+ if ( ! cardId ) return [ ] ;
440433 const installments = item . operations
441434 . reduce ( ( accumulator , operation ) => {
442435 if ( "borrow" in operation ) {
@@ -470,6 +463,7 @@ export default new Hono().get(
470463 if ( installments . length === 0 ) return [ ] ;
471464 return [
472465 {
466+ cardId,
473467 id : item . id ,
474468 timestamp : item . timestamp ,
475469 description : `${ item . merchant . name } ${ item . merchant . city ? `, ${ item . merchant . city } ` : "" } ` ,
@@ -478,6 +472,8 @@ export default new Hono().get(
478472 ] ;
479473 }
480474 if ( item . type === "card" && "borrow" in item ) {
475+ const cardId = cardLookup . get ( item . transactionHash ) ;
476+ if ( ! cardId ) return [ ] ;
481477 if ( "installments" in item . borrow ) {
482478 const events = borrows ?. get ( item . transactionHash ) ?. events ;
483479 if ( ! events ) return [ ] ;
@@ -490,6 +486,7 @@ export default new Hono().get(
490486 if ( installments . length === 0 ) return [ ] ;
491487 return [
492488 {
489+ cardId,
493490 id : item . id ,
494491 timestamp : item . timestamp ,
495492 description : `${ item . merchant . name } ${ item . merchant . city ? `, ${ item . merchant . city } ` : "" } ` ,
@@ -501,27 +498,28 @@ export default new Hono().get(
501498 if ( ! borrow || Number ( borrow . maturity ) !== maturity ) return [ ] ;
502499 return [
503500 {
501+ cardId,
504502 id : item . id ,
505503 timestamp : item . timestamp ,
506504 description : `${ item . merchant . name } ${ item . merchant . city ? `, ${ item . merchant . city } ` : "" } ` ,
507505 installments : [ { amount : Number ( borrow . assets + borrow . fee ) / 1e6 , current : 1 , total : 1 } ] ,
508506 } ,
509507 ] ;
510508 }
511- if ( item . type === "repay" ) {
512- if ( item . currency !== statementCurrency ) return [ ] ;
513- return [
514- {
515- id : item . id ,
516- timestamp : item . timestamp ,
517- currency : item . currency ,
518- positionAmount : item . positionAmount ,
519- amount : item . amount ,
520- } ,
521- ] ;
522- }
523509 return [ ] ;
524510 } ) ,
511+ ( { cardId } ) => cardId ,
512+ ) ;
513+ const statement = {
514+ account : `${ account . slice ( 0 , 6 ) } ...${ account . slice ( - 6 ) } ` ,
515+ maturity,
516+ cards : purchases . map ( ( { id, lastFour } ) => ( {
517+ lastFour,
518+ purchases : ( purchasesByCard . get ( id ) ?? [ ] ) . map ( ( { cardId : _ , ...rest } ) => rest ) ,
519+ } ) ) ,
520+ payments : response
521+ . filter ( ( { type, currency } ) => type === "repay" && currency === market ( marketUSDCAddress ) . symbol )
522+ . map ( ( { id, timestamp, amount } ) => ( { id, timestamp, amount } ) ) ,
525523 } ;
526524 return c . body ( new Uint8Array ( await renderToBuffer ( Statement ( statement ) ) ) , 200 , {
527525 "content-type" : "application/pdf" ,
0 commit comments