@@ -664,6 +664,134 @@ describe('InvoiceModule', () => {
664664 ) ;
665665 expect ( result . status ) . toBe ( 'paid' ) ;
666666 } ) ;
667+
668+ it ( 'should not mark paid when on-chain period was already collected' , async ( ) => {
669+ const existing = DraftInvoice ( {
670+ status : 'open' ,
671+ amount_due : 1099 ,
672+ amount_remaining : 1099 ,
673+ parent : {
674+ type : 'subscription_details' ,
675+ subscription_details : {
676+ subscription : 'sub_z_1' ,
677+ metadata : null ,
678+ subscription_proration_date : null ,
679+ } ,
680+ quote_details : null ,
681+ } ,
682+ payments : {
683+ object : 'list' ,
684+ data : [
685+ {
686+ id : 'inpay_z_1' ,
687+ object : 'invoice_payment' ,
688+ amount_paid : null ,
689+ amount_requested : 1099 ,
690+ created : GetFixedTimestamp ( ) ,
691+ currency : 'usdc' ,
692+ invoice : 'in_z_test001' ,
693+ is_default : true ,
694+ livemode : false ,
695+ payment : {
696+ charge : null ,
697+ payment_intent : 'pi_z_1' ,
698+ payment_record : null ,
699+ type : 'payment_intent' ,
700+ } ,
701+ status : 'open' ,
702+ status_transitions : { canceled_at : null , paid_at : null } ,
703+ platform_account : PLATFORM ,
704+ } ,
705+ ] ,
706+ has_more : false ,
707+ total_count : 1 ,
708+ url : '/v1/invoices/in_z_test001/payments' ,
709+ } ,
710+ } ) ;
711+
712+ let invoiceState : Invoice = existing ;
713+
714+ const paymentIntentModule = {
715+ MarkSucceeded : jest . fn ( ) ,
716+ MarkPaymentFailed : jest . fn ( ) . mockResolvedValue ( { id : 'pi_z_1' } ) ,
717+ } ;
718+ const chargeModule = {
719+ CreateFromPaymentAttempt : jest . fn ( ) ,
720+ AttachBalanceTransaction : jest . fn ( ) ,
721+ } ;
722+ const solana = {
723+ CollectSubscriptionPayment : jest . fn ( ) . mockResolvedValue ( {
724+ signature : 'already_collected' ,
725+ alreadyCollected : true ,
726+ } ) ,
727+ } ;
728+
729+ module = new InvoiceModule (
730+ mockDb ,
731+ eventService ,
732+ customerModule ,
733+ invoiceItemModule ,
734+ paymentIntentModule as never ,
735+ chargeModule as never ,
736+ undefined ,
737+ solana as never
738+ ) ;
739+
740+ mockDb . Get = jest . fn ( ) . mockImplementation ( async ( collection : string ) => {
741+ if ( collection === 'Invoices' ) return invoiceState ;
742+ if ( collection === 'Subscriptions' ) {
743+ return {
744+ id : 'sub_z_1' ,
745+ platform_account : PLATFORM ,
746+ subscription_delegation_pda : 'SubPda_1' ,
747+ default_payment_method : 'Wallet111' ,
748+ metadata : { } ,
749+ } ;
750+ }
751+ if ( collection === 'Prices' ) {
752+ return {
753+ id : 'price_z_1' ,
754+ platform_account : PLATFORM ,
755+ subscription_plan_pda : 'PlanPda_1' ,
756+ } ;
757+ }
758+ return null ;
759+ } ) as typeof mockDb . Get ;
760+
761+ mockDb . Find = jest . fn ( ) . mockResolvedValue ( [
762+ {
763+ id : 'si_z_1' ,
764+ subscription : 'sub_z_1' ,
765+ price : 'price_z_1' ,
766+ } ,
767+ ] ) as typeof mockDb . Find ;
768+
769+ mockDb . Update = jest
770+ . fn ( )
771+ . mockImplementation (
772+ async (
773+ _collection : string ,
774+ _id : string ,
775+ updates : Partial < Invoice >
776+ ) => {
777+ invoiceState = { ...invoiceState , ...updates } ;
778+ return invoiceState ;
779+ }
780+ ) as typeof mockDb . Update ;
781+
782+ const result = await module . PayInvoice ( existing . id , { } ) ;
783+
784+ expect ( solana . CollectSubscriptionPayment ) . toHaveBeenCalled ( ) ;
785+ expect ( chargeModule . CreateFromPaymentAttempt ) . not . toHaveBeenCalled ( ) ;
786+ expect ( paymentIntentModule . MarkSucceeded ) . not . toHaveBeenCalled ( ) ;
787+ expect ( result . status ) . not . toBe ( 'paid' ) ;
788+ expect ( result . attempted ) . toBe ( true ) ;
789+ expect ( eventService . Emit ) . toHaveBeenCalledWith (
790+ 'invoice.payment_failed' ,
791+ PLATFORM ,
792+ expect . anything ( )
793+ ) ;
794+ } ) ;
667795 } ) ;
668796
669797 describe ( 'VoidInvoice' , ( ) => {
0 commit comments