@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.
@@ -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-mytax.hasil.gov.my'
24
+ this.baseUrl = 'https://preprod-api.myinvois.hasil.gov.my'
25
25
  } else {
26
- this.baseUrl = 'https://mytax.hasil.gov.my'
26
+ this.baseUrl = 'https://api.myinvois.hasil.gov.my'
27
27
  }
28
28
  }
29
29
 
30
30
  private async refreshToken() {
31
- const tokenResponse = await ofetch<TokenResponse>(
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
- this.token = tokenResponse.access_token
48
- this.tokenExpiration = new Date(
49
- Date.now() + tokenResponse.expires_in * 1000,
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<T>(
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 ofetch<T>(`${this.baseUrl}${path}`, {
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<void>(
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
- return true
91
- } catch {
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
- vi.mock('ofetch', () => ({
6
- ofetch: vi.fn(),
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
- process.env.CLIENT_ID!,
23
- process.env.CLIENT_SECRET!,
21
+ 'test-id',
22
+ 'test-secret',
24
23
  'sandbox',
24
+ true,
25
25
  )
26
26
  expect((sandboxClient as any).baseUrl).toBe(
27
- 'https://preprod-mytax.hasil.gov.my',
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
- process.env.CLIENT_ID!,
34
- process.env.CLIENT_SECRET!,
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
- vi.mocked(ofetch).mockResolvedValueOnce(mockToken)
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(ofetch).toHaveBeenCalledWith(
53
- 'https://preprod-mytax.hasil.gov.my/connect/token',
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
- vi.advanceTimersByTime(8000)
64
- vi.mocked(ofetch)
65
- .mockResolvedValueOnce(mockToken)
66
- .mockResolvedValueOnce(undefined)
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(ofetch).toHaveBeenNthCalledWith(
83
+ expect(mockFetch).toHaveBeenNthCalledWith(
72
84
  1,
73
- 'https://preprod-mytax.hasil.gov.my/connect/token',
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(ofetch).toHaveBeenNthCalledWith(
101
+ expect(mockFetch).toHaveBeenNthCalledWith(
90
102
  2,
91
- `https://preprod-mytax.hasil.gov.my/api/v1.0/taxpayer/validate/123?idType=NRIC&idValue=456`,
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
- vi.mocked(ofetch).mockResolvedValueOnce(mockToken)
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(ofetch).toHaveBeenCalledWith(
120
- 'https://preprod-mytax.hasil.gov.my/connect/token',
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
- vi.mocked(ofetch)
134
- .mockResolvedValueOnce(mockToken) // Token call
135
- .mockResolvedValueOnce(undefined) // Verify call
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
- const result = await client.verifyTin('123', '456')
138
- expect(result).toBe(true)
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(ofetch).toHaveBeenCalledWith(
141
- `https://preprod-mytax.hasil.gov.my/api/v1.0/taxpayer/validate/123?idType=NRIC&idValue=456`,
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
- vi.mocked(ofetch)
158
- .mockResolvedValueOnce(mockToken) // Token call
159
- .mockRejectedValueOnce(new Error('Invalid TIN')) // Verify call
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!,
package/vitest.config.ts CHANGED
@@ -2,4 +2,7 @@ import { defineConfig } from 'vitest/config'
2
2
 
3
3
  export default defineConfig({
4
4
  root: '.',
5
+ test: {
6
+ setupFiles: ['dotenv/config'],
7
+ },
5
8
  })