@@ -601,6 +601,157 @@ export class InvoicesService {
601601 return url ;
602602 }
603603
604+ // ── Public checkout data (no auth — customer-facing) ──────────────────────
605+
606+ async getPublicCheckoutData ( invoiceId : string ) {
607+ const invoice = await this . prisma . invoice . findUnique ( {
608+ where : { id : invoiceId } ,
609+ } ) ;
610+ if ( ! invoice ) throw new NotFoundException ( 'Invoice not found' ) ;
611+
612+ const merchant = await this . prisma . merchant . findUnique ( {
613+ where : { id : invoice . merchantId } ,
614+ select : {
615+ name : true ,
616+ companyName : true ,
617+ logoUrl : true ,
618+ brandColor : true ,
619+ email : true ,
620+ } ,
621+ } ) ;
622+
623+ return {
624+ id : invoice . id ,
625+ invoiceNumber : invoice . invoiceNumber ?? null ,
626+ status : invoice . status ,
627+ currency : invoice . currency ,
628+ total : invoice . total . toString ( ) ,
629+ amountPaid : invoice . amountPaid . toString ( ) ,
630+ subtotal : invoice . subtotal . toString ( ) ,
631+ taxAmount : invoice . taxAmount ?. toString ( ) ?? null ,
632+ discount : invoice . discount ?. toString ( ) ?? null ,
633+ dueDate : invoice . dueDate ?. toISOString ( ) ?? null ,
634+ paidAt : invoice . paidAt ?. toISOString ( ) ?? null ,
635+ notes : invoice . notes ?? null ,
636+ customerName : invoice . customerName ?? null ,
637+ lineItems : invoice . lineItems ,
638+ merchant : {
639+ name : merchant ?. companyName ?? merchant ?. name ?? 'Merchant' ,
640+ logo : merchant ?. logoUrl ?? null ,
641+ brandColor : merchant ?. brandColor ?? null ,
642+ email : merchant ?. email ?? null ,
643+ } ,
644+ } ;
645+ }
646+
647+ // ── Initiate payment (creates quote + payment, no auth) ────────────────────
648+
649+ async initiatePayment ( invoiceId : string ) : Promise < { paymentId : string } > {
650+ const invoice = await this . prisma . invoice . findUnique ( {
651+ where : { id : invoiceId } ,
652+ } ) ;
653+ if ( ! invoice ) throw new NotFoundException ( 'Invoice not found' ) ;
654+
655+ const payableStatuses : InvoiceStatus [ ] = [
656+ InvoiceStatus . SENT ,
657+ InvoiceStatus . VIEWED ,
658+ InvoiceStatus . PARTIALLY_PAID ,
659+ InvoiceStatus . OVERDUE ,
660+ ] ;
661+ if ( ! payableStatuses . includes ( invoice . status ) ) {
662+ throw new BadRequestException (
663+ `Invoice cannot be paid in status: ${ invoice . status } ` ,
664+ ) ;
665+ }
666+
667+ const merchant = await this . prisma . merchant . findUnique ( {
668+ where : { id : invoice . merchantId } ,
669+ select : {
670+ name : true ,
671+ companyName : true ,
672+ logoUrl : true ,
673+ settlementAsset : true ,
674+ settlementChain : true ,
675+ settlementAddress : true ,
676+ } ,
677+ } ) ;
678+ if ( ! merchant ) throw new NotFoundException ( 'Merchant not found' ) ;
679+
680+ const amountDue =
681+ toNumber ( invoice . total ) - toNumber ( invoice . amountPaid ) ;
682+
683+ if ( amountDue <= 0 ) {
684+ throw new BadRequestException ( 'Invoice balance is already settled' ) ;
685+ }
686+
687+ // TTL: use invoice due date if set, otherwise 7 days
688+ const expiresAt = invoice . dueDate
689+ ? new Date (
690+ Math . max (
691+ invoice . dueDate . getTime ( ) ,
692+ Date . now ( ) + 60 * 60 * 1000 , // at least 1 h from now
693+ ) ,
694+ )
695+ : new Date ( Date . now ( ) + 7 * 24 * 60 * 60 * 1000 ) ;
696+
697+ const settlementAsset = merchant . settlementAsset ?? 'USDC' ;
698+ const settlementChain = merchant . settlementChain ?? 'stellar' ;
699+ const feeBps = 0 ; // invoice amounts are pre-agreed, no additional fee
700+ const feeAmount = 0 ;
701+
702+ // Create a 1:1 quote — invoice amounts are already final fiat figures
703+ const quote = await this . prisma . quote . create ( {
704+ data : {
705+ fromChain : 'fiat' ,
706+ fromAsset : invoice . currency ,
707+ fromAmount : amountDue ,
708+ toChain : settlementChain ,
709+ toAsset : settlementAsset ,
710+ toAmount : amountDue ,
711+ rate : 1 ,
712+ feeBps,
713+ feeAmount,
714+ expiresAt,
715+ } ,
716+ } ) ;
717+
718+ const lineItems = Array . isArray ( invoice . lineItems )
719+ ? ( invoice . lineItems as Array < { description : string ; qty : number ; unitPrice : number ; amount : number } > ) . map ( ( li ) => ( {
720+ label : li . description ,
721+ amount : li . amount ,
722+ } ) )
723+ : [ ] ;
724+
725+ const payment = await this . prisma . payment . create ( {
726+ data : {
727+ merchantId : invoice . merchantId ,
728+ quoteId : quote . id ,
729+ status : 'PENDING' ,
730+ sourceChain : 'fiat' ,
731+ sourceAsset : invoice . currency ,
732+ sourceAmount : amountDue ,
733+ destChain : settlementChain ,
734+ destAsset : settlementAsset ,
735+ destAmount : amountDue ,
736+ destAddress : merchant . settlementAddress ?? 'pending' ,
737+ metadata : {
738+ invoiceId : invoice . id ,
739+ invoiceNumber : invoice . invoiceNumber ?? null ,
740+ description : `Invoice ${ invoice . invoiceNumber ?? invoice . id . slice ( 0 , 8 ) . toUpperCase ( ) } ` ,
741+ merchantLogo : merchant . logoUrl ?? null ,
742+ lineItems,
743+ paymentMethods : [ 'card' , 'bank' ] ,
744+ } ,
745+ } ,
746+ } ) ;
747+
748+ this . logger . log (
749+ `Invoice payment initiated: invoice=${ invoiceId } , payment=${ payment . id } ` ,
750+ ) ;
751+
752+ return { paymentId : payment . id } ;
753+ }
754+
604755 async getPdfBuffer ( id : string , merchantId : string ) : Promise < Buffer > {
605756 const invoice = await this . getById ( id , merchantId ) ;
606757
0 commit comments