Skip to content

Commit 32d0984

Browse files
APIShortDate type for yyyy-MM-dd date formats (#193)
* APIShortDate type for yyyy-MM-dd date formats * little renaming --------- Co-authored-by: david ruiz <[email protected]>
1 parent 26b77a5 commit 32d0984

File tree

5 files changed

+310
-10
lines changed

5 files changed

+310
-10
lines changed

common/apishortdate.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package common
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
"time"
8+
)
9+
10+
type APIShortDate time.Time
11+
12+
func (t *APIShortDate) UnmarshalJSON(data []byte) error {
13+
// Remove quotes from JSON string
14+
str := strings.Trim(string(data), "\"")
15+
if str == "null" || str == "" {
16+
return nil
17+
}
18+
19+
// Try multiple date formats that your API might return
20+
formats := []string{
21+
"2006-01-02", // Specific yyyy-MM-dd Date only
22+
}
23+
24+
for _, format := range formats {
25+
if parsed, err := time.Parse(format, str); err == nil {
26+
*t = APIShortDate(parsed)
27+
return nil
28+
}
29+
}
30+
31+
return fmt.Errorf("unable to parse time: %s, APIShortDate only accepts yyyy-MM-dd format", str)
32+
}
33+
34+
func (t APIShortDate) MarshalJSON() ([]byte, error) {
35+
return json.Marshal(time.Time(t).Format("2006-01-02"))
36+
}

instruments/nas/client_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,9 @@ func getCreateTokenInstrumentRequest() *createTokenInstrumentRequest {
237237
}
238238

239239
func getCreateSepaInstrumentRequest() *createSepaInstrumentRequest {
240-
time := time.Now()
240+
// Create APIShortDate with current date
241+
apiDate := (*common.APIShortDate)(&time.Time{})
242+
*apiDate = common.APIShortDate(time.Now())
241243

242244
r := NewCreateSepaInstrumentRequest()
243245
r.InstrumentData = &InstrumentData{
@@ -246,7 +248,7 @@ func getCreateSepaInstrumentRequest() *createSepaInstrumentRequest {
246248
Currency: common.GBP,
247249
PaymentType: payments.Recurring,
248250
MandateId: "1234567890",
249-
DateOfSignature: &time,
251+
DateOfSignature: apiDate,
250252
}
251253
return r
252254
}

instruments/nas/instuments.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package nas
22

33
import (
4-
"time"
5-
64
"github.com/checkout/checkout-sdk-go/common"
75
"github.com/checkout/checkout-sdk-go/payments"
86
)
@@ -24,7 +22,7 @@ type InstrumentData struct {
2422
Currency common.Currency `json:"currency,omitempty"`
2523
PaymentType payments.PaymentType `json:"payment_type,omitempty"`
2624
MandateId string `json:"mandate_id,omitempty"`
27-
DateOfSignature *time.Time `json:"date_of_signature,omitempty"`
25+
DateOfSignature *common.APIShortDate `json:"date_of_signature,omitempty"`
2826
}
2927

3028
type CreateCustomerInstrumentRequest struct {

test/apishortdate_test.go

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package test
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
"time"
7+
8+
"github.com/checkout/checkout-sdk-go/common"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestAPIShortDateUnmarshalling(t *testing.T) {
13+
cases := []struct {
14+
name string
15+
jsonInput string
16+
expectedDate time.Time
17+
}{
18+
{
19+
name: "YYYY-MM-DD format (day > month)",
20+
jsonInput: `"2023-03-15"`,
21+
expectedDate: time.Date(2023, 3, 15, 0, 0, 0, 0, time.UTC),
22+
},
23+
{
24+
name: "YYYY-MM-DD format (day < month)",
25+
jsonInput: `"2023-12-05"`,
26+
expectedDate: time.Date(2023, 12, 5, 0, 0, 0, 0, time.UTC),
27+
},
28+
{
29+
name: "YYYY-MM-DD leap year",
30+
jsonInput: `"2024-02-29"`,
31+
expectedDate: time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC),
32+
},
33+
{
34+
name: "YYYY-MM-DD single digits",
35+
jsonInput: `"2023-01-09"`,
36+
expectedDate: time.Date(2023, 1, 9, 0, 0, 0, 0, time.UTC),
37+
},
38+
}
39+
40+
for _, tc := range cases {
41+
t.Run(tc.name, func(t *testing.T) {
42+
var apiDate common.APIShortDate
43+
44+
err := json.Unmarshal([]byte(tc.jsonInput), &apiDate)
45+
assert.Nil(t, err, "Unmarshalling should not fail")
46+
47+
actualTime := time.Time(apiDate)
48+
assert.Equal(t, tc.expectedDate.Year(), actualTime.Year(), "Year should match")
49+
assert.Equal(t, tc.expectedDate.Month(), actualTime.Month(), "Month should match")
50+
assert.Equal(t, tc.expectedDate.Day(), actualTime.Day(), "Day should match")
51+
})
52+
}
53+
}
54+
55+
func TestAPIShortDateMarshalling(t *testing.T) {
56+
cases := []struct {
57+
name string
58+
inputDate time.Time
59+
expectedJSON string
60+
}{
61+
{
62+
name: "Day > Month (15th of March)",
63+
inputDate: time.Date(2023, 3, 15, 10, 30, 45, 0, time.UTC),
64+
expectedJSON: `"2023-03-15"`,
65+
},
66+
{
67+
name: "Day < Month (5th of December)",
68+
inputDate: time.Date(2023, 12, 5, 14, 20, 30, 0, time.UTC),
69+
expectedJSON: `"2023-12-05"`,
70+
},
71+
{
72+
name: "Single digit month and day",
73+
inputDate: time.Date(2023, 1, 9, 0, 0, 0, 0, time.UTC),
74+
expectedJSON: `"2023-01-09"`,
75+
},
76+
}
77+
78+
for _, tc := range cases {
79+
t.Run(tc.name, func(t *testing.T) {
80+
apiDate := common.APIShortDate(tc.inputDate)
81+
82+
jsonBytes, err := json.Marshal(apiDate)
83+
assert.Nil(t, err, "Marshalling should not fail")
84+
85+
actualJSON := string(jsonBytes)
86+
assert.Equal(t, tc.expectedJSON, actualJSON, "JSON output should match expected format")
87+
})
88+
}
89+
}
90+
91+
func TestAPIShortDateFormatConfusion(t *testing.T) {
92+
cases := []struct {
93+
name string
94+
jsonInput string
95+
expectedDay int
96+
expectedMonth time.Month
97+
}{
98+
{
99+
name: "Day 15 Month 03",
100+
jsonInput: `"2023-03-15"`,
101+
expectedDay: 15,
102+
expectedMonth: time.March,
103+
},
104+
{
105+
name: "Day 05 Month 12",
106+
jsonInput: `"2023-12-05"`,
107+
expectedDay: 5,
108+
expectedMonth: time.December,
109+
},
110+
}
111+
112+
for _, tc := range cases {
113+
t.Run(tc.name, func(t *testing.T) {
114+
var apiDate common.APIShortDate
115+
116+
err := json.Unmarshal([]byte(tc.jsonInput), &apiDate)
117+
assert.Nil(t, err, "Unmarshalling should not fail")
118+
119+
actualTime := time.Time(apiDate)
120+
assert.Equal(t, tc.expectedDay, actualTime.Day(), "Day should be correctly parsed")
121+
assert.Equal(t, tc.expectedMonth, actualTime.Month(), "Month should be correctly parsed")
122+
})
123+
}
124+
}
125+
126+
func TestAPIShortDateInvalidFormats(t *testing.T) {
127+
cases := []struct {
128+
name string
129+
jsonInput string
130+
errorMsg string
131+
}{
132+
{
133+
name: "ISO 8601 with timezone should fail",
134+
jsonInput: `"2023-06-20T14:30:45Z"`,
135+
errorMsg: "should reject ISO format with time",
136+
},
137+
{
138+
name: "ISO 8601 with milliseconds should fail",
139+
jsonInput: `"2023-09-12T09:15:30.123Z"`,
140+
errorMsg: "should reject ISO format with milliseconds",
141+
},
142+
{
143+
name: "Date without timezone should fail",
144+
jsonInput: `"2023-11-25T18:45:00"`,
145+
errorMsg: "should reject datetime without timezone",
146+
},
147+
{
148+
name: "Date with space should fail",
149+
jsonInput: `"2023-07-08 12:00:00"`,
150+
errorMsg: "should reject date with space and time",
151+
},
152+
{
153+
name: "Invalid date format should fail",
154+
jsonInput: `"not-a-date"`,
155+
errorMsg: "should reject invalid date string",
156+
},
157+
{
158+
name: "Wrong date format MM/DD/YYYY should fail",
159+
jsonInput: `"03/15/2023"`,
160+
errorMsg: "should reject US date format",
161+
},
162+
{
163+
name: "Wrong date format DD/MM/YYYY should fail",
164+
jsonInput: `"15/03/2023"`,
165+
errorMsg: "should reject European date format",
166+
},
167+
{
168+
name: "Invalid date values should fail",
169+
jsonInput: `"2023-13-45"`,
170+
errorMsg: "should reject invalid month/day values",
171+
},
172+
{
173+
name: "Partial date should fail",
174+
jsonInput: `"2023-03"`,
175+
errorMsg: "should reject incomplete date",
176+
},
177+
{
178+
name: "Date with extra characters should fail",
179+
jsonInput: `"2023-03-15extra"`,
180+
errorMsg: "should reject date with extra characters",
181+
},
182+
}
183+
184+
for _, tc := range cases {
185+
t.Run(tc.name, func(t *testing.T) {
186+
var apiDate common.APIShortDate
187+
188+
err := json.Unmarshal([]byte(tc.jsonInput), &apiDate)
189+
assert.NotNil(t, err, tc.errorMsg)
190+
assert.Contains(t, err.Error(), "APIShortDate only accepts", "Error should mention format restriction")
191+
})
192+
}
193+
}
194+
195+
func TestAPIShortDateRoundTrip(t *testing.T) {
196+
cases := []struct {
197+
name string
198+
inputJSON string
199+
}{
200+
{
201+
name: "Day > Month case (March 25th)",
202+
inputJSON: `"2023-03-25"`,
203+
},
204+
{
205+
name: "Day < Month case (December 8th)",
206+
inputJSON: `"2023-12-08"`,
207+
},
208+
{
209+
name: "Leap year February 29th",
210+
inputJSON: `"2024-02-29"`,
211+
},
212+
{
213+
name: "Year boundary December 31st",
214+
inputJSON: `"2023-12-31"`,
215+
},
216+
{
217+
name: "Year boundary January 1st",
218+
inputJSON: `"2024-01-01"`,
219+
},
220+
}
221+
222+
for _, tc := range cases {
223+
t.Run(tc.name, func(t *testing.T) {
224+
// Step 1: Unmarshal input JSON
225+
var apiDate common.APIShortDate
226+
err := json.Unmarshal([]byte(tc.inputJSON), &apiDate)
227+
assert.Nil(t, err, "Initial unmarshalling should not fail")
228+
229+
originalTime := time.Time(apiDate)
230+
231+
// Step 2: Marshal back to JSON
232+
jsonBytes, err := json.Marshal(apiDate)
233+
assert.Nil(t, err, "Marshalling should not fail")
234+
235+
// Step 3: Verify output format is yyyy-MM-dd
236+
outputJSON := string(jsonBytes)
237+
assert.Contains(t, outputJSON, "-", "Output should contain dashes (yyyy-MM-dd format)")
238+
assert.Equal(t, tc.inputJSON, outputJSON, "Round-trip should preserve exact format")
239+
240+
// Step 4: Unmarshal the output back to verify round-trip integrity
241+
var roundTripDate common.APIShortDate
242+
err = json.Unmarshal(jsonBytes, &roundTripDate)
243+
assert.Nil(t, err, "Round-trip unmarshalling should work")
244+
245+
// Step 5: Verify dates represent the same day
246+
roundTripTime := time.Time(roundTripDate)
247+
assert.Equal(t, originalTime.Year(), roundTripTime.Year(), "Year should be preserved in round-trip")
248+
assert.Equal(t, originalTime.Month(), roundTripTime.Month(), "Month should be preserved in round-trip")
249+
assert.Equal(t, originalTime.Day(), roundTripTime.Day(), "Day should be preserved in round-trip")
250+
251+
// Step 6: Verify expected output format
252+
expectedOutput := originalTime.Format("2006-01-02")
253+
assert.Equal(t, `"`+expectedOutput+`"`, outputJSON, "Output should match yyyy-MM-dd format")
254+
})
255+
}
256+
}

test/instruments_test.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package test
22

33
import (
4-
"github.com/stretchr/testify/assert"
54
"net/http"
65
"testing"
76

7+
"github.com/stretchr/testify/assert"
8+
89
"github.com/checkout/checkout-sdk-go/common"
910
"github.com/checkout/checkout-sdk-go/errors"
1011
"github.com/checkout/checkout-sdk-go/instruments/nas"
@@ -216,11 +217,18 @@ func TestShouldDeleteInstrument(t *testing.T) {
216217

217218
func createSepaInstrument(t *testing.T) *nas.CreateSepaInstrumentResponse {
218219
request := nas.NewCreateSepaInstrumentRequest()
220+
221+
// Create APIShortDate with YYYY-MM-DD format to test custom date parsing
222+
dateOfSignature := &common.APIShortDate{}
223+
err := dateOfSignature.UnmarshalJSON([]byte(`"2023-12-15"`))
224+
assert.Nil(t, err)
225+
219226
request.InstrumentData = &nas.InstrumentData{
220-
AccountNumber: "FR7630006000011234567890189",
221-
Country: common.FR,
222-
Currency: common.EUR,
223-
PaymentType: payments.Recurring,
227+
AccountNumber: "FR7630006000011234567890189",
228+
Country: common.FR,
229+
Currency: common.EUR,
230+
PaymentType: payments.Recurring,
231+
DateOfSignature: dateOfSignature,
224232
}
225233
request.AccountHolder = &common.AccountHolder{
226234
FirstName: "Ali",

0 commit comments

Comments
 (0)