From 1c38ddf6ad52c36fb63bddcaacd902ee04f86ed5 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 3 Mar 2026 08:55:40 -0800 Subject: [PATCH 01/14] Convert all model and response classes to C# records All response classes in MaxMind.MinFraud.Response and request classes in MaxMind.MinFraud.Request are now C# records, matching the GeoIP2 records conversion. Request constructors are marked [Obsolete] in favor of object initializer syntax. SetLocales removed from IPAddress in favor of immutable WithLocales pattern. Co-Authored-By: Claude Opus 4.6 --- .../Request/AccountTest.cs | 2 + .../Request/BillingTest.cs | 2 + .../Request/CreditCardTest.cs | 2 + .../Request/DeviceTest.cs | 2 + .../Request/EmailTest.cs | 2 + .../Request/EventTest.cs | 2 + .../Request/MinFraudRequestTest.cs | 2 + .../Request/OrderTest.cs | 2 + .../Request/PaymentTest.cs | 2 + .../Request/ShippingTest.cs | 2 + .../Request/ShoppingCartItemTest.cs | 2 + .../Request/TestHelper.cs | 2 + .../Request/TransactionReportTest.cs | 2 + .../WebServiceClientTest.cs | 4 +- MaxMind.MinFraud/MaxMind.MinFraud.csproj | 4 +- MaxMind.MinFraud/Request/Account.cs | 17 +++---- MaxMind.MinFraud/Request/Billing.cs | 12 ++++- MaxMind.MinFraud/Request/CreditCard.cs | 18 +++----- MaxMind.MinFraud/Request/Device.cs | 25 +++++----- MaxMind.MinFraud/Request/Email.cs | 17 +++---- MaxMind.MinFraud/Request/Event.cs | 17 +++---- MaxMind.MinFraud/Request/Location.cs | 18 +++----- MaxMind.MinFraud/Request/Order.cs | 18 +++----- MaxMind.MinFraud/Request/Payment.cs | 17 +++---- MaxMind.MinFraud/Request/Shipping.cs | 18 ++++---- MaxMind.MinFraud/Request/ShoppingCartItem.cs | 20 ++++---- MaxMind.MinFraud/Request/Transaction.cs | 21 ++++----- MaxMind.MinFraud/Request/TransactionReport.cs | 18 +++----- MaxMind.MinFraud/Response/Address.cs | 12 +---- MaxMind.MinFraud/Response/BillingAddress.cs | 2 +- MaxMind.MinFraud/Response/CreditCard.cs | 11 +---- MaxMind.MinFraud/Response/Device.cs | 10 +--- MaxMind.MinFraud/Response/Disposition.cs | 10 +--- MaxMind.MinFraud/Response/Email.cs | 10 +--- MaxMind.MinFraud/Response/EmailDomain.cs | 10 +--- MaxMind.MinFraud/Response/EmailDomainVisit.cs | 10 +--- MaxMind.MinFraud/Response/Factors.cs | 13 +----- MaxMind.MinFraud/Response/GeoIP2Location.cs | 11 +---- MaxMind.MinFraud/Response/IPAddress.cs | 17 +------ MaxMind.MinFraud/Response/IPRiskReason.cs | 11 +---- MaxMind.MinFraud/Response/Insights.cs | 11 +---- MaxMind.MinFraud/Response/Issuer.cs | 12 +---- MaxMind.MinFraud/Response/MultiplierReason.cs | 11 +---- MaxMind.MinFraud/Response/Phone.cs | 11 +---- MaxMind.MinFraud/Response/RiskScoreReason.cs | 11 +---- MaxMind.MinFraud/Response/Score.cs | 13 +----- MaxMind.MinFraud/Response/ScoreIPAddress.cs | 11 +---- MaxMind.MinFraud/Response/ShippingAddress.cs | 11 +---- MaxMind.MinFraud/Response/Subscores.cs | 11 +---- MaxMind.MinFraud/Response/Warning.cs | 11 +---- MaxMind.MinFraud/WebServiceClient.cs | 24 ++++++++-- README.md | 46 ++++++++++--------- releasenotes.md | 14 +++++- 53 files changed, 214 insertions(+), 380 deletions(-) diff --git a/MaxMind.MinFraud.UnitTest/Request/AccountTest.cs b/MaxMind.MinFraud.UnitTest/Request/AccountTest.cs index 22e05d9e..01bfe7e5 100644 --- a/MaxMind.MinFraud.UnitTest/Request/AccountTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/AccountTest.cs @@ -1,6 +1,8 @@ using MaxMind.MinFraud.Request; using Xunit; +#pragma warning disable CS0618 // Type or member is obsolete + namespace MaxMind.MinFraud.UnitTest.Request { public class AccountTest diff --git a/MaxMind.MinFraud.UnitTest/Request/BillingTest.cs b/MaxMind.MinFraud.UnitTest/Request/BillingTest.cs index 7c758ba6..06dbb169 100644 --- a/MaxMind.MinFraud.UnitTest/Request/BillingTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/BillingTest.cs @@ -2,6 +2,8 @@ using System; using Xunit; +#pragma warning disable CS0618 // Type or member is obsolete + namespace MaxMind.MinFraud.UnitTest.Request { // This code is identical to code in ShippingTest. I couldn't diff --git a/MaxMind.MinFraud.UnitTest/Request/CreditCardTest.cs b/MaxMind.MinFraud.UnitTest/Request/CreditCardTest.cs index 7db1adc3..f4777459 100644 --- a/MaxMind.MinFraud.UnitTest/Request/CreditCardTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/CreditCardTest.cs @@ -2,6 +2,8 @@ using System; using Xunit; +#pragma warning disable CS0618 // Type or member is obsolete + namespace MaxMind.MinFraud.UnitTest.Request { public class CreditCardTest diff --git a/MaxMind.MinFraud.UnitTest/Request/DeviceTest.cs b/MaxMind.MinFraud.UnitTest/Request/DeviceTest.cs index 842bcef4..ce896cb6 100644 --- a/MaxMind.MinFraud.UnitTest/Request/DeviceTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/DeviceTest.cs @@ -3,6 +3,8 @@ using System.Net; using Xunit; +#pragma warning disable CS0618 // Type or member is obsolete + namespace MaxMind.MinFraud.UnitTest.Request { public class DeviceTest diff --git a/MaxMind.MinFraud.UnitTest/Request/EmailTest.cs b/MaxMind.MinFraud.UnitTest/Request/EmailTest.cs index 48ab9bff..74006e2a 100644 --- a/MaxMind.MinFraud.UnitTest/Request/EmailTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/EmailTest.cs @@ -3,6 +3,8 @@ using System.Text.Json; using Xunit; +#pragma warning disable CS0618 // Type or member is obsolete + namespace MaxMind.MinFraud.UnitTest.Request { public class EmailTest diff --git a/MaxMind.MinFraud.UnitTest/Request/EventTest.cs b/MaxMind.MinFraud.UnitTest/Request/EventTest.cs index 31d62512..f0107b64 100644 --- a/MaxMind.MinFraud.UnitTest/Request/EventTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/EventTest.cs @@ -3,6 +3,8 @@ using System.Text.Json; using Xunit; +#pragma warning disable CS0618 // Type or member is obsolete + namespace MaxMind.MinFraud.UnitTest.Request { public class EventTest diff --git a/MaxMind.MinFraud.UnitTest/Request/MinFraudRequestTest.cs b/MaxMind.MinFraud.UnitTest/Request/MinFraudRequestTest.cs index 16f74a13..bcae9875 100644 --- a/MaxMind.MinFraud.UnitTest/Request/MinFraudRequestTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/MinFraudRequestTest.cs @@ -3,6 +3,8 @@ using System.Net; using Xunit; +#pragma warning disable CS0618 // Type or member is obsolete + namespace MaxMind.MinFraud.UnitTest.Request { public class MinFraudRequestTest diff --git a/MaxMind.MinFraud.UnitTest/Request/OrderTest.cs b/MaxMind.MinFraud.UnitTest/Request/OrderTest.cs index c0871689..8365deaa 100644 --- a/MaxMind.MinFraud.UnitTest/Request/OrderTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/OrderTest.cs @@ -2,6 +2,8 @@ using System; using Xunit; +#pragma warning disable CS0618 // Type or member is obsolete + namespace MaxMind.MinFraud.UnitTest.Request { public class OrderTest diff --git a/MaxMind.MinFraud.UnitTest/Request/PaymentTest.cs b/MaxMind.MinFraud.UnitTest/Request/PaymentTest.cs index a116029a..97269688 100644 --- a/MaxMind.MinFraud.UnitTest/Request/PaymentTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/PaymentTest.cs @@ -1,6 +1,8 @@ using MaxMind.MinFraud.Request; using Xunit; +#pragma warning disable CS0618 // Type or member is obsolete + namespace MaxMind.MinFraud.UnitTest.Request { public class PaymentTest diff --git a/MaxMind.MinFraud.UnitTest/Request/ShippingTest.cs b/MaxMind.MinFraud.UnitTest/Request/ShippingTest.cs index 057c9162..661dc658 100644 --- a/MaxMind.MinFraud.UnitTest/Request/ShippingTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/ShippingTest.cs @@ -6,6 +6,8 @@ #endregion +#pragma warning disable CS0618 // Type or member is obsolete + namespace MaxMind.MinFraud.UnitTest.Request { public class ShippingTest diff --git a/MaxMind.MinFraud.UnitTest/Request/ShoppingCartItemTest.cs b/MaxMind.MinFraud.UnitTest/Request/ShoppingCartItemTest.cs index ebfaf180..85a0c712 100644 --- a/MaxMind.MinFraud.UnitTest/Request/ShoppingCartItemTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/ShoppingCartItemTest.cs @@ -1,6 +1,8 @@ using MaxMind.MinFraud.Request; using Xunit; +#pragma warning disable CS0618 // Type or member is obsolete + namespace MaxMind.MinFraud.UnitTest.Request { public class ShoppingCartItemTest diff --git a/MaxMind.MinFraud.UnitTest/Request/TestHelper.cs b/MaxMind.MinFraud.UnitTest/Request/TestHelper.cs index 72236ca7..0b637dd5 100644 --- a/MaxMind.MinFraud.UnitTest/Request/TestHelper.cs +++ b/MaxMind.MinFraud.UnitTest/Request/TestHelper.cs @@ -52,6 +52,7 @@ private static string GetTestDirectory() return currentDirectory; } +#pragma warning disable CS0618 // Type or member is obsolete public static Transaction CreateFullRequestUsingConstructors() { return new Transaction( @@ -167,6 +168,7 @@ public static Transaction CreateFullRequestUsingConstructors() } ); } +#pragma warning restore CS0618 // Type or member is obsolete public static Transaction CreateFullRequest() { diff --git a/MaxMind.MinFraud.UnitTest/Request/TransactionReportTest.cs b/MaxMind.MinFraud.UnitTest/Request/TransactionReportTest.cs index 4789749b..b3977b80 100644 --- a/MaxMind.MinFraud.UnitTest/Request/TransactionReportTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/TransactionReportTest.cs @@ -3,6 +3,8 @@ using System.Net; using Xunit; +#pragma warning disable CS0618 // Type or member is obsolete + namespace MaxMind.MinFraud.UnitTest.Request { public class TransactionReportTest diff --git a/MaxMind.MinFraud.UnitTest/WebServiceClientTest.cs b/MaxMind.MinFraud.UnitTest/WebServiceClientTest.cs index 63e2c24f..5bc24fe4 100644 --- a/MaxMind.MinFraud.UnitTest/WebServiceClientTest.cs +++ b/MaxMind.MinFraud.UnitTest/WebServiceClientTest.cs @@ -44,7 +44,7 @@ public async Task TestFullInsightsRequest() var response = await client.InsightsAsync(request); CompareJson(responseContent, response); - // The purpose here is to test that SetLocales worked as expected + // The purpose here is to test that WithLocales worked as expected Assert.Equal("London", response.IPAddress.City.Name); Assert.Equal("United Kingdom", response.IPAddress.Country.Name); @@ -81,6 +81,7 @@ public async Task TestFullFactorsRequestUsingConstructors() Assert.Equal("London", response.IPAddress.City.Name); } +#pragma warning disable CS0618 // Type or member is obsolete [Fact] public async Task TestFullReportRequest() { @@ -102,6 +103,7 @@ public async Task TestFullReportRequest() Assert.Null(exception); } +#pragma warning restore CS0618 // Type or member is obsolete [Fact] public async Task TestWebServiceClientOptionsConstructor() diff --git a/MaxMind.MinFraud/MaxMind.MinFraud.csproj b/MaxMind.MinFraud/MaxMind.MinFraud.csproj index c9e13572..c431e54c 100644 --- a/MaxMind.MinFraud/MaxMind.MinFraud.csproj +++ b/MaxMind.MinFraud/MaxMind.MinFraud.csproj @@ -2,7 +2,7 @@ API for MaxMind minFraud Score and Insights web services - 5.3.1 + 6.0.0-beta1 net10.0;net9.0;net8.0;netstandard2.1;netstandard2.0 true MaxMind.MinFraud @@ -33,7 +33,7 @@ - + diff --git a/MaxMind.MinFraud/Request/Account.cs b/MaxMind.MinFraud/Request/Account.cs index ea11ead5..3f7b5bfb 100644 --- a/MaxMind.MinFraud/Request/Account.cs +++ b/MaxMind.MinFraud/Request/Account.cs @@ -9,8 +9,13 @@ namespace MaxMind.MinFraud.Request /// Account related data information for the transaction being sent to the /// web service. /// - public sealed class Account + public sealed record Account { + /// + /// Constructor. + /// + public Account() { } + /// /// Constructor. /// @@ -22,6 +27,7 @@ public sealed class Account /// The username associated with the account. /// This is not the MD5 of username. Rather, the MD is automatically /// generated from this string. + [Obsolete("Use object initializer syntax.")] public Account( string? userId = null, string? username = null @@ -71,14 +77,5 @@ public string? UsernameMD5 .ToLower(); } } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"UserId: {UserId}, Username: {Username}, UsernameMD5: {UsernameMD5}"; - } } } diff --git a/MaxMind.MinFraud/Request/Billing.cs b/MaxMind.MinFraud/Request/Billing.cs index b4aca1ae..ce4f9e40 100644 --- a/MaxMind.MinFraud/Request/Billing.cs +++ b/MaxMind.MinFraud/Request/Billing.cs @@ -1,11 +1,18 @@ -namespace MaxMind.MinFraud.Request +using System; + +namespace MaxMind.MinFraud.Request { /// /// The billing information for the transaction being sent to the /// web service. /// - public sealed class Billing : Location + public sealed record Billing : Location { + /// + /// Constructor. + /// + public Billing() { } + /// /// Constructor. /// @@ -22,6 +29,7 @@ public sealed class Billing : Location /// The postal code of the user’s billing address. /// The phone number without the country code for the user’s billing address. /// The country code for phone number associated with the user’s billing address. + [Obsolete("Use object initializer syntax.")] public Billing( string? firstName = null, string? lastName = null, diff --git a/MaxMind.MinFraud/Request/CreditCard.cs b/MaxMind.MinFraud/Request/CreditCard.cs index d6fd4ebf..65a4ed60 100644 --- a/MaxMind.MinFraud/Request/CreditCard.cs +++ b/MaxMind.MinFraud/Request/CreditCard.cs @@ -8,7 +8,7 @@ namespace MaxMind.MinFraud.Request /// The credit card information for the transaction being sent to the /// web service. /// - public sealed class CreditCard + public sealed record CreditCard { private static readonly Regex CountryRe = new("^[A-Z]{2}$", RegexOptions.Compiled); private static readonly Regex IssuerIdNumberRe = new("^[0-9]{6}$|^[0-9]{8}$", RegexOptions.Compiled); @@ -17,6 +17,11 @@ public sealed class CreditCard private static readonly Regex TokenRe = new("^(?![0-9]{1,19}$)[\\x21-\\x7E]{1,255}$", RegexOptions.Compiled); + /// + /// Constructor. + /// + public CreditCard() { } + /// /// Constructor. /// @@ -55,6 +60,7 @@ public sealed class CreditCard /// processor does not provide them. The ISO 3166-1 alpha-2 country code /// should be used, e.g., "US". /// + [Obsolete("Use object initializer syntax.")] public CreditCard( string? issuerIdNumber = null, string? lastDigits = null, @@ -202,15 +208,5 @@ public string? Token /// [JsonPropertyName("was_3d_secure_successful")] public bool? Was3DSecureSuccessful { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return - $"IssuerIdNumber: {IssuerIdNumber}, LastDigits: {LastDigits}, BankName: {BankName}, BankPhoneCountryCode: {BankPhoneCountryCode}, BankPhoneNumber: {BankPhoneNumber}, Country: {Country}, AvsResult: {AvsResult}, CvvResult: {CvvResult}, Token: {Token}, Was3DSecureSuccessful: {Was3DSecureSuccessful}"; - } } } diff --git a/MaxMind.MinFraud/Request/Device.cs b/MaxMind.MinFraud/Request/Device.cs index b9ad7324..041123d9 100644 --- a/MaxMind.MinFraud/Request/Device.cs +++ b/MaxMind.MinFraud/Request/Device.cs @@ -9,16 +9,21 @@ namespace MaxMind.MinFraud.Request /// The device information for the transaction being sent to the /// web service. /// - public sealed class Device + public sealed record Device { + /// + /// Constructor. + /// + public Device() { } + /// /// Constructor. /// /// The IP address associated with the device /// used by the customer in the transaction. - /// The HTTP “User-Agent” header of the + /// The HTTP "User-Agent" header of the /// browser used in the transaction. - /// The HTTP “Accept-Language” header of + /// The HTTP "Accept-Language" header of /// the device used in the transaction. /// The number of seconds between the /// creation of the user's session and the time of the transaction. @@ -29,6 +34,7 @@ public sealed class Device /// site. /// The tracking token generated by the /// Device Tracking Add-on for explicit device linking. + [Obsolete("Use object initializer syntax.")] public Device( IPAddress? ipAddress = null, string? userAgent = null, @@ -69,14 +75,14 @@ public Device( public IPAddress? IPAddress { get; init; } /// - /// The HTTP “User-Agent” header of the browser used in the + /// The HTTP "User-Agent" header of the browser used in the /// transaction. /// [JsonPropertyName("user_agent")] public string? UserAgent { get; init; } /// - /// The HTTP “Accept-Language” header of the device used in the + /// The HTTP "Accept-Language" header of the device used in the /// transaction. /// [JsonPropertyName("accept_language")] @@ -126,14 +132,5 @@ public string? SessionId /// [JsonPropertyName("tracking_token")] public string? TrackingToken { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"IPAddress: {IPAddress}, UserAgent: {UserAgent}, AcceptLanguage: {AcceptLanguage}, SessionAge: {SessionAge}, SessionId: {SessionId}, TrackingToken: {TrackingToken}"; - } } } diff --git a/MaxMind.MinFraud/Request/Email.cs b/MaxMind.MinFraud/Request/Email.cs index ae22ea09..ddbf38f1 100644 --- a/MaxMind.MinFraud/Request/Email.cs +++ b/MaxMind.MinFraud/Request/Email.cs @@ -13,8 +13,13 @@ namespace MaxMind.MinFraud.Request /// The email information for the transaction being sent to the /// web service. /// - public sealed class Email + public sealed record Email { + /// + /// Constructor. + /// + public Email() { } + private static readonly Regex DuplicateDotComRe = new(@"(?:\.com){2,}$", RegexOptions.Compiled); private static readonly Regex GmailLeadingDigitRe = new(@"^\d+(?:gmail?\.com)$", RegexOptions.Compiled); private static readonly IdnMapping _idn = new(); @@ -290,6 +295,7 @@ public sealed class Email /// By default, the address will /// be sent in plain text. If hashAddress is set to true, /// the address will instead be sent as an MD5 hash. + [Obsolete("Use object initializer syntax.")] public Email( string? address = null, string? domain = null, @@ -380,15 +386,6 @@ public string? Domain [JsonIgnore] public bool HashAddress { get; init; } = false; - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"Address: {Address}, Domain: {Domain}"; - } - private static string CleanAddress(string address) { address = address.Trim().ToLower(); diff --git a/MaxMind.MinFraud/Request/Event.cs b/MaxMind.MinFraud/Request/Event.cs index 3c56cdbc..67392e57 100644 --- a/MaxMind.MinFraud/Request/Event.cs +++ b/MaxMind.MinFraud/Request/Event.cs @@ -67,8 +67,13 @@ public enum EventType /// Event information for the transaction being sent to the /// web service. /// - public sealed class Event + public sealed record Event { + /// + /// Constructor. + /// + public Event() { } + /// /// Constructor. /// @@ -84,6 +89,7 @@ public sealed class Event /// field is not in the request, the current time will be used. /// The type of event being scored. /// The party submitting the transaction. + [Obsolete("Use object initializer syntax.")] public Event( string? transactionId = null, string? shopId = null, @@ -147,14 +153,5 @@ public Event( [JsonConverter(typeof(EnumMemberValueConverter))] [JsonPropertyName("type")] public EventType? Type { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"Party: {Party}, TransactionId: {TransactionId}, ShopId: {ShopId}, Time: {Time}, Type: {Type}"; - } } } diff --git a/MaxMind.MinFraud/Request/Location.cs b/MaxMind.MinFraud/Request/Location.cs index 585ede83..a7b52c56 100644 --- a/MaxMind.MinFraud/Request/Location.cs +++ b/MaxMind.MinFraud/Request/Location.cs @@ -8,10 +8,15 @@ namespace MaxMind.MinFraud.Request /// The location information for the transaction being sent to the /// web service. /// - public abstract class Location + public abstract record Location { private static readonly Regex CountryRe = new("^[A-Z]{2}$", RegexOptions.Compiled); + /// + /// Constructor. + /// + protected Location() { } + /// /// Constructor. /// @@ -28,6 +33,7 @@ public abstract class Location /// The postal code of the user’s address. /// The phone number without the country code for the user’s address. /// The country code for phone number associated with the user’s address. + [Obsolete("Use object initializer syntax.")] protected Location( string? firstName = null, string? lastName = null, @@ -133,15 +139,5 @@ public string? Country /// [JsonPropertyName("phone_country_code")] public string? PhoneCountryCode { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return - $"FirstName: {FirstName}, LastName: {LastName}, Company: {Company}, Address: {Address}, Address2: {Address2}, Region: {Region}, Country: {Country}, Postal: {Postal}, City: {City}, PhoneNumber: {PhoneNumber}, PhoneCountryCode: {PhoneCountryCode}"; - } } } diff --git a/MaxMind.MinFraud/Request/Order.cs b/MaxMind.MinFraud/Request/Order.cs index b0e05095..673d85ec 100644 --- a/MaxMind.MinFraud/Request/Order.cs +++ b/MaxMind.MinFraud/Request/Order.cs @@ -8,10 +8,15 @@ namespace MaxMind.MinFraud.Request /// The order information for the transaction being sent to the /// web service. /// - public sealed class Order + public sealed record Order { private static readonly Regex CurrencyRe = new("^[A-Z]{3}$", RegexOptions.Compiled); + /// + /// Constructor. + /// + public Order() { } + /// /// Constructor. /// @@ -31,6 +36,7 @@ public sealed class Order /// purchaser. /// Whether the purchaser included a gift /// message. + [Obsolete("Use object initializer syntax.")] public Order( decimal? amount = null, string? currency = null, @@ -111,15 +117,5 @@ public string? Currency /// [JsonPropertyName("has_gift_message")] public bool? HasGiftMessage { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return - $"Amount: {Amount}, Currency: {Currency}, DiscountCode: {DiscountCode}, AffiliateId: {AffiliateId}, SubaffiliateId: {SubaffiliateId}, ReferrerUri: {ReferrerUri}, IsGift: {IsGift}, HasGiftMessage: {HasGiftMessage}"; - } } } \ No newline at end of file diff --git a/MaxMind.MinFraud/Request/Payment.cs b/MaxMind.MinFraud/Request/Payment.cs index 706278ca..80390047 100644 --- a/MaxMind.MinFraud/Request/Payment.cs +++ b/MaxMind.MinFraud/Request/Payment.cs @@ -561,8 +561,13 @@ public enum PaymentProcessor /// The payment information for the transaction being sent to the /// web service. /// - public sealed class Payment + public sealed record Payment { + /// + /// Constructor. + /// + public Payment() { } + /// /// Constructor. /// @@ -575,6 +580,7 @@ public sealed class Payment /// payment processor. If the transaction was not declined, do not /// include this field. /// The payment method associated with the transaction. + [Obsolete("Use object initializer syntax.")] public Payment( PaymentProcessor? processor = null, bool? wasAuthorized = null, @@ -628,14 +634,5 @@ public Payment( /// [JsonPropertyName("decline_code")] public string? DeclineCode { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"Method: {Method}, Processor: {Processor}, WasAuthorized: {WasAuthorized}, DeclineCode: {DeclineCode}"; - } } } diff --git a/MaxMind.MinFraud/Request/Shipping.cs b/MaxMind.MinFraud/Request/Shipping.cs index b7c68474..cfcfa0cf 100644 --- a/MaxMind.MinFraud/Request/Shipping.cs +++ b/MaxMind.MinFraud/Request/Shipping.cs @@ -1,4 +1,5 @@ using MaxMind.MinFraud.Util; +using System; using System.Runtime.Serialization; using System.Text.Json.Serialization; @@ -24,8 +25,13 @@ public enum ShippingDeliverySpeed /// The shipping information for the transaction being sent to the /// web service. /// - public sealed class Shipping : Location + public sealed record Shipping : Location { + /// + /// Constructor. + /// + public Shipping() { } + /// /// Constructor. /// @@ -43,6 +49,7 @@ public sealed class Shipping : Location /// The phone number without the country code for the user’s shipping address. /// The country code for phone number associated with the user’s shipping address. /// The shipping delivery speed for the order. + [Obsolete("Use object initializer syntax.")] public Shipping( string? firstName = null, string? lastName = null, @@ -79,14 +86,5 @@ public Shipping( [JsonConverter(typeof(EnumMemberValueConverter))] [JsonPropertyName("delivery_speed")] public ShippingDeliverySpeed? DeliverySpeed { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"{base.ToString()}, DeliverySpeed: {DeliverySpeed}"; - } } } diff --git a/MaxMind.MinFraud/Request/ShoppingCartItem.cs b/MaxMind.MinFraud/Request/ShoppingCartItem.cs index e6416ed1..c0f29e84 100644 --- a/MaxMind.MinFraud/Request/ShoppingCartItem.cs +++ b/MaxMind.MinFraud/Request/ShoppingCartItem.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System; +using System.Text.Json.Serialization; namespace MaxMind.MinFraud.Request { @@ -6,8 +7,13 @@ namespace MaxMind.MinFraud.Request /// Information for an item in the shopping cart for the transaction /// being sent to the web service. /// - public sealed class ShoppingCartItem + public sealed record ShoppingCartItem { + /// + /// Constructor. + /// + public ShoppingCartItem() { } + /// /// Constructor /// @@ -17,6 +23,7 @@ public sealed class ShoppingCartItem /// This must be positive /// The price of the item in the shopping cart. This /// should be the same currency as the order currency. + [Obsolete("Use object initializer syntax.")] public ShoppingCartItem( string? category = null, string? itemId = null, @@ -54,14 +61,5 @@ public ShoppingCartItem( /// [JsonPropertyName("price")] public decimal? Price { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"Category: {Category}, ItemId: {ItemId}, Quantity: {Quantity}, Price: {Price}"; - } } } \ No newline at end of file diff --git a/MaxMind.MinFraud/Request/Transaction.cs b/MaxMind.MinFraud/Request/Transaction.cs index 6efe95f3..36d4d159 100644 --- a/MaxMind.MinFraud/Request/Transaction.cs +++ b/MaxMind.MinFraud/Request/Transaction.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text.Json.Serialization; namespace MaxMind.MinFraud.Request @@ -6,8 +7,13 @@ namespace MaxMind.MinFraud.Request /// /// The transaction to be sent to the web service. /// - public sealed class Transaction + public sealed record Transaction { + /// + /// Constructor. + /// + public Transaction() { } + /// /// Constructor. See /// @@ -27,6 +33,7 @@ public sealed class Transaction /// Information about the payment processing. /// Shipping information used in the transaction. /// List of shopping items in the transaction. + [Obsolete("Use object initializer syntax.")] public Transaction( Device? device = null, Account? account = null, @@ -119,15 +126,5 @@ public Transaction( /// [JsonPropertyName("shopping_cart")] public IList? ShoppingCart { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return - $"Account: {Account}, Billing: {Billing}, CreditCard: {CreditCard}, Device: {Device}, Email: {Email}, Event: {Event}, Order: {Order}, Payment: {Payment}, Shipping: {Shipping}, ShoppingCart: {ShoppingCart}"; - } } } diff --git a/MaxMind.MinFraud/Request/TransactionReport.cs b/MaxMind.MinFraud/Request/TransactionReport.cs index 84b7f582..80e463c0 100644 --- a/MaxMind.MinFraud/Request/TransactionReport.cs +++ b/MaxMind.MinFraud/Request/TransactionReport.cs @@ -42,8 +42,13 @@ public enum TransactionReportTag /// The transaction information for a report you would like to file with /// MaxMind. /// - public sealed class TransactionReport + public sealed record TransactionReport { + /// + /// Constructor. + /// + public TransactionReport() { } + /// /// Constructor with validation. /// @@ -79,6 +84,7 @@ public sealed class TransactionReport /// one of the transaction's ipAddress, maxmindId, /// or minfraudId>. You are encouraged to provide it, if /// possible. + [Obsolete("Use object initializer syntax.")] public TransactionReport( TransactionReportTag tag, string? chargebackCode = null, @@ -195,15 +201,5 @@ public string? MaxMindId /// [JsonPropertyName("transaction_id")] public string? TransactionId { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"IPAddress: {IPAddress}, Tag: {Tag}, ChargebackCode: {ChargebackCode}, MaxMindId: {MaxMindId}, " - + $"MinFraudId: {MinFraudId}, Notes: {Notes}, TransactionId: {TransactionId}"; - } } } \ No newline at end of file diff --git a/MaxMind.MinFraud/Response/Address.cs b/MaxMind.MinFraud/Response/Address.cs index a9721941..779945b8 100644 --- a/MaxMind.MinFraud/Response/Address.cs +++ b/MaxMind.MinFraud/Response/Address.cs @@ -5,7 +5,7 @@ namespace MaxMind.MinFraud.Response /// /// General address response data. /// - public abstract class Address + public abstract record Address { /// /// This property is true if the address is in the @@ -42,15 +42,5 @@ public abstract class Address /// [JsonPropertyName("distance_to_ip_location")] public int? DistanceToIPLocation { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return - $"IsInIPCountry: {IsInIPCountry}, IsPostalInCity: {IsPostalInCity}, Latitude: {Latitude}, Longitude: {Longitude}, DistanceToIPLocation: {DistanceToIPLocation}"; - } } } diff --git a/MaxMind.MinFraud/Response/BillingAddress.cs b/MaxMind.MinFraud/Response/BillingAddress.cs index d06fc466..a7348ec9 100644 --- a/MaxMind.MinFraud/Response/BillingAddress.cs +++ b/MaxMind.MinFraud/Response/BillingAddress.cs @@ -3,7 +3,7 @@ /// /// Information about the billing address. /// - public sealed class BillingAddress : Address + public sealed record BillingAddress : Address { } } \ No newline at end of file diff --git a/MaxMind.MinFraud/Response/CreditCard.cs b/MaxMind.MinFraud/Response/CreditCard.cs index 4c3c3339..025a98b0 100644 --- a/MaxMind.MinFraud/Response/CreditCard.cs +++ b/MaxMind.MinFraud/Response/CreditCard.cs @@ -5,7 +5,7 @@ namespace MaxMind.MinFraud.Response /// /// Information about the credit card based on the issuer ID number. /// - public sealed class CreditCard + public sealed record CreditCard { /// /// The credit card brand. @@ -62,14 +62,5 @@ public sealed class CreditCard /// [JsonPropertyName("type")] public string? Type { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"{nameof(Brand)}: {Brand}, {nameof(Country)}: {Country}, {nameof(IsBusiness)}: {IsBusiness}, {nameof(IsIssuedInBillingAddressCountry)}: {IsIssuedInBillingAddressCountry}, {nameof(IsPrepaid)}: {IsPrepaid}, {nameof(IsVirtual)}: {IsVirtual}, {nameof(Issuer)}: {Issuer}, {nameof(Type)}: {Type}"; - } } } diff --git a/MaxMind.MinFraud/Response/Device.cs b/MaxMind.MinFraud/Response/Device.cs index 2498a603..68700862 100644 --- a/MaxMind.MinFraud/Response/Device.cs +++ b/MaxMind.MinFraud/Response/Device.cs @@ -11,7 +11,7 @@ namespace MaxMind.MinFraud.Response /// Device /// Tracking Add-on. /// - public sealed class Device + public sealed record Device { /// /// A number representing the confidence that the DeviceId @@ -42,13 +42,5 @@ public sealed class Device /// [JsonPropertyName("local_time")] public DateTimeOffset? LocalTime { get; init; } - - /// - /// Returns a string that represents the current object. - /// - public override string ToString() - { - return $"Confidence: {Confidence}, Id: {Id}, LastSeen: {LastSeen}, LocalTime: {LocalTime}"; - } } } diff --git a/MaxMind.MinFraud/Response/Disposition.cs b/MaxMind.MinFraud/Response/Disposition.cs index 87cd44af..194c05ab 100644 --- a/MaxMind.MinFraud/Response/Disposition.cs +++ b/MaxMind.MinFraud/Response/Disposition.cs @@ -5,7 +5,7 @@ namespace MaxMind.MinFraud.Response /// /// This object contains the disposition set by custom rules. /// - public sealed class Disposition + public sealed record Disposition { /// /// The action to take on the transaction as defined by your custom @@ -32,13 +32,5 @@ public sealed class Disposition /// [JsonPropertyName("rule_label")] public string? RuleLabel { get; init; } - - /// - /// Returns a string that represents the current object. - /// - public override string ToString() - { - return $"Action: {Action}, Reason: {Reason}, Rule Label: {RuleLabel}"; - } } } diff --git a/MaxMind.MinFraud/Response/Email.cs b/MaxMind.MinFraud/Response/Email.cs index 1260b0e2..10d3a0cf 100644 --- a/MaxMind.MinFraud/Response/Email.cs +++ b/MaxMind.MinFraud/Response/Email.cs @@ -7,7 +7,7 @@ namespace MaxMind.MinFraud.Response /// /// This object contains information about the email address passed in the request. /// - public sealed class Email + public sealed record Email { /// /// An object containing information about the email address domain. @@ -44,13 +44,5 @@ public sealed class Email /// [JsonPropertyName("is_high_risk")] public bool? IsHighRisk { get; init; } - - /// - /// Returns a string that represents the current object. - /// - public override string ToString() - { - return $"Domain: {Domain}, FirstSeen: {FirstSeen}, IsDisposable: {IsDisposable}, IsFree: {IsFree}, IsHighRiskFree: {IsHighRisk}"; - } } } \ No newline at end of file diff --git a/MaxMind.MinFraud/Response/EmailDomain.cs b/MaxMind.MinFraud/Response/EmailDomain.cs index 1798ee4b..f608943a 100644 --- a/MaxMind.MinFraud/Response/EmailDomain.cs +++ b/MaxMind.MinFraud/Response/EmailDomain.cs @@ -8,7 +8,7 @@ namespace MaxMind.MinFraud.Response /// This object contains information about the email address domain passed /// in the request. /// - public sealed class EmailDomain + public sealed record EmailDomain { /// /// The classification of the email domain. @@ -44,13 +44,5 @@ public sealed class EmailDomain /// [JsonPropertyName("visit")] public EmailDomainVisit? Visit { get; init; } - - /// - /// Returns a string that represents the current object. - /// - public override string ToString() - { - return $"Classification: {Classification}, FirstSeen: {FirstSeen}, Risk: {Risk}, Volume: {Volume}, Visit: {Visit}"; - } } } \ No newline at end of file diff --git a/MaxMind.MinFraud/Response/EmailDomainVisit.cs b/MaxMind.MinFraud/Response/EmailDomainVisit.cs index 87b2fb7e..8814c3f9 100644 --- a/MaxMind.MinFraud/Response/EmailDomainVisit.cs +++ b/MaxMind.MinFraud/Response/EmailDomainVisit.cs @@ -7,7 +7,7 @@ namespace MaxMind.MinFraud.Response /// /// This object contains information about the visit to the email domain. /// - public sealed class EmailDomainVisit + public sealed record EmailDomainVisit { /// /// This property is true if the domain automatically forwards visitors @@ -30,13 +30,5 @@ public sealed class EmailDomainVisit [JsonConverter(typeof(EnumMemberValueConverter))] [JsonPropertyName("status")] public DomainVisitStatus? Status { get; init; } - - /// - /// Returns a string that represents the current object. - /// - public override string ToString() - { - return $"HasRedirect: {HasRedirect}, LastVisitedOn: {LastVisitedOn}, Status: {Status}"; - } } } diff --git a/MaxMind.MinFraud/Response/Factors.cs b/MaxMind.MinFraud/Response/Factors.cs index deafbd5e..f2b78e6b 100644 --- a/MaxMind.MinFraud/Response/Factors.cs +++ b/MaxMind.MinFraud/Response/Factors.cs @@ -7,7 +7,7 @@ namespace MaxMind.MinFraud.Response /// /// Model for Insights response. /// - public sealed class Factors : Insights + public sealed record Factors : Insights { /// /// This list contains objects that describe risk score reasons for a given transaction @@ -27,16 +27,5 @@ public sealed class Factors : Insights [JsonPropertyName("subscores")] [Obsolete("Replaced by RiskScoreReasons")] public Subscores Subscores { get; init; } = new Subscores(); - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { -#pragma warning disable 0618 - return $"{base.ToString()}, Subscores: {Subscores}, RiskScoreReasons: {RiskScoreReasons}"; -#pragma warning restore 0618 - } } } diff --git a/MaxMind.MinFraud/Response/GeoIP2Location.cs b/MaxMind.MinFraud/Response/GeoIP2Location.cs index 5cb34b21..635abaa8 100644 --- a/MaxMind.MinFraud/Response/GeoIP2Location.cs +++ b/MaxMind.MinFraud/Response/GeoIP2Location.cs @@ -8,7 +8,7 @@ namespace MaxMind.MinFraud.Response /// A subclass of the GeoIP2 Location model with minFraud-specific /// additions. /// - public sealed class GeoIP2Location : Location + public sealed record GeoIP2Location : Location { /// /// The date and time of the transaction in the time @@ -16,14 +16,5 @@ public sealed class GeoIP2Location : Location /// [JsonPropertyName("local_time")] public DateTimeOffset? LocalTime { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"{base.ToString()}, LocalTime: {LocalTime}"; - } } } \ No newline at end of file diff --git a/MaxMind.MinFraud/Response/IPAddress.cs b/MaxMind.MinFraud/Response/IPAddress.cs index 45b79a81..a8cfba3d 100644 --- a/MaxMind.MinFraud/Response/IPAddress.cs +++ b/MaxMind.MinFraud/Response/IPAddress.cs @@ -8,7 +8,7 @@ namespace MaxMind.MinFraud.Response /// /// Model for minFraud GeoIP2 Insights data. /// - public sealed class IPAddress : InsightsResponse, IIPAddress + public sealed record IPAddress : InsightsResponse, IIPAddress { /// /// Location object for the requested IP address. @@ -38,20 +38,5 @@ public sealed class IPAddress : InsightsResponse, IIPAddress [JsonPropertyName("risk_reasons")] public IReadOnlyList RiskReasons { get; init; } = new List().AsReadOnly(); - - internal new void SetLocales(IReadOnlyList locales) - { - var l = new List(locales); - base.SetLocales(l); - } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"{base.ToString()}, Country: {Country}, Location: {Location}"; - } } } \ No newline at end of file diff --git a/MaxMind.MinFraud/Response/IPRiskReason.cs b/MaxMind.MinFraud/Response/IPRiskReason.cs index 00ef2e8c..37c1eb72 100644 --- a/MaxMind.MinFraud/Response/IPRiskReason.cs +++ b/MaxMind.MinFraud/Response/IPRiskReason.cs @@ -9,7 +9,7 @@ namespace MaxMind.MinFraud.Response /// This class provides both a machine-readable code and a human-readable /// explanation of the reason for the IP risk score. /// - public sealed class IPRiskReason + public sealed record IPRiskReason { /// /// This property is a machine-readable code identifying the reason. @@ -64,14 +64,5 @@ public sealed class IPRiskReason /// [JsonPropertyName("reason")] public string? Reason { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"Code: {Code}, Reason: {Reason}"; - } } } \ No newline at end of file diff --git a/MaxMind.MinFraud/Response/Insights.cs b/MaxMind.MinFraud/Response/Insights.cs index 59d822d1..a6adabcc 100644 --- a/MaxMind.MinFraud/Response/Insights.cs +++ b/MaxMind.MinFraud/Response/Insights.cs @@ -5,7 +5,7 @@ namespace MaxMind.MinFraud.Response /// /// Model for Insights response. /// - public class Insights : Score + public record Insights : Score { /// /// An object containing GeoIP2 and minFraud Insights information about @@ -62,14 +62,5 @@ public class Insights : Score /// [JsonPropertyName("billing_phone")] public Phone BillingPhone { get; init; } = new Phone(); - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"{base.ToString()}, IPAddress: {IPAddress}, CreditCard: {CreditCard}, Device: {Device}, Email: {Email}, ShippingAddress: {ShippingAddress}, ShippingPhone: {ShippingPhone}, BillingAddress: {BillingAddress}, BillingPhone: {BillingPhone}"; - } } } diff --git a/MaxMind.MinFraud/Response/Issuer.cs b/MaxMind.MinFraud/Response/Issuer.cs index ce0baded..20cbaf3a 100644 --- a/MaxMind.MinFraud/Response/Issuer.cs +++ b/MaxMind.MinFraud/Response/Issuer.cs @@ -5,7 +5,7 @@ namespace MaxMind.MinFraud.Response /// /// Model for the credit card issuer data from minFraud. /// - public sealed class Issuer + public sealed record Issuer { /// /// The name of the bank which issued the credit card. @@ -40,15 +40,5 @@ public sealed class Issuer /// [JsonPropertyName("matches_provided_phone_number")] public bool? MatchesProvidedPhoneNumber { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return - $"Name: {Name}, MatchesProvidedName: {MatchesProvidedName}, PhoneNumber: {PhoneNumber}, MatchesProvidedPhoneNumber: {MatchesProvidedPhoneNumber}"; - } } } \ No newline at end of file diff --git a/MaxMind.MinFraud/Response/MultiplierReason.cs b/MaxMind.MinFraud/Response/MultiplierReason.cs index 65eb95c0..d04b759b 100644 --- a/MaxMind.MinFraud/Response/MultiplierReason.cs +++ b/MaxMind.MinFraud/Response/MultiplierReason.cs @@ -10,7 +10,7 @@ namespace MaxMind.MinFraud.Response /// explanation of the reason for the risk score, see /// the documentation. /// - public sealed class MultiplierReason + public sealed record MultiplierReason { /// /// This property is a machine-readable code identifying the reason. @@ -187,14 +187,5 @@ public sealed class MultiplierReason /// [JsonPropertyName("reason")] public string? Reason { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"Code: {Code}, Reason: {Reason}"; - } } } diff --git a/MaxMind.MinFraud/Response/Phone.cs b/MaxMind.MinFraud/Response/Phone.cs index fecd18dc..72f47ca3 100644 --- a/MaxMind.MinFraud/Response/Phone.cs +++ b/MaxMind.MinFraud/Response/Phone.cs @@ -5,7 +5,7 @@ namespace MaxMind.MinFraud.Response /// /// This object contains information about the billing or shipping phone. /// - public sealed class Phone + public sealed record Phone { /// /// A two-character ISO 3166-1 country code for the country associated @@ -50,14 +50,5 @@ public sealed class Phone /// [JsonPropertyName("number_type")] public string? NumberType { get; init; } - - /// - /// Returns a string that represents the current object. - /// - public override string ToString() - { - return $"Country: {Country}, IsVoip: {IsVoip}, MatchesPostal: {MatchesPostal}" + - $"NetworkOperator: {NetworkOperator}, NumberType: {NumberType}"; - } } } diff --git a/MaxMind.MinFraud/Response/RiskScoreReason.cs b/MaxMind.MinFraud/Response/RiskScoreReason.cs index 95dd04b5..4ebfdab7 100644 --- a/MaxMind.MinFraud/Response/RiskScoreReason.cs +++ b/MaxMind.MinFraud/Response/RiskScoreReason.cs @@ -6,7 +6,7 @@ namespace MaxMind.MinFraud.Response /// /// Model for a risk score multiplier and reasons for that multiplier. /// - public sealed class RiskScoreReason + public sealed record RiskScoreReason { /// /// The factor by which the risk score is increased (if the value is greater than 1) @@ -23,14 +23,5 @@ public sealed class RiskScoreReason [JsonPropertyName("reasons")] public IReadOnlyList Reasons { get; init; } = new List().AsReadOnly(); - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"Multiplier: {Multiplier}, Reasons: {Reasons}"; - } } } diff --git a/MaxMind.MinFraud/Response/Score.cs b/MaxMind.MinFraud/Response/Score.cs index 3de98b70..9f6720c1 100644 --- a/MaxMind.MinFraud/Response/Score.cs +++ b/MaxMind.MinFraud/Response/Score.cs @@ -1,7 +1,6 @@ using MaxMind.MinFraud.Util; using System; using System.Collections.Generic; -using System.Linq; using System.Text.Json.Serialization; namespace MaxMind.MinFraud.Response @@ -9,7 +8,7 @@ namespace MaxMind.MinFraud.Response /// /// Model class for Score response. /// - public class Score + public record Score { /// /// This object contains information about the disposition set by @@ -65,15 +64,5 @@ public class Score /// [JsonPropertyName("warnings")] public IReadOnlyList Warnings { get; init; } = new List().AsReadOnly(); - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - var warnings = string.Join("; ", Warnings.Select(x => x.Message)); - return $"Warnings: [{warnings}], Disposition: {Disposition}, FundsRemaining: {FundsRemaining}, Id: {Id}, QueriesRemaining: {QueriesRemaining}, RiskScore: {RiskScore}"; - } } } \ No newline at end of file diff --git a/MaxMind.MinFraud/Response/ScoreIPAddress.cs b/MaxMind.MinFraud/Response/ScoreIPAddress.cs index ee1425e3..68352487 100644 --- a/MaxMind.MinFraud/Response/ScoreIPAddress.cs +++ b/MaxMind.MinFraud/Response/ScoreIPAddress.cs @@ -5,7 +5,7 @@ namespace MaxMind.MinFraud.Response /// /// The IP addresses risk. /// - public sealed class ScoreIPAddress : IIPAddress + public sealed record ScoreIPAddress : IIPAddress { /// /// The risk associated with the IP address. The value ranges from 0.01 @@ -13,14 +13,5 @@ public sealed class ScoreIPAddress : IIPAddress /// [JsonPropertyName("risk")] public double? Risk { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"Risk: {Risk}"; - } } } \ No newline at end of file diff --git a/MaxMind.MinFraud/Response/ShippingAddress.cs b/MaxMind.MinFraud/Response/ShippingAddress.cs index 70fdac7f..cac0e25b 100644 --- a/MaxMind.MinFraud/Response/ShippingAddress.cs +++ b/MaxMind.MinFraud/Response/ShippingAddress.cs @@ -5,7 +5,7 @@ namespace MaxMind.MinFraud.Response /// /// Information about the shipping address. /// - public sealed class ShippingAddress : Address + public sealed record ShippingAddress : Address { /// /// This property is true if the shipping address is in @@ -23,14 +23,5 @@ public sealed class ShippingAddress : Address /// [JsonPropertyName("distance_to_billing_address")] public int? DistanceToBillingAddress { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"{base.ToString()}, IsHighRisk: {IsHighRisk}, DistanceToBillingAddress: {DistanceToBillingAddress}"; - } } } \ No newline at end of file diff --git a/MaxMind.MinFraud/Response/Subscores.cs b/MaxMind.MinFraud/Response/Subscores.cs index 00a56009..7dedd13e 100644 --- a/MaxMind.MinFraud/Response/Subscores.cs +++ b/MaxMind.MinFraud/Response/Subscores.cs @@ -8,7 +8,7 @@ namespace MaxMind.MinFraud.Response /// factors that are used to calculate the overall risk score. /// [Obsolete("Replaced by RiskScoreReason")] - public sealed class Subscores + public sealed record Subscores { /// /// The risk associated with the AVS result. If present, this is a @@ -146,14 +146,5 @@ public sealed class Subscores /// [JsonPropertyName("time_of_day")] public double? TimeOfDay { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"AvsResult: {AvsResult}, BillingAddress: {BillingAddress}, BillingAddressDistanceToIPLocation: {BillingAddressDistanceToIPLocation}, Browser: {Browser}, Chargeback: {Chargeback}, Country: {Country}, CountryMismatch: {CountryMismatch}, CvvResult: {CvvResult}, EmailAddress: {EmailAddress}, EmailDomain: {EmailDomain}, IssuerIdMumber: {IssuerIdNumber}, OrderAmount: {OrderAmount}, PhoneNumber: {PhoneNumber}, ShippingAddressDistanceToIPLocation: {ShippingAddressDistanceToIPLocation}, TimeOfDay: {TimeOfDay}"; - } } } diff --git a/MaxMind.MinFraud/Response/Warning.cs b/MaxMind.MinFraud/Response/Warning.cs index ca29fb19..b12d759e 100644 --- a/MaxMind.MinFraud/Response/Warning.cs +++ b/MaxMind.MinFraud/Response/Warning.cs @@ -5,7 +5,7 @@ namespace MaxMind.MinFraud.Response /// /// A warning returned by the web service. /// - public sealed class Warning + public sealed record Warning { /// /// This value is a machine-readable code identifying the @@ -32,14 +32,5 @@ public sealed class Warning /// [JsonPropertyName("input_pointer")] public string? InputPointer { get; init; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - { - return $"Code: {Code}, Message: {Message}, InputPointer: {InputPointer}"; - } } } diff --git a/MaxMind.MinFraud/WebServiceClient.cs b/MaxMind.MinFraud/WebServiceClient.cs index 6b42ad17..cde89737 100644 --- a/MaxMind.MinFraud/WebServiceClient.cs +++ b/MaxMind.MinFraud/WebServiceClient.cs @@ -122,8 +122,16 @@ HttpClient httpClient public async Task FactorsAsync(Transaction transaction) { var factors = await MakeResponse(transaction).ConfigureAwait(false); - factors.IPAddress.SetLocales(_locales); - return factors; + var withLocales = ((GeoIP2.Responses.AbstractResponse)factors.IPAddress) + .WithLocales(_locales); + if (withLocales is not Response.IPAddress ipAddress) + { + throw new MinFraudException( + $"WithLocales returned {withLocales.GetType().FullName} instead of " + + $"the expected {typeof(Response.IPAddress).FullName}. " + + "This may indicate an incompatible GeoIP2 library version."); + } + return factors with { IPAddress = ipAddress }; } /// @@ -136,8 +144,16 @@ public async Task FactorsAsync(Transaction transaction) public async Task InsightsAsync(Transaction transaction) { var insights = await MakeResponse(transaction).ConfigureAwait(false); - insights.IPAddress.SetLocales(_locales); - return insights; + var withLocales = ((GeoIP2.Responses.AbstractResponse)insights.IPAddress) + .WithLocales(_locales); + if (withLocales is not Response.IPAddress ipAddress) + { + throw new MinFraudException( + $"WithLocales returned {withLocales.GetType().FullName} instead of " + + $"the expected {typeof(Response.IPAddress).FullName}. " + + "This may indicate an incompatible GeoIP2 library version."); + } + return insights with { IPAddress = ipAddress }; } /// diff --git a/README.md b/README.md index 5d1c9e1c..c945c7f5 100644 --- a/README.md +++ b/README.md @@ -69,18 +69,20 @@ Create a new `Transaction` object. This represents the transaction that you are sending to minFraud: ```csharp -var transaction = new Transaction( - device: new Device( - ipAddress: System.Net.IPAddress.Parse("152.216.7.110"), - userAgent: "Mozilla/5.0 (X11; Linux x86_64)", - acceptLanguage: "en-US,en;q=0.8" - ), - account: - new Account( - userId: "3132", - username: "fred" - ) - ); +var transaction = new Transaction +{ + Device = new Device + { + IPAddress = System.Net.IPAddress.Parse("152.216.7.110"), + UserAgent = "Mozilla/5.0 (X11; Linux x86_64)", + AcceptLanguage = "en-US,en;q=0.8" + }, + Account = new Account + { + UserId = "3132", + Username = "fred" + } +}; ``` After creating the request object, send a non-blocking minFraud Score request @@ -168,14 +170,16 @@ If a transaction was scored incorrectly or you received a chargeback, you may report it to MaxMind. To do this, create a new `TransactionReport` object: ```csharp -var report = new TransactionReport( - tag: TransactionReportTag.Chargeback, - chargebackCode: "BL", - ipAddress: IPAddress.Parse("104.16.148.244"), - maxmindId: "abcd1234", - minfraudId: new Guid("01c25cb0-f067-4e02-8ed0-a094c580f5e4"), - notes: "Suspicious account behavior", - transactionId: "txn123"); +var report = new TransactionReport +{ + Tag = TransactionReportTag.Chargeback, + ChargebackCode = "BL", + IPAddress = IPAddress.Parse("104.16.148.244"), + MaxMindId = "abcd1234", + MinFraudId = new Guid("01c25cb0-f067-4e02-8ed0-a094c580f5e4"), + Notes = "Suspicious account behavior", + TransactionId = "txn123" +}; ``` A valid `tag` and at least one of the following are required parameters: @@ -304,7 +308,7 @@ public class MinFraudExample BankPhoneNumber = "123-456-1234", AvsResult = 'Y', CvvResult = 'N', - Last4Digits = "7643", + LastDigits = "7643", Token = "123456abc1234", Was3DSecureSuccessful = true }, diff --git a/releasenotes.md b/releasenotes.md index a874fb28..236681ca 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -1,15 +1,27 @@ Release Notes ============= -5.4.0 +6.0.0-beta1 ------------------ +* **BREAKING:** All response classes in `MaxMind.MinFraud.Response` have been + converted from classes to records. +* **BREAKING:** All request classes in `MaxMind.MinFraud.Request` have been + converted from classes to records. +* **BREAKING:** All request class constructors have been marked `[Obsolete]`. + Use object initializer syntax instead. +* **BREAKING:** The `SetLocales` method has been removed from + `MaxMind.MinFraud.Response.IPAddress`. * Added `TrackingToken` property to `MaxMind.MinFraud.Request.Device`. This is the token generated by the [Device Tracking Add-on](https://dev.maxmind.com/minfraud/track-devices) for explicit device linking. * Added `Banquest`, `SummitPayments`, and `Yaadpay` to the `PaymentProcessor` enum. +* **BREAKING:** The minimum `MaxMind.GeoIP2` dependency has been bumped to + 6.0.0 (a transitive breaking change). +* Custom `ToString()` overrides have been removed from all response and request + classes. Records generate `ToString()` automatically. 5.3.1 (2025-11-24) ------------------ From c83fedf017077f9303d33ceec201e1097b84bb02 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 20 Mar 2026 16:04:14 +0000 Subject: [PATCH 02/14] Validate TransactionReport in ReportAsync before sending With the move to object initializer syntax, the constructor validation that ensured at least one identifier (IPAddress, MinFraudId, MaxMindId, TransactionId) is set can be bypassed. Move this validation to ReportAsync so it applies regardless of how the report is constructed. Co-Authored-By: Claude Opus 4.6 (1M context) --- MaxMind.MinFraud/WebServiceClient.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/MaxMind.MinFraud/WebServiceClient.cs b/MaxMind.MinFraud/WebServiceClient.cs index cde89737..eeaf4e98 100644 --- a/MaxMind.MinFraud/WebServiceClient.cs +++ b/MaxMind.MinFraud/WebServiceClient.cs @@ -180,6 +180,16 @@ public async Task ScoreAsync(Transaction transaction) /// this API will throw an exception if there is an error. public async Task ReportAsync(TransactionReport report) { + if (report.IPAddress == null + && (report.MinFraudId == null || report.MinFraudId == Guid.Empty) + && string.IsNullOrEmpty(report.MaxMindId) + && string.IsNullOrEmpty(report.TransactionId)) + { + throw new ArgumentException( + "The report must include at least one of the following: " + + "IPAddress, MinFraudId, MaxMindId, TransactionId."); + } + await MakeRequest("transactions/report", report); } From 3db37b349b670f329eeeb3be39a1e40d73cae4f7 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 20 Mar 2026 16:35:50 +0000 Subject: [PATCH 03/14] Migrate tests from obsolete constructors to object initializer syntax Convert all request test files to use `new Foo { Prop = value }` instead of `new Foo(prop: value)`, exercising the recommended API surface. This removes the file-wide `#pragma warning disable CS0618` from 12 test files and the localized pragma around TestFullReportRequest. The cross-property validation test (TestRequired) in TransactionReportTest is replaced by TestReportRequiresIdentifier in WebServiceClientTest, since that validation now lives only in ReportAsync. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Request/AccountTest.cs | 22 ++--- .../Request/BillingTest.cs | 39 +++++---- .../Request/CreditCardTest.cs | 62 ++++++-------- .../Request/DeviceTest.cs | 22 +++-- .../Request/EmailTest.cs | 80 +++++++++---------- .../Request/EventTest.cs | 35 ++++---- .../Request/MinFraudRequestTest.cs | 30 +++---- .../Request/OrderTest.cs | 28 +++---- .../Request/PaymentTest.cs | 20 +++-- .../Request/ShippingTest.cs | 41 +++++----- .../Request/ShoppingCartItemTest.cs | 14 ++-- .../Request/TransactionReportTest.cs | 69 ++++------------ .../WebServiceClientTest.cs | 33 +++++--- 13 files changed, 224 insertions(+), 271 deletions(-) diff --git a/MaxMind.MinFraud.UnitTest/Request/AccountTest.cs b/MaxMind.MinFraud.UnitTest/Request/AccountTest.cs index 01bfe7e5..ffa6651a 100644 --- a/MaxMind.MinFraud.UnitTest/Request/AccountTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/AccountTest.cs @@ -1,8 +1,6 @@ -using MaxMind.MinFraud.Request; +using MaxMind.MinFraud.Request; using Xunit; -#pragma warning disable CS0618 // Type or member is obsolete - namespace MaxMind.MinFraud.UnitTest.Request { public class AccountTest @@ -10,9 +8,10 @@ public class AccountTest [Fact] public void TestUserId() { - var account = new Account( - userId: "usr" - ); + var account = new Account + { + UserId = "usr" + }; Assert.Equal("usr", account.UserId); } @@ -20,9 +19,10 @@ public void TestUserId() [Fact] public void TestUsername() { - var account = new Account( - username: "username" - ); + var account = new Account + { + Username = "username" + }; Assert.Equal("14c4b06b824ec593239362517f538b29", account.UsernameMD5); } @@ -32,8 +32,8 @@ public void TestUsername() [Fact] public void TestNullUsername() { - var account = new Account(username: null); + var account = new Account { Username = null }; Assert.Null(account.UsernameMD5); } } -} \ No newline at end of file +} diff --git a/MaxMind.MinFraud.UnitTest/Request/BillingTest.cs b/MaxMind.MinFraud.UnitTest/Request/BillingTest.cs index 06dbb169..8537e697 100644 --- a/MaxMind.MinFraud.UnitTest/Request/BillingTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/BillingTest.cs @@ -1,9 +1,7 @@ -using MaxMind.MinFraud.Request; +using MaxMind.MinFraud.Request; using System; using Xunit; -#pragma warning disable CS0618 // Type or member is obsolete - namespace MaxMind.MinFraud.UnitTest.Request { // This code is identical to code in ShippingTest. I couldn't @@ -15,83 +13,84 @@ public class BillingTest [Fact] public void TestFirstName() { - var loc = new Billing( - firstName: "frst" - ); + var loc = new Billing + { + FirstName = "frst" + }; Assert.Equal("frst", loc.FirstName); } [Fact] public void TestLastName() { - var loc = new Billing(lastName: "last"); + var loc = new Billing { LastName = "last" }; Assert.Equal("last", loc.LastName); } [Fact] public void TestCompany() { - var loc = new Billing(company: "company"); + var loc = new Billing { Company = "company" }; Assert.Equal("company", loc.Company); } [Fact] public void TestAddress() { - var loc = new Billing(address: "addr"); + var loc = new Billing { Address = "addr" }; Assert.Equal("addr", loc.Address); } [Fact] public void TestAddress2() { - var loc = new Billing(address2: "addr2"); + var loc = new Billing { Address2 = "addr2" }; Assert.Equal("addr2", loc.Address2); } [Fact] public void TestCity() { - var loc = new Billing(city: "Pdx"); + var loc = new Billing { City = "Pdx" }; Assert.Equal("Pdx", loc.City); } [Fact] public void TestRegion() { - var loc = new Billing(region: "MN"); + var loc = new Billing { Region = "MN" }; Assert.Equal("MN", loc.Region); } [Fact] public void TestCountry() { - var loc = new Billing(country: "US"); + var loc = new Billing { Country = "US" }; Assert.Equal("US", loc.Country); } [Fact] public void TestCountryThatIsTooLong() { - Assert.Throws(() => new Billing(country: "USA")); + Assert.Throws(() => new Billing { Country = "USA" }); } [Fact] public void TestCountryWithNumbers() { - Assert.Throws(() => new Billing(country: "U1")); + Assert.Throws(() => new Billing { Country = "U1" }); } [Fact] public void TestCountryInWrongCase() { - Assert.Throws(() => new Billing(country: "us")); + Assert.Throws(() => new Billing { Country = "us" }); } [Fact] public void TestPostal() { - var loc = new Billing(postal: "03231"); + var loc = new Billing { Postal = "03231" }; Assert.Equal("03231", loc.Postal); } @@ -99,15 +98,15 @@ public void TestPostal() public void TestPhoneNumber() { var phone = "321-321-3213"; - var loc = new Billing(phoneNumber: phone); + var loc = new Billing { PhoneNumber = phone }; Assert.Equal(phone, loc.PhoneNumber); } [Fact] public void TestPhoneCountryCode() { - var loc = new Billing(phoneCountryCode: "1"); + var loc = new Billing { PhoneCountryCode = "1" }; Assert.Equal("1", loc.PhoneCountryCode); } } -} \ No newline at end of file +} diff --git a/MaxMind.MinFraud.UnitTest/Request/CreditCardTest.cs b/MaxMind.MinFraud.UnitTest/Request/CreditCardTest.cs index f4777459..02995214 100644 --- a/MaxMind.MinFraud.UnitTest/Request/CreditCardTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/CreditCardTest.cs @@ -1,9 +1,7 @@ -using MaxMind.MinFraud.Request; +using MaxMind.MinFraud.Request; using System; using Xunit; -#pragma warning disable CS0618 // Type or member is obsolete - namespace MaxMind.MinFraud.UnitTest.Request { public class CreditCardTest @@ -11,111 +9,101 @@ public class CreditCardTest [Fact] public void TestCountry() { - var cc = new CreditCard(country: "US"); - Assert.Equal("US", cc.Country); - } - - [Fact] - public void TestCountryInit() - { - var cc = new CreditCard - { - Country = "US" - }; + var cc = new CreditCard { Country = "US" }; Assert.Equal("US", cc.Country); } [Fact] public void TestCountryThatIsTooLong() { - Assert.Throws(() => new CreditCard(country: "USA")); + Assert.Throws(() => new CreditCard { Country = "USA" }); } [Fact] public void TestCountryWithNumbers() { - Assert.Throws(() => new CreditCard(country: "U1")); + Assert.Throws(() => new CreditCard { Country = "U1" }); } [Fact] public void TestCountryInWrongCase() { - Assert.Throws(() => new CreditCard(country: "us")); + Assert.Throws(() => new CreditCard { Country = "us" }); } [Fact] public void TestIssuerIdNumber() { - var cc6 = new CreditCard(issuerIdNumber: "123456"); + var cc6 = new CreditCard { IssuerIdNumber = "123456" }; Assert.Equal("123456", cc6.IssuerIdNumber); - var cc8 = new CreditCard(issuerIdNumber: "12345678"); + var cc8 = new CreditCard { IssuerIdNumber = "12345678" }; Assert.Equal("12345678", cc8.IssuerIdNumber); } [Fact] public void TestIssuerIdNumberThatIsInvalidLength() { - Assert.Throws(() => new CreditCard(issuerIdNumber: "1234567")); + Assert.Throws(() => new CreditCard { IssuerIdNumber = "1234567" }); } [Fact] public void TestIssuerIdNumberThatIsTooLong() { - Assert.Throws(() => new CreditCard(issuerIdNumber: "123456789")); + Assert.Throws(() => new CreditCard { IssuerIdNumber = "123456789" }); } [Fact] public void TestIssuerIdNumberThatIsTooShort() { - Assert.Throws(() => new CreditCard(issuerIdNumber: "12345")); + Assert.Throws(() => new CreditCard { IssuerIdNumber = "12345" }); } [Fact] public void TestIssuerIdNumberThatHasLetters() { - Assert.Throws(() => new CreditCard(issuerIdNumber: "12345a")); + Assert.Throws(() => new CreditCard { IssuerIdNumber = "12345a" }); } [Fact] public void TestLastDigits() { - var cc2 = new CreditCard(lastDigits: "12"); + var cc2 = new CreditCard { LastDigits = "12" }; Assert.Equal("12", cc2.LastDigits); - var cc4 = new CreditCard(lastDigits: "1234"); + var cc4 = new CreditCard { LastDigits = "1234" }; Assert.Equal("1234", cc4.LastDigits); } [Fact] public void TestLastDigitsThatIsTooLong() { - Assert.Throws(() => new CreditCard(lastDigits: "12345")); + Assert.Throws(() => new CreditCard { LastDigits = "12345" }); } [Fact] public void TestLastDigitsThatIsTooShort() { - Assert.Throws(() => new CreditCard(lastDigits: "1")); + Assert.Throws(() => new CreditCard { LastDigits = "1" }); } [Fact] public void TestLastDigitsThatHasLetters() { - Assert.Throws(() => new CreditCard(lastDigits: "123a")); + Assert.Throws(() => new CreditCard { LastDigits = "123a" }); } [Fact] public void TestBankName() { - var cc = new CreditCard(bankName: "Bank"); + var cc = new CreditCard { BankName = "Bank" }; Assert.Equal("Bank", cc.BankName); } [Fact] public void TestBankPhoneCountryCode() { - var cc = new CreditCard(bankPhoneCountryCode: "1"); + var cc = new CreditCard { BankPhoneCountryCode = "1" }; Assert.Equal("1", cc.BankPhoneCountryCode); } @@ -123,21 +111,21 @@ public void TestBankPhoneCountryCode() public void TestBankPhoneNumber() { var phone = "231-323-3123"; - var cc = new CreditCard(bankPhoneNumber: phone); + var cc = new CreditCard { BankPhoneNumber = phone }; Assert.Equal(phone, cc.BankPhoneNumber); } [Fact] public void TestAvsResult() { - var cc = new CreditCard(avsResult: 'Y'); + var cc = new CreditCard { AvsResult = 'Y' }; Assert.Equal('Y', cc.AvsResult); } [Fact] public void TestCvvResult() { - var cc = new CreditCard(cvvResult: 'N'); + var cc = new CreditCard { CvvResult = 'N' }; Assert.Equal('N', cc.CvvResult); } @@ -150,7 +138,7 @@ public void TestCvvResult() "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")] public void TestInvalidToken(string token) { - Assert.Throws(() => new CreditCard(token: token)); + Assert.Throws(() => new CreditCard { Token = token }); } [Theory] @@ -159,15 +147,15 @@ public void TestInvalidToken(string token) [InlineData("valid_token")] public void TestValidToken(string token) { - var cc = new CreditCard(token: token); + var cc = new CreditCard { Token = token }; Assert.Equal(token, cc.Token); } [Fact] public void TestWas3DSecureSuccessful() { - var cc = new CreditCard(was3DSecureSuccessful: true); + var cc = new CreditCard { Was3DSecureSuccessful = true }; Assert.True(cc.Was3DSecureSuccessful); } } -} \ No newline at end of file +} diff --git a/MaxMind.MinFraud.UnitTest/Request/DeviceTest.cs b/MaxMind.MinFraud.UnitTest/Request/DeviceTest.cs index ce896cb6..54fc6618 100644 --- a/MaxMind.MinFraud.UnitTest/Request/DeviceTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/DeviceTest.cs @@ -1,10 +1,8 @@ -using MaxMind.MinFraud.Request; +using MaxMind.MinFraud.Request; using System; using System.Net; using Xunit; -#pragma warning disable CS0618 // Type or member is obsolete - namespace MaxMind.MinFraud.UnitTest.Request { public class DeviceTest @@ -13,7 +11,7 @@ public class DeviceTest public void TestIPAddress() { var ip = IPAddress.Parse("1.1.1.1"); - var device = new Device(ipAddress: ip); + var device = new Device { IPAddress = ip }; Assert.Equal(ip, device.IPAddress); } @@ -21,7 +19,7 @@ public void TestIPAddress() public void TestUserAgent() { var ua = "Mozila 5"; - var device = new Device(userAgent: ua); + var device = new Device { UserAgent = ua }; Assert.Equal(ua, device.UserAgent); } @@ -29,42 +27,42 @@ public void TestUserAgent() public void TestAcceptLanguage() { var al = "en-US"; - var device = new Device(acceptLanguage: al); + var device = new Device { AcceptLanguage = al }; Assert.Equal(al, device.AcceptLanguage); } [Fact] public void TestSessionAge() { - var device = new Device(sessionAge: 3600); + var device = new Device { SessionAge = 3600 }; Assert.Equal(3600, device.SessionAge); } [Fact] public void TestSessionAgeIsNegative() { - Assert.Throws(() => new Device(sessionAge: -1)); + Assert.Throws(() => new Device { SessionAge = -1 }); } [Fact] public void TestSessionId() { - var device = new Device(sessionId: "foo"); + var device = new Device { SessionId = "foo" }; Assert.Equal("foo", device.SessionId); } [Fact] public void TestSessionIdIsTooLong() { - Assert.Throws(() => new Device(sessionId: new string('x', 256))); + Assert.Throws(() => new Device { SessionId = new string('x', 256) }); } [Fact] public void TestTrackingToken() { var token = "a]L@E*bnoqHa9&SBSwbB8X3#1E"; - var device = new Device(trackingToken: token); + var device = new Device { TrackingToken = token }; Assert.Equal(token, device.TrackingToken); } } -} \ No newline at end of file +} diff --git a/MaxMind.MinFraud.UnitTest/Request/EmailTest.cs b/MaxMind.MinFraud.UnitTest/Request/EmailTest.cs index 74006e2a..3ada1b9c 100644 --- a/MaxMind.MinFraud.UnitTest/Request/EmailTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/EmailTest.cs @@ -1,10 +1,8 @@ -using MaxMind.MinFraud.Request; +using MaxMind.MinFraud.Request; using System; using System.Text.Json; using Xunit; -#pragma warning disable CS0618 // Type or member is obsolete - namespace MaxMind.MinFraud.UnitTest.Request { public class EmailTest @@ -15,7 +13,7 @@ public void TestAddress() var address = "test@maxmind.com"; var domain = "maxmind.com"; - var email = new Email(address: address); + var email = new Email { Address = address }; Assert.Equal(address, email.Address); Assert.Equal("977577b140bfb7c516e4746204fbdb01", email.AddressMD5); Assert.Equal(domain, email.Domain); @@ -44,7 +42,7 @@ public void TestAddressWithHashing() var md5 = "977577b140bfb7c516e4746204fbdb01"; var domain = "maxmind.com"; - var email = new Email(address: address, hashAddress: true); + var email = new Email { Address = address, HashAddress = true }; Assert.Equal(address, email.Address); Assert.Equal(md5, email.AddressMD5); Assert.Equal("maxmind.com", email.Domain); @@ -68,151 +66,151 @@ public void TestAddressWithHashing() [Fact] public void TestNormalizing() { - var e = new Email(address: "test@maxmind.com", hashAddress: true); + var e = new Email { Address = "test@maxmind.com", HashAddress = true }; Assert.Equal("977577b140bfb7c516e4746204fbdb01", e.AddressMD5); Assert.Equal("maxmind.com", e.Domain); - e = new Email(address: "Test@maxmind.com", hashAddress: true); + e = new Email { Address = "Test@maxmind.com", HashAddress = true }; Assert.Equal("977577b140bfb7c516e4746204fbdb01", e.AddressMD5); Assert.Equal("maxmind.com", e.Domain); - e = new Email(address: " Test@maxmind.com", hashAddress: true); + e = new Email { Address = " Test@maxmind.com", HashAddress = true }; Assert.Equal("977577b140bfb7c516e4746204fbdb01", e.AddressMD5); Assert.Equal("maxmind.com", e.Domain); - e = new Email(address: "Test+alias@maxmind.com", hashAddress: true); + e = new Email { Address = "Test+alias@maxmind.com", HashAddress = true }; Assert.Equal("977577b140bfb7c516e4746204fbdb01", e.AddressMD5); Assert.Equal("maxmind.com", e.Domain); - e = new Email(address: "Test+007+008@maxmind.com", hashAddress: true); + e = new Email { Address = "Test+007+008@maxmind.com", HashAddress = true }; Assert.Equal("977577b140bfb7c516e4746204fbdb01", e.AddressMD5); Assert.Equal("maxmind.com", e.Domain); - e = new Email(address: "Test+@maxmind.com", hashAddress: true); + e = new Email { Address = "Test+@maxmind.com", HashAddress = true }; Assert.Equal("977577b140bfb7c516e4746204fbdb01", e.AddressMD5); Assert.Equal("maxmind.com", e.Domain); - e = new Email(address: "Test@maxmind.com.", hashAddress: true); + e = new Email { Address = "Test@maxmind.com.", HashAddress = true }; Assert.Equal("977577b140bfb7c516e4746204fbdb01", e.AddressMD5); Assert.Equal("maxmind.com.", e.Domain); - e = new Email(address: "+@maxmind.com", hashAddress: true); + e = new Email { Address = "+@maxmind.com", HashAddress = true }; Assert.Equal("aa57884e48f0dda9fc6f4cb2bffb1dd2", e.AddressMD5); Assert.Equal("maxmind.com", e.Domain); - e = new Email(address: "Test@ maxmind.com", hashAddress: true); + e = new Email { Address = "Test@ maxmind.com", HashAddress = true }; Assert.Equal("977577b140bfb7c516e4746204fbdb01", e.AddressMD5); Assert.Equal(" maxmind.com", e.Domain); - e = new Email(address: "Test+foo@yahoo.com", hashAddress: true); + e = new Email { Address = "Test+foo@yahoo.com", HashAddress = true }; Assert.Equal("a5f830c699fd71ad653aa59fa688c6d9", e.AddressMD5); Assert.Equal("yahoo.com", e.Domain); - e = new Email(address: "Test-foo@yahoo.com", hashAddress: true); + e = new Email { Address = "Test-foo@yahoo.com", HashAddress = true }; Assert.Equal("88e478531ab3bc303f1b5da82c2e9bbb", e.AddressMD5); Assert.Equal("yahoo.com", e.Domain); - e = new Email(address: "Test-foo-foo2@yahoo.com", hashAddress: true); + e = new Email { Address = "Test-foo-foo2@yahoo.com", HashAddress = true }; Assert.Equal("88e478531ab3bc303f1b5da82c2e9bbb", e.AddressMD5); Assert.Equal("yahoo.com", e.Domain); - e = new Email(address: "Test-foo@gmail.com", hashAddress: true); + e = new Email { Address = "Test-foo@gmail.com", HashAddress = true }; Assert.Equal("6f3ff986fa5e830dbbf08a942777a17c", e.AddressMD5); Assert.Equal("gmail.com", e.Domain); - e = new Email(address: "test@gmail.com", hashAddress: true); + e = new Email { Address = "test@gmail.com", HashAddress = true }; Assert.Equal("1aedb8d9dc4751e229a335e371db8058", e.AddressMD5); Assert.Equal("gmail.com", e.Domain); - e = new Email(address: "test@gamil.com", hashAddress: true); + e = new Email { Address = "test@gamil.com", HashAddress = true }; Assert.Equal("1aedb8d9dc4751e229a335e371db8058", e.AddressMD5); Assert.Equal("gamil.com", e.Domain); - e = new Email(address: "test@bücher.com", hashAddress: true); + e = new Email { Address = "test@bücher.com", HashAddress = true }; Assert.Equal("24948acabac551360cd510d5e5e2b464", e.AddressMD5); Assert.Equal("bücher.com", e.Domain); - e = new Email(address: "Test+alias@Bücher.com", hashAddress: true); + e = new Email { Address = "Test+alias@Bücher.com", HashAddress = true }; Assert.Equal("24948acabac551360cd510d5e5e2b464", e.AddressMD5); Assert.Equal("Bücher.com", e.Domain); - e = new Email(address: "test@", hashAddress: true); + e = new Email { Address = "test@", HashAddress = true }; Assert.Equal("246a848af2f8394e3adbc738dbe43720", e.AddressMD5); Assert.Equal("", e.Domain); - e = new Email(address: "foo@googlemail.com", hashAddress: true); + e = new Email { Address = "foo@googlemail.com", HashAddress = true }; Assert.Equal("6c0fbec2cc554c35c3d2b8b51840b49d", e.AddressMD5); Assert.Equal("googlemail.com", e.Domain); - e = new Email(address: "foo.bar@gmail.com", hashAddress: true); + e = new Email { Address = "foo.bar@gmail.com", HashAddress = true }; Assert.Equal("726f7c3769f32d3da4656ea906d02adb", e.AddressMD5); Assert.Equal("gmail.com", e.Domain); - e = new Email(address: "alias@user.fastmail.com", hashAddress: true); + e = new Email { Address = "alias@user.fastmail.com", HashAddress = true }; Assert.Equal("2dc11f44b436d1bc4ecfd4806e469d33", e.AddressMD5); Assert.Equal("user.fastmail.com", e.Domain); - e = new Email(address: "foo-bar@ymail.com", hashAddress: true); + e = new Email { Address = "foo-bar@ymail.com", HashAddress = true }; Assert.Equal("fead35da88f8414ec0414ef5f25d49c8", e.AddressMD5); Assert.Equal("ymail.com", e.Domain); - e = new Email(address: "foo@example.com.com", hashAddress: true); + e = new Email { Address = "foo@example.com.com", HashAddress = true }; Assert.Equal("b48def645758b95537d4424c84d1a9ff", e.AddressMD5); Assert.Equal("example.com.com", e.Domain); - e = new Email(address: "foo@example.comfoo", hashAddress: true); + e = new Email { Address = "foo@example.comfoo", HashAddress = true }; Assert.Equal("f235e180832a24cbeb724db47e000ffa", e.AddressMD5); Assert.Equal("example.comfoo", e.Domain); - e = new Email(address: "foo@example.cam", hashAddress: true); + e = new Email { Address = "foo@example.cam", HashAddress = true }; Assert.Equal("0434eeacd3b34ec807df2b57f79640fa", e.AddressMD5); Assert.Equal("example.cam", e.Domain); - e = new Email(address: "foo@10000gmail.com", hashAddress: true); + e = new Email { Address = "foo@10000gmail.com", HashAddress = true }; Assert.Equal("6c0fbec2cc554c35c3d2b8b51840b49d", e.AddressMD5); Assert.Equal("10000gmail.com", e.Domain); - e = new Email(address: "foo@example.comcom", hashAddress: true); + e = new Email { Address = "foo@example.comcom", HashAddress = true }; Assert.Equal("b48def645758b95537d4424c84d1a9ff", e.AddressMD5); Assert.Equal("example.comcom", e.Domain); - e = new Email(address: "foo@example.com.", hashAddress: true); + e = new Email { Address = "foo@example.com.", HashAddress = true }; Assert.Equal("b48def645758b95537d4424c84d1a9ff", e.AddressMD5); Assert.Equal("example.com.", e.Domain); - e = new Email(address: "foo@example.com...", hashAddress: true); + e = new Email { Address = "foo@example.com...", HashAddress = true }; Assert.Equal("b48def645758b95537d4424c84d1a9ff", e.AddressMD5); Assert.Equal("example.com...", e.Domain); - e = new Email(address: "example@bu\u0308cher.com", hashAddress: true); + e = new Email { Address = "example@bu\u0308cher.com", HashAddress = true }; Assert.Equal("2b21bc76dab3c8b1622837c1d698936c", e.AddressMD5); - e = new Email(address: "example@b\u00FCcher.com", hashAddress: true); + e = new Email { Address = "example@b\u00FCcher.com", HashAddress = true }; Assert.Equal("2b21bc76dab3c8b1622837c1d698936c", e.AddressMD5); - e = new Email(address: "bu\u0308cher@example.com", hashAddress: true); + e = new Email { Address = "bu\u0308cher@example.com", HashAddress = true }; Assert.Equal("53550c712b146287a2d0dd30e5ed6f4b", e.AddressMD5); - e = new Email(address: "b\u00FCcher@example.com", hashAddress: true); + e = new Email { Address = "b\u00FCcher@example.com", HashAddress = true }; Assert.Equal("53550c712b146287a2d0dd30e5ed6f4b", e.AddressMD5); } [Fact] public void TestInvalidAddress() { - Assert.Throws(() => new Email(address: "no-domain")); + Assert.Throws(() => new Email { Address = "no-domain" }); } [Fact] public void TestDomain() { var domain = "domain.com"; - var email = new Email(domain: domain); + var email = new Email { Domain = domain }; Assert.Equal(domain, email.Domain); } [Fact] public void TestInvalidDomain() { - Assert.Throws(() => new Email(domain: " domain.com")); + Assert.Throws(() => new Email { Domain = " domain.com" }); } } } diff --git a/MaxMind.MinFraud.UnitTest/Request/EventTest.cs b/MaxMind.MinFraud.UnitTest/Request/EventTest.cs index f0107b64..a6c82bc1 100644 --- a/MaxMind.MinFraud.UnitTest/Request/EventTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/EventTest.cs @@ -1,10 +1,8 @@ -using MaxMind.MinFraud.Request; +using MaxMind.MinFraud.Request; using System; using System.Text.Json; using Xunit; -#pragma warning disable CS0618 // Type or member is obsolete - namespace MaxMind.MinFraud.UnitTest.Request { public class EventTest @@ -12,14 +10,14 @@ public class EventTest [Fact] public void TestTransactionId() { - var eventReq = new Event(transactionId: "t12"); + var eventReq = new Event { TransactionId = "t12" }; Assert.Equal("t12", eventReq.TransactionId); } [Fact] public void TestShopId() { - var eventReq = new Event(shopId: "s12"); + var eventReq = new Event { ShopId = "s12" }; Assert.Equal("s12", eventReq.ShopId); } @@ -27,57 +25,58 @@ public void TestShopId() public void TestTime() { var date = new DateTimeOffset(); - var eventReq = new Event(time: date); + var eventReq = new Event { Time = date }; Assert.Equal(date, eventReq.Time); } [Fact] public void TestType() { - var eventReq = new Event(type: EventType.AccountCreation); + var eventReq = new Event { Type = EventType.AccountCreation }; Assert.Equal(EventType.AccountCreation, eventReq.Type); } [Fact] public void TestCreditApplicationType() { - var eventReq = new Event(type: EventType.CreditApplication); + var eventReq = new Event { Type = EventType.CreditApplication }; Assert.Equal(EventType.CreditApplication, eventReq.Type); } [Fact] public void TestFundTransferType() { - var eventReq = new Event(type: EventType.FundTransfer); + var eventReq = new Event { Type = EventType.FundTransfer }; Assert.Equal(EventType.FundTransfer, eventReq.Type); } [Fact] public void TestAgentParty() { - var eventReq = new Event(party: EventParty.Agent); + var eventReq = new Event { Party = EventParty.Agent }; Assert.Equal(EventParty.Agent, eventReq.Party); } [Fact] public void TestCustomerParty() { - var eventReq = new Event(party: EventParty.Customer); + var eventReq = new Event { Party = EventParty.Customer }; Assert.Equal(EventParty.Customer, eventReq.Party); } [Fact] public void TestSerialization() { - var eventReq = new Event( - transactionId: "txn123", - shopId: "shop123", - time: new DateTimeOffset(2020, 7, 12, + var eventReq = new Event + { + TransactionId = "txn123", + ShopId = "shop123", + Time = new DateTimeOffset(2020, 7, 12, 15, 30, 0, 0, new TimeSpan(2, 0, 0)), - type: EventType.AccountCreation, - party: EventParty.Agent - ); + Type = EventType.AccountCreation, + Party = EventParty.Agent + }; var json = JsonSerializer.Serialize(eventReq); var comparer = new JsonElementComparer(); diff --git a/MaxMind.MinFraud.UnitTest/Request/MinFraudRequestTest.cs b/MaxMind.MinFraud.UnitTest/Request/MinFraudRequestTest.cs index bcae9875..9e6ab395 100644 --- a/MaxMind.MinFraud.UnitTest/Request/MinFraudRequestTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/MinFraudRequestTest.cs @@ -1,10 +1,8 @@ -using MaxMind.MinFraud.Request; +using MaxMind.MinFraud.Request; using System.Collections.Generic; using System.Net; using Xunit; -#pragma warning disable CS0618 // Type or member is obsolete - namespace MaxMind.MinFraud.UnitTest.Request { public class MinFraudRequestTest @@ -12,21 +10,21 @@ public class MinFraudRequestTest [Fact] public void TestAccount() { - var request = new Transaction(account: new Account(userId: "1")); + var request = new Transaction { Account = new Account { UserId = "1" } }; Assert.Equal("1", request.Account!.UserId); } [Fact] public void TestBilling() { - var request = new Transaction(billing: new Billing(address: "add")); + var request = new Transaction { Billing = new Billing { Address = "add" } }; Assert.Equal("add", request.Billing!.Address); } [Fact] public void TestCreditCard() { - var request = new Transaction(creditCard: new CreditCard(bankName: "name")); + var request = new Transaction { CreditCard = new CreditCard { BankName = "name" } }; Assert.Equal("name", request.CreditCard!.BankName); } @@ -34,51 +32,53 @@ public void TestCreditCard() public void TestDevice() { var ip = IPAddress.Parse("1.1.1.1"); - var request = new Transaction(device: new Device(ip)); + var request = new Transaction { Device = new Device { IPAddress = ip } }; Assert.Equal(ip, request.Device?.IPAddress); } [Fact] public void TestEmail() { - var request = new Transaction(email: new Email(domain: "test.com")); + var request = new Transaction { Email = new Email { Domain = "test.com" } }; Assert.Equal("test.com", request.Email!.Domain); } [Fact] public void TestEvent() { - var request = new Transaction(userEvent: new Event(shopId: "1")); + var request = new Transaction { Event = new Event { ShopId = "1" } }; Assert.Equal("1", request.Event!.ShopId); } [Fact] public void TestOrder() { - var request = new Transaction(order: new Order(affiliateId: "af1")); + var request = new Transaction { Order = new Order { AffiliateId = "af1" } }; Assert.Equal("af1", request.Order!.AffiliateId); } [Fact] public void TestPayment() { - var request = new Transaction(payment: new Payment(declineCode: "d")); + var request = new Transaction { Payment = new Payment { DeclineCode = "d" } }; Assert.Equal("d", request.Payment!.DeclineCode); } [Fact] public void TestShipping() { - var request = new Transaction(shipping: new Shipping(lastName: "l")); + var request = new Transaction { Shipping = new Shipping { LastName = "l" } }; Assert.Equal("l", request.Shipping!.LastName); } [Fact] public void TestShoppingCart() { - var request = new Transaction( - shoppingCart: new List { new ShoppingCartItem(itemId: "1") }); + var request = new Transaction + { + ShoppingCart = new List { new ShoppingCartItem { ItemId = "1" } } + }; Assert.Equal("1", request.ShoppingCart![0].ItemId); } } -} \ No newline at end of file +} diff --git a/MaxMind.MinFraud.UnitTest/Request/OrderTest.cs b/MaxMind.MinFraud.UnitTest/Request/OrderTest.cs index 8365deaa..aeb89933 100644 --- a/MaxMind.MinFraud.UnitTest/Request/OrderTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/OrderTest.cs @@ -1,9 +1,7 @@ -using MaxMind.MinFraud.Request; +using MaxMind.MinFraud.Request; using System; using Xunit; -#pragma warning disable CS0618 // Type or member is obsolete - namespace MaxMind.MinFraud.UnitTest.Request { public class OrderTest @@ -11,59 +9,59 @@ public class OrderTest [Fact] public void TestAmount() { - var order = new Order(amount: 1.1m); + var order = new Order { Amount = 1.1m }; Assert.Equal(1.1m, order.Amount); } [Fact] public void TestCurrency() { - var order = new Order(currency: "USD"); + var order = new Order { Currency = "USD" }; Assert.Equal("USD", order.Currency); } [Fact] public void TestCurrencyWithDigits() { - Assert.Throws(() => new Order(currency: "US1")); + Assert.Throws(() => new Order { Currency = "US1" }); } [Fact] public void TestCurrencyThatIsTooShort() { - Assert.Throws(() => new Order(currency: "US")); + Assert.Throws(() => new Order { Currency = "US" }); } [Fact] public void TestCurrencyThatIsTooLong() { - Assert.Throws(() => new Order(currency: "USDE")); + Assert.Throws(() => new Order { Currency = "USDE" }); } [Fact] public void TestCurrencyInWrongCase() { - Assert.Throws(() => new Order(currency: "usd")); + Assert.Throws(() => new Order { Currency = "usd" }); } [Fact] public void TestDiscountCode() { - var order = new Order(discountCode: "dsc"); + var order = new Order { DiscountCode = "dsc" }; Assert.Equal("dsc", order.DiscountCode); } [Fact] public void TestAffiliateId() { - var order = new Order(affiliateId: "af"); + var order = new Order { AffiliateId = "af" }; Assert.Equal("af", order.AffiliateId); } [Fact] public void TestSubaffiliateId() { - var order = new Order(subaffiliateId: "saf"); + var order = new Order { SubaffiliateId = "saf" }; Assert.Equal("saf", order.SubaffiliateId); } @@ -71,21 +69,21 @@ public void TestSubaffiliateId() public void TestReferrerUri() { var uri = new Uri("http://www.mm.com/"); - var order = new Order(referrerUri: uri); + var order = new Order { ReferrerUri = uri }; Assert.Equal(uri, order.ReferrerUri); } [Fact] public void TestIsGift() { - var order = new Order(isGift: true); + var order = new Order { IsGift = true }; Assert.True(order.IsGift); } [Fact] public void TestHasGiftMessage() { - var order = new Order(hasGiftMessage: true); + var order = new Order { HasGiftMessage = true }; Assert.True(order.HasGiftMessage); } } diff --git a/MaxMind.MinFraud.UnitTest/Request/PaymentTest.cs b/MaxMind.MinFraud.UnitTest/Request/PaymentTest.cs index 97269688..51485e04 100644 --- a/MaxMind.MinFraud.UnitTest/Request/PaymentTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/PaymentTest.cs @@ -1,8 +1,6 @@ -using MaxMind.MinFraud.Request; +using MaxMind.MinFraud.Request; using Xunit; -#pragma warning disable CS0618 // Type or member is obsolete - namespace MaxMind.MinFraud.UnitTest.Request { public class PaymentTest @@ -10,56 +8,56 @@ public class PaymentTest [Fact] public void TestProcessor() { - var payment = new Payment(processor: PaymentProcessor.Adyen); + var payment = new Payment { Processor = PaymentProcessor.Adyen }; Assert.Equal(PaymentProcessor.Adyen, payment.Processor); } [Fact] public void TestWasAuthorized() { - var payment = new Payment(wasAuthorized: true); + var payment = new Payment { WasAuthorized = true }; Assert.True(payment.WasAuthorized); } [Fact] public void TestDeclineCode() { - var payment = new Payment(declineCode: "declined"); + var payment = new Payment { DeclineCode = "declined" }; Assert.Equal("declined", payment.DeclineCode); } [Fact] public void TestBankDebitMethod() { - var payment = new Payment(method: PaymentMethod.BankDebit); + var payment = new Payment { Method = PaymentMethod.BankDebit }; Assert.Equal(PaymentMethod.BankDebit, payment.Method); } [Fact] public void TestCardMethod() { - var payment = new Payment(method: PaymentMethod.Card); + var payment = new Payment { Method = PaymentMethod.Card }; Assert.Equal(PaymentMethod.Card, payment.Method); } [Fact] public void TestCryptoMethod() { - var payment = new Payment(method: PaymentMethod.Crypto); + var payment = new Payment { Method = PaymentMethod.Crypto }; Assert.Equal(PaymentMethod.Crypto, payment.Method); } [Fact] public void TestDigitalWalletMethod() { - var payment = new Payment(method: PaymentMethod.DigitalWallet); + var payment = new Payment { Method = PaymentMethod.DigitalWallet }; Assert.Equal(PaymentMethod.DigitalWallet, payment.Method); } [Fact] public void TestRewardsMethod() { - var payment = new Payment(method: PaymentMethod.Rewards); + var payment = new Payment { Method = PaymentMethod.Rewards }; Assert.Equal(PaymentMethod.Rewards, payment.Method); } } diff --git a/MaxMind.MinFraud.UnitTest/Request/ShippingTest.cs b/MaxMind.MinFraud.UnitTest/Request/ShippingTest.cs index 661dc658..9209e618 100644 --- a/MaxMind.MinFraud.UnitTest/Request/ShippingTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/ShippingTest.cs @@ -1,4 +1,4 @@ -#region +#region using MaxMind.MinFraud.Request; using System; @@ -6,8 +6,6 @@ #endregion -#pragma warning disable CS0618 // Type or member is obsolete - namespace MaxMind.MinFraud.UnitTest.Request { public class ShippingTest @@ -16,83 +14,84 @@ public class ShippingTest [Fact] public void TestFirstName() { - var loc = new Shipping( - firstName: "frst" - ); + var loc = new Shipping + { + FirstName = "frst" + }; Assert.Equal("frst", loc.FirstName); } [Fact] public void TestLastName() { - var loc = new Shipping(lastName: "last"); + var loc = new Shipping { LastName = "last" }; Assert.Equal("last", loc.LastName); } [Fact] public void TestCompany() { - var loc = new Shipping(company: "company"); + var loc = new Shipping { Company = "company" }; Assert.Equal("company", loc.Company); } [Fact] public void TestAddress() { - var loc = new Shipping(address: "addr"); + var loc = new Shipping { Address = "addr" }; Assert.Equal("addr", loc.Address); } [Fact] public void TestAddress2() { - var loc = new Shipping(address2: "addr2"); + var loc = new Shipping { Address2 = "addr2" }; Assert.Equal("addr2", loc.Address2); } [Fact] public void TestCity() { - var loc = new Shipping(city: "Pdx"); + var loc = new Shipping { City = "Pdx" }; Assert.Equal("Pdx", loc.City); } [Fact] public void TestRegion() { - var loc = new Shipping(region: "MN"); + var loc = new Shipping { Region = "MN" }; Assert.Equal("MN", loc.Region); } [Fact] public void TestCountry() { - var loc = new Shipping(country: "US"); + var loc = new Shipping { Country = "US" }; Assert.Equal("US", loc.Country); } [Fact] public void TestCountryThatIsTooLong() { - Assert.Throws(() => new Shipping(country: "USA")); + Assert.Throws(() => new Shipping { Country = "USA" }); } [Fact] public void TestCountryWithNumbers() { - Assert.Throws(() => new Shipping(country: "U1")); + Assert.Throws(() => new Shipping { Country = "U1" }); } [Fact] public void TestCountryInWrongCase() { - Assert.Throws(() => new Shipping(country: "us")); + Assert.Throws(() => new Shipping { Country = "us" }); } [Fact] public void TestPostal() { - var loc = new Shipping(postal: "03231"); + var loc = new Shipping { Postal = "03231" }; Assert.Equal("03231", loc.Postal); } @@ -100,14 +99,14 @@ public void TestPostal() public void TestPhoneNumber() { var phone = "321-321-3213"; - var loc = new Shipping(phoneNumber: phone); + var loc = new Shipping { PhoneNumber = phone }; Assert.Equal(phone, loc.PhoneNumber); } [Fact] public void TestPhoneCountryCode() { - var loc = new Shipping(phoneCountryCode: "1"); + var loc = new Shipping { PhoneCountryCode = "1" }; Assert.Equal("1", loc.PhoneCountryCode); } @@ -116,8 +115,8 @@ public void TestPhoneCountryCode() [Fact] public void TestDeliverySpeed() { - var loc = new Shipping(deliverySpeed: ShippingDeliverySpeed.Expedited); + var loc = new Shipping { DeliverySpeed = ShippingDeliverySpeed.Expedited }; Assert.Equal(ShippingDeliverySpeed.Expedited, loc.DeliverySpeed); } } -} \ No newline at end of file +} diff --git a/MaxMind.MinFraud.UnitTest/Request/ShoppingCartItemTest.cs b/MaxMind.MinFraud.UnitTest/Request/ShoppingCartItemTest.cs index 85a0c712..9a12af1d 100644 --- a/MaxMind.MinFraud.UnitTest/Request/ShoppingCartItemTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/ShoppingCartItemTest.cs @@ -1,8 +1,6 @@ -using MaxMind.MinFraud.Request; +using MaxMind.MinFraud.Request; using Xunit; -#pragma warning disable CS0618 // Type or member is obsolete - namespace MaxMind.MinFraud.UnitTest.Request { public class ShoppingCartItemTest @@ -10,29 +8,29 @@ public class ShoppingCartItemTest [Fact] public void TestCategory() { - var item = new ShoppingCartItem(category: "cat1"); + var item = new ShoppingCartItem { Category = "cat1" }; Assert.Equal("cat1", item.Category); } [Fact] public void TestItemId() { - var item = new ShoppingCartItem(itemId: "id5"); + var item = new ShoppingCartItem { ItemId = "id5" }; Assert.Equal("id5", item.ItemId); } [Fact] public void TestQuantity() { - var item = new ShoppingCartItem(quantity: 100); + var item = new ShoppingCartItem { Quantity = 100 }; Assert.Equal(100, item.Quantity); } [Fact] public void TestPrice() { - var item = new ShoppingCartItem(price: 10m); + var item = new ShoppingCartItem { Price = 10m }; Assert.Equal(10m, item.Price); } } -} \ No newline at end of file +} diff --git a/MaxMind.MinFraud.UnitTest/Request/TransactionReportTest.cs b/MaxMind.MinFraud.UnitTest/Request/TransactionReportTest.cs index b3977b80..9ed98521 100644 --- a/MaxMind.MinFraud.UnitTest/Request/TransactionReportTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/TransactionReportTest.cs @@ -1,54 +1,14 @@ -using MaxMind.MinFraud.Request; +using MaxMind.MinFraud.Request; using System; using System.Net; using Xunit; -#pragma warning disable CS0618 // Type or member is obsolete - namespace MaxMind.MinFraud.UnitTest.Request { public class TransactionReportTest { private IPAddress IP { get; } = IPAddress.Parse("1.1.1.1"); - [Fact] - public void TestRequired() - { - var maxmindId = "12345678"; - var minfraudId = Guid.NewGuid(); - var tag = TransactionReportTag.NotFraud; - var transactionId = "txn123"; - - TransactionReport report; - - // ipAddress supplied as identifier - Assert.Throws(() => new TransactionReport(tag: tag, ipAddress: null)); - report = new TransactionReport(tag: tag, ipAddress: IP); - Assert.Equal(TransactionReportTag.NotFraud, report.Tag); - Assert.Equal(IP, report.IPAddress); - - // maxmindId supplied as identifier - Assert.Throws(() => new TransactionReport(tag: tag, maxmindId: "")); - report = new TransactionReport(tag: tag, ipAddress: null, maxmindId: maxmindId); - Assert.Equal(TransactionReportTag.NotFraud, report.Tag); - Assert.Null(report.IPAddress); - Assert.Equal(maxmindId, report.MaxMindId); - - // minfraudId supplied as identifier - Assert.Throws(() => new TransactionReport(tag: tag, minfraudId: Guid.Empty)); - report = new TransactionReport(tag: tag, ipAddress: null, minfraudId: minfraudId); - Assert.Equal(TransactionReportTag.NotFraud, report.Tag); - Assert.Null(report.IPAddress); - Assert.Equal(minfraudId, report.MinFraudId); - - // tranactionId supplied as identifier - Assert.Throws(() => new TransactionReport(tag: tag, transactionId: "")); - report = new TransactionReport(tag: tag, ipAddress: null, transactionId: transactionId); - Assert.Equal(TransactionReportTag.NotFraud, report.Tag); - Assert.Null(report.IPAddress); - Assert.Equal(transactionId, report.TransactionId); - } - [Fact] public void TestAll() { @@ -58,14 +18,16 @@ public void TestAll() var notes = "This was an account takeover."; var transactionId = "txn123"; - var report = new TransactionReport( - ipAddress: IP, - tag: TransactionReportTag.Chargeback, - chargebackCode: chargebackCode, - maxmindId: maxmindId, - minfraudId: minfraudId, - notes: notes, - transactionId: transactionId); + var report = new TransactionReport + { + IPAddress = IP, + Tag = TransactionReportTag.Chargeback, + ChargebackCode = chargebackCode, + MaxMindId = maxmindId, + MinFraudId = minfraudId, + Notes = notes, + TransactionId = transactionId + }; Assert.Equal(IP, report.IPAddress); Assert.Equal(TransactionReportTag.Chargeback, report.Tag); @@ -81,8 +43,11 @@ public void TestAll() [InlineData("abcd12345")] public void TestMaxMindIdIsInvalid(string? maxmindId) { - Assert.Throws(() => new TransactionReport( - tag: TransactionReportTag.SpamOrAbuse, maxmindId: maxmindId)); + Assert.Throws(() => new TransactionReport + { + Tag = TransactionReportTag.SpamOrAbuse, + MaxMindId = maxmindId + }); } } -} \ No newline at end of file +} diff --git a/MaxMind.MinFraud.UnitTest/WebServiceClientTest.cs b/MaxMind.MinFraud.UnitTest/WebServiceClientTest.cs index 5bc24fe4..d723d2db 100644 --- a/MaxMind.MinFraud.UnitTest/WebServiceClientTest.cs +++ b/MaxMind.MinFraud.UnitTest/WebServiceClientTest.cs @@ -81,7 +81,6 @@ public async Task TestFullFactorsRequestUsingConstructors() Assert.Equal("London", response.IPAddress.City.Name); } -#pragma warning disable CS0618 // Type or member is obsolete [Fact] public async Task TestFullReportRequest() { @@ -91,19 +90,33 @@ public async Task TestFullReportRequest() "application/json", "" ); - var request = new TransactionReport( - ipAddress: IPAddress.Parse("1.1.1.1"), - tag: TransactionReportTag.SuspectedFraud, - chargebackCode: "AA", - maxmindId: "a1b2c3d4", - minfraudId: new Guid("9194a1ac-0a81-475a-bf81-9bf8543a3f8f"), - notes: "note", - transactionId: "txn1"); + var request = new TransactionReport + { + IPAddress = IPAddress.Parse("1.1.1.1"), + Tag = TransactionReportTag.SuspectedFraud, + ChargebackCode = "AA", + MaxMindId = "a1b2c3d4", + MinFraudId = new Guid("9194a1ac-0a81-475a-bf81-9bf8543a3f8f"), + Notes = "note", + TransactionId = "txn1" + }; var exception = await Record.ExceptionAsync(() => client.ReportAsync(request)); Assert.Null(exception); } -#pragma warning restore CS0618 // Type or member is obsolete + + [Fact] + public async Task TestReportRequiresIdentifier() + { + var client = CreateClient( + "transactions/report", + HttpStatusCode.NoContent, + "application/json", + "" + ); + var report = new TransactionReport { Tag = TransactionReportTag.NotFraud }; + await Assert.ThrowsAsync(() => client.ReportAsync(report)); + } [Fact] public async Task TestWebServiceClientOptionsConstructor() From 2cd73cc6283cbe2a96eb26e83373e2683dfeb3fd Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 20 Mar 2026 16:53:03 +0000 Subject: [PATCH 04/14] Add mise config to manage .NET SDK versions Co-Authored-By: Claude Opus 4.6 (1M context) --- mise.lock | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ mise.toml | 20 +++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 mise.lock create mode 100644 mise.toml diff --git a/mise.lock b/mise.lock new file mode 100644 index 00000000..b3c22404 --- /dev/null +++ b/mise.lock @@ -0,0 +1,88 @@ +# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html + +[[tools.dotnet]] +version = "10.0.201" +backend = "core:dotnet" + +[[tools.dotnet]] +version = "9.0.312" +backend = "core:dotnet" + +[[tools.dotnet]] +version = "8.0.419" +backend = "core:dotnet" + +[[tools."github:houseabsolute/precious"]] +version = "0.10.2" +backend = "github:houseabsolute/precious" + +[tools."github:houseabsolute/precious"."platforms.linux-arm64"] +checksum = "sha256:8fbaead9f9626170549c3121e67d1bc81193b3bb086e29576f548aefa839fcc4" +url = "https://github.com/houseabsolute/precious/releases/download/v0.10.2/precious-Linux-musl-arm64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/345520042" + +[tools."github:houseabsolute/precious"."platforms.linux-arm64-musl"] +checksum = "sha256:8fbaead9f9626170549c3121e67d1bc81193b3bb086e29576f548aefa839fcc4" +url = "https://github.com/houseabsolute/precious/releases/download/v0.10.2/precious-Linux-musl-arm64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/345520042" + +[tools."github:houseabsolute/precious"."platforms.linux-x64"] +checksum = "sha256:3d717d906db338f63017766b07982dc9055773e1b3bec6d3f432d1f0ad9676bb" +url = "https://github.com/houseabsolute/precious/releases/download/v0.10.2/precious-Linux-musl-x86_64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/345519861" + +[tools."github:houseabsolute/precious"."platforms.linux-x64-musl"] +checksum = "sha256:3d717d906db338f63017766b07982dc9055773e1b3bec6d3f432d1f0ad9676bb" +url = "https://github.com/houseabsolute/precious/releases/download/v0.10.2/precious-Linux-musl-x86_64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/345519861" + +[tools."github:houseabsolute/precious"."platforms.macos-arm64"] +checksum = "sha256:04157c64459bb6ab029295b21b112077040ad2575b34508d84b19a839551cddb" +url = "https://github.com/houseabsolute/precious/releases/download/v0.10.2/precious-macOS-arm64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/345519985" + +[tools."github:houseabsolute/precious"."platforms.macos-x64"] +checksum = "sha256:9932defd246d0771530357463bdb55582557fd7381853cb4dc2074e36ad0cc84" +url = "https://github.com/houseabsolute/precious/releases/download/v0.10.2/precious-macOS-x86_64.tar.gz" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/345519772" + +[tools."github:houseabsolute/precious"."platforms.windows-x64"] +checksum = "sha256:9d683d1730e302c646ccb90a23d313e7a548c8b23b5abf7d24e19ff6befe763d" +url = "https://github.com/houseabsolute/precious/releases/download/v0.10.2/precious-Windows-msvc-x86_64.zip" +url_api = "https://api.github.com/repos/houseabsolute/precious/releases/assets/345520544" + +[[tools.node]] +version = "25.8.1" +backend = "core:node" + +[tools.node."platforms.linux-arm64"] +checksum = "sha256:d990ec3c21ce8bdb6f76ed4e1c875d6e3e4b75a02d018e85df0662c0bad83b53" +url = "https://nodejs.org/dist/v25.8.1/node-v25.8.1-linux-arm64.tar.gz" + +[tools.node."platforms.linux-arm64-musl"] +checksum = "sha256:d990ec3c21ce8bdb6f76ed4e1c875d6e3e4b75a02d018e85df0662c0bad83b53" +url = "https://nodejs.org/dist/v25.8.1/node-v25.8.1-linux-arm64.tar.gz" + +[tools.node."platforms.linux-x64"] +checksum = "sha256:6fe3b8fa448579f728f7a0e5bbb3ab6a352d2c6307e13ae37a86106a3e4c9aaf" +url = "https://nodejs.org/dist/v25.8.1/node-v25.8.1-linux-x64.tar.gz" + +[tools.node."platforms.linux-x64-musl"] +checksum = "sha256:6fe3b8fa448579f728f7a0e5bbb3ab6a352d2c6307e13ae37a86106a3e4c9aaf" +url = "https://nodejs.org/dist/v25.8.1/node-v25.8.1-linux-x64.tar.gz" + +[tools.node."platforms.macos-arm64"] +checksum = "sha256:c667629236e3213616f0917b84eb52706e213c0e8a2312402335fff6fc7463c4" +url = "https://nodejs.org/dist/v25.8.1/node-v25.8.1-darwin-arm64.tar.gz" + +[tools.node."platforms.macos-x64"] +checksum = "sha256:1e5ebf69955e01216f5c60b9c989d1bdda8e5022e2f60c75e1baf309c5bff50e" +url = "https://nodejs.org/dist/v25.8.1/node-v25.8.1-darwin-x64.tar.gz" + +[tools.node."platforms.windows-x64"] +checksum = "sha256:09a5a0dbb2f4cefa800880012810e2dfaac0016a62e75f064c4ab7f3606b2d78" +url = "https://nodejs.org/dist/v25.8.1/node-v25.8.1-win-x64.zip" + +[[tools."npm:prettier"]] +version = "3.8.1" +backend = "npm:prettier" diff --git a/mise.toml b/mise.toml new file mode 100644 index 00000000..234f6553 --- /dev/null +++ b/mise.toml @@ -0,0 +1,20 @@ +[settings] +experimental = true +lockfile = true +disable_backends = [ + "asdf", + "vfox", +] + +[tools] +dotnet = ["latest", "9", "8"] +"github:houseabsolute/precious" = "latest" +node = "latest" +"npm:prettier" = "latest" + +[hooks] +enter = "mise install --quiet --locked" + +[[watch_files]] +patterns = ["mise.toml", "mise.lock"] +run = "mise install --quiet --locked" From 3a31030b60c47bd00e73de2d16c7e12c24f9c8de Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 20 Mar 2026 16:53:30 +0000 Subject: [PATCH 05/14] Add precious and prettier config with GitHub Action Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/precious.yml | 23 +++++++++++++++++++++++ .precious.toml | 30 ++++++++++++++++++++++++++++++ .prettierrc.json | 6 ++++++ 3 files changed, 59 insertions(+) create mode 100644 .github/workflows/precious.yml create mode 100644 .precious.toml create mode 100644 .prettierrc.json diff --git a/.github/workflows/precious.yml b/.github/workflows/precious.yml new file mode 100644 index 00000000..df1c5427 --- /dev/null +++ b/.github/workflows/precious.yml @@ -0,0 +1,23 @@ +name: precious +on: + push: + pull_request: + schedule: + - cron: "5 5 * * SUN" +permissions: {} +jobs: + precious: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Setup mise + uses: jdx/mise-action@6d1e696aa24c1aa1bcc1adea0212707c71ab78a8 # v3.6.1 + with: + cache: true + env: + MISE_JOBS: "1" # Avoids race condition with parallel dotnet SDK installs + - name: Run precious lint + run: precious lint --all diff --git a/.precious.toml b/.precious.toml new file mode 100644 index 00000000..15e61fe5 --- /dev/null +++ b/.precious.toml @@ -0,0 +1,30 @@ +exclude = [ + ".git", +] + +[commands.prettier-markdown] +type = "both" +cmd = ["prettier", "--prose-wrap", "always"] +lint-flags = ["--check"] +tidy-flags = ["--write"] +path-args = "absolute-file" +include = "**/*.md" +ok-exit-codes = 0 + +[commands.prettier-json] +type = "both" +cmd = ["prettier"] +lint-flags = ["--check"] +tidy-flags = ["--write"] +path-args = "absolute-file" +include = "**/*.json" +ok-exit-codes = 0 + +[commands.prettier-yaml] +type = "both" +cmd = ["prettier"] +lint-flags = ["--check"] +tidy-flags = ["--write"] +path-args = "absolute-file" +include = ["**/*.yml", "**/*.yaml"] +ok-exit-codes = 0 diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..70ac1753 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "proseWrap": "always", + "printWidth": 80, + "tabWidth": 2, + "useTabs": false +} From 1b6b136e06000743fef2a3228ae41ae411269f0c Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 20 Mar 2026 16:55:21 +0000 Subject: [PATCH 06/14] Run prettier on existing markdown and YAML files Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/dependabot.yml | 6 +- .github/workflows/codeql-analysis.yml | 95 +- .github/workflows/test.yml | 2 +- CLAUDE.md | 112 ++- .../TestData/factors-response.json | 4 +- .../TestData/full-request.json | 7 +- .../TestData/report-request.json | 2 +- .../TestData/score-response.json | 2 +- README.dev.md | 3 +- README.md | 108 +-- releasenotes.md | 849 ++++++++---------- 11 files changed, 594 insertions(+), 596 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cf54de77..df05ac80 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,10 +2,10 @@ version: 2 updates: - package-ecosystem: nuget directories: - - '**/*' + - "**/*" schedule: interval: daily - time: '14:00' + time: "14:00" open-pull-requests-limit: 10 cooldown: default-days: 7 @@ -13,6 +13,6 @@ updates: directory: / schedule: interval: daily - time: '14:00' + time: "14:00" cooldown: default-days: 7 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 56db1554..33c7cfa2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -3,62 +3,61 @@ name: "Code scanning - action" on: push: branches-ignore: - - 'dependabot/**' + - "dependabot/**" pull_request: schedule: - - cron: '0 10 * * 5' + - cron: "0 10 * * 5" jobs: CodeQL-Build: - runs-on: ubuntu-latest permissions: security-events: write steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - persist-credentials: false - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - - name: Setup .NET - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 - with: - dotnet-version: | - 8.0.x - 9.0.x - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 - # Override language selection by uncommenting this and choosing your languages - # with: - # languages: go, javascript, csharp, python, cpp, java - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + persist-credentials: false + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + - name: Setup .NET + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: | + 8.0.x + 9.0.x + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 + # Override language selection by uncommenting this and choosing your languages + # with: + # languages: go, javascript, csharp, python, cpp, java + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 67d23c97..b271f719 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ on: push: pull_request: schedule: - - cron: '3 20 * * SUN' + - cron: "3 20 * * SUN" permissions: {} diff --git a/CLAUDE.md b/CLAUDE.md index 218b3846..316fed7f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,18 +1,26 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to Claude Code (claude.ai/code) when working with +code in this repository. ## Project Overview -**minfraud-api-dotnet** is MaxMind's official .NET client library for the minFraud fraud detection web services: +**minfraud-api-dotnet** is MaxMind's official .NET client library for the +minFraud fraud detection web services: + - **minFraud Score**: Risk score for transactions - **minFraud Insights**: Score plus GeoIP2 data and device/email intelligence -- **minFraud Factors**: All Insights data plus risk reasons and deprecated subscores -- **Transaction Reporting API**: Report chargebacks and fraud to improve minFraud accuracy +- **minFraud Factors**: All Insights data plus risk reasons and deprecated + subscores +- **Transaction Reporting API**: Report chargebacks and fraud to improve + minFraud accuracy -The library provides both synchronous and asynchronous methods, supports ASP.NET Core dependency injection, and integrates deeply with MaxMind's GeoIP2 library for IP intelligence. +The library provides both synchronous and asynchronous methods, supports ASP.NET +Core dependency injection, and integrates deeply with MaxMind's GeoIP2 library +for IP intelligence. **Key Technologies:** + - .NET 10.0, .NET 9.0, .NET 8.0, .NET Standard 2.1, and .NET Standard 2.0 - System.Text.Json for JSON serialization/deserialization - MaxMind.GeoIP2 library (critical dependency for response models) @@ -22,6 +30,7 @@ The library provides both synchronous and asynchronous methods, supports ASP.NET ## Development Commands ### Building + ```bash # Build main library dotnet build MaxMind.MinFraud @@ -34,6 +43,7 @@ dotnet build MaxMind.MinFraud.sln ``` ### Testing + ```bash # Run all tests dotnet test MaxMind.MinFraud.UnitTest/MaxMind.MinFraud.UnitTest.csproj @@ -46,6 +56,7 @@ dotnet test -v normal ``` ### Other Commands + ```bash # Check for outdated dependencies dotnet-outdated @@ -85,7 +96,8 @@ MaxMind.MinFraud.UnitTest/ #### 1. **Request Model Composition with Init-Only Properties** -The `Transaction` model aggregates 11 optional sub-models using init-only properties: +The `Transaction` model aggregates 11 optional sub-models using init-only +properties: ```csharp var transaction = new Transaction { @@ -97,6 +109,7 @@ var transaction = new Transaction { ``` **Key Points:** + - All request properties use `init` setters (immutable after construction) - Validation happens immediately in property setters (fail-fast) - JSON serialization uses `[JsonPropertyName("snake_case")]` attributes @@ -115,9 +128,12 @@ Score (base) ``` **Key Points:** + - Each level adds more detail without breaking compatibility -- Interface-based polymorphism for `IIPAddress` (Score uses minimal, Insights uses full) -- Response models inherit from GeoIP2 models (e.g., `IPAddress : InsightsResponse`) +- Interface-based polymorphism for `IIPAddress` (Score uses minimal, Insights + uses full) +- Response models inherit from GeoIP2 models (e.g., + `IPAddress : InsightsResponse`) - All response properties are init-only with default empty objects (never null) #### 3. **GeoIP2 Integration via Inheritance** @@ -135,13 +151,16 @@ public sealed class IPAddress : InsightsResponse, IIPAddress ``` **Implications:** + - Changes to GeoIP2 models affect minFraud responses - When adding fields, check if they belong in GeoIP2 or minFraud layer - Use GeoIP2 patterns for location-related data #### 4. **Email Address Normalization** -The `Email` class performs sophisticated normalization when `HashAddress = true`: +The `Email` class performs sophisticated normalization when +`HashAddress = true`: + - Domain typo fixes: `gmai.com` → `gmail.com` - TLD typo fixes: `example.comm` → `example.com` - Equivalent domains: `googlemail.com` → `gmail.com` @@ -166,6 +185,7 @@ var customInputs = new CustomInputs.Builder { ``` **Key Points:** + - One-time use (builder invalidated after `Build()`) - Type-safe overloads for supported types - Validation: key format `^[a-z0-9_]{1,25}$`, numeric range ±10^13 @@ -186,6 +206,7 @@ await client.ReportAsync(report); ``` **Key Points:** + - Thread-safe, designed for singleton/reuse across requests - Implements `IDisposable` for proper HttpClient lifecycle - Base path: `https://minfraud.maxmind.com/minfraud/v2.0/{endpoint}` @@ -216,6 +237,7 @@ public MyController(WebServiceClient client) { ... } ## Testing Conventions ### Test Structure + - Tests use xUnit framework - HTTP mocking via RichardSzalay.MockHttp - JSON fixtures in `TestData/` for response deserialization tests @@ -224,6 +246,7 @@ public MyController(WebServiceClient client) { ... } ### Test Patterns **1. HTTP Mocking:** + ```csharp var mockHttp = new MockHttpMessageHandler(); mockHttp.When(HttpMethod.Post, "https://minfraud.maxmind.com/minfraud/v2.0/score") @@ -233,6 +256,7 @@ var client = new WebServiceClient(new HttpClient(mockHttp), options); ``` **2. Validation Testing:** + ```csharp [Theory] [InlineData(-1.0)] // Invalid @@ -240,14 +264,16 @@ var client = new WebServiceClient(new HttpClient(mockHttp), options); public void TestSessionAge(double age) { ... } ``` -**3. JSON Round-Trip Testing:** -Test that response objects can be serialized back to JSON and match the original structure. +**3. JSON Round-Trip Testing:** Test that response objects can be serialized +back to JSON and match the original structure. ## Working with This Codebase ### Adding New Fields to Existing Request Models -1. **Add property** with JSON attribute and validation using C# 14 `field` keyword: +1. **Add property** with JSON attribute and validation using C# 14 `field` + keyword: + ```csharp [JsonPropertyName("field_name")] public double? FieldName { @@ -261,9 +287,10 @@ Test that response objects can be serialized back to JSON and match the original } ``` - **Note:** Use the `field` keyword (C# 14) instead of explicit backing fields. This eliminates - boilerplate while maintaining validation logic. Only use explicit backing fields if you need - cross-property assignments (e.g., Email.cs where `_domain` is set from `Address`). + **Note:** Use the `field` keyword (C# 14) instead of explicit backing fields. + This eliminates boilerplate while maintaining validation logic. Only use + explicit backing fields if you need cross-property assignments (e.g., + Email.cs where `_domain` is set from `Address`). 2. **Update `releasenotes.md`** with the change @@ -276,13 +303,16 @@ Test that response objects can be serialized back to JSON and match the original - minFraud: Risk, device intelligence, email intelligence → Add here 2. **Add property** with init-only setter: + ```csharp [JsonInclude] [JsonPropertyName("field_name")] public TypeName? FieldName { get; init; } ``` -3. **For MINOR version releases**: Add deprecated constructor matching old signature to avoid breaking changes: +3. **For MINOR version releases**: Add deprecated constructor matching old + signature to avoid breaking changes: + ```csharp // New constructor with added parameter public ResponseClass( @@ -307,6 +337,7 @@ Test that response objects can be serialized back to JSON and match the original When MaxMind adds new payment processors, event types, etc.: 1. **Add to enum** (e.g., `PaymentProcessor`, `EventType`): + ```csharp [EnumMember(Value = "new_processor")] NewProcessor, @@ -316,11 +347,17 @@ When MaxMind adds new payment processors, event types, etc.: 3. **No tests needed** for simple enum additions -**Forward Compatibility**: The `EnumMemberValueConverter` gracefully handles unknown enum values from the API by returning `null` instead of throwing exceptions. This ensures that when MaxMind adds new enum values to the web service, existing client versions won't break - the new value will simply be treated as null. This is critical for maintaining backward compatibility and preventing client breakage during API evolution. +**Forward Compatibility**: The `EnumMemberValueConverter` gracefully handles +unknown enum values from the API by returning `null` instead of throwing +exceptions. This ensures that when MaxMind adds new enum values to the web +service, existing client versions won't break - the new value will simply be +treated as null. This is critical for maintaining backward compatibility and +preventing client breakage during API evolution. ### Exception Handling Strategy **Exception Hierarchy:** + ``` System.Exception ├─ IOException @@ -333,8 +370,11 @@ System.Exception ``` **Error Handling:** -- HTTP-level errors (network, 500s) → `HttpException` (inherits from `IOException`) -- Service errors (4xx) → Parse JSON error body → Specific `MinFraudException` subclass + +- HTTP-level errors (network, 500s) → `HttpException` (inherits from + `IOException`) +- Service errors (4xx) → Parse JSON error body → Specific `MinFraudException` + subclass - Validation errors (construction) → `ArgumentException` (fail-fast) - Non-fatal issues → `Score.Warnings` list (not thrown) @@ -343,13 +383,12 @@ System.Exception Always update `releasenotes.md` for user-facing changes: ```markdown -5.x.0 (YYYY-MM-DD) ------------------- +## 5.x.0 (YYYY-MM-DD) -* Added `NewProperty` property to `MaxMind.MinFraud.Response.ResponseClass`. +- Added `NewProperty` property to `MaxMind.MinFraud.Response.ResponseClass`. This provides information about... -* Added `NewValue` to the `EnumName` enum. -* The `OldProperty` property in `MaxMind.MinFraud.Model.ModelClass` has been +- Added `NewValue` to the `EnumName` enum. +- The `OldProperty` property in `MaxMind.MinFraud.Model.ModelClass` has been marked `Obsolete`. Please use `NewProperty` instead. ``` @@ -358,6 +397,7 @@ For MAJOR versions, prefix breaking changes with `**BREAKING:**` ### Deprecation Guidelines 1. **Use `[Obsolete]` attribute** with helpful messages: + ```csharp [Obsolete("Use NewProperty instead. This will be removed in v6.0.0.")] public string? OldProperty { get; init; } @@ -372,6 +412,7 @@ For MAJOR versions, prefix breaking changes with `**BREAKING:**` ### Code Quality Standards The project enforces strict standards: + - **EnforceCodeStyleInBuild**: Code style violations are build errors - **TreatWarningsAsErrors**: All warnings must be resolved - **EnableNETAnalyzers**: .NET code analyzers enabled @@ -384,7 +425,8 @@ The project enforces strict standards: This library targets multiple frameworks. When adding features: 1. **Prefer standard types** that work across all targets -2. **For newer types** (e.g., `DateOnly` in .NET 6+), use conditional compilation: +2. **For newer types** (e.g., `DateOnly` in .NET 6+), use conditional + compilation: ```csharp #if NET6_0_OR_GREATER public DateOnly? SomeDate { get; init; } @@ -395,12 +437,14 @@ This library targets multiple frameworks. When adding features: ## Common Patterns and Conventions ### Pattern: Immutability + - All request/response models use init-only properties - No public setters after construction - Defensive copying (readonly collections, immutable types) - Use `IReadOnlyList` for collections in responses ### Pattern: Validation in Property Setters with C# 14 `field` Keyword + ```csharp public double? Value { get => field; @@ -413,11 +457,12 @@ public double? Value { } ``` -The `field` keyword (C# 14) creates a compiler-synthesized backing field, eliminating the need for -explicit `private readonly` declarations. Use this for all properties with validation logic unless -you need cross-property assignments. +The `field` keyword (C# 14) creates a compiler-synthesized backing field, +eliminating the need for explicit `private readonly` declarations. Use this for +all properties with validation logic unless you need cross-property assignments. ### Pattern: Default Empty Objects (Never Null) + ```csharp public CreditCard CreditCard { get; init; } = new(); public IReadOnlyList Warnings { get; init; } = []; @@ -426,6 +471,7 @@ public IReadOnlyList Warnings { get; init; } = []; This prevents null reference exceptions and simplifies client code. ### Pattern: Computed Properties (Not Serialized) + ```csharp [JsonIgnore] public string? Username { get; init; } @@ -440,6 +486,7 @@ public string? UsernameMD5 { ``` ### Pattern: EnumMember for JSON Serialization + ```csharp public enum EventType { @@ -458,20 +505,25 @@ Requires `EnumMemberValueConverter` in JSON options. ### Critical Dependencies **MaxMind.GeoIP2** -- Provides base models for response classes (InsightsResponse, Location, Traits, etc.) + +- Provides base models for response classes (InsightsResponse, Location, Traits, + etc.) - minFraud responses inherit from and extend these models - Changes to GeoIP2 models can affect minFraud API surface **System.Text.Json** + - Modern JSON serialization (not Newtonsoft.Json) - Requires custom converters for enums, IP addresses, GeoIP2 types - Uses snake_case naming via `[JsonPropertyName]` attributes **Microsoft.Extensions.Options** + - ASP.NET Core configuration binding - Enables dependency injection pattern **IsExternalInit** (.NET Standard only) + - Polyfill for C# 9.0 `init` keyword on older frameworks ## Version Requirements @@ -491,4 +543,4 @@ Requires `EnumMemberValueConverter` in JSON options. --- -*Last Updated: 2025-11-18* +_Last Updated: 2025-11-18_ diff --git a/MaxMind.MinFraud.UnitTest/TestData/factors-response.json b/MaxMind.MinFraud.UnitTest/TestData/factors-response.json index 53cce8c9..3cc3ce4a 100644 --- a/MaxMind.MinFraud.UnitTest/TestData/factors-response.json +++ b/MaxMind.MinFraud.UnitTest/TestData/factors-response.json @@ -227,7 +227,7 @@ "country_mismatch": 0.07, "cvv_result": 0.08, "device": 0.09, - "email_address": 0.10, + "email_address": 0.1, "email_domain": 0.11, "email_local_part": 0.12, "issuer_id_number": 0.15, @@ -235,7 +235,7 @@ "phone_number": 0.17, "shipping_address": 0.18, "shipping_address_distance_to_ip_location": 0.19, - "time_of_day": 0.20 + "time_of_day": 0.2 }, "warnings": [ { diff --git a/MaxMind.MinFraud.UnitTest/TestData/full-request.json b/MaxMind.MinFraud.UnitTest/TestData/full-request.json index 6efac212..0330288a 100644 --- a/MaxMind.MinFraud.UnitTest/TestData/full-request.json +++ b/MaxMind.MinFraud.UnitTest/TestData/full-request.json @@ -47,7 +47,7 @@ "decline_code": "invalid number" }, "credit_card": { - "country": "US", + "country": "US", "issuer_id_number": "411111", "last_digits": "7643", "bank_name": "Bank of No Hope", @@ -90,11 +90,10 @@ ], "device": { "ip_address": "152.216.7.110", - "user_agent": - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36", + "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36", "accept_language": "en-US,en;q=0.8", "session_age": 3600.5, "session_id": "foobar", "tracking_token": "tst_abc123" } -} \ No newline at end of file +} diff --git a/MaxMind.MinFraud.UnitTest/TestData/report-request.json b/MaxMind.MinFraud.UnitTest/TestData/report-request.json index 5db728e0..fd9b16f7 100644 --- a/MaxMind.MinFraud.UnitTest/TestData/report-request.json +++ b/MaxMind.MinFraud.UnitTest/TestData/report-request.json @@ -6,4 +6,4 @@ "minfraud_id": "9194a1ac-0a81-475a-bf81-9bf8543a3f8f", "notes": "note", "transaction_id": "txn1" -} \ No newline at end of file +} diff --git a/MaxMind.MinFraud.UnitTest/TestData/score-response.json b/MaxMind.MinFraud.UnitTest/TestData/score-response.json index 0e84861b..1b5010d4 100644 --- a/MaxMind.MinFraud.UnitTest/TestData/score-response.json +++ b/MaxMind.MinFraud.UnitTest/TestData/score-response.json @@ -23,4 +23,4 @@ "warning": "Encountered value at \/account\/username_md5 that does meet the required constraints" } ] -} \ No newline at end of file +} diff --git a/README.dev.md b/README.dev.md index c8733cf5..d6eb5a61 100644 --- a/README.dev.md +++ b/README.dev.md @@ -9,4 +9,5 @@ To publish the to NuGet: 4. Run dev-bin/release.sh. This will build the project, create a version commit, and make a GitHub release. 5. Approve the workflow run in the GitHub Actions UI. -6. Verify the release on [NuGet](https://www.nuget.org/packages/MaxMind.MinFraud/). +6. Verify the release on + [NuGet](https://www.nuget.org/packages/MaxMind.MinFraud/). diff --git a/README.md b/README.md index c945c7f5..f274605b 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,40 @@ -# .NET API for MaxMind minFraud Services # +# .NET API for MaxMind minFraud Services [![NuGet](https://img.shields.io/nuget/v/MaxMind.MinFraud)](https://www.nuget.org/packages/MaxMind.MinFraud) [![API Docs](https://www.fuget.org/packages/MaxMind.MinFraud/badge.svg)](https://www.fuget.org/packages/MaxMind.MinFraud) -## Description ## +## Description -This package provides an API for the [MaxMind minFraud web services](https://dev.maxmind.com/minfraud?lang=en). -This includes minFraud Score, Insights, and Factors. It also includes our +This package provides an API for the +[MaxMind minFraud web services](https://dev.maxmind.com/minfraud?lang=en). This +includes minFraud Score, Insights, and Factors. It also includes our [minFraud Report Transaction API](https://dev.maxmind.com/minfraud/report-a-transaction?lang=en). -The legacy minFraud Standard and Premium services are not supported by this -API. +The legacy minFraud Standard and Premium services are not supported by this API. -## Requirements ## +## Requirements -This library works with .NET Framework version 4.6.1 and above and .NET -Standard 2.0 or above. +This library works with .NET Framework version 4.6.1 and above and .NET Standard +2.0 or above. This library depends on [GeoIP2](https://www.nuget.org/packages/MaxMind.GeoIP2/) and its dependencies. -## Installation ## +## Installation -### NuGet ### +### NuGet -We recommend installing this library with NuGet. To do this, type the -following into the Visual Studio Package Manager Console: +We recommend installing this library with NuGet. To do this, type the following +into the Visual Studio Package Manager Console: ``` install-package MaxMind.MinFraud ``` -## Usage ## +## Usage -This API uses the [async/await -feature](https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/) +This API uses the +[async/await feature](https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/) to provide non-blocking calls to the minFraud web services. To use this API, first create a new `WebServiceClient` object. The constructor @@ -44,29 +44,30 @@ takes your MaxMind account ID and license key: var client = new WebServiceClient(10, "LICENSEKEY"); ``` -To use the Sandbox web service instead of the production web service, you can provide the host argument: - +To use the Sandbox web service instead of the production web service, you can +provide the host argument: + ```csharp var client = new WebServiceClient( - accountId: 10, - licenseKey: "LICENSEKEY", + accountId: 10, + licenseKey: "LICENSEKEY", host: "sandbox.maxmind.com" ); ``` -You may also specify the fall-back locales, the host, or the timeout as -optional parameters. See the API docs for more information. +You may also specify the fall-back locales, the host, or the timeout as optional +parameters. See the API docs for more information. This object is safe to share across threads. If you are making multiple -requests, the object should be reused to so that new connections are not -created for each request. Once you have finished making requests, you -should dispose of the object to ensure the connections are closed and any -resources are promptly returned to the system. +requests, the object should be reused to so that new connections are not created +for each request. Once you have finished making requests, you should dispose of +the object to ensure the connections are closed and any resources are promptly +returned to the system. -### Making a minFraud Score, Insights, or Factors Request ### +### Making a minFraud Score, Insights, or Factors Request -Create a new `Transaction` object. This represents the transaction that you -are sending to minFraud: +Create a new `Transaction` object. This represents the transaction that you are +sending to minFraud: ```csharp var transaction = new Transaction @@ -85,9 +86,9 @@ var transaction = new Transaction }; ``` -After creating the request object, send a non-blocking minFraud Score request -by calling the `ScoreAsync` method using the `await` keyword from a method -marked as async: +After creating the request object, send a non-blocking minFraud Score request by +calling the `ScoreAsync` method using the `await` keyword from a method marked +as async: ```csharp var score = await client.ScoreAsync(transaction); @@ -110,9 +111,9 @@ the request fails, an exception will be thrown. See the API documentation for more details. -### ASP.NET Core Usage ### +### ASP.NET Core Usage -To use the web service API with HttpClient factory pattern as a +To use the web service API with HttpClient factory pattern as a [Typed client](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.1#typed-clients) you need to do the following: @@ -126,7 +127,8 @@ services.Configure(Configuration.GetSection("MinFraud") services.AddHttpClient(); ``` -2. Add configuration in your `appsettings.json` with your account ID and license key. +2. Add configuration in your `appsettings.json` with your account ID and license + key. ```jsonc ... @@ -163,8 +165,7 @@ public class MinFraudController : ControllerBase } ``` - -### Reporting a Transaction to MaxMind ### +### Reporting a Transaction to MaxMind If a transaction was scored incorrectly or you received a chargeback, you may report it to MaxMind. To do this, create a new `TransactionReport` object: @@ -191,28 +192,28 @@ Send the report by calling the `ReportAsync` method using the `await` keyword: await client.ReportAsync(report); ``` -The endpoint does not return any data and the method does not return a value. -If there is an error, an exception will be thrown. +The endpoint does not return any data and the method does not return a value. If +there is an error, an exception will be thrown. -### Exceptions ### +### Exceptions Thrown by the request models: -* `ArgumentException` - Thrown when invalid data is passed to the model +- `ArgumentException` - Thrown when invalid data is passed to the model constructor. Thrown by `WebServiceClient` method calls: -* `AuthenticationException` - Thrown when the server is unable to authenticate +- `AuthenticationException` - Thrown when the server is unable to authenticate the request, e.g., if the license key or account ID is invalid. -* `InsufficientFundsException` - Thrown when your account is out of funds. -* `PermissionRequiredException` - Thrown when your account does not have +- `InsufficientFundsException` - Thrown when your account is out of funds. +- `PermissionRequiredException` - Thrown when your account does not have permission to access the service. -* `InvalidRequestException` - Thrown when the server rejects the request for +- `InvalidRequestException` - Thrown when the server rejects the request for another reason such as invalid JSON in the POST. -* `MinFraudException` - Thrown when the server returns an unexpected response. +- `MinFraudException` - Thrown when the server returns an unexpected response. This also serves as the base class for the above exceptions. -* `HttpException` - Thrown when an unexpected HTTP error occurs such as an +- `HttpException` - Thrown when an unexpected HTTP error occurs such as an internal server error or other unexpected status code. ## Example @@ -365,25 +366,24 @@ public class MinFraudExample } ``` -## Support ## +## Support Please report all issues with this code using the [GitHub issue tracker](https://github.com/maxmind/minfraud-api-dotnet/issues). -If you are having an issue with the minFraud service that is not specific -to the client API, please see -[our support page](https://www.maxmind.com/en/support). +If you are having an issue with the minFraud service that is not specific to the +client API, please see [our support page](https://www.maxmind.com/en/support). -## Contributing ## +## Contributing Patches and pull requests are encouraged. Please include unit tests whenever possible. -## Versioning ## +## Versioning This API uses [Semantic Versioning](https://semver.org/). -## Copyright and License ## +## Copyright and License This software is Copyright (c) 2015-2026 by MaxMind, Inc. diff --git a/releasenotes.md b/releasenotes.md index 236681ca..08ff41e1 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -1,614 +1,561 @@ -Release Notes -============= +# Release Notes -6.0.0-beta1 ------------------- +## 6.0.0-beta1 -* **BREAKING:** All response classes in `MaxMind.MinFraud.Response` have been +- **BREAKING:** All response classes in `MaxMind.MinFraud.Response` have been converted from classes to records. -* **BREAKING:** All request classes in `MaxMind.MinFraud.Request` have been +- **BREAKING:** All request classes in `MaxMind.MinFraud.Request` have been converted from classes to records. -* **BREAKING:** All request class constructors have been marked `[Obsolete]`. +- **BREAKING:** All request class constructors have been marked `[Obsolete]`. Use object initializer syntax instead. -* **BREAKING:** The `SetLocales` method has been removed from +- **BREAKING:** The `SetLocales` method has been removed from `MaxMind.MinFraud.Response.IPAddress`. -* Added `TrackingToken` property to `MaxMind.MinFraud.Request.Device`. - This is the token generated by the - [Device Tracking Add-on](https://dev.maxmind.com/minfraud/track-devices) - for explicit device linking. -* Added `Banquest`, `SummitPayments`, and `Yaadpay` to the `PaymentProcessor` +- Added `TrackingToken` property to `MaxMind.MinFraud.Request.Device`. This is + the token generated by the + [Device Tracking Add-on](https://dev.maxmind.com/minfraud/track-devices) for + explicit device linking. +- Added `Banquest`, `SummitPayments`, and `Yaadpay` to the `PaymentProcessor` enum. -* **BREAKING:** The minimum `MaxMind.GeoIP2` dependency has been bumped to - 6.0.0 (a transitive breaking change). -* Custom `ToString()` overrides have been removed from all response and request +- **BREAKING:** The minimum `MaxMind.GeoIP2` dependency has been bumped to 6.0.0 + (a transitive breaking change). +- Custom `ToString()` overrides have been removed from all response and request classes. Records generate `ToString()` automatically. -5.3.1 (2025-11-24) ------------------- +## 5.3.1 (2025-11-24) -* First release via Trusted Publishing. +- First release via Trusted Publishing. -5.3.0 (2025-11-20) ------------------- +## 5.3.0 (2025-11-20) -* .NET 10.0 has been added as a target. -* The language version has been updated to C# 14.0. -* Added `Securepay` to the `PaymentProcessor` enum. -* Added `CreditApplication`, `FundTransfer`, and `SimSwap` to the `EventType` enum. -* Added the input `/event/party`. This is the party submitting the - transaction. You may provide this using the `Party` property on +- .NET 10.0 has been added as a target. +- The language version has been updated to C# 14.0. +- Added `Securepay` to the `PaymentProcessor` enum. +- Added `CreditApplication`, `FundTransfer`, and `SimSwap` to the `EventType` + enum. +- Added the input `/event/party`. This is the party submitting the transaction. + You may provide this using the `Party` property on `MaxMind.MinFraud.Request.Event`. -* Added the input `/payment/method`. This is the payment method associated - with the transaction. You may provide this using the `Method` property on +- Added the input `/payment/method`. This is the payment method associated with + the transaction. You may provide this using the `Method` property on `MaxMind.MinFraud.Request.Payment`. -* Added support for new `/email/domain` outputs. These are available in - minFraud Insights and Factors responses: - * `/email/domain/classification` - The classification of the email domain. +- Added support for new `/email/domain` outputs. These are available in minFraud + Insights and Factors responses: + - `/email/domain/classification` - The classification of the email domain. Available via the `Classification` property on - `MaxMind.MinFraud.Response.EmailDomain`. Possible values include - `Business`, `Education`, `Government`, and `IspEmail`. - * `/email/domain/risk` - A risk score for the email domain, ranging from - 0.01 to 99. Available via the `Risk` property on + `MaxMind.MinFraud.Response.EmailDomain`. Possible values include `Business`, + `Education`, `Government`, and `IspEmail`. + - `/email/domain/risk` - A risk score for the email domain, ranging from 0.01 + to 99. Available via the `Risk` property on `MaxMind.MinFraud.Response.EmailDomain`. - * `/email/domain/volume` - The activity indicator for the email domain - across the minFraud network, expressed as sightings per million. Available - via the `Volume` property on `MaxMind.MinFraud.Response.EmailDomain`. - * `/email/domain/visit/has_redirect` - Whether the domain automatically + - `/email/domain/volume` - The activity indicator for the email domain across + the minFraud network, expressed as sightings per million. Available via the + `Volume` property on `MaxMind.MinFraud.Response.EmailDomain`. + - `/email/domain/visit/has_redirect` - Whether the domain automatically forwards visitors elsewhere. Available via the `HasRedirect` property on `MaxMind.MinFraud.Response.EmailDomainVisit`. - * `/email/domain/visit/last_visited_on` - The date when the domain was last + - `/email/domain/visit/last_visited_on` - The date when the domain was last visited by MaxMind's automated inspection system. Available via the `LastVisitedOn` property on `MaxMind.MinFraud.Response.EmailDomainVisit`. - * `/email/domain/visit/status` - The status of the domain from automated + - `/email/domain/visit/status` - The status of the domain from automated inspection. Available via the `Status` property on `MaxMind.MinFraud.Response.EmailDomainVisit`. Possible values include `Live`, `DnsError`, `NetworkError`, `HttpError`, `Parked`, and `PreDevelopment`. -5.2.0 (2025-05-23) ------------------- +## 5.2.0 (2025-05-23) -* Added support for the `/billing_phone/matches_postal` and +- Added support for the `/billing_phone/matches_postal` and `/shipping_phone/matches_postal` outputs. These are available as the `MatchesPostal` property on `MaxMind.MinFraud.Response.Phone`. -* Added `Cryptomus` to the `PaymentProcessor` enum. +- Added `Cryptomus` to the `PaymentProcessor` enum. -5.1.0 (2025-02-10) ------------------- +## 5.1.0 (2025-02-10) -* .NET 6.0 and .NET 7.0 have been removed as targets as they have both - reach their end of support from Microsoft. If you are using these versions, - the .NET Standard 2.1 target should continue working for you. -* .NET 9.0 has been added as a target. -* The minFraud Factors subscores have been deprecated. They will be removed - in March 2025. Please see [our release notes](https://dev.maxmind.com/minfraud/release-notes/2024/#deprecation-of-risk-factor-scoressubscores) +- .NET 6.0 and .NET 7.0 have been removed as targets as they have both reach + their end of support from Microsoft. If you are using these versions, the .NET + Standard 2.1 target should continue working for you. +- .NET 9.0 has been added as a target. +- The minFraud Factors subscores have been deprecated. They will be removed in + March 2025. Please see + [our release notes](https://dev.maxmind.com/minfraud/release-notes/2024/#deprecation-of-risk-factor-scoressubscores) for more information. -* Added `Epayco` to the `PaymentProcessor` enum. +- Added `Epayco` to the `PaymentProcessor` enum. -5.1.0-beta.1 (2024-09-06) -------------------------- +## 5.1.0-beta.1 (2024-09-06) -* Added support for the new risk reasons outputs in minFraud Factors. The risk +- Added support for the new risk reasons outputs in minFraud Factors. The risk reasons output codes and reasons are currently in beta and are subject to change. We recommend that you use these beta outputs with caution and avoid relying on them for critical applications. -5.0.0 (2024-07-08) ------------------- +## 5.0.0 (2024-07-08) -* Updated `TransactionReport` to make the `ipAddress` parameter optional. Now +- Updated `TransactionReport` to make the `ipAddress` parameter optional. Now the `tag` and at least one of the following parameters must be supplied: `ipAddress`, `maxmindId`, `minfraudID`, `transactionID`. -* Added `BillingPhone` and `ShippingPhone` properties to the minFraud Insights - and Factors response models. These contain objects with information about - the respective phone numbers. Please see [our developer - site](https://dev.maxmind.com/minfraud/api-documentation/responses/) for - more information. -* Added `Payconex` to the `PaymentProcessor` enum. -* Removed several obsolete constructors as well as the `Last4Digits` - property on `MaxMind.MinFraud.Request.CreditCard`. - -4.3.0 (2024-04-16) ------------------- - -* Added `PxpFinancial` and `Trustpay` to the `PaymentProcessor` enum. -* Equivalent domain names are now normalized when `hashAddress` is used. - For example, `googlemail.com` will become `gmail.com`. -* Periods are now removed from `gmail.com` email address local parts when +- Added `BillingPhone` and `ShippingPhone` properties to the minFraud Insights + and Factors response models. These contain objects with information about the + respective phone numbers. Please see + [our developer site](https://dev.maxmind.com/minfraud/api-documentation/responses/) + for more information. +- Added `Payconex` to the `PaymentProcessor` enum. +- Removed several obsolete constructors as well as the `Last4Digits` property on + `MaxMind.MinFraud.Request.CreditCard`. + +## 4.3.0 (2024-04-16) + +- Added `PxpFinancial` and `Trustpay` to the `PaymentProcessor` enum. +- Equivalent domain names are now normalized when `hashAddress` is used. For + example, `googlemail.com` will become `gmail.com`. +- Periods are now removed from `gmail.com` email address local parts when `hashAddress` is used. For example, `f.o.o@gmail.com` will become `foo@gmail.com`. -* Fastmail alias subdomain email addresses are now normalized when - `hashAddress` is used. For example, `alias@user.fastmail.com` will - become `user@fastmail.com`. -* Additional `yahoo.com` email addresses now have aliases removed from - their local part when `hashAddress` is used. For example, - `foo-bar@yahoo.com` will become `foo@yahoo.com` for additional - `yahoo.com` domains. -* Duplicate `.com`s are now removed from email domain names when - `hashAddress` is used. For example, `example.com.com` will become - `example.com`. -* Certain TLD typos are now normalized when `hashAddress` is used. For - example, `example.comcom` will become `example.com`. -* Additional `gmail.com` domain names with leading digits are now - normalized when `hashAddress` is used. For example, `100gmail.com` will - become `gmail.com`. -* Additional `gmail.com` typos are now normalized when `hashAddress` is - used. For example, `gmali.com` will become `gmail.com`. -* When `hashAddress` is used, all trailing periods are now removed from an - email address domain. Previously only a single period was removed. -* When `hashAddress` is used, the local part of an email address is now +- Fastmail alias subdomain email addresses are now normalized when `hashAddress` + is used. For example, `alias@user.fastmail.com` will become + `user@fastmail.com`. +- Additional `yahoo.com` email addresses now have aliases removed from their + local part when `hashAddress` is used. For example, `foo-bar@yahoo.com` will + become `foo@yahoo.com` for additional `yahoo.com` domains. +- Duplicate `.com`s are now removed from email domain names when `hashAddress` + is used. For example, `example.com.com` will become `example.com`. +- Certain TLD typos are now normalized when `hashAddress` is used. For example, + `example.comcom` will become `example.com`. +- Additional `gmail.com` domain names with leading digits are now normalized + when `hashAddress` is used. For example, `100gmail.com` will become + `gmail.com`. +- Additional `gmail.com` typos are now normalized when `hashAddress` is used. + For example, `gmali.com` will become `gmail.com`. +- When `hashAddress` is used, all trailing periods are now removed from an email + address domain. Previously only a single period was removed. +- When `hashAddress` is used, the local part of an email address is now normalized to NFC. -4.2.0 (2023-12-05) ------------------- +## 4.2.0 (2023-12-05) -* .NET 5.0 has been removed as a target as it has reach its end of life. +- .NET 5.0 has been removed as a target as it has reach its end of life. However, if you are using .NET 5.0, the .NET Standard 2.1 target should continue working for you. -* .NET 7.0 and .NET 8.0 have been added as a target. -* Added `GooglePay`, `Placetopay`, and `ShopifyPayments` to the +- .NET 7.0 and .NET 8.0 have been added as a target. +- Added `GooglePay`, `Placetopay`, and `ShopifyPayments` to the `PaymentProcessor` enum. -* Added `IWebServiceClient` to facilitate mocking of `WebServiceClient`. - Pull request by Ian Göbl. GitHub #152. -* Updated `MaxMind.GeoIP2` dependency to version that includes the `IsAnycast` +- Added `IWebServiceClient` to facilitate mocking of `WebServiceClient`. Pull + request by Ian Göbl. GitHub #152. +- Updated `MaxMind.GeoIP2` dependency to version that includes the `IsAnycast` property on `MaxMind.GeoIP2.Model.Traits`. This is `true` if the IP address - belongs to an [anycast network](https://en.wikipedia.org/wiki/Anycast). - This is available in minFraud Insights and Factors. + belongs to an [anycast network](https://en.wikipedia.org/wiki/Anycast). This + is available in minFraud Insights and Factors. -4.1.0 (2022-03-28) ------------------- +## 4.1.0 (2022-03-28) -* Added the `Country` property to `MaxMind.MinFraud.Request.CreditCard`. - This is the country where the issuer of the card is located. This may be - passed instead of `IssuerIdNumber` if you do not wish to pass partial - account numbers or if your payment processor does not provide them. +- Added the `Country` property to `MaxMind.MinFraud.Request.CreditCard`. This is + the country where the issuer of the card is located. This may be passed + instead of `IssuerIdNumber` if you do not wish to pass partial account numbers + or if your payment processor does not provide them. -4.0.0 (2022-02-07) ------------------- +## 4.0.0 (2022-02-07) -* This library no longer targets .NET 4.6.1. -* .NET 6.0 was added as a target. -* Removed deprecated code, include: - * The class `MaxMind.MinFraud.Response.GeoIP2Country` was removed. This was - a subclass of `MaxMind.GeoIP2.Model.Country` that added an `IsHighRisk` +- This library no longer targets .NET 4.6.1. +- .NET 6.0 was added as a target. +- Removed deprecated code, include: + - The class `MaxMind.MinFraud.Response.GeoIP2Country` was removed. This was a + subclass of `MaxMind.GeoIP2.Model.Country` that added an `IsHighRisk` property. This property was marked obsolete in 2019. Now `MaxMind.GeoIP2.Model.Country` is used directly. - * In `MaxMind.MinFraud.Response.Subscores`, the properties `EmailTenure` - and `IPTenure` were removed. These were deprecated in the web service - and always return a fixed value. + - In `MaxMind.MinFraud.Response.Subscores`, the properties `EmailTenure` and + `IPTenure` were removed. These were deprecated in the web service and always + return a fixed value. -3.3.0 (2022-01-27) ------------------- +## 3.3.0 (2022-01-27) -* Upgraded `MaxMind.GeoIP2` to 4.1.0. This adds mobile country code (MCC) - and mobile network code (MNC) to minFraud Insights and Factors responses. - These are available at `response.IPAddress.Traits.MobileCountryCode` and +- Upgraded `MaxMind.GeoIP2` to 4.1.0. This adds mobile country code (MCC) and + mobile network code (MNC) to minFraud Insights and Factors responses. These + are available at `response.IPAddress.Traits.MobileCountryCode` and `response.IPAddress.Traits.MobileNetworkCode`. We expect this data to be available by late January 2022. -* Added the following new values to the `PaymentProcessor` enum: - * `Boacompra` - * `Boku` - * `Coregateway` - * `Fiserv` - * `Neopay` - * `Neosurf` - * `Openbucks` - * `Paysera` - * `Payvision` - * `Trustly` - * `Windcave` -* The `last4Digits` constructor parameter and and `Last4Digits` property of +- Added the following new values to the `PaymentProcessor` enum: + - `Boacompra` + - `Boku` + - `Coregateway` + - `Fiserv` + - `Neopay` + - `Neosurf` + - `Openbucks` + - `Paysera` + - `Payvision` + - `Trustly` + - `Windcave` +- The `last4Digits` constructor parameter and and `Last4Digits` property of `MaxMind.MinFraud.Request.CreditCard` have been deprecated in favor of `lastDigits` / `LastDigits` respectively and will be removed in a future release. `lastDigits` / `LastDigits` also now supports two digit values in addition to the previous four digit values. -* Eight digit `MaxMind.MinFraud.Request.CreditCard.issuerIdNumber` inputs are - now supported in addition to the previously accepted six digit `issuerIdNumber`. - In most cases, you should send the last four digits for - `MaxMind.MinFraud.Request.CreditCard.lastDigits`. If you send a `issuerIdNumber` - that contains an eight digit IIN, and if the credit card brand is not one of the - following, you should send the last two digits for `lastDigits`: - * `Discover` - * `JCB` - * `Mastercard` - * `UnionPay` - * `Visa` - -3.2.0 (2021-08-27) ------------------- - -* Added the following new values to the `PaymentProcessor` enum: - * `Cardknox` - * `Creditguard` - * `Credorax` - * `Datacap` - * `Dlocal` - * `Onpay` - * `Safecharge` -* Documented the new "test" disposition. -* Added support for the `/disposition/risk_label` output in Score, Insights and +- Eight digit `MaxMind.MinFraud.Request.CreditCard.issuerIdNumber` inputs are + now supported in addition to the previously accepted six digit + `issuerIdNumber`. In most cases, you should send the last four digits for + `MaxMind.MinFraud.Request.CreditCard.lastDigits`. If you send a + `issuerIdNumber` that contains an eight digit IIN, and if the credit card + brand is not one of the following, you should send the last two digits for + `lastDigits`: + - `Discover` + - `JCB` + - `Mastercard` + - `UnionPay` + - `Visa` + +## 3.2.0 (2021-08-27) + +- Added the following new values to the `PaymentProcessor` enum: + - `Cardknox` + - `Creditguard` + - `Credorax` + - `Datacap` + - `Dlocal` + - `Onpay` + - `Safecharge` +- Documented the new "test" disposition. +- Added support for the `/disposition/risk_label` output in Score, Insights and Factors. This is the available as the `RiskLabel` property of `MaxMind.MinFraud.Response.Disposition`, and is the label of the custom rule that was triggered by the transaction. -* Added support for the `/credit_card/was_3d_secure_successful` input in Score, - Insights and Factors. This input should indicate whether or not the outcome - of 3-D Secure verification (e.g. Safekey, SecureCode, Verified by Visa) was - successful. `true` if customer verification was successful, or `false` if - the customer failed verification. If 3-D Secure verification was not used, was +- Added support for the `/credit_card/was_3d_secure_successful` input in Score, + Insights and Factors. This input should indicate whether or not the outcome of + 3-D Secure verification (e.g. Safekey, SecureCode, Verified by Visa) was + successful. `true` if customer verification was successful, or `false` if the + customer failed verification. If 3-D Secure verification was not used, was unavailable, or resulted in another outcome other than success or failure, do not include this field. -3.1.0 (2021-02-02) ------------------- +## 3.1.0 (2021-02-02) -* Added `ApplePay` and `ApsPayments` to the `PaymentProcessor` enum. -* Added additional normalizing of the request email address in +- Added `ApplePay` and `ApsPayments` to the `PaymentProcessor` enum. +- Added additional normalizing of the request email address in `MaxMind.MinFraud.Request.Email` when `hashAddress` is set to `true`. -* Added support for the IP address risk reasons in the minFraud Insights and - Factors responses. This is available at `response.IPAddress.RiskReasons`. - It is a list of `MaxMind.MinFraud.Response.IPRiskReason` objects. - -3.0.0 (2020-11-24) ------------------- - -* This library now requires .NET Framework 4.6.1 or greater or .NET Standard - 2.0 or greater. -* .NET 5.0 was added as a target framework. -* `System.Text.Json` is now used for serializing and deserializing JSON. - If you are serializing the objects yourself, the `Newtonsoft.Json` - attributes have been removed and you will need to switch to - `System.Text.Json`. -* When creating a `Transaction` object, you may now use init-only properties +- Added support for the IP address risk reasons in the minFraud Insights and + Factors responses. This is available at `response.IPAddress.RiskReasons`. It + is a list of `MaxMind.MinFraud.Response.IPRiskReason` objects. + +## 3.0.0 (2020-11-24) + +- This library now requires .NET Framework 4.6.1 or greater or .NET Standard 2.0 + or greater. +- .NET 5.0 was added as a target framework. +- `System.Text.Json` is now used for serializing and deserializing JSON. If you + are serializing the objects yourself, the `Newtonsoft.Json` attributes have + been removed and you will need to switch to `System.Text.Json`. +- When creating a `Transaction` object, you may now use init-only properties rather than constructor parameters. -* You may now create `WebServiceClient` as a typed client with +- You may now create `WebServiceClient` as a typed client with `IHttpClientFactory` in .NET Core 2.1+. -* Exception objects now correctly implement `ISerializable`. -* The `Warnings` properties on the response models are now an +- Exception objects now correctly implement `ISerializable`. +- The `Warnings` properties on the response models are now an `IReadOnlyList` rather than an `IList`. -* The `Names` properties on `NamedEntity` models are now +- The `Names` properties on `NamedEntity` models are now `IReadOnlyDictionary`. -* The `Subdivisions` property on `MaxMind.MinFraud.Response.IPAddress` is now - an `IReadOnlyList`. -* `GeoNameId` properties on `NamedEntity` models are now `long?` rather than +- The `Subdivisions` property on `MaxMind.MinFraud.Response.IPAddress` is now an + `IReadOnlyList`. +- `GeoNameId` properties on `NamedEntity` models are now `long?` rather than `int?` to match the underlying database. -2.9.0 (2020-10-14) ------------------- - -* This library now requires .NET Framework 4.5 or greater or .NET Standard - 2.0 or greater. -* The device IP address is no longer a required input. -* Added `Tsys` to the `PaymentProcessor` enum. - -2.8.0 (2020-09-25) ------------------- - -* Added `response.IPAddress.Traits.IsResidentialProxy` to the minFraud - Insights and Factors models. This indicates whether the IP address is on a - suspected anonymizing network and belongs to a residential ISP. - -2.7.0 (2020-07-31) ------------------- - -* Added the following new values to the `PaymentProcessor` enum: - * `Cashfree` - * `FirstAtlanticCommerce` - * `Komoju` - * `Paytm` - * `Razorpay` - * `Systempay` -* Added support for new Subscores outputs. These are - available as the `Device`, `EmailLocalPart` and `ShippingAddress` properties - on `MaxMind.MinFraud.Response.Subscores` on minFraud Factors response - objects. +## 2.9.0 (2020-10-14) + +- This library now requires .NET Framework 4.5 or greater or .NET Standard 2.0 + or greater. +- The device IP address is no longer a required input. +- Added `Tsys` to the `PaymentProcessor` enum. + +## 2.8.0 (2020-09-25) -2.6.0 (2020-06-19) ------------------- +- Added `response.IPAddress.Traits.IsResidentialProxy` to the minFraud Insights + and Factors models. This indicates whether the IP address is on a suspected + anonymizing network and belongs to a residential ISP. -* Added support for the minFraud Report Transaction API. Reporting - transactions to MaxMind helps us detect about 10-50% more fraud and - reduce false positives for you. +## 2.7.0 (2020-07-31) -2.5.0 (2020-04-06) ------------------- +- Added the following new values to the `PaymentProcessor` enum: + - `Cashfree` + - `FirstAtlanticCommerce` + - `Komoju` + - `Paytm` + - `Razorpay` + - `Systempay` +- Added support for new Subscores outputs. These are available as the `Device`, + `EmailLocalPart` and `ShippingAddress` properties on + `MaxMind.MinFraud.Response.Subscores` on minFraud Factors response objects. -* Added support for the new credit card output `/credit_card/is_business`. - This indicates whether the card is a business card. It may be accessed via - `response.CreditCard.IsBusiness` on the minFraud Insights and Factors - response objects. +## 2.6.0 (2020-06-19) -2.4.0 (2020-03-26) ------------------- +- Added support for the minFraud Report Transaction API. Reporting transactions + to MaxMind helps us detect about 10-50% more fraud and reduce false positives + for you. -* Added support for the `/email/domain/first_seen` output in minFraud Insights +## 2.5.0 (2020-04-06) + +- Added support for the new credit card output `/credit_card/is_business`. This + indicates whether the card is a business card. It may be accessed via + `response.CreditCard.IsBusiness` on the minFraud Insights and Factors response + objects. + +## 2.4.0 (2020-03-26) + +- Added support for the `/email/domain/first_seen` output in minFraud Insights and Factors. This is available as the `FirstSeen` property on `MaxMind.MinFraud.Response.EmailDomain`. -* Added the following new values to the `PaymentProcessor` enum: - * `Cardpay` - * `Epx` +- Added the following new values to the `PaymentProcessor` enum: + - `Cardpay` + - `Epx` -2.3.0 (2020-02-21) ------------------- +## 2.3.0 (2020-02-21) -* Added support for the `/email/is_disposable` output in minFraud Insights - and Factors. This is available as the `IsDisposable` property on +- Added support for the `/email/is_disposable` output in minFraud Insights and + Factors. This is available as the `IsDisposable` property on `MaxMind.MinFraud.Response.Email`. -* Added the following new values to the `PaymentProcessor` enum: - * `Cetelem` - * `Ecommpay` - * `G2aPay` - * `Mercanet` +- Added the following new values to the `PaymentProcessor` enum: + - `Cetelem` + - `Ecommpay` + - `G2aPay` + - `Mercanet` -2.2.0 (2019-12-10) ------------------- +## 2.2.0 (2019-12-10) -* This library has been updated to support the nullable reference types +- This library has been updated to support the nullable reference types introduced in C# 8.0. -* The client-side validation for numeric custom inputs has been updated to - match the server-side validation. The valid range is -9,999,999,999,999 - to 9,999,999,999,999. Previously, larger numbers were allowed. -* Added the following new values to the `PaymentProcessor` enum: - * `Affirm` - * `Interac` -* Deprecated the `EmailTenure` and `IPTenure` properties of +- The client-side validation for numeric custom inputs has been updated to match + the server-side validation. The valid range is -9,999,999,999,999 to + 9,999,999,999,999. Previously, larger numbers were allowed. +- Added the following new values to the `PaymentProcessor` enum: + - `Affirm` + - `Interac` +- Deprecated the `EmailTenure` and `IPTenure` properties of `MaxMind.MinFraud.Response.Subscores`. -* Deprecated the `IsHighRisk` property of `MaxMind.MinFraud.Response.GeoIP2Country`. -* `netstandard2.1` was added as a target framework. - -2.1.0 (2019-08-12) ------------------- - -* Previously, `FactorsAsync` would incorrectly return an `Insights` - object, hiding Factors-specific fields. Bug reported by Bogdan - Polovyk. GitHub #49. -* Added the following new values to the `PaymentProcessor` enum: - * `Afterpay` - * `DataCash` - * `Dotpay` - * `GoCardless` - * `Klarna` - * `Payeezy` - * `Paylike` - * `PaymentExpress` - * `Paysafecard` - * `SmartDebit` - * `SynapseFI` -* Dependencies were updated. - -2.0.0 (2018-04-11) ------------------- - -* The `userId` constructor parameter for `WebServiceClient` was renamed to +- Deprecated the `IsHighRisk` property of + `MaxMind.MinFraud.Response.GeoIP2Country`. +- `netstandard2.1` was added as a target framework. + +## 2.1.0 (2019-08-12) + +- Previously, `FactorsAsync` would incorrectly return an `Insights` object, + hiding Factors-specific fields. Bug reported by Bogdan Polovyk. GitHub #49. +- Added the following new values to the `PaymentProcessor` enum: + - `Afterpay` + - `DataCash` + - `Dotpay` + - `GoCardless` + - `Klarna` + - `Payeezy` + - `Paylike` + - `PaymentExpress` + - `Paysafecard` + - `SmartDebit` + - `SynapseFI` +- Dependencies were updated. + +## 2.0.0 (2018-04-11) + +- The `userId` constructor parameter for `WebServiceClient` was renamed to `accountId` and support was added for the error code `ACCOUNT_ID_REQUIRED`. -* Added the following new values to the `PaymentProcessor` enum: - * `Ccavenue` - * `CtPayments` - * `Dalenys` - * `Oney` - * `Posconnect` -* Added support for the `/device/local_time` output. This is exposed as - the `LocalTime` property on `MaxMind.MinFraud.Response.Device`. -* Added support for the `/credit_card/is_virtual` output. This is exposed as - the `IsVirtual` property on `MaxMind.MinFraud.Response.CreditCard`. -* Added `PayoutChange` to the `EventType` enum. - -1.6.0 (2018-01-22) ------------------- - -* Updated `MaxMind.GeoIP2` dependency. With this version, the +- Added the following new values to the `PaymentProcessor` enum: + - `Ccavenue` + - `CtPayments` + - `Dalenys` + - `Oney` + - `Posconnect` +- Added support for the `/device/local_time` output. This is exposed as the + `LocalTime` property on `MaxMind.MinFraud.Response.Device`. +- Added support for the `/credit_card/is_virtual` output. This is exposed as the + `IsVirtual` property on `MaxMind.MinFraud.Response.CreditCard`. +- Added `PayoutChange` to the `EventType` enum. + +## 1.6.0 (2018-01-22) + +- Updated `MaxMind.GeoIP2` dependency. With this version, the `IsInEuropeanUnion` property is now available on `MaxMind.MinFraud.Response.GeoIP2Country` and `MaxMind.GeoIP2.Model.RepresentedCountry`. `IsInEuropeanUnion` is `true` if the country is a member state of the European Union. -* Added the following new values to the `PaymentProcessor` enum: - * `Cybersource` - * `TransactPro` - * `Wirecard` +- Added the following new values to the `PaymentProcessor` enum: + - `Cybersource` + - `TransactPro` + - `Wirecard` -1.5.0 (2017-11-01) ------------------- +## 1.5.0 (2017-11-01) -* Behavior change: When sending an email address to MaxMind, this library now +- Behavior change: When sending an email address to MaxMind, this library now defaults to sending the plain text email address rather than its MD5 hash. Previously only the MD5 hash of the email address would be sent and sending the plain text email address was not possible. If you wish to send only the MD5 hash of the email address, you must now set the `hashAddress` constructor parameter to `true` on `MaxMind.MinFraud.Request.Email`. -* Previously, it was possible to get an `IndexOutOfRangeException` when calling +- Previously, it was possible to get an `IndexOutOfRangeException` when calling the `MaxMind.MinFraud.Request.Email` constructor with an invalid email address. Now an `ArgumentException` will be thrown. -* When sending a hashed email address, the address is now lower-cased before - the MD5 is calculated. -* Added the following new values to the `PaymentProcessor` enum: - * `Bpoint` - * `CheckoutCom` - * `Emerchantpay` - * `Heartland` - * `Payway` -* A `netstandard2.0` target was added to eliminate additional dependencies +- When sending a hashed email address, the address is now lower-cased before the + MD5 is calculated. +- Added the following new values to the `PaymentProcessor` enum: + - `Bpoint` + - `CheckoutCom` + - `Emerchantpay` + - `Heartland` + - `Payway` +- A `netstandard2.0` target was added to eliminate additional dependencies required by the `netstandard1.4` target. -* As part of the above work, the separate Mono build files were dropped. As - of Mono 5.0.0, `msbuild` is supported. -* Updated `MaxMind.GeoIP2` dependency to add support for GeoIP2 Precision +- As part of the above work, the separate Mono build files were dropped. As of + Mono 5.0.0, `msbuild` is supported. +- Updated `MaxMind.GeoIP2` dependency to add support for GeoIP2 Precision Insights anonymizer fields. -1.4.1 (2017-07-21) ------------------- +## 1.4.1 (2017-07-21) -* Fixed an issue where the client would throw an exception if the +- Fixed an issue where the client would throw an exception if the `Content-Length` header was missing in the response. This could happen with chunked responses. -1.4.0 (2017-07-07) ------------------- +## 1.4.0 (2017-07-07) -* Added support for custom inputs. These can be set up from your account portal. -* Added support for the `/device/session_age` and `/device/session_id` inputs. +- Added support for custom inputs. These can be set up from your account portal. +- Added support for the `/device/session_age` and `/device/session_id` inputs. Use the `sessionAge` and `sessionId` constructor parameters on `MaxMind.MinFraud.Request.Device` to set them. -* Added support for the `/email/first_seen` output. Use `FirstSeen` on +- Added support for the `/email/first_seen` output. Use `FirstSeen` on `MaxMind.MinFraud.Response.FirstSeen` to access it. -* Added the following new values to the `PaymentProcessor` enum: - * `AmericanExpressPaymentGateway` - * `Bluesnap` - * `Commdoo` - * `Curopayments` - * `Ebs` - * `Exact` - * `Hipay` - * `LemonWay`. - * `Oceanpayment` - * `Paymentwall` - * `Payza` - * `Securetrading` - * `SolidtrustPay` - * `Vantiv` - * `Vericheck` - * `Vpos` -* Updated the docs for `MaxMind.MinFraud.Response.Address` now that - `IsPostalInCity` may be returned for addresses world-wide. -* Switched to the updated MSBuild .NET Core build system. -* Moved tests from NUnit to xUnit.net. - -1.3.0 (2016-11-22) ------------------- - -* The disposition was added to the minFraud response models. This is used to +- Added the following new values to the `PaymentProcessor` enum: + - `AmericanExpressPaymentGateway` + - `Bluesnap` + - `Commdoo` + - `Curopayments` + - `Ebs` + - `Exact` + - `Hipay` + - `LemonWay`. + - `Oceanpayment` + - `Paymentwall` + - `Payza` + - `Securetrading` + - `SolidtrustPay` + - `Vantiv` + - `Vericheck` + - `Vpos` +- Updated the docs for `MaxMind.MinFraud.Response.Address` now that + `IsPostalInCity` may be returned for addresses world-wide. +- Switched to the updated MSBuild .NET Core build system. +- Moved tests from NUnit to xUnit.net. + +## 1.3.0 (2016-11-22) + +- The disposition was added to the minFraud response models. This is used to return the disposition of the transaction as set by the custom rules for the account. -* Updated to .NET Core 1.1. +- Updated to .NET Core 1.1. -1.2.1 (2016-11-21) ------------------- +## 1.2.1 (2016-11-21) -* Fixed a bug where a null `username` passed to the request `Account` model +- Fixed a bug where a null `username` passed to the request `Account` model would cause an exception when attempting to encode the username as an MD5. -1.2.0 (2016-11-14) ------------------- +## 1.2.0 (2016-11-14) -* Added `/credit_card/token` input. Use the `token` constructor parameter on +- Added `/credit_card/token` input. Use the `token` constructor parameter on `MaxMind.MinFraud.Request.CreditCard` to set it. -* All validation regular expressions are now pre-compiled. -* Use framework assembly for `System.Net.Http` on .NET 4.5 rather than NuGet +- All validation regular expressions are now pre-compiled. +- Use framework assembly for `System.Net.Http` on .NET 4.5 rather than NuGet package. -1.1.0 (2016-10-11) ------------------- +## 1.1.0 (2016-10-11) -* Added the following new values to the `EventType` enum: `EmailChange` and +- Added the following new values to the `EventType` enum: `EmailChange` and `PasswordReset`. -1.0.0 (2016-09-16) ------------------- +## 1.0.0 (2016-09-16) -* First production release. No code changes. +- First production release. No code changes. -0.7.1 (2016-08-08) ------------------- +## 0.7.1 (2016-08-08) -* Re-release of 0.7.0 to fix strong-name issue. No code changes. +- Re-release of 0.7.0 to fix strong-name issue. No code changes. -0.7.0 (2016-08-01) ------------------- +## 0.7.0 (2016-08-01) -* .NET Core 1.0 support. -* Clarification of unit price in documentation. +- .NET Core 1.0 support. +- Clarification of unit price in documentation. -0.7.0-beta1 (2016-06-15) ------------------------- +## 0.7.0-beta1 (2016-06-15) -* .NET Core support. -* `Email` request class constructor now takes a `string` email address rather +- .NET Core support. +- `Email` request class constructor now takes a `string` email address rather than a `MailAddress`. This was does as CoreFX does not currently have `System.Net.Mail`. -* Exceptions are no longer serializable. -* Added the following new values to the `PaymentProcessor` enum: +- Exceptions are no longer serializable. +- Added the following new values to the `PaymentProcessor` enum: `ConceptPayments`, `Ecomm365`, `Orangepay`, and `PacnetServices`. -0.6.0 (2016-06-08) ------------------- +## 0.6.0 (2016-06-08) -* BREAKING CHANGE: `CreditsRemaining` has been removed from the web service +- BREAKING CHANGE: `CreditsRemaining` has been removed from the web service models and has been replaced by `QueriesRemaining`. -* Added `QueriesRemaining` and `FundsRemaining`. Note that `FundsRemaining` - will not be returned by the web service until our new credit system is in - place. -* `Confidence` and `LastSeen` were added to the `Device` response model. -* `LocalTime` in the `GeoIP2Location` model is now nullable. - -0.5.0 (2016-05-23) ------------------- - -* Added support for the minFraud Factors. -* Added IP address risk to the minFraud Score model. -* Handle `PERMISSION_REQUIRED` errors by throwing a +- Added `QueriesRemaining` and `FundsRemaining`. Note that `FundsRemaining` will + not be returned by the web service until our new credit system is in place. +- `Confidence` and `LastSeen` were added to the `Device` response model. +- `LocalTime` in the `GeoIP2Location` model is now nullable. + +## 0.5.0 (2016-05-23) + +- Added support for the minFraud Factors. +- Added IP address risk to the minFraud Score model. +- Handle `PERMISSION_REQUIRED` errors by throwing a `PermissionRequiredException`. -* Updated dependencies. -* Added the following new values to the `PaymentProcessor` enum: - `Ccnow`, `Dalpay`, `Epay` (replaces `Epayeu`), `Payplus`, `Pinpayments`, - `Quickpay`, and `Verepay`. +- Updated dependencies. +- Added the following new values to the `PaymentProcessor` enum: `Ccnow`, + `Dalpay`, `Epay` (replaces `Epayeu`), `Payplus`, `Pinpayments`, `Quickpay`, + and `Verepay`. -0.4.0-beta2 (2016-01-29) ------------------------- +## 0.4.0-beta2 (2016-01-29) -* The target framework is now .NET 4.5 rather than 4.5.2. -* Updated GeoIP2 to 2.6.0-beta2. +- The target framework is now .NET 4.5 rather than 4.5.2. +- Updated GeoIP2 to 2.6.0-beta2. -0.4.0-beta1 (2016-01-28) ------------------------- +## 0.4.0-beta1 (2016-01-28) -* Added support for new minFraud Insights outputs. These are: - * `/credit_card/brand` - * `/credit_card/type` - * `/device/id` - * `/email/is_free` - * `/email/is_high_risk` -* `Input` on the `Warning` response model has been replaced with - `InputPointer`. The latter is a JSON pointer to the input that caused the - warning. -* Updated GeoIP2 to 2.6.0-beta1. +- Added support for new minFraud Insights outputs. These are: + - `/credit_card/brand` + - `/credit_card/type` + - `/device/id` + - `/email/is_free` + - `/email/is_high_risk` +- `Input` on the `Warning` response model has been replaced with `InputPointer`. + The latter is a JSON pointer to the input that caused the warning. +- Updated GeoIP2 to 2.6.0-beta1. -0.3.1 (2015-12-04) ------------------- +## 0.3.1 (2015-12-04) -* Actually remove the BCL dependency. +- Actually remove the BCL dependency. -0.3.0 (2015-12-04) ------------------- +## 0.3.0 (2015-12-04) -* Update `MaxMind.GeoIP2` to 2.5.0. This removes the BCL dependency. -* Upgrade to NUnit 3. +- Update `MaxMind.GeoIP2` to 2.5.0. This removes the BCL dependency. +- Upgrade to NUnit 3. -0.2.0 (2015-09-23) ------------------- +## 0.2.0 (2015-09-23) -* Update `MaxMind.GeoIP2` to 2.4.0. +- Update `MaxMind.GeoIP2` to 2.4.0. -0.2.0-beta1 (2015-09-10) ------------------------- +## 0.2.0-beta1 (2015-09-10) -* Add `is_gift` and `has_gift_message` order inputs. -* Upgrade to the latest GeoIP2 release. -* `Score.CreditsRemaining` was change from a `ulong?` to `long?` in order to - be more CLS compliant. -* GeoIP2 dependency was updated to a version that does not depend on - RestSharp. -* Some parameters and properties were changed from using concrete classes to +- Add `is_gift` and `has_gift_message` order inputs. +- Upgrade to the latest GeoIP2 release. +- `Score.CreditsRemaining` was change from a `ulong?` to `long?` in order to be + more CLS compliant. +- GeoIP2 dependency was updated to a version that does not depend on RestSharp. +- Some parameters and properties were changed from using concrete classes to interfaces. -* The library now has a strong name. +- The library now has a strong name. -0.1.1-beta1 (2015-06-30) ------------------------- +## 0.1.1-beta1 (2015-06-30) -* Upgrade to Json.NET 7.0.1 and GeoIP2 2.3.1-beta1. +- Upgrade to Json.NET 7.0.1 and GeoIP2 2.3.1-beta1. -0.1.0 (2015-06-29) ------------------- +## 0.1.0 (2015-06-29) -* Release as beta. +- Release as beta. -0.0.1 (2015-06-18) ------------------- +## 0.0.1 (2015-06-18) -* Initial release. +- Initial release. From 099779d1f10d218a5f71cb1440c15661d5601bf2 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 20 Mar 2026 16:55:39 +0000 Subject: [PATCH 07/14] Update release script for ATX headings Co-Authored-By: Claude Opus 4.6 (1M context) --- dev-bin/release.sh | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/dev-bin/release.sh b/dev-bin/release.sh index 6ffbd5ee..a8b11f31 100755 --- a/dev-bin/release.sh +++ b/dev-bin/release.sh @@ -52,8 +52,12 @@ fi changelog=$(cat releasenotes.md) -# minfraud-api-dotnet format: "5.3.0 (2025-11-20)" followed by "---" -regex='([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.[0-9]+)?)?) \(([0-9]{4}-[0-9]{2}-[0-9]{2})\)' +regex=' +## ([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.[0-9]+)?)?) \(([0-9]{4}-[0-9]{2}-[0-9]{2})\) + +((.| +)*) +' if [[ ! $changelog =~ $regex ]]; then echo "Could not find version/date in releasenotes.md!" @@ -62,14 +66,7 @@ fi version="${BASH_REMATCH[1]}" date="${BASH_REMATCH[4]}" - -# Extract release notes: everything after "---" line until next version header -notes=$(awk -v ver="$version" ' - $0 ~ "^" ver " \\(" { found=1; next } - found && /^-+$/ { in_notes=1; next } - in_notes && /^[0-9]+\.[0-9]+\.[0-9]+.* \([0-9]{4}-[0-9]{2}-[0-9]{2}\)/ { exit } - in_notes { print } -' releasenotes.md | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}') +notes="$(echo "${BASH_REMATCH[5]}" | sed -n -e '/^## [0-9]\+\.[0-9]\+\.[0-9]\+/,$!p')" if [[ "$date" != "$(date +"%Y-%m-%d")" ]]; then echo "$date is not today!" From cab7c4290e74578d4a8263805c7fc365a443a9ec Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 20 Mar 2026 16:55:54 +0000 Subject: [PATCH 08/14] Add build.binlog and other entries to .gitignore Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index db751b6a..26fa4960 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ ipch/ # ReSharper is a .NET coding add-in _ReSharper* +*.DotSettings # NCrunch *.ncrunch* @@ -101,6 +102,12 @@ AppPackages/ .idea *.iml +# Build logs +*.binlog + +# Worktrees +.worktrees + # Others [Bb]in [Oo]bj From 85b39aa998310f804a6ef69f128ab3db471c0192 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 20 Mar 2026 16:56:15 +0000 Subject: [PATCH 09/14] Add 10.0.x to CodeQL analysis Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/codeql-analysis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 33c7cfa2..e350df30 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,6 +35,7 @@ jobs: dotnet-version: | 8.0.x 9.0.x + 10.0.x # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL From b4ce8e4dcbbaf81a2c3d774b7706b236dcaf636d Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 20 Mar 2026 22:31:10 +0000 Subject: [PATCH 10/14] Normalize Guid.Empty to null in MinFraudId init accessor The old constructor intentionally suppressed Guid.Empty, but the new object-initializer path preserved it, allowing the zero GUID to reach the API. Normalize in the init accessor so both paths behave consistently. Remove the now-redundant Guid.Empty check from ReportAsync. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Request/TransactionReportTest.cs | 11 +++++++++++ MaxMind.MinFraud/Request/TransactionReport.cs | 6 +++++- MaxMind.MinFraud/WebServiceClient.cs | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/MaxMind.MinFraud.UnitTest/Request/TransactionReportTest.cs b/MaxMind.MinFraud.UnitTest/Request/TransactionReportTest.cs index 9ed98521..bd157a51 100644 --- a/MaxMind.MinFraud.UnitTest/Request/TransactionReportTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/TransactionReportTest.cs @@ -37,6 +37,17 @@ public void TestAll() Assert.Equal(transactionId, report.TransactionId); } + [Fact] + public void TestGuidEmptyIsNormalized() + { + var report = new TransactionReport + { + Tag = TransactionReportTag.NotFraud, + MinFraudId = Guid.Empty + }; + Assert.Null(report.MinFraudId); + } + [Theory] [InlineData("abcd123")] [InlineData("")] diff --git a/MaxMind.MinFraud/Request/TransactionReport.cs b/MaxMind.MinFraud/Request/TransactionReport.cs index 80e463c0..7a05fd52 100644 --- a/MaxMind.MinFraud/Request/TransactionReport.cs +++ b/MaxMind.MinFraud/Request/TransactionReport.cs @@ -183,7 +183,11 @@ public string? MaxMindId /// provide it if the request was made to one of these services. /// [JsonPropertyName("minfraud_id")] - public Guid? MinFraudId { get; init; } + public Guid? MinFraudId + { + get => field; + init => field = value == Guid.Empty ? null : value; + } /// /// Your notes on the fraud tag associated with the transaction. We diff --git a/MaxMind.MinFraud/WebServiceClient.cs b/MaxMind.MinFraud/WebServiceClient.cs index eeaf4e98..923b5019 100644 --- a/MaxMind.MinFraud/WebServiceClient.cs +++ b/MaxMind.MinFraud/WebServiceClient.cs @@ -181,7 +181,7 @@ public async Task ScoreAsync(Transaction transaction) public async Task ReportAsync(TransactionReport report) { if (report.IPAddress == null - && (report.MinFraudId == null || report.MinFraudId == Guid.Empty) + && report.MinFraudId == null && string.IsNullOrEmpty(report.MaxMindId) && string.IsNullOrEmpty(report.TransactionId)) { From 6e7fd1d6702dddf7b7c261802da1e9511e3438e4 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 20 Mar 2026 22:33:00 +0000 Subject: [PATCH 11/14] Make Tag a required property on TransactionReport Without this, `new TransactionReport { IPAddress = ip }` silently defaults Tag to NotFraud (the first enum member), sending an incorrect report. The `required` keyword makes omitting Tag a compile-time error. Add polyfill attributes for netstandard2.0/2.1 where RequiredMemberAttribute and friends don't exist. Mark both obsolete constructors with [SetsRequiredMembers] so they remain usable. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Polyfill/RequiredMemberAttributes.cs | 24 +++++++++++++++++++ MaxMind.MinFraud/Request/TransactionReport.cs | 5 +++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 MaxMind.MinFraud/Polyfill/RequiredMemberAttributes.cs diff --git a/MaxMind.MinFraud/Polyfill/RequiredMemberAttributes.cs b/MaxMind.MinFraud/Polyfill/RequiredMemberAttributes.cs new file mode 100644 index 00000000..a7040bac --- /dev/null +++ b/MaxMind.MinFraud/Polyfill/RequiredMemberAttributes.cs @@ -0,0 +1,24 @@ +#if !NET7_0_OR_GREATER + +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field + | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + internal sealed class RequiredMemberAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] + internal sealed class CompilerFeatureRequiredAttribute : Attribute + { + public CompilerFeatureRequiredAttribute(string featureName) => FeatureName = featureName; + public string FeatureName { get; } + public bool IsOptional { get; init; } + } +} + +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] + internal sealed class SetsRequiredMembersAttribute : Attribute { } +} + +#endif diff --git a/MaxMind.MinFraud/Request/TransactionReport.cs b/MaxMind.MinFraud/Request/TransactionReport.cs index 7a05fd52..0711c2d0 100644 --- a/MaxMind.MinFraud/Request/TransactionReport.cs +++ b/MaxMind.MinFraud/Request/TransactionReport.cs @@ -1,5 +1,6 @@ using MaxMind.MinFraud.Util; using System; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Runtime.Serialization; using System.Text.Json.Serialization; @@ -85,6 +86,7 @@ public TransactionReport() { } /// or minfraudId>. You are encouraged to provide it, if /// possible. [Obsolete("Use object initializer syntax.")] + [SetsRequiredMembers] public TransactionReport( TransactionReportTag tag, string? chargebackCode = null, @@ -121,6 +123,7 @@ public TransactionReport( /// Constructor for backwards compatibility. /// [Obsolete] + [SetsRequiredMembers] public TransactionReport( IPAddress? ipAddress, TransactionReportTag tagObsolete, @@ -146,7 +149,7 @@ public TransactionReport( /// [JsonConverter(typeof(EnumMemberValueConverter))] [JsonPropertyName("tag")] - public TransactionReportTag Tag { get; init; } + public required TransactionReportTag Tag { get; init; } /// /// A string which is provided by your payment processor indicating From 0c53866995acb2459fa3026e2362e7e27b8268ae Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 20 Mar 2026 22:33:09 +0000 Subject: [PATCH 12/14] Add missing .ConfigureAwait(false) in ReportAsync All other async methods in WebServiceClient use ConfigureAwait(false) but ReportAsync was missing it, which could cause deadlocks in synchronous-over-async contexts. Co-Authored-By: Claude Opus 4.6 (1M context) --- MaxMind.MinFraud/WebServiceClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MaxMind.MinFraud/WebServiceClient.cs b/MaxMind.MinFraud/WebServiceClient.cs index 923b5019..c6f9d15b 100644 --- a/MaxMind.MinFraud/WebServiceClient.cs +++ b/MaxMind.MinFraud/WebServiceClient.cs @@ -190,7 +190,7 @@ public async Task ReportAsync(TransactionReport report) + "IPAddress, MinFraudId, MaxMindId, TransactionId."); } - await MakeRequest("transactions/report", report); + await MakeRequest("transactions/report", report).ConfigureAwait(false); } private async Task MakeResponse(Transaction request) where T : Score From 9ee972e4540995d657e1e4eeab84a284e1185b4c Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 20 Mar 2026 22:33:50 +0000 Subject: [PATCH 13/14] Add test for obsolete constructor validation Exercise the deprecated constructor path to verify it requires at least one identifier, sets properties correctly, and normalizes Guid.Empty to null. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Request/TransactionReportTest.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/MaxMind.MinFraud.UnitTest/Request/TransactionReportTest.cs b/MaxMind.MinFraud.UnitTest/Request/TransactionReportTest.cs index bd157a51..89bf0c3b 100644 --- a/MaxMind.MinFraud.UnitTest/Request/TransactionReportTest.cs +++ b/MaxMind.MinFraud.UnitTest/Request/TransactionReportTest.cs @@ -48,6 +48,30 @@ public void TestGuidEmptyIsNormalized() Assert.Null(report.MinFraudId); } + [Fact] + public void TestConstructorValidation() + { +#pragma warning disable CS0618 // Type or member is obsolete + // Constructor requires at least one identifier + Assert.Throws(() => + new TransactionReport(tag: TransactionReportTag.NotFraud)); + + // Constructor with valid identifier succeeds + var report = new TransactionReport( + tag: TransactionReportTag.Chargeback, + ipAddress: IPAddress.Parse("1.1.1.1")); + Assert.Equal(TransactionReportTag.Chargeback, report.Tag); + Assert.Equal(IPAddress.Parse("1.1.1.1"), report.IPAddress); + + // Constructor normalizes Guid.Empty to null + report = new TransactionReport( + tag: TransactionReportTag.NotFraud, + ipAddress: IPAddress.Parse("1.1.1.1"), + minfraudId: Guid.Empty); + Assert.Null(report.MinFraudId); +#pragma warning restore CS0618 + } + [Theory] [InlineData("abcd123")] [InlineData("")] From fb94f353ab57b28df5d106839c91bec14f71a9eb Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 20 Mar 2026 22:34:04 +0000 Subject: [PATCH 14/14] Update README exceptions section for init-property validation Validation now happens at property init time and in ReportAsync, not in constructors. Update the documentation to reflect this. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f274605b..187ef729 100644 --- a/README.md +++ b/README.md @@ -199,8 +199,8 @@ there is an error, an exception will be thrown. Thrown by the request models: -- `ArgumentException` - Thrown when invalid data is passed to the model - constructor. +- `ArgumentException` - Thrown when invalid data is passed to a request model + property or to `ReportAsync`. Thrown by `WebServiceClient` method calls: