diff --git a/src/CheckoutSdk/Issuing/Cards/Responses/Create/AbstractCardControlsResponse.cs b/src/CheckoutSdk/Issuing/Cards/Responses/Create/AbstractCardControlsResponse.cs index 16f33946..edd4381a 100644 --- a/src/CheckoutSdk/Issuing/Cards/Responses/Create/AbstractCardControlsResponse.cs +++ b/src/CheckoutSdk/Issuing/Cards/Responses/Create/AbstractCardControlsResponse.cs @@ -1,24 +1,68 @@ using Checkout.Issuing.Common; +using System; namespace Checkout.Issuing.Cards.Responses.Create { + /// + /// A control that was set on the card when the virtual card was created, discriminated on control_type. + /// public abstract class AbstractCardControlsResponse { + /// + /// The control's type. + /// A velocity_limit determines how much can be spent over a given period of time. An mcc_limit determines + /// the types of businesses from which transactions can be processed. A mid_limit specifies the merchants + /// from whom transactions can be processed. + /// [Required] + /// public IssuingControlType? ControlType { get; set; } + /// + /// The control's unique identifier. + /// [Required] + /// ^ctr_[a-z0-9]{26}$ + /// 30 characters + /// public string Id { get; set; } + /// + /// The ID of the card the control applies to. + /// Not returned by the API in this response; still deserialized if present, otherwise null. + /// [Optional] + /// + [Obsolete("Not returned by the API in this response. Will be removed in the next major version.")] public string TargetId { get; set; } + /// + /// A description for the control. + /// [Required] + /// max 256 characters + /// public string Description { get; set; } + /// + /// The date and time the control was created. + /// Not returned by the API in this response; still deserialized if present, otherwise null. + /// [Optional] + /// + [Obsolete("Not returned by the API in this response. Will be removed in the next major version.")] public string CreatedDate { get; set; } + /// + /// The date and time the control was last modified. + /// Not returned by the API in this response; still deserialized if present, otherwise null. + /// [Optional] + /// + [Obsolete("Not returned by the API in this response. Will be removed in the next major version.")] public string LastModifiedDate { get; set; } + /// + /// Initializes the control with its discriminator value. + /// + /// The control's type. protected AbstractCardControlsResponse(IssuingControlType controlType) { ControlType = controlType; } } -} \ No newline at end of file +} diff --git a/src/CheckoutSdk/Issuing/Cards/Responses/Create/AbstractCardCreateResponse.cs b/src/CheckoutSdk/Issuing/Cards/Responses/Create/AbstractCardCreateResponse.cs index d46359d6..121ead0d 100644 --- a/src/CheckoutSdk/Issuing/Cards/Responses/Create/AbstractCardCreateResponse.cs +++ b/src/CheckoutSdk/Issuing/Cards/Responses/Create/AbstractCardCreateResponse.cs @@ -4,6 +4,10 @@ namespace Checkout.Issuing.Cards.Responses.Create { + /// + /// The response returned when a card is created, discriminated on type (virtual or physical). + /// _links (CardLinks: self, credentials, revoke, controls and more) is exposed through Links. + /// public abstract class AbstractCardCreateResponse : Resource { /// @@ -16,7 +20,7 @@ public abstract class AbstractCardCreateResponse : Resource /// The card's unique identifier. /// [Required] /// ^crd_[a-z0-9]{26}$ - /// min 30 characters, max 30 characters + /// 30 characters /// public string Id { get; set; } @@ -24,7 +28,7 @@ public abstract class AbstractCardCreateResponse : Resource /// The client's unique identifier. /// [Required] /// ^cli_[a-z0-9]{26}$ - /// min 30 characters, max 30 characters + /// 30 characters /// public string ClientId { get; set; } @@ -32,7 +36,7 @@ public abstract class AbstractCardCreateResponse : Resource /// The entity's unique identifier. /// [Required] /// ^ent_[a-z0-9]{26}$ - /// min 30 characters, max 30 characters + /// 30 characters /// public string EntityId { get; set; } @@ -48,62 +52,75 @@ public abstract class AbstractCardCreateResponse : Resource /// The last four digits of the card number, also known as the PAN. /// [Required] /// ^[0-9]{4}$ + /// 4 characters /// public string LastFour { get; set; } /// /// The card's expiration month. /// [Required] - /// >= 1 - /// <= 12 + /// Format: int32 + /// min 1, max 12 /// public int? ExpiryMonth { get; set; } /// /// The card's expiration year. /// [Required] - /// min 4 characters, max 4 characters + /// Format: int32 + /// 4 digits /// public int? ExpiryYear { get; set; } /// /// The issuing currency, as a three-letter ISO currency code. /// [Required] + /// Format: ISO4217 /// ^[a-zA-Z]{3}$ - /// min 3 characters, max 3 characters + /// 3 characters /// public Currency? BillingCurrency { get; set; } /// /// The issuing country, as a two-letter ISO country code. /// [Required] - /// min 2 characters, max 2 characters + /// 2 characters /// public CountryCode? IssuingCountry { get; set; } /// - /// The date and time the card was created, in UTC. - /// [Optional] + /// The card scheme. + /// [Required] + /// + public IssuingScheme? Scheme { get; set; } + + /// + /// The date and time the card was created. + /// [Required] /// Format: date-time (RFC 3339) /// public DateTime? CreatedDate { get; set; } /// /// The card's status, which determines whether it can approve incoming authorizations. + /// inactive: the default state, authorizations are declined until the card is activated. + /// active: authorization requests can be approved. + /// suspended: authorization requests are declined; the card can be reactivated. + /// revoked: authorization requests are permanently declined; the card cannot be reactivated. /// [Required] - /// Enum: "active" "inactive" "revoked" "suspended" /// public CardStatus? Status { get; set; } /// /// Your reference. /// [Optional] - /// <= 256 characters + /// max 256 characters /// public string Reference { get; set; } /// /// The card will be revoked at midnight UTC on the date specified. + /// Not listed in add-card-response, but sent on add-card-request and returned by get-card-response. /// [Optional] /// Format: yyyy-MM-dd /// @@ -112,14 +129,19 @@ public abstract class AbstractCardCreateResponse : Resource /// /// The date and time the card was last activated. If the card has never been activated, this field /// returns null. - /// [Optional, read-only] + /// [Optional] /// Format: date-time (RFC 3339) + /// Nullable, read-only /// public DateTime? LastActivatedOn { get; set; } + /// + /// Initializes the card response with its discriminator value. + /// + /// The card type. protected AbstractCardCreateResponse(IssuingCardType? type) { Type = type; } } -} \ No newline at end of file +} diff --git a/src/CheckoutSdk/Issuing/Cards/Responses/Create/CardControlsResponseConverter.cs b/src/CheckoutSdk/Issuing/Cards/Responses/Create/CardControlsResponseConverter.cs index bbf1bec7..8a5734a4 100644 --- a/src/CheckoutSdk/Issuing/Cards/Responses/Create/CardControlsResponseConverter.cs +++ b/src/CheckoutSdk/Issuing/Cards/Responses/Create/CardControlsResponseConverter.cs @@ -68,7 +68,9 @@ private static AbstractCardControlsResponse Create(string type) private static string GetSourceType(JToken jObject) { - return jObject.SelectToken(CheckoutUtils.Type)?.Value()?.ToLowerInvariant(); + // The spec discriminates card controls on control_type; type is kept as a fallback. + var token = jObject.SelectToken(CheckoutUtils.ControlType) ?? jObject.SelectToken(CheckoutUtils.Type); + return token?.Value()?.ToLowerInvariant(); } } } \ No newline at end of file diff --git a/src/CheckoutSdk/Issuing/Cards/Responses/Create/Credentials.cs b/src/CheckoutSdk/Issuing/Cards/Responses/Create/Credentials.cs index 0979dd5b..2def7673 100644 --- a/src/CheckoutSdk/Issuing/Cards/Responses/Create/Credentials.cs +++ b/src/CheckoutSdk/Issuing/Cards/Responses/Create/Credentials.cs @@ -1,9 +1,25 @@ namespace Checkout.Issuing.Cards.Responses.Create { + /// + /// The virtual card's credentials, returned when they were requested on creation. + /// public class Credentials { + /// + /// The unencrypted card number. + /// [Optional] + /// Format: pan + /// ^[0-9]{16}$ + /// 16 characters + /// public string Number { get; set; } - + + /// + /// The card's verification code, also known as the CVV or CVC2. + /// [Optional] + /// ^[0-9]{3}$ + /// 3 characters + /// public string Cvc2 { get; set; } } -} \ No newline at end of file +} diff --git a/src/CheckoutSdk/Issuing/Cards/Responses/Create/MccCardControlsResponse.cs b/src/CheckoutSdk/Issuing/Cards/Responses/Create/MccCardControlsResponse.cs index ef5a76d7..394b8419 100644 --- a/src/CheckoutSdk/Issuing/Cards/Responses/Create/MccCardControlsResponse.cs +++ b/src/CheckoutSdk/Issuing/Cards/Responses/Create/MccCardControlsResponse.cs @@ -2,12 +2,23 @@ namespace Checkout.Issuing.Cards.Responses.Create { + /// + /// A card control of type mcc_limit set when the virtual card was created. + /// public class MccCardControlsResponse : AbstractCardControlsResponse { + /// + /// Initializes an mcc_limit control. + /// public MccCardControlsResponse() : base(IssuingControlType.MccLimit) { } + /// + /// The merchant category code (MCC) rule, which determines the types of businesses transactions can be + /// processed from. + /// [Required] + /// public MccLimit MccLimit { get; set; } } -} \ No newline at end of file +} diff --git a/src/CheckoutSdk/Issuing/Cards/Responses/Create/MidCardControlsResponse.cs b/src/CheckoutSdk/Issuing/Cards/Responses/Create/MidCardControlsResponse.cs index 5d5ca910..61948814 100644 --- a/src/CheckoutSdk/Issuing/Cards/Responses/Create/MidCardControlsResponse.cs +++ b/src/CheckoutSdk/Issuing/Cards/Responses/Create/MidCardControlsResponse.cs @@ -1,13 +1,39 @@ using Checkout.Issuing.Common; +using Newtonsoft.Json; +using System; namespace Checkout.Issuing.Cards.Responses.Create { + /// + /// A card control of type mid_limit set when the virtual card was created. + /// public class MidCardControlsResponse : AbstractCardControlsResponse { + /// + /// Initializes a mid_limit control. + /// public MidCardControlsResponse() : base(IssuingControlType.MidLimit) { } - public MidLimit VelocityLimit { get; set; } + /// + /// The merchant identification (MID) code rule, which determines the merchants from whom transactions can + /// be processed. + /// [Required] + /// + public MidLimit MidLimit { get; set; } + + /// + /// Former name of . It was mapped to velocity_limit, so it was never populated. + /// It now reads and writes and is not serialized. + /// [Optional] + /// + [Obsolete("Use MidLimit instead. Will be removed in the next major version.")] + [JsonIgnore] + public MidLimit VelocityLimit + { + get => MidLimit; + set => MidLimit = value; + } } -} \ No newline at end of file +} diff --git a/src/CheckoutSdk/Issuing/Cards/Responses/Create/PhysicalCardCreateResponse.cs b/src/CheckoutSdk/Issuing/Cards/Responses/Create/PhysicalCardCreateResponse.cs index 9c4de4ac..0636b04e 100644 --- a/src/CheckoutSdk/Issuing/Cards/Responses/Create/PhysicalCardCreateResponse.cs +++ b/src/CheckoutSdk/Issuing/Cards/Responses/Create/PhysicalCardCreateResponse.cs @@ -2,10 +2,16 @@ namespace Checkout.Issuing.Cards.Responses.Create { + /// + /// The response returned when a physical card is created (add-physical-card-response). + /// public class PhysicalCardCreateResponse : AbstractCardCreateResponse { + /// + /// Initializes a physical card response. + /// public PhysicalCardCreateResponse() : base(IssuingCardType.Physical) { } } -} \ No newline at end of file +} diff --git a/src/CheckoutSdk/Issuing/Cards/Responses/Create/VelocityCardControlsResponse.cs b/src/CheckoutSdk/Issuing/Cards/Responses/Create/VelocityCardControlsResponse.cs index fab70e90..2624aed3 100644 --- a/src/CheckoutSdk/Issuing/Cards/Responses/Create/VelocityCardControlsResponse.cs +++ b/src/CheckoutSdk/Issuing/Cards/Responses/Create/VelocityCardControlsResponse.cs @@ -2,12 +2,22 @@ namespace Checkout.Issuing.Cards.Responses.Create { + /// + /// A card control of type velocity_limit set when the virtual card was created. + /// public class VelocityCardControlsResponse : AbstractCardControlsResponse { + /// + /// Initializes a velocity_limit control. + /// public VelocityCardControlsResponse() : base(IssuingControlType.VelocityLimit) { } + /// + /// The velocity limit, which determines how much a target card can spend over a given timeframe. + /// [Required] + /// public VelocityLimit VelocityLimit { get; set; } } -} \ No newline at end of file +} diff --git a/src/CheckoutSdk/Issuing/Cards/Responses/Create/VirtualCardCreateResponse.cs b/src/CheckoutSdk/Issuing/Cards/Responses/Create/VirtualCardCreateResponse.cs index dd46aaad..80cec968 100644 --- a/src/CheckoutSdk/Issuing/Cards/Responses/Create/VirtualCardCreateResponse.cs +++ b/src/CheckoutSdk/Issuing/Cards/Responses/Create/VirtualCardCreateResponse.cs @@ -3,14 +3,28 @@ namespace Checkout.Issuing.Cards.Responses.Create { + /// + /// The response returned when a virtual card is created (add-virtual-card-response). + /// public class VirtualCardCreateResponse : AbstractCardCreateResponse { + /// + /// Initializes a virtual card response. + /// public VirtualCardCreateResponse() : base(IssuingCardType.Virtual) { } - + + /// + /// The card's credentials. + /// [Optional] + /// public Credentials Credentials { get; set; } + /// + /// The controls that were set on the card. + /// [Optional] + /// public IList Controls { get; set; } } -} \ No newline at end of file +} diff --git a/src/CheckoutSdk/Issuing/Common/MccLimit.cs b/src/CheckoutSdk/Issuing/Common/MccLimit.cs index 461065e8..822f9621 100644 --- a/src/CheckoutSdk/Issuing/Common/MccLimit.cs +++ b/src/CheckoutSdk/Issuing/Common/MccLimit.cs @@ -2,10 +2,22 @@ namespace Checkout.Issuing.Common { + /// + /// The merchant category code (MCC) rule, which determines the types of businesses transactions can be + /// processed from. + /// public class MccLimit { + /// + /// Sets whether to allow or block the list of MCCs supplied. + /// [Required] + /// public LimitControlType? Type { get; set; } + /// + /// The list of MCCs to allow or block transactions from, as 4-digit ISO 18245 codes. + /// [Required] + /// public IList MccList { get; set; } } -} \ No newline at end of file +} diff --git a/src/CheckoutSdk/Issuing/Common/MidLimit.cs b/src/CheckoutSdk/Issuing/Common/MidLimit.cs index b2308147..b96d5038 100644 --- a/src/CheckoutSdk/Issuing/Common/MidLimit.cs +++ b/src/CheckoutSdk/Issuing/Common/MidLimit.cs @@ -2,10 +2,24 @@ namespace Checkout.Issuing.Common { + /// + /// The merchant identification (MID) code rule, which determines the merchants from whom transactions can be + /// processed. + /// public class MidLimit { + /// + /// Sets whether to allow or block the list of MIDs supplied. + /// [Required] + /// public LimitControlType? Type { get; set; } + /// + /// The list of merchant identification (MID) codes to allow or block transactions from. + /// [Required] + /// Each item: ^[A-Za-z0-9{}\[\] ,+\-=.();'\/&@*]{1,15}$ + /// Each item: min 1 characters, max 15 characters + /// public IList MidList { get; set; } } -} \ No newline at end of file +} diff --git a/src/CheckoutSdk/Issuing/Common/VelocityLimit.cs b/src/CheckoutSdk/Issuing/Common/VelocityLimit.cs index 8ead32be..90f6fb7d 100644 --- a/src/CheckoutSdk/Issuing/Common/VelocityLimit.cs +++ b/src/CheckoutSdk/Issuing/Common/VelocityLimit.cs @@ -4,16 +4,26 @@ namespace Checkout.Issuing.Common { /// /// The velocity limit, which determines how much a target card can spend over a given timeframe. - /// + /// public class VelocityLimit { /// /// The amount the target can spend, in minor units. - /// >= 0 /// [Required] + /// Format: int64 + /// min 0 /// public long? AmountLimit { get; set; } + /// + /// The remaining amount that can be spent, in minor units. + /// Returned on get and update control responses only; leave it null on requests (null values are not sent). + /// [Optional] + /// Format: int64 + /// min 0 + /// + public long? AmountRemaining { get; set; } + /// /// The period of time over which the specified amount_limit can be spent. /// [Required] @@ -26,7 +36,7 @@ public class VelocityLimit /// [Optional] /// public IList MccList { get; set; } - + /// /// The list of merchant identification (MID) codes to allow or block transactions from. /// You can provide either mcc_list or mid_list, but not both. @@ -34,4 +44,4 @@ public class VelocityLimit /// public IList MidList { get; set; } } -} \ No newline at end of file +} diff --git a/src/CheckoutSdk/Issuing/Common/VelocityWindow.cs b/src/CheckoutSdk/Issuing/Common/VelocityWindow.cs index b0792598..e20d2428 100644 --- a/src/CheckoutSdk/Issuing/Common/VelocityWindow.cs +++ b/src/CheckoutSdk/Issuing/Common/VelocityWindow.cs @@ -1,7 +1,14 @@ namespace Checkout.Issuing.Common { + /// + /// The period of time over which the specified amount_limit can be spent. + /// public class VelocityWindow { + /// + /// The velocity window's unit of time. + /// [Required] + /// public VelocityWindowType? Type { get; set; } } -} \ No newline at end of file +} diff --git a/src/CheckoutSdk/Issuing/Controls/Responses/Create/AbstractCardControlResponse.cs b/src/CheckoutSdk/Issuing/Controls/Responses/Create/AbstractCardControlResponse.cs index 1739fdf7..a409bbe2 100644 --- a/src/CheckoutSdk/Issuing/Controls/Responses/Create/AbstractCardControlResponse.cs +++ b/src/CheckoutSdk/Issuing/Controls/Responses/Create/AbstractCardControlResponse.cs @@ -1,26 +1,77 @@ +using Checkout.Common; using Checkout.Issuing.Common; namespace Checkout.Issuing.Controls.Responses.Create { - public abstract class AbstractCardControlResponse : HttpMetadata + /// + /// A card control returned by the create, get, list and update control operations, + /// discriminated on control_type. + /// Links (_links, ControlLinks with the self link to the control's details) are returned by the create + /// operation only; get, list and update leave Links empty. + /// + public abstract class AbstractCardControlResponse : Resource { + /// + /// The control's type. + /// A velocity_limit determines how much can be spent over a given period of time. An mcc_limit determines + /// the types of businesses from which transactions can be processed. A mid_limit specifies the merchants + /// from whom transactions can be processed. + /// [Required] + /// public IssuingControlType? ControlType { get; set; } + /// + /// The control's unique identifier. + /// [Required] + /// ^ctr_[a-z0-9]{26}$ + /// 30 characters + /// public string Id { get; set; } + /// + /// The ID of the card or control profile. + /// [Required] + /// ^(crd|cpr)_[a-z0-9]{26}$ + /// 30 characters + /// public string TargetId { get; set; } - + + /// + /// Indicates whether you can change this control. + /// false: an immutable control applied by Checkout.com that you cannot change. + /// true: you applied this control and can change it. + /// [Required] + /// public bool? IsEditable { get; set; } - + + /// + /// The date and time the control was created. + /// [Required] + /// Format: UTC date-time + /// public string CreatedDate { get; set; } + /// + /// The date and time the control was last modified. + /// [Required] + /// Format: UTC date-time + /// public string LastModifiedDate { get; set; } - + + /// + /// The description of the control. + /// [Optional] + /// max 256 characters + /// public string Description { get; set; } + /// + /// Initializes the control with its discriminator value. + /// + /// The control's type. protected AbstractCardControlResponse(IssuingControlType controlType) { ControlType = controlType; } } -} \ No newline at end of file +} diff --git a/src/CheckoutSdk/Issuing/Controls/Responses/Create/MccCardControlResponse.cs b/src/CheckoutSdk/Issuing/Controls/Responses/Create/MccCardControlResponse.cs index fee5e763..5d0de242 100644 --- a/src/CheckoutSdk/Issuing/Controls/Responses/Create/MccCardControlResponse.cs +++ b/src/CheckoutSdk/Issuing/Controls/Responses/Create/MccCardControlResponse.cs @@ -2,12 +2,23 @@ namespace Checkout.Issuing.Controls.Responses.Create { + /// + /// A card control of type mcc_limit. + /// public class MccCardControlResponse : AbstractCardControlResponse { + /// + /// Initializes an mcc_limit control. + /// public MccCardControlResponse() : base(IssuingControlType.MccLimit) { } + /// + /// The merchant category code (MCC) rule, which determines the types of businesses transactions can be + /// processed from. + /// [Optional] + /// public MccLimit MccLimit { get; set; } } -} \ No newline at end of file +} diff --git a/src/CheckoutSdk/Issuing/Controls/Responses/Create/MidCardControlResponse.cs b/src/CheckoutSdk/Issuing/Controls/Responses/Create/MidCardControlResponse.cs index 10c90f38..2202a69b 100644 --- a/src/CheckoutSdk/Issuing/Controls/Responses/Create/MidCardControlResponse.cs +++ b/src/CheckoutSdk/Issuing/Controls/Responses/Create/MidCardControlResponse.cs @@ -1,13 +1,39 @@ using Checkout.Issuing.Common; +using Newtonsoft.Json; +using System; namespace Checkout.Issuing.Controls.Responses.Create { + /// + /// A card control of type mid_limit. + /// public class MidCardControlResponse : AbstractCardControlResponse { + /// + /// Initializes a mid_limit control. + /// public MidCardControlResponse() : base(IssuingControlType.MidLimit) { } - public MidLimit VelocityLimit { get; set; } + /// + /// The merchant identification (MID) code rule, which determines the merchants from whom transactions can + /// be processed. + /// [Optional] + /// + public MidLimit MidLimit { get; set; } + + /// + /// Former name of . It was mapped to velocity_limit, so it was never populated. + /// It now reads and writes and is not serialized. + /// [Optional] + /// + [Obsolete("Use MidLimit instead. Will be removed in the next major version.")] + [JsonIgnore] + public MidLimit VelocityLimit + { + get => MidLimit; + set => MidLimit = value; + } } -} \ No newline at end of file +} diff --git a/src/CheckoutSdk/Issuing/Controls/Responses/Create/VelocityCardControlResponse.cs b/src/CheckoutSdk/Issuing/Controls/Responses/Create/VelocityCardControlResponse.cs index 827aa2be..dd57570c 100644 --- a/src/CheckoutSdk/Issuing/Controls/Responses/Create/VelocityCardControlResponse.cs +++ b/src/CheckoutSdk/Issuing/Controls/Responses/Create/VelocityCardControlResponse.cs @@ -2,12 +2,23 @@ namespace Checkout.Issuing.Controls.Responses.Create { + /// + /// A card control of type velocity_limit. + /// public class VelocityCardControlResponse : AbstractCardControlResponse { + /// + /// Initializes a velocity_limit control. + /// public VelocityCardControlResponse() : base(IssuingControlType.VelocityLimit) { } + /// + /// The velocity limit, which determines how much a target card can spend over a given timeframe. + /// On get and update responses it also carries amount_remaining. + /// [Optional] + /// public VelocityLimit VelocityLimit { get; set; } } -} \ No newline at end of file +} diff --git a/test/CheckoutSdkTest/Issuing/Cards/CardCreateResponseSerializationTest.cs b/test/CheckoutSdkTest/Issuing/Cards/CardCreateResponseSerializationTest.cs new file mode 100644 index 00000000..79a94f21 --- /dev/null +++ b/test/CheckoutSdkTest/Issuing/Cards/CardCreateResponseSerializationTest.cs @@ -0,0 +1,241 @@ +using Checkout.Common; +using Checkout.Issuing.Cards.Responses.Create; +using Checkout.Issuing.Common; +using Newtonsoft.Json.Linq; +using Shouldly; +using System; +using System.Collections.Generic; +using Xunit; + +namespace Checkout.Issuing.Cards +{ + /// + /// Schema validation tests for the card create response (add-card-response) in Checkout.Issuing.Cards. + /// + public class CardCreateResponseSerializationTest + { + private static readonly JsonSerializer Serializer = new JsonSerializer(); + + // ------------------------------------------------------------------------ + // AbstractCardControlsResponse subtypes + // Covers add-virtual-card-response-control-response (id, description, + // control_type) and its velocity, mcc and mid subtypes. + // ------------------------------------------------------------------------ + + [Fact] + public void ShouldDeserializeControlsForVirtualCardCreateResponse() + { + const string json = @"{ + ""id"": ""crd_fa6psq42dcdd6fdn5gifcq1491"", + ""type"": ""virtual"", + ""last_four"": ""1234"", + ""controls"": [ + { ""id"": ""ctr_s6tfwc6jpieexgcet2pljss6tu"", ""description"": ""velocity"", ""control_type"": ""velocity_limit"", + ""velocity_limit"": { ""amount_limit"": 5000, ""velocity_window"": { ""type"": ""weekly"" }, ""mcc_list"": [""4121""] } }, + { ""id"": ""ctr_s6tfwc6jpieexgcet2pljss6tu"", ""description"": ""mcc"", ""control_type"": ""mcc_limit"", + ""mcc_limit"": { ""type"": ""allow"", ""mcc_list"": [5932, 5411] } }, + { ""id"": ""ctr_s6tfwc6jpieexgcet2pljss6tu"", ""description"": ""Allow the card to be used only in AZ Pizza"", ""control_type"": ""mid_limit"", + ""mid_limit"": { ""type"": ""allow"", ""mid_list"": [593278, 541114] } } + ] + }"; + + var response = (AbstractCardCreateResponse)Serializer.Deserialize(json, typeof(AbstractCardCreateResponse)); + + var card = response.ShouldBeOfType(); + card.Controls.Count.ShouldBe(3); + + var velocity = card.Controls[0].ShouldBeOfType(); + velocity.Id.ShouldBe("ctr_s6tfwc6jpieexgcet2pljss6tu"); + velocity.Description.ShouldBe("velocity"); + velocity.ControlType.ShouldBe(IssuingControlType.VelocityLimit); + velocity.VelocityLimit.AmountLimit.ShouldBe(5000); + velocity.VelocityLimit.VelocityWindow.Type.ShouldBe(VelocityWindowType.Weekly); + velocity.VelocityLimit.MccList.ShouldBe(new List { "4121" }); + + var mcc = card.Controls[1].ShouldBeOfType(); + mcc.ControlType.ShouldBe(IssuingControlType.MccLimit); + mcc.MccLimit.Type.ShouldBe(LimitControlType.Allow); + mcc.MccLimit.MccList.ShouldBe(new List { "5932", "5411" }); + + var mid = card.Controls[2].ShouldBeOfType(); + mid.ControlType.ShouldBe(IssuingControlType.MidLimit); + mid.Description.ShouldBe("Allow the card to be used only in AZ Pizza"); + mid.MidLimit.ShouldNotBeNull(); + mid.MidLimit.Type.ShouldBe(LimitControlType.Allow); + mid.MidLimit.MidList.ShouldBe(new List { "593278", "541114" }); +#pragma warning disable CS0618 + mid.VelocityLimit.ShouldBeSameAs(mid.MidLimit); +#pragma warning restore CS0618 + } + + [Fact] + public void ShouldForwardDeprecatedVelocityLimitToMidLimitAndSerializeOnceForMidCardControlsResponse() + { + var limit = new MidLimit { Type = LimitControlType.Block, MidList = new List { "593278" } }; +#pragma warning disable CS0618 + var control = new MidCardControlsResponse { Id = "ctr_s6tfwc6jpieexgcet2pljss6tu", Description = "mid", VelocityLimit = limit }; + control.MidLimit.ShouldBeSameAs(limit); +#pragma warning restore CS0618 + + var json = JObject.Parse(Serializer.Serialize(control)); + + json.ContainsKey("mid_limit").ShouldBeTrue(); + json.ContainsKey("velocity_limit").ShouldBeFalse(); + json["control_type"].Value().ShouldBe("mid_limit"); + } + + // ------------------------------------------------------------------------ + // VirtualCardCreateResponse / PhysicalCardCreateResponse + // Covers every property of add-virtual-card-response and + // add-physical-card-response, including _links and scheme. + // ------------------------------------------------------------------------ + + private const string SwaggerBody = @"{ + ""id"": ""crd_fa6psq242dcd6fdn5gifcq1491"", + ""client_id"": ""cli_vkuhvk4vjn2edkps7dfsq6emqm"", + ""entity_id"": ""ent_fa6psq242dcd6fdn5gifcq1491"", + ""last_four"": 1234, + ""expiry_month"": 5, + ""expiry_year"": 2025, + ""billing_currency"": ""USD"", + ""issuing_country"": ""US"", + ""status"": ""active"", + ""type"": ""TYPE"", + ""scheme"": ""mastercard"", + ""created_date"": ""2019-09-10T10:11:12.0000000+00:00"" + }"; + + [Theory] + [InlineData("virtual", typeof(VirtualCardCreateResponse))] + [InlineData("physical", typeof(PhysicalCardCreateResponse))] + public void ShouldDeserializeSwaggerExampleForCardCreateResponse(string type, Type expected) + { + var response = (AbstractCardCreateResponse)Serializer.Deserialize( + SwaggerBody.Replace("TYPE", type), typeof(AbstractCardCreateResponse)); + + response.ShouldBeOfType(expected); + response.Id.ShouldBe("crd_fa6psq242dcd6fdn5gifcq1491"); + response.ClientId.ShouldBe("cli_vkuhvk4vjn2edkps7dfsq6emqm"); + response.EntityId.ShouldBe("ent_fa6psq242dcd6fdn5gifcq1491"); + response.LastFour.ShouldBe("1234"); + response.ExpiryMonth.ShouldBe(5); + response.ExpiryYear.ShouldBe(2025); + response.BillingCurrency.ShouldBe(Currency.USD); + response.IssuingCountry.ShouldBe(CountryCode.US); + response.Status.ShouldBe(CardStatus.Active); + response.Scheme.ShouldBe(IssuingScheme.Mastercard); + response.CreatedDate.ShouldNotBeNull(); + } + + [Fact] + public void ShouldRoundTripAllPropertiesForVirtualCardCreateResponse() + { + var original = new VirtualCardCreateResponse + { + Credentials = new Credentials { Number = "4242424242424242", Cvc2 = "604" }, + Controls = new List + { + new MidCardControlsResponse + { + Id = "ctr_s6tfwc6jpieexgcet2pljss6tu", Description = "mid", + MidLimit = new MidLimit { Type = LimitControlType.Allow, MidList = new List { "593278" } } + } + } + }; + PopulateCard(original); + + var result = (AbstractCardCreateResponse)Serializer.Deserialize( + Serializer.Serialize(original), typeof(AbstractCardCreateResponse)); + + var card = result.ShouldBeOfType(); + AssertCard(card); + card.Credentials.Number.ShouldBe("4242424242424242"); + card.Credentials.Cvc2.ShouldBe("604"); + card.Controls.Count.ShouldBe(1); + card.Controls[0].ShouldBeOfType().MidLimit.MidList.ShouldBe(new List { "593278" }); + } + + [Fact] + public void ShouldRoundTripAllPropertiesForPhysicalCardCreateResponse() + { + var original = new PhysicalCardCreateResponse(); + PopulateCard(original); + + var result = (AbstractCardCreateResponse)Serializer.Deserialize( + Serializer.Serialize(original), typeof(AbstractCardCreateResponse)); + + AssertCard(result.ShouldBeOfType()); + } + + [Fact] + public void ShouldLeaveDeprecatedFieldsNullForSpecBodyOfCardControlsResponse() + { + const string json = @"{ ""id"": ""ctr_s6tfwc6jpieexgcet2pljss6tu"", ""description"": ""mcc"", ""control_type"": ""mcc_limit"", + ""mcc_limit"": { ""type"": ""block"", ""mcc_list"": [""5411""] } }"; + + var control = (AbstractCardControlsResponse)Serializer.Deserialize(json, typeof(AbstractCardControlsResponse)); + + control.ShouldBeOfType(); +#pragma warning disable CS0618 + control.TargetId.ShouldBeNull(); + control.CreatedDate.ShouldBeNull(); + control.LastModifiedDate.ShouldBeNull(); +#pragma warning restore CS0618 + } + + [Fact] + public void ShouldStillDeserializeDeprecatedFieldsWhenPresentOnCardControlsResponse() + { + const string json = @"{ ""id"": ""ctr_s6tfwc6jpieexgcet2pljss6tu"", ""control_type"": ""mcc_limit"", + ""target_id"": ""crd_fa6psq42dcdd6fdn5gifcq1491"", ""created_date"": ""a"", ""last_modified_date"": ""b"" }"; + + var control = (AbstractCardControlsResponse)Serializer.Deserialize(json, typeof(AbstractCardControlsResponse)); + +#pragma warning disable CS0618 + control.TargetId.ShouldBe("crd_fa6psq42dcdd6fdn5gifcq1491"); + control.CreatedDate.ShouldBe("a"); + control.LastModifiedDate.ShouldBe("b"); +#pragma warning restore CS0618 + } + + private static void PopulateCard(AbstractCardCreateResponse card) + { + card.Id = "crd_fa6psq242dcd6fdn5gifcq1491"; + card.ClientId = "cli_vkuhvk4vjn2edkps7dfsq6emqm"; + card.EntityId = "ent_fa6psq242dcd6fdn5gifcq1491"; + card.DisplayName = "JOHN KENNEDY"; + card.LastFour = "1234"; + card.ExpiryMonth = 5; + card.ExpiryYear = 2025; + card.BillingCurrency = Currency.USD; + card.IssuingCountry = CountryCode.US; + card.Scheme = IssuingScheme.Visa; + card.CreatedDate = new DateTime(2019, 9, 10, 10, 11, 12, DateTimeKind.Utc); + card.Status = CardStatus.Suspended; + card.Reference = "X-123456-N11"; + card.ScheduledRevocationDate = "2027-03-12"; + card.LastActivatedOn = new DateTime(2019, 9, 11, 10, 11, 12, DateTimeKind.Utc); + card.Links = new Dictionary { { "self", new Link { Href = "https://api.checkout.com/issuing/cards/crd_fa6psq242dcd6fdn5gifcq1491" } } }; + } + + private static void AssertCard(AbstractCardCreateResponse card) + { + card.Id.ShouldBe("crd_fa6psq242dcd6fdn5gifcq1491"); + card.ClientId.ShouldBe("cli_vkuhvk4vjn2edkps7dfsq6emqm"); + card.EntityId.ShouldBe("ent_fa6psq242dcd6fdn5gifcq1491"); + card.DisplayName.ShouldBe("JOHN KENNEDY"); + card.LastFour.ShouldBe("1234"); + card.ExpiryMonth.ShouldBe(5); + card.ExpiryYear.ShouldBe(2025); + card.BillingCurrency.ShouldBe(Currency.USD); + card.IssuingCountry.ShouldBe(CountryCode.US); + card.Scheme.ShouldBe(IssuingScheme.Visa); + card.CreatedDate.ShouldBe(new DateTime(2019, 9, 10, 10, 11, 12, DateTimeKind.Utc)); + card.Status.ShouldBe(CardStatus.Suspended); + card.Reference.ShouldBe("X-123456-N11"); + card.ScheduledRevocationDate.ShouldBe("2027-03-12"); + card.LastActivatedOn.ShouldBe(new DateTime(2019, 9, 11, 10, 11, 12, DateTimeKind.Utc)); + card.GetSelfLink().Href.ShouldBe("https://api.checkout.com/issuing/cards/crd_fa6psq242dcd6fdn5gifcq1491"); + } + } +} diff --git a/test/CheckoutSdkTest/Issuing/Controls/ControlsSerializationTest.cs b/test/CheckoutSdkTest/Issuing/Controls/ControlsSerializationTest.cs new file mode 100644 index 00000000..a54357e8 --- /dev/null +++ b/test/CheckoutSdkTest/Issuing/Controls/ControlsSerializationTest.cs @@ -0,0 +1,292 @@ +using Checkout.Issuing.Common; +using Checkout.Issuing.Controls.Requests.Update; +using Checkout.Issuing.Controls.Responses.Create; +using Checkout.Issuing.Controls.Responses.Query; +using Newtonsoft.Json.Linq; +using Shouldly; +using System.Collections.Generic; +using Xunit; + +namespace Checkout.Issuing.Controls +{ + /// + /// Schema validation tests for Checkout.Issuing.Controls. + /// Grouped by domain; each section below covers one subject. + /// + public class ControlsSerializationTest + { + private static readonly JsonSerializer Serializer = new JsonSerializer(); + + private const string ControlId = "ctr_gp7vkmxayztufjz6top5bjcdra"; + private const string TargetId = "crd_fa6psq42dcdd6fdn5gifcq1491"; + private const string Date = "2023-03-12T18:20:12.0000000+00:00"; + + // ------------------------------------------------------------------------ + // VelocityCardControlResponse + // Covers the base control fields plus description and velocity_limit + // (VelocityLimitWithRemainingAmount) against update-control-velocity-limit-response. + // ------------------------------------------------------------------------ + + [Fact] + public void ShouldDeserializeSwaggerExampleForVelocityCardControlResponse() + { + const string json = @"{ + ""id"": ""ctr_gp7vkmxayztufjz6top5bjcdra"", + ""target_id"": ""crd_fa6psq42dcdd6fdn5gifcq1491"", + ""description"": ""Maximum spend of 500 per week for restaurants"", + ""control_type"": ""velocity_limit"", + ""is_editable"": true, + ""created_date"": ""2023-03-12T18:20:12.0000000+00:00"", + ""last_modified_date"": ""2023-03-12T18:20:12.0000000+00:00"", + ""velocity_limit"": { + ""amount_remaining"": 45000, + ""amount_limit"": 50000, + ""velocity_window"": { ""type"": ""weekly"" }, + ""mcc_list"": [4121, 4582] + } + }"; + + var response = (AbstractCardControlResponse)Serializer.Deserialize(json, typeof(AbstractCardControlResponse)); + + var velocity = response.ShouldBeOfType(); + AssertBase(velocity, IssuingControlType.VelocityLimit, "Maximum spend of 500 per week for restaurants"); + velocity.VelocityLimit.AmountRemaining.ShouldBe(45000); + velocity.VelocityLimit.AmountLimit.ShouldBe(50000); + velocity.VelocityLimit.VelocityWindow.Type.ShouldBe(VelocityWindowType.Weekly); + velocity.VelocityLimit.MccList.ShouldBe(new List { "4121", "4582" }); + velocity.VelocityLimit.MidList.ShouldBeNull(); + } + + [Fact] + public void ShouldRoundTripAllPropertiesForVelocityCardControlResponse() + { + var original = new VelocityCardControlResponse + { + VelocityLimit = new VelocityLimit + { + AmountLimit = 50000, + AmountRemaining = 45000, + VelocityWindow = new VelocityWindow { Type = VelocityWindowType.AllTime }, + MccList = new List { "4121" }, + MidList = new List { "593278" } + } + }; + PopulateBase(original, "velocity"); + + var json = Serializer.Serialize(original); + var result = (AbstractCardControlResponse)Serializer.Deserialize(json, typeof(AbstractCardControlResponse)); + + var velocity = result.ShouldBeOfType(); + AssertBase(velocity, IssuingControlType.VelocityLimit, "velocity"); + velocity.VelocityLimit.AmountLimit.ShouldBe(50000); + velocity.VelocityLimit.AmountRemaining.ShouldBe(45000); + velocity.VelocityLimit.VelocityWindow.Type.ShouldBe(VelocityWindowType.AllTime); + velocity.VelocityLimit.MccList.ShouldBe(new List { "4121" }); + velocity.VelocityLimit.MidList.ShouldBe(new List { "593278" }); + } + + [Fact] + public void ShouldNotSendAmountRemainingOnVelocityCardControlUpdate() + { + var request = new VelocityCardControlUpdate + { + Description = "velocity", + VelocityLimit = new VelocityLimit + { + AmountLimit = 100, + VelocityWindow = new VelocityWindow { Type = VelocityWindowType.Daily } + } + }; + + var json = JObject.Parse(Serializer.Serialize(request)); + + ((JObject)json["velocity_limit"]).ContainsKey("amount_remaining").ShouldBeFalse(); + } + + // ------------------------------------------------------------------------ + // MccCardControlResponse + // Covers the base control fields plus description and mcc_limit against + // update-control-mcc-limit-response. + // ------------------------------------------------------------------------ + + [Fact] + public void ShouldDeserializeSwaggerExampleForMccCardControlResponse() + { + const string json = @"{ + ""id"": ""ctr_gp7vkmxayztufjz6top5bjcdra"", + ""target_id"": ""crd_fa6psq42dcdd6fdn5gifcq1491"", + ""description"": ""Allow the card to be used only in restaurants and supermarkets"", + ""control_type"": ""mcc_limit"", + ""is_editable"": true, + ""created_date"": ""2023-03-12T18:20:12.0000000+00:00"", + ""last_modified_date"": ""2023-03-12T18:20:12.0000000+00:00"", + ""mcc_limit"": { ""type"": ""allow"", ""mcc_list"": [5932, 5411] } + }"; + + var response = (AbstractCardControlResponse)Serializer.Deserialize(json, typeof(AbstractCardControlResponse)); + + var mcc = response.ShouldBeOfType(); + AssertBase(mcc, IssuingControlType.MccLimit, "Allow the card to be used only in restaurants and supermarkets"); + mcc.MccLimit.Type.ShouldBe(LimitControlType.Allow); + mcc.MccLimit.MccList.ShouldBe(new List { "5932", "5411" }); + } + + [Fact] + public void ShouldRoundTripAllPropertiesForMccCardControlResponse() + { + var original = new MccCardControlResponse + { + MccLimit = new MccLimit { Type = LimitControlType.Block, MccList = new List { "5932" } } + }; + PopulateBase(original, "mcc"); + + var json = Serializer.Serialize(original); + var result = (AbstractCardControlResponse)Serializer.Deserialize(json, typeof(AbstractCardControlResponse)); + + var mcc = result.ShouldBeOfType(); + AssertBase(mcc, IssuingControlType.MccLimit, "mcc"); + mcc.MccLimit.Type.ShouldBe(LimitControlType.Block); + mcc.MccLimit.MccList.ShouldBe(new List { "5932" }); + } + + // ------------------------------------------------------------------------ + // MidCardControlResponse + // Covers the base control fields plus description and mid_limit against + // update-control-mid-limit-response, and the deprecated VelocityLimit alias. + // ------------------------------------------------------------------------ + + [Fact] + public void ShouldDeserializeSwaggerExampleForMidCardControlResponse() + { + const string json = @"{ + ""id"": ""ctr_gp7vkmxayztufjz6top5bjcdra"", + ""target_id"": ""crd_fa6psq42dcdd6fdn5gifcq1491"", + ""description"": ""Allow the card to be used only in AZ Pizza"", + ""control_type"": ""mid_limit"", + ""is_editable"": true, + ""created_date"": ""2023-03-12T18:20:12.0000000+00:00"", + ""last_modified_date"": ""2023-03-12T18:20:12.0000000+00:00"", + ""mid_limit"": { ""type"": ""allow"", ""mid_list"": [593278, 541114] } + }"; + + var response = (AbstractCardControlResponse)Serializer.Deserialize(json, typeof(AbstractCardControlResponse)); + + var mid = response.ShouldBeOfType(); + AssertBase(mid, IssuingControlType.MidLimit, "Allow the card to be used only in AZ Pizza"); + mid.MidLimit.ShouldNotBeNull(); + mid.MidLimit.Type.ShouldBe(LimitControlType.Allow); + mid.MidLimit.MidList.ShouldBe(new List { "593278", "541114" }); +#pragma warning disable CS0618 + mid.VelocityLimit.ShouldBeSameAs(mid.MidLimit); +#pragma warning restore CS0618 + } + + [Fact] + public void ShouldRoundTripAllPropertiesForMidCardControlResponse() + { + var original = new MidCardControlResponse + { + MidLimit = new MidLimit { Type = LimitControlType.Block, MidList = new List { "AZ-PIZZA" } } + }; + PopulateBase(original, "mid"); + + var json = Serializer.Serialize(original); + var result = (AbstractCardControlResponse)Serializer.Deserialize(json, typeof(AbstractCardControlResponse)); + + var mid = result.ShouldBeOfType(); + AssertBase(mid, IssuingControlType.MidLimit, "mid"); + mid.MidLimit.Type.ShouldBe(LimitControlType.Block); + mid.MidLimit.MidList.ShouldBe(new List { "AZ-PIZZA" }); + } + + [Fact] + public void ShouldForwardDeprecatedVelocityLimitToMidLimitAndSerializeOnceForMidCardControlResponse() + { + var limit = new MidLimit { Type = LimitControlType.Allow, MidList = new List { "593278" } }; +#pragma warning disable CS0618 + var response = new MidCardControlResponse { VelocityLimit = limit }; + response.MidLimit.ShouldBeSameAs(limit); + response.VelocityLimit.ShouldBeSameAs(response.MidLimit); +#pragma warning restore CS0618 + + var json = JObject.Parse(Serializer.Serialize(response)); + + json.ContainsKey("mid_limit").ShouldBeTrue(); + json.ContainsKey("velocity_limit").ShouldBeFalse(); + } + + // ------------------------------------------------------------------------ + // Create control response (add-control-response) + // The create operation also returns _links (ControlLinks). + // ------------------------------------------------------------------------ + + [Fact] + public void ShouldDeserializeLinksForCreateCardControlResponse() + { + const string json = @"{ + ""id"": ""ctr_gp7vkmxayztufjz6top5bjcdra"", + ""target_id"": ""crd_fa6psq42dcdd6fdn5gifcq1491"", + ""description"": ""Allow the card to be used only in AZ Pizza"", + ""control_type"": ""mid_limit"", + ""is_editable"": true, + ""created_date"": ""2023-03-12T18:20:12.0000000+00:00"", + ""last_modified_date"": ""2023-03-12T18:20:12.0000000+00:00"", + ""mid_limit"": { ""type"": ""allow"", ""mid_list"": [""593278""] }, + ""_links"": { ""self"": { ""href"": ""https://api.checkout.com/issuing/controls/ctr_gp7vkmxayztufjz6top5bjcdra"" } } + }"; + + var response = (AbstractCardControlResponse)Serializer.Deserialize(json, typeof(AbstractCardControlResponse)); + + var mid = response.ShouldBeOfType(); + AssertBase(mid, IssuingControlType.MidLimit, "Allow the card to be used only in AZ Pizza"); + mid.GetSelfLink().ShouldNotBeNull(); + mid.GetSelfLink().Href.ShouldBe("https://api.checkout.com/issuing/controls/ctr_gp7vkmxayztufjz6top5bjcdra"); + } + + // ------------------------------------------------------------------------ + // CardControlsQueryResponse + // The list operation returns the same discriminated subtypes. + // ------------------------------------------------------------------------ + + [Fact] + public void ShouldDeserializeEverySubtypeForCardControlsQueryResponse() + { + const string json = @"{ ""controls"": [ + { ""id"": ""ctr_gp7vkmxayztufjz6top5bjcdra"", ""control_type"": ""velocity_limit"", + ""velocity_limit"": { ""amount_remaining"": 1, ""amount_limit"": 2, ""velocity_window"": { ""type"": ""daily"" } } }, + { ""id"": ""ctr_gp7vkmxayztufjz6top5bjcdra"", ""control_type"": ""mcc_limit"", + ""mcc_limit"": { ""type"": ""block"", ""mcc_list"": [""5411""] } }, + { ""id"": ""ctr_gp7vkmxayztufjz6top5bjcdra"", ""control_type"": ""mid_limit"", + ""mid_limit"": { ""type"": ""block"", ""mid_list"": [""593278""] } } + ] }"; + + var response = (CardControlsQueryResponse)Serializer.Deserialize(json, typeof(CardControlsQueryResponse)); + + response.Controls.Count.ShouldBe(3); + response.Controls[0].ShouldBeOfType().VelocityLimit.AmountRemaining.ShouldBe(1); + response.Controls[1].ShouldBeOfType().MccLimit.MccList.ShouldBe(new List { "5411" }); + response.Controls[2].ShouldBeOfType().MidLimit.MidList.ShouldBe(new List { "593278" }); + } + + private static void PopulateBase(AbstractCardControlResponse response, string description) + { + response.Id = ControlId; + response.TargetId = TargetId; + response.IsEditable = true; + response.CreatedDate = Date; + response.LastModifiedDate = Date; + response.Description = description; + } + + private static void AssertBase(AbstractCardControlResponse response, IssuingControlType type, string description) + { + response.ControlType.ShouldBe(type); + response.Id.ShouldBe(ControlId); + response.TargetId.ShouldBe(TargetId); + response.IsEditable.ShouldBe(true); + response.CreatedDate.ShouldNotBeNullOrEmpty(); + response.LastModifiedDate.ShouldNotBeNullOrEmpty(); + response.Description.ShouldBe(description); + } + } +}