@ripwords/myinvois-client 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +0 -11
- package/dist/cjs/index.cjs +33 -507
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.cjs +75 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +33 -500
- package/package.json +14 -13
- package/rolldown.config.ts +1 -1
- package/src/utils/MyInvoisClient.ts +36 -25
- package/test/MyInvoiClientRealData.test.ts +19 -0
- package/test/MyInvoisClient.test.ts +62 -38
- package/vitest.config.ts +3 -0
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { ofetch } from 'ofetch/node'
|
|
2
|
-
|
|
3
1
|
interface TokenResponse {
|
|
4
2
|
access_token: string
|
|
5
3
|
expires_in: number
|
|
@@ -9,6 +7,7 @@ export class MyInvoisClient {
|
|
|
9
7
|
private readonly baseUrl: string
|
|
10
8
|
private readonly clientId: string
|
|
11
9
|
private readonly clientSecret: string
|
|
10
|
+
private readonly debug: boolean
|
|
12
11
|
private token = ''
|
|
13
12
|
private tokenExpiration: Date | undefined
|
|
14
13
|
|
|
@@ -16,58 +15,63 @@ export class MyInvoisClient {
|
|
|
16
15
|
clientId: string,
|
|
17
16
|
clientSecret: string,
|
|
18
17
|
environment: 'sandbox' | 'production',
|
|
18
|
+
debug: boolean = false,
|
|
19
19
|
) {
|
|
20
20
|
this.clientId = clientId
|
|
21
21
|
this.clientSecret = clientSecret
|
|
22
|
-
|
|
22
|
+
this.debug = debug
|
|
23
23
|
if (environment === 'sandbox') {
|
|
24
|
-
this.baseUrl = 'https://preprod-
|
|
24
|
+
this.baseUrl = 'https://preprod-api.myinvois.hasil.gov.my'
|
|
25
25
|
} else {
|
|
26
|
-
this.baseUrl = 'https://
|
|
26
|
+
this.baseUrl = 'https://api.myinvois.hasil.gov.my'
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
private async refreshToken() {
|
|
31
|
-
|
|
32
|
-
`${this.baseUrl}/connect/token`,
|
|
33
|
-
{
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(`${this.baseUrl}/connect/token`, {
|
|
34
33
|
method: 'POST',
|
|
35
34
|
headers: {
|
|
36
35
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
37
36
|
},
|
|
38
|
-
body: {
|
|
37
|
+
body: new URLSearchParams({
|
|
39
38
|
grant_type: 'client_credentials',
|
|
40
39
|
client_id: this.clientId,
|
|
41
40
|
client_secret: this.clientSecret,
|
|
42
41
|
scope: 'InvoicingAPI',
|
|
43
|
-
},
|
|
44
|
-
}
|
|
45
|
-
|
|
42
|
+
}),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const tokenResponse: TokenResponse = await response.json()
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
this.token = tokenResponse.access_token
|
|
48
|
+
this.tokenExpiration = new Date(
|
|
49
|
+
Date.now() + tokenResponse.expires_in * 1000,
|
|
50
|
+
)
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (this.debug) {
|
|
53
|
+
console.error(error)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
private async getToken() {
|
|
54
59
|
if (!this.tokenExpiration || this.tokenExpiration < new Date()) {
|
|
60
|
+
if (this.debug) {
|
|
61
|
+
console.log('Refreshing token')
|
|
62
|
+
}
|
|
55
63
|
await this.refreshToken()
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
return this.token
|
|
59
67
|
}
|
|
60
68
|
|
|
61
|
-
private async fetch<
|
|
62
|
-
path: string,
|
|
63
|
-
options: Parameters<typeof ofetch>[1] = {},
|
|
64
|
-
) {
|
|
69
|
+
private async fetch(path: string, options: Parameters<typeof fetch>[1] = {}) {
|
|
65
70
|
const token = await this.getToken()
|
|
66
71
|
|
|
67
|
-
return
|
|
72
|
+
return fetch(`${this.baseUrl}${path}`, {
|
|
68
73
|
...options,
|
|
69
74
|
headers: { ...options.headers, Authorization: `Bearer ${token}` },
|
|
70
|
-
responseType: 'json',
|
|
71
75
|
})
|
|
72
76
|
}
|
|
73
77
|
|
|
@@ -80,15 +84,22 @@ export class MyInvoisClient {
|
|
|
80
84
|
*/
|
|
81
85
|
async verifyTin(tin: string, nric: string) {
|
|
82
86
|
try {
|
|
83
|
-
await this.fetch
|
|
87
|
+
const response = await this.fetch(
|
|
84
88
|
`/api/v1.0/taxpayer/validate/${tin}?idType=NRIC&idValue=${nric}`,
|
|
85
89
|
{
|
|
86
90
|
method: 'GET',
|
|
87
91
|
},
|
|
88
92
|
)
|
|
89
93
|
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
if (response.status === 200) {
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return false
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (this.debug) {
|
|
101
|
+
console.error(error)
|
|
102
|
+
}
|
|
92
103
|
return false
|
|
93
104
|
}
|
|
94
105
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { MyInvoisClient } from '../src/utils/MyInvoisClient'
|
|
3
|
+
|
|
4
|
+
describe('MyInvoisClientRealData', () => {
|
|
5
|
+
it('should verify TIN with real data', async () => {
|
|
6
|
+
const client = new MyInvoisClient(
|
|
7
|
+
process.env.CLIENT_ID!,
|
|
8
|
+
process.env.CLIENT_SECRET!,
|
|
9
|
+
'sandbox',
|
|
10
|
+
)
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
await client.refreshToken()
|
|
13
|
+
const result = await client.verifyTin(
|
|
14
|
+
process.env.TIN_VALUE!,
|
|
15
|
+
process.env.NRIC_VALUE!,
|
|
16
|
+
)
|
|
17
|
+
expect(result).toBe(true)
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
2
|
import { MyInvoisClient } from '../src/utils/MyInvoisClient'
|
|
3
|
-
import { ofetch } from 'ofetch/node'
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
// Mock global fetch
|
|
5
|
+
const mockFetch = vi.fn()
|
|
6
|
+
vi.stubGlobal('fetch', mockFetch)
|
|
8
7
|
|
|
9
8
|
vi.useFakeTimers()
|
|
10
9
|
|
|
@@ -13,28 +12,32 @@ describe('MyInvoisClient', () => {
|
|
|
13
12
|
|
|
14
13
|
beforeEach(() => {
|
|
15
14
|
vi.clearAllMocks()
|
|
16
|
-
client = new MyInvoisClient('test-id', 'test-secret', 'sandbox')
|
|
15
|
+
client = new MyInvoisClient('test-id', 'test-secret', 'sandbox', false)
|
|
17
16
|
})
|
|
18
17
|
|
|
19
18
|
describe('constructor', () => {
|
|
20
19
|
it('should set sandbox URL when environment is sandbox', () => {
|
|
21
20
|
const sandboxClient = new MyInvoisClient(
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
'test-id',
|
|
22
|
+
'test-secret',
|
|
24
23
|
'sandbox',
|
|
24
|
+
true,
|
|
25
25
|
)
|
|
26
26
|
expect((sandboxClient as any).baseUrl).toBe(
|
|
27
|
-
'https://preprod-
|
|
27
|
+
'https://preprod-api.myinvois.hasil.gov.my',
|
|
28
28
|
)
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
it('should set production URL when environment is production', () => {
|
|
32
32
|
const prodClient = new MyInvoisClient(
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
'test-id',
|
|
34
|
+
'test-secret',
|
|
35
35
|
'production',
|
|
36
|
+
true,
|
|
37
|
+
)
|
|
38
|
+
expect((prodClient as any).baseUrl).toBe(
|
|
39
|
+
'https://api.myinvois.hasil.gov.my',
|
|
36
40
|
)
|
|
37
|
-
expect((prodClient as any).baseUrl).toBe('https://mytax.hasil.gov.my')
|
|
38
41
|
})
|
|
39
42
|
})
|
|
40
43
|
|
|
@@ -45,12 +48,15 @@ describe('MyInvoisClient', () => {
|
|
|
45
48
|
expires_in: 3600,
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
mockFetch.mockResolvedValueOnce({
|
|
52
|
+
ok: true,
|
|
53
|
+
json: () => Promise.resolve(mockToken),
|
|
54
|
+
} as Response)
|
|
49
55
|
|
|
50
56
|
await client.verifyTin('123', '456')
|
|
51
57
|
|
|
52
|
-
expect(
|
|
53
|
-
'https://preprod-
|
|
58
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
59
|
+
'https://preprod-api.myinvois.hasil.gov.my/connect/token',
|
|
54
60
|
expect.any(Object),
|
|
55
61
|
)
|
|
56
62
|
})
|
|
@@ -60,41 +66,46 @@ describe('MyInvoisClient', () => {
|
|
|
60
66
|
access_token: 'test-token',
|
|
61
67
|
expires_in: 3600,
|
|
62
68
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.mockResolvedValueOnce(
|
|
66
|
-
|
|
69
|
+
|
|
70
|
+
mockFetch
|
|
71
|
+
.mockResolvedValueOnce({
|
|
72
|
+
ok: true,
|
|
73
|
+
json: () => Promise.resolve(mockToken),
|
|
74
|
+
} as Response)
|
|
75
|
+
.mockResolvedValueOnce({
|
|
76
|
+
ok: true,
|
|
77
|
+
json: () => Promise.resolve(undefined),
|
|
78
|
+
} as Response)
|
|
67
79
|
|
|
68
80
|
await client.verifyTin('123', '456')
|
|
69
81
|
|
|
70
82
|
// Check first call (token request)
|
|
71
|
-
expect(
|
|
83
|
+
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
72
84
|
1,
|
|
73
|
-
'https://preprod-
|
|
85
|
+
'https://preprod-api.myinvois.hasil.gov.my/connect/token',
|
|
74
86
|
{
|
|
75
87
|
method: 'POST',
|
|
76
88
|
headers: {
|
|
77
89
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
78
90
|
},
|
|
79
|
-
body: {
|
|
91
|
+
body: new URLSearchParams({
|
|
80
92
|
grant_type: 'client_credentials',
|
|
81
93
|
client_id: 'test-id',
|
|
82
94
|
client_secret: 'test-secret',
|
|
83
95
|
scope: 'InvoicingAPI',
|
|
84
|
-
},
|
|
96
|
+
}),
|
|
85
97
|
},
|
|
86
98
|
)
|
|
87
99
|
|
|
88
100
|
// Check second call (verifyTin request)
|
|
89
|
-
expect(
|
|
101
|
+
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
90
102
|
2,
|
|
91
|
-
`https://preprod-
|
|
103
|
+
`https://preprod-api.myinvois.hasil.gov.my/api/v1.0/taxpayer/validate/123?idType=NRIC&idValue=456`,
|
|
92
104
|
{
|
|
93
105
|
method: 'GET',
|
|
94
106
|
headers: {
|
|
95
107
|
Authorization: `Bearer ${mockToken.access_token}`,
|
|
96
108
|
},
|
|
97
|
-
responseType: 'json',
|
|
98
109
|
},
|
|
99
110
|
)
|
|
100
111
|
})
|
|
@@ -105,7 +116,10 @@ describe('MyInvoisClient', () => {
|
|
|
105
116
|
expires_in: 3600,
|
|
106
117
|
}
|
|
107
118
|
|
|
108
|
-
|
|
119
|
+
mockFetch.mockResolvedValueOnce({
|
|
120
|
+
ok: true,
|
|
121
|
+
json: () => Promise.resolve(mockToken),
|
|
122
|
+
} as Response)
|
|
109
123
|
|
|
110
124
|
// First call to get token
|
|
111
125
|
await client.verifyTin(process.env.TIN_VALUE!, process.env.NRIC_VALUE!)
|
|
@@ -116,8 +130,8 @@ describe('MyInvoisClient', () => {
|
|
|
116
130
|
await client.verifyTin(process.env.TIN_VALUE!, process.env.NRIC_VALUE!)
|
|
117
131
|
|
|
118
132
|
// Token endpoint should only be called once
|
|
119
|
-
expect(
|
|
120
|
-
'https://preprod-
|
|
133
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
134
|
+
'https://preprod-api.myinvois.hasil.gov.my/connect/token',
|
|
121
135
|
expect.any(Object),
|
|
122
136
|
)
|
|
123
137
|
})
|
|
@@ -130,15 +144,22 @@ describe('MyInvoisClient', () => {
|
|
|
130
144
|
expires_in: 3600,
|
|
131
145
|
}
|
|
132
146
|
|
|
133
|
-
|
|
134
|
-
.mockResolvedValueOnce(
|
|
135
|
-
|
|
147
|
+
mockFetch
|
|
148
|
+
.mockResolvedValueOnce({
|
|
149
|
+
ok: true,
|
|
150
|
+
json: () => Promise.resolve(mockToken),
|
|
151
|
+
} as Response)
|
|
152
|
+
.mockResolvedValueOnce({
|
|
153
|
+
ok: true,
|
|
154
|
+
json: () => Promise.resolve(undefined),
|
|
155
|
+
} as Response)
|
|
136
156
|
|
|
137
|
-
|
|
138
|
-
|
|
157
|
+
await client.verifyTin('123', '456')
|
|
158
|
+
vi.setSystemTime(new Date(Date.now() + 1000 * 8000))
|
|
159
|
+
await client.verifyTin('123', '456')
|
|
139
160
|
|
|
140
|
-
expect(
|
|
141
|
-
`https://preprod-
|
|
161
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
162
|
+
`https://preprod-api.myinvois.hasil.gov.my/api/v1.0/taxpayer/validate/123?idType=NRIC&idValue=456`,
|
|
142
163
|
expect.objectContaining({
|
|
143
164
|
method: 'GET',
|
|
144
165
|
headers: {
|
|
@@ -154,9 +175,12 @@ describe('MyInvoisClient', () => {
|
|
|
154
175
|
expires_in: 3600,
|
|
155
176
|
}
|
|
156
177
|
|
|
157
|
-
|
|
158
|
-
.mockResolvedValueOnce(
|
|
159
|
-
|
|
178
|
+
mockFetch
|
|
179
|
+
.mockResolvedValueOnce({
|
|
180
|
+
ok: true,
|
|
181
|
+
json: () => Promise.resolve(mockToken),
|
|
182
|
+
} as Response)
|
|
183
|
+
.mockRejectedValueOnce(new Error('Invalid TIN'))
|
|
160
184
|
|
|
161
185
|
const result = await client.verifyTin(
|
|
162
186
|
process.env.TIN_VALUE!,
|