Skip to content

Commit f976574

Browse files
committed
Merge branch 'patch/32.1.3' into HEAD
2 parents 8950d19 + cfacb6e commit f976574

File tree

5 files changed

+352
-5
lines changed

5 files changed

+352
-5
lines changed

.github/workflows/build-net-6.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Manual CI for .NET 6.0 legacy support.
2+
# Not auto-triggered: We only maintain v6.0 support for older version compatibility.
3+
name: "Legacy Support: .NET 6.0 Build & Test"
4+
5+
on:
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
dotnet6-build-and-unit-test:
13+
name: Build & Test (.NET 6.0) - ${{ matrix.os }}
14+
runs-on: ${{ matrix.os }}
15+
16+
strategy:
17+
matrix:
18+
os: [ ubuntu-latest, windows-latest ]
19+
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Setup .NET SDKs
24+
uses: actions/setup-dotnet@v4
25+
with:
26+
dotnet-version: |
27+
6.0.x
28+
8.0.x
29+
30+
- name: Restore
31+
run: dotnet restore
32+
33+
- name: Build (Debug)
34+
run: dotnet build --configuration Debug --framework net6.0 --no-restore
35+
36+
- name: Test
37+
run: dotnet test --no-build --configuration Debug --framework net6.0 --no-restore Adyen.Test/Adyen.Test.csproj
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System;
2+
using Newtonsoft.Json;
3+
using Newtonsoft.Json.Converters;
4+
using System.Runtime.Serialization;
5+
using Microsoft.VisualStudio.TestTools.UnitTesting;
6+
7+
namespace Adyen.Test
8+
{
9+
[TestClass]
10+
public class StringEnumConverterTest
11+
{
12+
[JsonConverter(typeof(StringEnumConverter))]
13+
private enum TestEnum
14+
{
15+
[EnumMember(Value = "Value1")]
16+
Value1 = 1,
17+
18+
[EnumMember(Value = "Value2")]
19+
Value2 = 2
20+
}
21+
22+
private class TestModel
23+
{
24+
[JsonProperty("enumField")]
25+
public TestEnum? EnumField { get; set; }
26+
}
27+
28+
[TestMethod]
29+
[ExpectedException(typeof(JsonSerializationException))]
30+
public void Given_StringEnumConverter_When_UnknownValue_Throws_JsonSerializationException()
31+
{
32+
var json = @"{""enumField"": ""UnknownValue""}";
33+
JsonConvert.DeserializeObject<TestModel>(json);
34+
}
35+
36+
[TestMethod]
37+
public void Given_StringEnumConverter_When_Null_Result_TestModel_EnumField_Is_Null()
38+
{
39+
var json = @"{""enumField"": null}";
40+
41+
var result = JsonConvert.DeserializeObject<TestModel>(json);
42+
Assert.IsNull(result.EnumField);
43+
}
44+
45+
[TestMethod]
46+
public void Given_StringEnumConverter_When_Value1_Returns_Value1_As_Enum()
47+
{
48+
var json = @"{""enumField"": ""Value1""}";
49+
var result = JsonConvert.DeserializeObject<TestModel>(json);
50+
51+
Assert.IsNotNull(result);
52+
Assert.AreEqual(TestEnum.Value1, result.EnumField);
53+
}
54+
}
55+
}
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
using Adyen.Model.Checkout;
2+
using Adyen.Model.TransferWebhooks;
3+
using Adyen.Util;
4+
using Adyen.Webhooks;
5+
using Newtonsoft.Json;
6+
using Microsoft.VisualStudio.TestTools.UnitTesting;
7+
8+
namespace Adyen.Test
9+
{
10+
[TestClass]
11+
public class UnknownEnumDeserializationTest
12+
{
13+
[TestMethod]
14+
public void TestUnknownEnumValueDeserializesToNull()
15+
{
16+
// Test JSON with an unknown enum value
17+
var json = @"{
18+
""channel"": ""UnknownChannelValue"",
19+
""amount"": {
20+
""value"": 1000,
21+
""currency"": ""EUR""
22+
},
23+
""reference"": ""test-ref"",
24+
""merchantAccount"": ""test-merchant""
25+
}";
26+
27+
// This should not throw an exception when using JsonOperation.Deserialize
28+
var paymentRequest = JsonOperation.Deserialize<PaymentRequest>(json);
29+
30+
// The unknown enum value should deserialize to null
31+
Assert.IsNotNull(paymentRequest);
32+
Assert.IsNull(paymentRequest.Channel);
33+
}
34+
35+
[TestMethod]
36+
public void TestKnownEnumValueDeserializesCorrectly()
37+
{
38+
// Test JSON with a known enum value
39+
var json = @"{
40+
""channel"": ""Web"",
41+
""amount"": {
42+
""value"": 1000,
43+
""currency"": ""EUR""
44+
},
45+
""reference"": ""test-ref"",
46+
""merchantAccount"": ""test-merchant""
47+
}";
48+
49+
var paymentRequest = JsonOperation.Deserialize<PaymentRequest>(json);
50+
51+
Assert.IsNotNull(paymentRequest);
52+
Assert.IsNotNull(paymentRequest.Channel);
53+
Assert.AreEqual(PaymentRequest.ChannelEnum.Web, paymentRequest.Channel);
54+
}
55+
56+
[TestMethod]
57+
public void TestMissingEnumValueDeserializesToNull()
58+
{
59+
// Test JSON without the enum field
60+
var json = @"{
61+
""amount"": {
62+
""value"": 1000,
63+
""currency"": ""EUR""
64+
},
65+
""reference"": ""test-ref"",
66+
""merchantAccount"": ""test-merchant""
67+
}";
68+
69+
var paymentRequest = JsonOperation.Deserialize<PaymentRequest>(json);
70+
71+
Assert.IsNotNull(paymentRequest);
72+
Assert.IsNull(paymentRequest.Channel);
73+
}
74+
75+
[TestMethod]
76+
public void TestMultipleUnknownEnumValues()
77+
{
78+
// Test JSON with multiple unknown enum values
79+
var json = @"{
80+
""channel"": ""FutureChannel"",
81+
""entityType"": ""UnknownEntityType"",
82+
""industryUsage"": ""newUsageType"",
83+
""amount"": {
84+
""value"": 1000,
85+
""currency"": ""EUR""
86+
},
87+
""reference"": ""test-ref"",
88+
""merchantAccount"": ""test-merchant""
89+
}";
90+
91+
var paymentRequest = JsonOperation.Deserialize<PaymentRequest>(json);
92+
93+
Assert.IsNotNull(paymentRequest);
94+
Assert.IsNull(paymentRequest.Channel);
95+
Assert.IsNull(paymentRequest.EntityType);
96+
Assert.IsNull(paymentRequest.IndustryUsage);
97+
}
98+
99+
[TestMethod]
100+
public void TestSerializationOfNullEnumValue()
101+
{
102+
// Create a payment request with null enum values
103+
var paymentRequest = new PaymentRequest
104+
{
105+
Channel = null,
106+
EntityType = null
107+
};
108+
109+
var json = JsonConvert.SerializeObject(paymentRequest);
110+
111+
// Null values should not be serialized (based on EmitDefaultValue = false)
112+
Assert.IsFalse(json.Contains("\"channel\""));
113+
Assert.IsFalse(json.Contains("\"entityType\""));
114+
}
115+
116+
[TestMethod]
117+
public void TestTransferWebhookWithUnknownReasonEnum()
118+
{
119+
var json = @"{
120+
""data"": {
121+
""balancePlatform"": ""N2F_SANDBOX"",
122+
""creationDate"": ""2026-01-14T14:26:59+01:00"",
123+
""id"": ""38EBJS69NWKG7GSL"",
124+
""accountHolder"": {
125+
""description"": ""AccountId : 9819 - LegalEntityId : 58019 - LegalEntityName : Sandbox 01"",
126+
""id"": ""AH32CM322322995MS737SFD8T"",
127+
""reference"": ""dd92c730-ae54-47c5-9c5f-25307e13efd4""
128+
},
129+
""amount"": { ""currency"": ""EUR"", ""value"": 100 },
130+
""balanceAccount"": {
131+
""description"": ""AccountId : 9819 - LegalEntityId : 58019 - LegalEntityName : Sandbox 01"",
132+
""id"": ""BA32CM322322995MS737TFDPH"",
133+
""reference"": ""dd92c730-ae54-47c5-9c5f-25307e13efd4""
134+
},
135+
""category"": ""issuedCard"",
136+
""categoryData"": {
137+
""authorisationType"": ""finalAuthorisation"",
138+
""panEntryMode"": ""manual"",
139+
""processingType"": ""ecommerce"",
140+
""relayedAuthorisationData"": { ""metadata"": { ""RefusedByN2F"": ""RefusedByRelayedRules"" }, ""reference"": """" },
141+
""schemeUniqueTransactionId"": ""MCS9S6YGY0114"",
142+
""type"": ""issuedCard"",
143+
""validationFacts"": [
144+
{ ""result"": ""valid"", ""type"": ""accountLookup"" },
145+
{ ""result"": ""valid"", ""type"": ""cardAuthentication"" },
146+
{ ""result"": ""notValidated"", ""type"": ""mitAllowedMerchant"" },
147+
{ ""result"": ""valid"", ""type"": ""paymentInstrumentFound"" },
148+
{ ""result"": ""valid"", ""type"": ""transactionValidation"" },
149+
{ ""result"": ""notApplicable"", ""type"": ""authorisedPaymentInstrumentUser"" },
150+
{ ""result"": ""valid"", ""type"": ""screening"" },
151+
{ ""result"": ""valid"", ""type"": ""partyScreening"" },
152+
{ ""result"": ""valid"", ""type"": ""transactionRules"" },
153+
{ ""result"": ""invalid"", ""type"": ""relayedAuthorisation"" },
154+
{ ""result"": ""valid"", ""type"": ""inputExpiryDateCheck"" },
155+
{ ""result"": ""valid"", ""type"": ""paymentInstrument"" },
156+
{ ""result"": ""valid"", ""type"": ""cardholderAuthentication"" },
157+
{ ""result"": ""valid"", ""type"": ""paymentInstrumentExpirationCheck"" },
158+
{ ""result"": ""valid"", ""type"": ""balanceCheck"" },
159+
{ ""result"": ""valid"", ""type"": ""paymentInstrumentActive"" },
160+
{ ""result"": ""valid"", ""type"": ""realBalanceAvailable"" }
161+
]
162+
},
163+
""counterparty"": {
164+
""merchant"": {
165+
""acquirerId"": ""013445"",
166+
""mcc"": ""7999"",
167+
""merchantId"": ""526567789012346"",
168+
""nameLocation"": { ""city"": ""Amsterdam"", ""country"": ""NL"", ""name"": ""N2F_FR_SANDBOX_TEST"" },
169+
""postalCode"": ""1011 DJ"",
170+
""city"": ""Amsterdam"",
171+
""country"": ""NETHERLANDS"",
172+
""name"": ""N2F_FR_SANDBOX_TEST""
173+
}
174+
},
175+
""createdAt"": ""2026-01-14T14:26:58+01:00"",
176+
""direction"": ""outgoing"",
177+
""paymentInstrument"": { ""id"": ""PI32CM722322B35NR3F6RBNN8"", ""reference"": ""dd92c730-ae54-47c5-9c5f-25307e13efd4"" },
178+
""reason"": ""UNKNWON_REASON_HERE_000"",
179+
""reference"": ""229BZZ4LB5LGGWS5-MCS9S6YGY"",
180+
""status"": ""refused"",
181+
""type"": ""payment"",
182+
""balances"": [{ ""currency"": ""EUR"", ""received"": -100 }],
183+
""eventId"": ""EVJN4295V223223W5NR3GPKDS32BBH"",
184+
""events"": [
185+
{
186+
""amount"": { ""currency"": ""EUR"", ""value"": -100 },
187+
""bookingDate"": ""2026-01-14T14:26:59+01:00"",
188+
""id"": ""EVJN42CSX223223W5NR3GPKDNB4DBC"",
189+
""mutations"": [{ ""currency"": ""EUR"", ""received"": -100 }],
190+
""originalAmount"": { ""currency"": ""EUR"", ""value"": -100 },
191+
""status"": ""received"",
192+
""type"": ""accounting""
193+
},
194+
{
195+
""amount"": { ""currency"": ""EUR"", ""value"": 100 },
196+
""bookingDate"": ""2026-01-14T14:26:59+01:00"",
197+
""id"": ""EVJN4295V223223W5NR3GPKDS32BBH"",
198+
""mutations"": [{ ""currency"": ""EUR"", ""received"": 100 }],
199+
""originalAmount"": { ""currency"": ""EUR"", ""value"": 100 },
200+
""status"": ""refused"",
201+
""type"": ""accounting""
202+
}
203+
],
204+
""sequenceNumber"": 2,
205+
""transactionRulesResult"": { ""allHardBlockRulesPassed"": true, ""score"": 0 },
206+
""updatedAt"": ""2026-01-14T14:26:59+01:00""
207+
},
208+
""environment"": ""test"",
209+
""timestamp"": ""2026-01-14T13:27:01.805Z"",
210+
""type"": ""balancePlatform.transfer.updated""
211+
}";
212+
213+
var handler = new BalancePlatformWebhookHandler();
214+
var transferNotificationRequest = handler.GetGenericBalancePlatformWebhook(json) as TransferNotificationRequest;
215+
216+
Assert.IsNotNull(transferNotificationRequest);
217+
Assert.IsNotNull(transferNotificationRequest.Data);
218+
Assert.IsNull(transferNotificationRequest.Data.Reason); // Issue #1228
219+
Assert.AreEqual(TransferData.StatusEnum.Refused, transferNotificationRequest.Data.Status);
220+
}
221+
}
222+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System;
2+
using Newtonsoft.Json;
3+
using Newtonsoft.Json.Converters;
4+
5+
namespace Adyen.Util
6+
{
7+
/// <summary>
8+
/// A custom JSON converter for enums that returns null for unknown values instead of throwing an exception.
9+
/// This ensures forward compatibility when new enum values are added to the API.
10+
/// </summary>
11+
public class SafeStringEnumConverter : StringEnumConverter
12+
{
13+
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
14+
{
15+
try
16+
{
17+
return base.ReadJson(reader, objectType, existingValue, serializer);
18+
}
19+
catch (JsonSerializationException)
20+
{
21+
// If the enum value is unknown, return null for nullable enums
22+
// This allows the application to continue processing even when encountering unknown enum values
23+
if (Nullable.GetUnderlyingType(objectType) != null)
24+
{
25+
return null;
26+
}
27+
28+
// For non-nullable enums, we still throw to maintain backward compatibility
29+
throw;
30+
}
31+
}
32+
}
33+
}

templates/csharp/modelGeneric.mustache

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
{{#deprecated}}
4343
[Obsolete("{{#vendorExtensions.x-deprecatedInVersion}}Deprecated since {{#appName}}{{{.}}}{{/appName}} v{{#vendorExtensions.x-deprecatedInVersion}}{{.}}{{/vendorExtensions.x-deprecatedInVersion}}.{{/vendorExtensions.x-deprecatedInVersion}}{{#vendorExtensions.x-deprecatedMessage}} {{{.}}}{{/vendorExtensions.x-deprecatedMessage}}")]
4444
{{/deprecated}}
45-
public {{{complexType}}}{{^complexType}}{{{datatypeWithEnum}}}{{/complexType}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}} {{name}} { get; set; }
45+
public {{{complexType}}}{{^complexType}}{{{datatypeWithEnum}}}{{/complexType}}{{^isContainer}}?{{/isContainer}} {{name}} { get; set; }
4646
{{#isReadOnly}}
4747

4848
/// <summary>
@@ -61,7 +61,7 @@
6161
{{#deprecated}}
6262
[Obsolete("{{#vendorExtensions.x-deprecatedInVersion}}Deprecated since {{#appName}}{{{.}}}{{/appName}} v{{#vendorExtensions.x-deprecatedInVersion}}{{.}}{{/vendorExtensions.x-deprecatedInVersion}}.{{/vendorExtensions.x-deprecatedInVersion}}{{#vendorExtensions.x-deprecatedMessage}} {{{.}}}{{/vendorExtensions.x-deprecatedMessage}}")]
6363
{{/deprecated}}
64-
public {{{complexType}}}{{^complexType}}{{{datatypeWithEnum}}}{{/complexType}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}} {{name}} { get; set; }
64+
public {{{complexType}}}{{^complexType}}{{{datatypeWithEnum}}}{{/complexType}}{{^isContainer}}?{{/isContainer}} {{name}} { get; set; }
6565

6666
/// <summary>
6767
/// Returns false as {{name}} should not be serialized given that it's read-only.
@@ -78,7 +78,7 @@
7878
{{#deprecated}}
7979
[Obsolete("{{#vendorExtensions.x-deprecatedInVersion}}Deprecated since {{#appName}}{{{.}}}{{/appName}} v{{#vendorExtensions.x-deprecatedInVersion}}{{.}}{{/vendorExtensions.x-deprecatedInVersion}}.{{/vendorExtensions.x-deprecatedInVersion}}{{#vendorExtensions.x-deprecatedMessage}} {{{.}}}{{/vendorExtensions.x-deprecatedMessage}}")]
8080
{{/deprecated}}
81-
public {{{complexType}}}{{^complexType}}{{{datatypeWithEnum}}}{{/complexType}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}} {{name}}
81+
public {{{complexType}}}{{^complexType}}{{{datatypeWithEnum}}}{{/complexType}}{{^isContainer}}?{{/isContainer}} {{name}}
8282
{
8383
get{ return _{{name}};}
8484
set
@@ -87,7 +87,7 @@
8787
_flag{{name}} = true;
8888
}
8989
}
90-
private {{{complexType}}}{{^complexType}}{{{datatypeWithEnum}}}{{/complexType}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}} _{{name}};
90+
private {{{complexType}}}{{^complexType}}{{{datatypeWithEnum}}}{{/complexType}}{{^isContainer}}?{{/isContainer}} _{{name}};
9191
private bool _flag{{name}};
9292

9393
/// <summary>
@@ -128,7 +128,7 @@
128128
{{#hasOnlyReadOnly}}
129129
[JsonConstructorAttribute]
130130
{{/hasOnlyReadOnly}}
131-
public {{classname}}({{#readWriteVars}}{{{datatypeWithEnum}}}{{#isNumeric}}?{{/isNumeric}}{{#isBoolean}}{{^isNullable}}?{{/isNullable}}{{/isBoolean}}{{#isEnum}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}}{{/isEnum}} {{#lambda.camelcase_param}}{{name}}{{/lambda.camelcase_param}} = {{#defaultValue}}{{^isDateTime}}{{{defaultValue}}}{{/isDateTime}}{{#isDateTime}}default({{{datatypeWithEnum}}}){{/isDateTime}}{{/defaultValue}}{{^defaultValue}}default({{{datatypeWithEnum}}}{{#isNumeric}}?{{/isNumeric}}{{#isBoolean}}{{^isNullable}}?{{/isNullable}}{{/isBoolean}}{{#isEnum}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}}{{/isEnum}}){{/defaultValue}}{{^-last}}, {{/-last}}{{/readWriteVars}}){{#parent}} : base({{#parentVars}}{{#lambda.camelcase_param}}{{name}}{{/lambda.camelcase_param}}{{^-last}}, {{/-last}}{{/parentVars}}){{/parent}}
131+
public {{classname}}({{#readWriteVars}}{{{datatypeWithEnum}}}{{#isNumeric}}?{{/isNumeric}}{{#isBoolean}}{{^isNullable}}?{{/isNullable}}{{/isBoolean}}{{#isEnum}}{{^isContainer}}?{{/isContainer}}{{/isEnum}} {{#lambda.camelcase_param}}{{name}}{{/lambda.camelcase_param}} = {{#defaultValue}}{{^isDateTime}}{{{defaultValue}}}{{/isDateTime}}{{#isDateTime}}default({{{datatypeWithEnum}}}){{/isDateTime}}{{/defaultValue}}{{^defaultValue}}default({{{datatypeWithEnum}}}{{#isNumeric}}?{{/isNumeric}}{{#isBoolean}}{{^isNullable}}?{{/isNullable}}{{/isBoolean}}{{#isEnum}}{{^isContainer}}?{{/isContainer}}{{/isEnum}}){{/defaultValue}}{{^-last}}, {{/-last}}{{/readWriteVars}}){{#parent}} : base({{#parentVars}}{{#lambda.camelcase_param}}{{name}}{{/lambda.camelcase_param}}{{^-last}}, {{/-last}}{{/parentVars}}){{/parent}}
132132
{
133133
{{#vars}}
134134
{{^isInherited}}

0 commit comments

Comments
 (0)