@@ -205,25 +205,127 @@ public async Task ReactivateSubscriptionAsync(string subscriptionId)
205205 #region Custom Portal Support
206206
207207 /// <inheritdoc />
208- public async Task < List < InvoiceInfo > > GetInvoicesAsync ( string customerId , int limit = 10 )
208+ public async Task < List < InvoiceInfo > > GetInvoicesAsync ( string subscriptionId , int limit = 10 )
209209 {
210- // PayPal's invoicing API is separate from subscriptions
211- // For now, return an empty list - invoices are managed through PayPal's dashboard
212- return new List < InvoiceInfo > ( ) ;
210+ if ( ! IsEnabled || string . IsNullOrEmpty ( subscriptionId ) )
211+ {
212+ return new List < InvoiceInfo > ( ) ;
213+ }
214+
215+ try
216+ {
217+ var accessToken = await GetAccessTokenAsync ( ) ;
218+
219+ // PayPal Subscriptions API - list transactions for a subscription
220+ // Time range: last year to now
221+ var startTime = DateTime . UtcNow . AddYears ( - 1 ) . ToString ( "yyyy-MM-ddTHH:mm:ssZ" ) ;
222+ var endTime = DateTime . UtcNow . ToString ( "yyyy-MM-ddTHH:mm:ssZ" ) ;
223+
224+ var request = new HttpRequestMessage ( HttpMethod . Get ,
225+ $ "{ PayPalConfig ! . ApiBaseUrl } /v1/billing/subscriptions/{ subscriptionId } /transactions?start_time={ startTime } &end_time={ endTime } ") ;
226+ request . Headers . Authorization = new AuthenticationHeaderValue ( "Bearer" , accessToken ) ;
227+
228+ var response = await _httpClient . SendAsync ( request ) ;
229+
230+ if ( ! response . IsSuccessStatusCode )
231+ {
232+ var errorBody = await response . Content . ReadAsStringAsync ( ) ;
233+ _logger . LogWarning ( "PayPal get transactions failed: {StatusCode} - {Body}" ,
234+ response . StatusCode , errorBody ) ;
235+ return new List < InvoiceInfo > ( ) ;
236+ }
237+
238+ var responseBody = await response . Content . ReadAsStringAsync ( ) ;
239+ var transactionsResponse = JsonSerializer . Deserialize < PayPalTransactionsResponse > ( responseBody , JsonOptions ) ;
240+
241+ if ( transactionsResponse ? . Transactions == null )
242+ {
243+ return new List < InvoiceInfo > ( ) ;
244+ }
245+
246+ return transactionsResponse . Transactions
247+ . Take ( limit )
248+ . Select ( t => new InvoiceInfo
249+ {
250+ InvoiceId = t . Id ?? "" ,
251+ InvoiceNumber = t . Id ,
252+ Status = MapTransactionStatus ( t . Status ) ,
253+ AmountTotal = ParsePayPalAmount ( t . AmountWithBreakdown ? . GrossAmount ? . Value ) ,
254+ AmountPaid = t . Status == "COMPLETED" ? ParsePayPalAmount ( t . AmountWithBreakdown ? . GrossAmount ? . Value ) : 0 ,
255+ Currency = t . AmountWithBreakdown ? . GrossAmount ? . CurrencyCode ? . ToLowerInvariant ( ) ?? "usd" ,
256+ CreatedAt = t . Time ?? DateTime . UtcNow ,
257+ PaidAt = t . Status == "COMPLETED" ? t . Time : null ,
258+ DueDate = null ,
259+ PdfUrl = null , // PayPal doesn't provide PDF receipts via API
260+ HostedUrl = $ "https://www.paypal.com/activity/payment/{ t . Id } ",
261+ Description = $ "Subscription payment",
262+ PeriodStart = null ,
263+ PeriodEnd = null
264+ } )
265+ . ToList ( ) ;
266+ }
267+ catch ( Exception ex )
268+ {
269+ _logger . LogWarning ( ex , "Failed to get PayPal transactions for subscription {SubscriptionId}" , subscriptionId ) ;
270+ return new List < InvoiceInfo > ( ) ;
271+ }
213272 }
214273
215274 /// <inheritdoc />
216- public Task < PaymentMethodInfo ? > GetPaymentMethodAsync ( string customerId )
275+ public async Task < PaymentMethodInfo ? > GetPaymentMethodAsync ( string subscriptionId )
217276 {
218- // PayPal doesn't expose payment method details in the same way
219- // Return a generic PayPal payment method
220- return Task . FromResult < PaymentMethodInfo ? > ( new PaymentMethodInfo
277+ if ( ! IsEnabled || string . IsNullOrEmpty ( subscriptionId ) )
221278 {
222- PaymentMethodId = "paypal" ,
223- Type = PaymentMethodType . PayPal ,
224- PayPalEmail = null , // Would need to query subscription details
225- IsDefault = true
226- } ) ;
279+ return null ;
280+ }
281+
282+ try
283+ {
284+ // Get subscription details to retrieve subscriber info including email
285+ var accessToken = await GetAccessTokenAsync ( ) ;
286+
287+ var request = new HttpRequestMessage ( HttpMethod . Get ,
288+ $ "{ PayPalConfig ! . ApiBaseUrl } /v1/billing/subscriptions/{ subscriptionId } ") ;
289+ request . Headers . Authorization = new AuthenticationHeaderValue ( "Bearer" , accessToken ) ;
290+
291+ var response = await _httpClient . SendAsync ( request ) ;
292+
293+ if ( ! response . IsSuccessStatusCode )
294+ {
295+ _logger . LogWarning ( "PayPal get subscription for payment method failed: {StatusCode}" ,
296+ response . StatusCode ) ;
297+ // Return generic PayPal info if we can't fetch details
298+ return new PaymentMethodInfo
299+ {
300+ PaymentMethodId = "paypal" ,
301+ Type = PaymentMethodType . PayPal ,
302+ PayPalEmail = null ,
303+ IsDefault = true
304+ } ;
305+ }
306+
307+ var responseBody = await response . Content . ReadAsStringAsync ( ) ;
308+ var subscription = JsonSerializer . Deserialize < PayPalSubscriptionResponse > ( responseBody , JsonOptions ) ;
309+
310+ return new PaymentMethodInfo
311+ {
312+ PaymentMethodId = subscription ? . Subscriber ? . PayerId ?? "paypal" ,
313+ Type = PaymentMethodType . PayPal ,
314+ PayPalEmail = subscription ? . Subscriber ? . EmailAddress ,
315+ IsDefault = true
316+ } ;
317+ }
318+ catch ( Exception ex )
319+ {
320+ _logger . LogWarning ( ex , "Failed to get PayPal payment method for subscription {SubscriptionId}" , subscriptionId ) ;
321+ return new PaymentMethodInfo
322+ {
323+ PaymentMethodId = "paypal" ,
324+ Type = PaymentMethodType . PayPal ,
325+ PayPalEmail = null ,
326+ IsDefault = true
327+ } ;
328+ }
227329 }
228330
229331 /// <inheritdoc />
@@ -233,6 +335,30 @@ public Task<string> GetUpdatePaymentMethodUrlAsync(string customerId, string ret
233335 return Task . FromResult ( "https://www.paypal.com/myaccount/autopay" ) ;
234336 }
235337
338+ private static InvoiceStatus MapTransactionStatus ( string ? status )
339+ {
340+ return status ? . ToUpperInvariant ( ) switch
341+ {
342+ "COMPLETED" => InvoiceStatus . Paid ,
343+ "PENDING" => InvoiceStatus . Open ,
344+ "DECLINED" => InvoiceStatus . Uncollectible ,
345+ "REFUNDED" => InvoiceStatus . Void ,
346+ "PARTIALLY_REFUNDED" => InvoiceStatus . Paid ,
347+ _ => InvoiceStatus . Open
348+ } ;
349+ }
350+
351+ private static long ParsePayPalAmount ( string ? value )
352+ {
353+ if ( string . IsNullOrEmpty ( value ) || ! decimal . TryParse ( value , out var amount ) )
354+ {
355+ return 0 ;
356+ }
357+ // PayPal returns amounts as decimal strings (e.g., "9.00")
358+ // Convert to cents for consistency with Stripe
359+ return ( long ) ( amount * 100 ) ;
360+ }
361+
236362 #endregion
237363
238364 #region Native Portal
@@ -564,5 +690,69 @@ private class PayPalWebhookResource
564690 public PayPalBillingInfo ? BillingInfo { get ; set ; }
565691 }
566692
693+ // Models for Transactions API response
694+ private class PayPalTransactionsResponse
695+ {
696+ [ JsonPropertyName ( "transactions" ) ]
697+ public List < PayPalTransaction > ? Transactions { get ; set ; }
698+
699+ [ JsonPropertyName ( "total_items" ) ]
700+ public int ? TotalItems { get ; set ; }
701+
702+ [ JsonPropertyName ( "total_pages" ) ]
703+ public int ? TotalPages { get ; set ; }
704+ }
705+
706+ private class PayPalTransaction
707+ {
708+ [ JsonPropertyName ( "id" ) ]
709+ public string ? Id { get ; set ; }
710+
711+ [ JsonPropertyName ( "status" ) ]
712+ public string ? Status { get ; set ; }
713+
714+ [ JsonPropertyName ( "payer_email" ) ]
715+ public string ? PayerEmail { get ; set ; }
716+
717+ [ JsonPropertyName ( "payer_name" ) ]
718+ public PayPalPayerName ? PayerName { get ; set ; }
719+
720+ [ JsonPropertyName ( "amount_with_breakdown" ) ]
721+ public PayPalAmountWithBreakdown ? AmountWithBreakdown { get ; set ; }
722+
723+ [ JsonPropertyName ( "time" ) ]
724+ public DateTime ? Time { get ; set ; }
725+ }
726+
727+ private class PayPalPayerName
728+ {
729+ [ JsonPropertyName ( "given_name" ) ]
730+ public string ? GivenName { get ; set ; }
731+
732+ [ JsonPropertyName ( "surname" ) ]
733+ public string ? Surname { get ; set ; }
734+ }
735+
736+ private class PayPalAmountWithBreakdown
737+ {
738+ [ JsonPropertyName ( "gross_amount" ) ]
739+ public PayPalMoney ? GrossAmount { get ; set ; }
740+
741+ [ JsonPropertyName ( "fee_amount" ) ]
742+ public PayPalMoney ? FeeAmount { get ; set ; }
743+
744+ [ JsonPropertyName ( "net_amount" ) ]
745+ public PayPalMoney ? NetAmount { get ; set ; }
746+ }
747+
748+ private class PayPalMoney
749+ {
750+ [ JsonPropertyName ( "currency_code" ) ]
751+ public string ? CurrencyCode { get ; set ; }
752+
753+ [ JsonPropertyName ( "value" ) ]
754+ public string ? Value { get ; set ; }
755+ }
756+
567757 #endregion
568758}
0 commit comments