@@ -998,3 +998,202 @@ func (s *LineEngineTestSuite) TestOnPaymentSettledFailureTransitionsToRetryableP
998998 s .Equal (ombilling .StandardInvoiceStatusPaid , invoice .Status )
999999 })
10001000}
1001+
1002+ func (s * LineEngineTestSuite ) TestOnPaymentSettledIsCalledAfterAuthorization () {
1003+ var (
1004+ ctx = s .T ().Context ()
1005+ namespace = s .GetUniqueNamespace ("ns-line-engine-on-payment-settled-after-authorization" )
1006+ mockEngine = & mockCollectionCompletedLineEngine {engineType : ombilling .LineEngineTypeChargeCreditPurchase }
1007+ customInvoicingApp = s .SetupCustomInvoicing (namespace ).App
1008+ invoice ombilling.StandardInvoice
1009+ collectionAt time.Time
1010+ onPaymentSettledCnt int
1011+ )
1012+
1013+ clockBase := lo .Must (time .Parse (time .RFC3339 , "2024-09-02T12:13:14Z" ))
1014+ clock .SetTime (clockBase )
1015+ defer clock .ResetTime ()
1016+ defer func () { _ = s .MeterAdapter .ReplaceMeters (ctx , []meter.Meter {}) }()
1017+ defer s .MockStreamingConnector .Reset ()
1018+ s .registerMockLineEngine (s .T (), mockEngine )
1019+ defer s .unregisterLineEngine (s .T (), mockEngine )
1020+
1021+ s .Run ("Given a draft invoice waiting for collection with a payment-settled hook after authorization" , func () {
1022+ defer mockEngine .Reset ()
1023+
1024+ mockEngine .buildStandardInvoiceLines = func (_ context.Context , input ombilling.BuildStandardInvoiceLinesInput ) (ombilling.StandardLines , error ) {
1025+ return mustAsNewStandardLines (input ), nil
1026+ }
1027+
1028+ invoice , collectionAt = s .createMeteredDraftInvoiceWaitingForCollectionForApp (
1029+ ctx ,
1030+ namespace ,
1031+ customInvoicingApp .GetID (),
1032+ mockEngine .GetLineEngineType (),
1033+ "UBP - payment settled hook after authorization" ,
1034+ )
1035+ })
1036+
1037+ s .Run ("When the invoice is collected, issued, marked authorized, and then marked paid" , func () {
1038+ defer mockEngine .Reset ()
1039+
1040+ mockEngine .onCollectionCompleted = func (_ context.Context , input ombilling.OnCollectionCompletedInput ) (ombilling.StandardLines , error ) {
1041+ lines := input .Lines
1042+ for _ , stdLine := range lines {
1043+ if stdLine .UsageBased == nil {
1044+ stdLine .UsageBased = & ombilling.UsageBasedLine {}
1045+ }
1046+
1047+ stdLine .UsageBased .Quantity = lo .ToPtr (alpacadecimal .NewFromInt (7 ))
1048+ stdLine .UsageBased .MeteredQuantity = lo .ToPtr (alpacadecimal .NewFromInt (7 ))
1049+ stdLine .UsageBased .PreLinePeriodQuantity = lo .ToPtr (alpacadecimal .Zero )
1050+ stdLine .UsageBased .MeteredPreLinePeriodQuantity = lo .ToPtr (alpacadecimal .Zero )
1051+ }
1052+
1053+ return lines , nil
1054+ }
1055+
1056+ mockEngine .onInvoiceIssued = func (_ context.Context , input ombilling.OnInvoiceIssuedInput ) error {
1057+ s .Equal (invoice .ID , input .Invoice .ID )
1058+ return nil
1059+ }
1060+
1061+ mockEngine .onPaymentSettled = func (_ context.Context , input ombilling.OnPaymentSettledInput ) error {
1062+ onPaymentSettledCnt ++
1063+ s .Equal (ombilling .StandardInvoiceStatusPaymentProcessingBookingSettled , input .Invoice .Status )
1064+ s .Equal (invoice .ID , input .Invoice .ID )
1065+ s .Len (input .Lines , 1 )
1066+ s .Equal (invoice .ID , input .Lines [0 ].InvoiceID )
1067+ s .Equal (mockEngine .GetLineEngineType (), input .Lines [0 ].Engine )
1068+ return nil
1069+ }
1070+
1071+ clock .SetTime (collectionAt .Add (time .Minute ))
1072+
1073+ var err error
1074+ invoice , err = s .BillingService .AdvanceInvoice (ctx , invoice .GetInvoiceID ())
1075+ s .Require ().NoError (err )
1076+ s .Equal (ombilling .StandardInvoiceStatusDraftWaitingAutoApproval , invoice .Status )
1077+
1078+ invoice , err = s .BillingService .ApproveInvoice (ctx , invoice .GetInvoiceID ())
1079+ s .Require ().NoError (err )
1080+ s .Equal (ombilling .StandardInvoiceStatusPaymentProcessingPending , invoice .Status )
1081+
1082+ invoice = s .markInvoiceAuthorized (ctx , invoice .GetInvoiceID ())
1083+ s .Equal (ombilling .StandardInvoiceStatusPaymentProcessingAuthorized , invoice .Status )
1084+
1085+ invoice = s .markInvoicePaid (ctx , invoice .GetInvoiceID ())
1086+ s .Equal (ombilling .StandardInvoiceStatusPaid , invoice .Status )
1087+ })
1088+
1089+ s .Run ("Then the payment-settled hook is called once" , func () {
1090+ s .Equal (1 , onPaymentSettledCnt )
1091+ s .Equal (ombilling .StandardInvoiceStatusPaid , invoice .Status )
1092+ })
1093+ }
1094+
1095+ func (s * LineEngineTestSuite ) TestOnPaymentSettledFailureAfterAuthorizationTransitionsToRetryablePaymentState () {
1096+ var (
1097+ ctx = s .T ().Context ()
1098+ namespace = s .GetUniqueNamespace ("ns-line-engine-on-payment-settled-failed-after-authorization" )
1099+ mockEngine = & mockCollectionCompletedLineEngine {engineType : ombilling .LineEngineTypeChargeCreditPurchase }
1100+ customInvoicingApp = s .SetupCustomInvoicing (namespace ).App
1101+ invoice ombilling.StandardInvoice
1102+ collectionAt time.Time
1103+ onPaymentSettledCnt int
1104+ )
1105+
1106+ clockBase := lo .Must (time .Parse (time .RFC3339 , "2024-09-02T12:13:14Z" ))
1107+ clock .SetTime (clockBase )
1108+ defer clock .ResetTime ()
1109+ defer func () { _ = s .MeterAdapter .ReplaceMeters (ctx , []meter.Meter {}) }()
1110+ defer s .MockStreamingConnector .Reset ()
1111+ s .registerMockLineEngine (s .T (), mockEngine )
1112+ defer s .unregisterLineEngine (s .T (), mockEngine )
1113+
1114+ s .Run ("Given a draft invoice waiting for collection with a failing payment-settled hook after authorization" , func () {
1115+ defer mockEngine .Reset ()
1116+
1117+ mockEngine .buildStandardInvoiceLines = func (_ context.Context , input ombilling.BuildStandardInvoiceLinesInput ) (ombilling.StandardLines , error ) {
1118+ return mustAsNewStandardLines (input ), nil
1119+ }
1120+
1121+ invoice , collectionAt = s .createMeteredDraftInvoiceWaitingForCollectionForApp (
1122+ ctx ,
1123+ namespace ,
1124+ customInvoicingApp .GetID (),
1125+ mockEngine .GetLineEngineType (),
1126+ "UBP - payment settled hook failed after authorization" ,
1127+ )
1128+ })
1129+
1130+ s .Run ("When the invoice is collected, issued, marked authorized, and payment settlement hits the failing hook" , func () {
1131+ defer mockEngine .Reset ()
1132+
1133+ mockEngine .onCollectionCompleted = func (_ context.Context , input ombilling.OnCollectionCompletedInput ) (ombilling.StandardLines , error ) {
1134+ lines := input .Lines
1135+ for _ , stdLine := range lines {
1136+ if stdLine .UsageBased == nil {
1137+ stdLine .UsageBased = & ombilling.UsageBasedLine {}
1138+ }
1139+
1140+ stdLine .UsageBased .Quantity = lo .ToPtr (alpacadecimal .NewFromInt (7 ))
1141+ stdLine .UsageBased .MeteredQuantity = lo .ToPtr (alpacadecimal .NewFromInt (7 ))
1142+ stdLine .UsageBased .PreLinePeriodQuantity = lo .ToPtr (alpacadecimal .Zero )
1143+ stdLine .UsageBased .MeteredPreLinePeriodQuantity = lo .ToPtr (alpacadecimal .Zero )
1144+ }
1145+
1146+ return lines , nil
1147+ }
1148+
1149+ mockEngine .onInvoiceIssued = func (_ context.Context , input ombilling.OnInvoiceIssuedInput ) error {
1150+ s .Equal (invoice .ID , input .Invoice .ID )
1151+ return nil
1152+ }
1153+
1154+ mockEngine .onPaymentSettled = func (_ context.Context , input ombilling.OnPaymentSettledInput ) error {
1155+ onPaymentSettledCnt ++
1156+ s .Equal (ombilling .StandardInvoiceStatusPaymentProcessingBookingSettled , input .Invoice .Status )
1157+ s .Equal (invoice .ID , input .Invoice .ID )
1158+ return errors .New ("simulated payment settled failure" )
1159+ }
1160+
1161+ clock .SetTime (collectionAt .Add (time .Minute ))
1162+
1163+ var err error
1164+ invoice , err = s .BillingService .AdvanceInvoice (ctx , invoice .GetInvoiceID ())
1165+ s .Require ().NoError (err )
1166+ s .Equal (ombilling .StandardInvoiceStatusDraftWaitingAutoApproval , invoice .Status )
1167+
1168+ invoice , err = s .BillingService .ApproveInvoice (ctx , invoice .GetInvoiceID ())
1169+ s .Require ().NoError (err )
1170+ s .Equal (ombilling .StandardInvoiceStatusPaymentProcessingPending , invoice .Status )
1171+
1172+ invoice = s .markInvoiceAuthorized (ctx , invoice .GetInvoiceID ())
1173+ s .Equal (ombilling .StandardInvoiceStatusPaymentProcessingAuthorized , invoice .Status )
1174+
1175+ invoice = s .markInvoicePaid (ctx , invoice .GetInvoiceID ())
1176+ s .Equal (ombilling .StandardInvoiceStatusPaymentProcessingBookingSettledFailed , invoice .Status )
1177+ s .True (invoice .StatusDetails .Failed )
1178+ s .NotNil (invoice .StatusDetails .AvailableActions .Retry )
1179+ s .NotEmpty (invoice .ValidationIssues )
1180+ s .Equal (ombilling .ValidationIssueSeverityCritical , invoice .ValidationIssues [0 ].Severity )
1181+ s .Equal ("simulated payment settled failure" , invoice .ValidationIssues [0 ].Message )
1182+ })
1183+
1184+ s .Run ("Then retry succeeds without restarting payment processing" , func () {
1185+ defer mockEngine .Reset ()
1186+
1187+ mockEngine .onPaymentSettled = func (_ context.Context , input ombilling.OnPaymentSettledInput ) error {
1188+ onPaymentSettledCnt ++
1189+ s .Equal (invoice .ID , input .Invoice .ID )
1190+ return nil
1191+ }
1192+
1193+ var err error
1194+ invoice , err = s .BillingService .RetryInvoice (ctx , invoice .GetInvoiceID ())
1195+ s .Require ().NoError (err )
1196+ s .Equal (2 , onPaymentSettledCnt )
1197+ s .Equal (ombilling .StandardInvoiceStatusPaid , invoice .Status )
1198+ })
1199+ }
0 commit comments