@ripwords/myinvois-client 0.0.2 → 0.0.4
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 +5 -2
- package/dist/{index.cjs → cjs/index.cjs} +5 -4
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/{multipart-parser-Bu3ikqFQ.cjs → cjs/multipart-parser-Bu3ikqFQ.cjs} +2 -1
- package/dist/cjs/multipart-parser-Bu3ikqFQ.cjs.map +1 -0
- package/dist/{node-qs2pnN6F.cjs → cjs/node-qs2pnN6F.cjs} +2 -1
- package/dist/cjs/node-qs2pnN6F.cjs.map +1 -0
- package/dist/index.js +3 -3
- package/package.json +4 -3
- package/rolldown.config.ts +2 -1
- package/src/utils/MyInvoisClient.ts +3 -3
- package/test/MyInvoisClient.test.ts +169 -0
- package/tsconfig.json +2 -1
- package/vitest.config.ts +5 -0
- package/dist/multipart-parser-G0iFJuWv.js +0 -181
- package/dist/node-BMeCs5nV.js +0 -4123
package/rolldown.config.ts
CHANGED
|
@@ -10,7 +10,7 @@ export class MyInvoisClient {
|
|
|
10
10
|
private readonly clientId: string
|
|
11
11
|
private readonly clientSecret: string
|
|
12
12
|
private token = ''
|
|
13
|
-
private tokenExpiration
|
|
13
|
+
private tokenExpiration: Date | undefined
|
|
14
14
|
|
|
15
15
|
constructor(
|
|
16
16
|
clientId: string,
|
|
@@ -21,7 +21,7 @@ export class MyInvoisClient {
|
|
|
21
21
|
this.clientSecret = clientSecret
|
|
22
22
|
|
|
23
23
|
if (environment === 'sandbox') {
|
|
24
|
-
this.baseUrl = 'https://preprod-mytax.hasil.gov.my
|
|
24
|
+
this.baseUrl = 'https://preprod-mytax.hasil.gov.my'
|
|
25
25
|
} else {
|
|
26
26
|
this.baseUrl = 'https://mytax.hasil.gov.my'
|
|
27
27
|
}
|
|
@@ -51,7 +51,7 @@ export class MyInvoisClient {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
private async getToken() {
|
|
54
|
-
if (this.tokenExpiration < new Date()) {
|
|
54
|
+
if (!this.tokenExpiration || this.tokenExpiration < new Date()) {
|
|
55
55
|
await this.refreshToken()
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { MyInvoisClient } from '../src/utils/MyInvoisClient'
|
|
3
|
+
import { ofetch } from 'ofetch'
|
|
4
|
+
|
|
5
|
+
vi.mock('ofetch', () => ({
|
|
6
|
+
ofetch: vi.fn(),
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
vi.useFakeTimers()
|
|
10
|
+
|
|
11
|
+
describe('MyInvoisClient', () => {
|
|
12
|
+
let client: MyInvoisClient
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.clearAllMocks()
|
|
16
|
+
client = new MyInvoisClient('test-id', 'test-secret', 'sandbox')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('constructor', () => {
|
|
20
|
+
it('should set sandbox URL when environment is sandbox', () => {
|
|
21
|
+
const sandboxClient = new MyInvoisClient(
|
|
22
|
+
process.env.CLIENT_ID!,
|
|
23
|
+
process.env.CLIENT_SECRET!,
|
|
24
|
+
'sandbox',
|
|
25
|
+
)
|
|
26
|
+
expect((sandboxClient as any).baseUrl).toBe(
|
|
27
|
+
'https://preprod-mytax.hasil.gov.my',
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should set production URL when environment is production', () => {
|
|
32
|
+
const prodClient = new MyInvoisClient(
|
|
33
|
+
process.env.CLIENT_ID!,
|
|
34
|
+
process.env.CLIENT_SECRET!,
|
|
35
|
+
'production',
|
|
36
|
+
)
|
|
37
|
+
expect((prodClient as any).baseUrl).toBe('https://mytax.hasil.gov.my')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('token management', () => {
|
|
42
|
+
it('should get a new token if token does not exist', async () => {
|
|
43
|
+
const mockToken = {
|
|
44
|
+
access_token: 'test-token',
|
|
45
|
+
expires_in: 3600,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
vi.mocked(ofetch).mockResolvedValueOnce(mockToken)
|
|
49
|
+
|
|
50
|
+
await client.verifyTin('123', '456')
|
|
51
|
+
|
|
52
|
+
expect(ofetch).toHaveBeenCalledWith(
|
|
53
|
+
'https://preprod-mytax.hasil.gov.my/connect/token',
|
|
54
|
+
expect.any(Object),
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should get a new token when token is expired', async () => {
|
|
59
|
+
const mockToken = {
|
|
60
|
+
access_token: 'test-token',
|
|
61
|
+
expires_in: 3600,
|
|
62
|
+
}
|
|
63
|
+
vi.advanceTimersByTime(8000)
|
|
64
|
+
vi.mocked(ofetch)
|
|
65
|
+
.mockResolvedValueOnce(mockToken)
|
|
66
|
+
.mockResolvedValueOnce(undefined)
|
|
67
|
+
|
|
68
|
+
await client.verifyTin('123', '456')
|
|
69
|
+
|
|
70
|
+
// Check first call (token request)
|
|
71
|
+
expect(ofetch).toHaveBeenNthCalledWith(
|
|
72
|
+
1,
|
|
73
|
+
'https://preprod-mytax.hasil.gov.my/connect/token',
|
|
74
|
+
{
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: {
|
|
77
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
78
|
+
},
|
|
79
|
+
body: {
|
|
80
|
+
grant_type: 'client_credentials',
|
|
81
|
+
client_id: 'test-id',
|
|
82
|
+
client_secret: 'test-secret',
|
|
83
|
+
scope: 'InvoicingAPI',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
// Check second call (verifyTin request)
|
|
89
|
+
expect(ofetch).toHaveBeenNthCalledWith(
|
|
90
|
+
2,
|
|
91
|
+
`https://preprod-mytax.hasil.gov.my/api/v1.0/taxpayer/validate/123?idType=NRIC&idValue=456`,
|
|
92
|
+
{
|
|
93
|
+
method: 'GET',
|
|
94
|
+
headers: {
|
|
95
|
+
Authorization: `Bearer ${mockToken.access_token}`,
|
|
96
|
+
},
|
|
97
|
+
responseType: 'json',
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should reuse existing token if not expired', async () => {
|
|
103
|
+
const mockToken = {
|
|
104
|
+
access_token: 'test-token',
|
|
105
|
+
expires_in: 3600,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
vi.mocked(ofetch).mockResolvedValueOnce(mockToken)
|
|
109
|
+
|
|
110
|
+
// First call to get token
|
|
111
|
+
await client.verifyTin(process.env.TIN_VALUE!, process.env.NRIC_VALUE!)
|
|
112
|
+
|
|
113
|
+
vi.advanceTimersByTime(10)
|
|
114
|
+
|
|
115
|
+
// Second call should reuse token
|
|
116
|
+
await client.verifyTin(process.env.TIN_VALUE!, process.env.NRIC_VALUE!)
|
|
117
|
+
|
|
118
|
+
// Token endpoint should only be called once
|
|
119
|
+
expect(ofetch).toHaveBeenCalledWith(
|
|
120
|
+
'https://preprod-mytax.hasil.gov.my/connect/token',
|
|
121
|
+
expect.any(Object),
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('verifyTin', () => {
|
|
127
|
+
it('should return true when verification succeeds', async () => {
|
|
128
|
+
const mockToken = {
|
|
129
|
+
access_token: 'test-token',
|
|
130
|
+
expires_in: 3600,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
vi.mocked(ofetch)
|
|
134
|
+
.mockResolvedValueOnce(mockToken) // Token call
|
|
135
|
+
.mockResolvedValueOnce(undefined) // Verify call
|
|
136
|
+
|
|
137
|
+
const result = await client.verifyTin('123', '456')
|
|
138
|
+
expect(result).toBe(true)
|
|
139
|
+
|
|
140
|
+
expect(ofetch).toHaveBeenCalledWith(
|
|
141
|
+
`https://preprod-mytax.hasil.gov.my/api/v1.0/taxpayer/validate/123?idType=NRIC&idValue=456`,
|
|
142
|
+
expect.objectContaining({
|
|
143
|
+
method: 'GET',
|
|
144
|
+
headers: {
|
|
145
|
+
Authorization: 'Bearer test-token',
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should return false when verification fails', async () => {
|
|
152
|
+
const mockToken = {
|
|
153
|
+
access_token: 'test-token',
|
|
154
|
+
expires_in: 3600,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
vi.mocked(ofetch)
|
|
158
|
+
.mockResolvedValueOnce(mockToken) // Token call
|
|
159
|
+
.mockRejectedValueOnce(new Error('Invalid TIN')) // Verify call
|
|
160
|
+
|
|
161
|
+
const result = await client.verifyTin(
|
|
162
|
+
process.env.TIN_VALUE!,
|
|
163
|
+
process.env.NRIC_VALUE!,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
expect(result).toBe(false)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
})
|
package/tsconfig.json
CHANGED
package/vitest.config.ts
ADDED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
const require_node = require('./node-BMeCs5nV.js');
|
|
3
|
-
const node_http = require_node.__toESM(require("node:http"));
|
|
4
|
-
const node_https = require_node.__toESM(require("node:https"));
|
|
5
|
-
const node_zlib = require_node.__toESM(require("node:zlib"));
|
|
6
|
-
const node_stream = require_node.__toESM(require("node:stream"));
|
|
7
|
-
const node_buffer = require_node.__toESM(require("node:buffer"));
|
|
8
|
-
const node_util = require_node.__toESM(require("node:util"));
|
|
9
|
-
const node_url = require_node.__toESM(require("node:url"));
|
|
10
|
-
const node_net = require_node.__toESM(require("node:net"));
|
|
11
|
-
const node_fs = require_node.__toESM(require("node:fs"));
|
|
12
|
-
const node_path = require_node.__toESM(require("node:path"));
|
|
13
|
-
|
|
14
|
-
//#region node_modules/node-fetch-native/dist/chunks/multipart-parser.mjs
|
|
15
|
-
var U = Object.defineProperty;
|
|
16
|
-
var E = (_, o) => U(_, "name", {
|
|
17
|
-
value: o,
|
|
18
|
-
configurable: !0
|
|
19
|
-
});
|
|
20
|
-
let D = 0;
|
|
21
|
-
const t = {
|
|
22
|
-
START_BOUNDARY: D++,
|
|
23
|
-
HEADER_FIELD_START: D++,
|
|
24
|
-
HEADER_FIELD: D++,
|
|
25
|
-
HEADER_VALUE_START: D++,
|
|
26
|
-
HEADER_VALUE: D++,
|
|
27
|
-
HEADER_VALUE_ALMOST_DONE: D++,
|
|
28
|
-
HEADERS_ALMOST_DONE: D++,
|
|
29
|
-
PART_DATA_START: D++,
|
|
30
|
-
PART_DATA: D++,
|
|
31
|
-
END: D++
|
|
32
|
-
};
|
|
33
|
-
let F = 1;
|
|
34
|
-
const u = {
|
|
35
|
-
PART_BOUNDARY: F,
|
|
36
|
-
LAST_BOUNDARY: F *= 2
|
|
37
|
-
}, g = 10, N = 13, V = 32, S = 45, Y = 58, x = 97, C = 122, I = E((_) => _ | 32, "lower"), p = E(() => {}, "noop");
|
|
38
|
-
var M = class {
|
|
39
|
-
static {
|
|
40
|
-
E(this, "MultipartParser");
|
|
41
|
-
}
|
|
42
|
-
constructor(o) {
|
|
43
|
-
this.index = 0, this.flags = 0, this.onHeaderEnd = p, this.onHeaderField = p, this.onHeadersEnd = p, this.onHeaderValue = p, this.onPartBegin = p, this.onPartData = p, this.onPartEnd = p, this.boundaryChars = {}, o = `\r
|
|
44
|
-
--` + o;
|
|
45
|
-
const n = new Uint8Array(o.length);
|
|
46
|
-
for (let r = 0; r < o.length; r++) n[r] = o.charCodeAt(r), this.boundaryChars[n[r]] = !0;
|
|
47
|
-
this.boundary = n, this.lookbehind = new Uint8Array(this.boundary.length + 8), this.state = t.START_BOUNDARY;
|
|
48
|
-
}
|
|
49
|
-
write(o) {
|
|
50
|
-
let n = 0;
|
|
51
|
-
const r = o.length;
|
|
52
|
-
let d = this.index, { lookbehind: l, boundary: c, boundaryChars: m, index: e, state: i, flags: A } = this;
|
|
53
|
-
const H = this.boundary.length, O = H - 1, y = o.length;
|
|
54
|
-
let a, L;
|
|
55
|
-
const f = E((h) => {
|
|
56
|
-
this[h + "Mark"] = n;
|
|
57
|
-
}, "mark"), s = E((h) => {
|
|
58
|
-
delete this[h + "Mark"];
|
|
59
|
-
}, "clear"), T = E((h, P, R, k) => {
|
|
60
|
-
(P === void 0 || P !== R) && this[h](k && k.subarray(P, R));
|
|
61
|
-
}, "callback"), b = E((h, P) => {
|
|
62
|
-
const R = h + "Mark";
|
|
63
|
-
R in this && (P ? (T(h, this[R], n, o), delete this[R]) : (T(h, this[R], o.length, o), this[R] = 0));
|
|
64
|
-
}, "dataCallback");
|
|
65
|
-
for (n = 0; n < r; n++) switch (a = o[n], i) {
|
|
66
|
-
case t.START_BOUNDARY:
|
|
67
|
-
if (e === c.length - 2) {
|
|
68
|
-
if (a === S) A |= u.LAST_BOUNDARY;
|
|
69
|
-
else if (a !== N) return;
|
|
70
|
-
e++;
|
|
71
|
-
break;
|
|
72
|
-
} else if (e - 1 === c.length - 2) {
|
|
73
|
-
if (A & u.LAST_BOUNDARY && a === S) i = t.END, A = 0;
|
|
74
|
-
else if (!(A & u.LAST_BOUNDARY) && a === g) e = 0, T("onPartBegin"), i = t.HEADER_FIELD_START;
|
|
75
|
-
else return;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
a !== c[e + 2] && (e = -2), a === c[e + 2] && e++;
|
|
79
|
-
break;
|
|
80
|
-
case t.HEADER_FIELD_START: i = t.HEADER_FIELD, f("onHeaderField"), e = 0;
|
|
81
|
-
case t.HEADER_FIELD:
|
|
82
|
-
if (a === N) {
|
|
83
|
-
s("onHeaderField"), i = t.HEADERS_ALMOST_DONE;
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
if (e++, a === S) break;
|
|
87
|
-
if (a === Y) {
|
|
88
|
-
if (e === 1) return;
|
|
89
|
-
b("onHeaderField", !0), i = t.HEADER_VALUE_START;
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
if (L = I(a), L < x || L > C) return;
|
|
93
|
-
break;
|
|
94
|
-
case t.HEADER_VALUE_START:
|
|
95
|
-
if (a === V) break;
|
|
96
|
-
f("onHeaderValue"), i = t.HEADER_VALUE;
|
|
97
|
-
case t.HEADER_VALUE:
|
|
98
|
-
a === N && (b("onHeaderValue", !0), T("onHeaderEnd"), i = t.HEADER_VALUE_ALMOST_DONE);
|
|
99
|
-
break;
|
|
100
|
-
case t.HEADER_VALUE_ALMOST_DONE:
|
|
101
|
-
if (a !== g) return;
|
|
102
|
-
i = t.HEADER_FIELD_START;
|
|
103
|
-
break;
|
|
104
|
-
case t.HEADERS_ALMOST_DONE:
|
|
105
|
-
if (a !== g) return;
|
|
106
|
-
T("onHeadersEnd"), i = t.PART_DATA_START;
|
|
107
|
-
break;
|
|
108
|
-
case t.PART_DATA_START: i = t.PART_DATA, f("onPartData");
|
|
109
|
-
case t.PART_DATA:
|
|
110
|
-
if (d = e, e === 0) {
|
|
111
|
-
for (n += O; n < y && !(o[n] in m);) n += H;
|
|
112
|
-
n -= O, a = o[n];
|
|
113
|
-
}
|
|
114
|
-
if (e < c.length) c[e] === a ? (e === 0 && b("onPartData", !0), e++) : e = 0;
|
|
115
|
-
else if (e === c.length) e++, a === N ? A |= u.PART_BOUNDARY : a === S ? A |= u.LAST_BOUNDARY : e = 0;
|
|
116
|
-
else if (e - 1 === c.length) if (A & u.PART_BOUNDARY) {
|
|
117
|
-
if (e = 0, a === g) {
|
|
118
|
-
A &= ~u.PART_BOUNDARY, T("onPartEnd"), T("onPartBegin"), i = t.HEADER_FIELD_START;
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
} else A & u.LAST_BOUNDARY && a === S ? (T("onPartEnd"), i = t.END, A = 0) : e = 0;
|
|
122
|
-
if (e > 0) l[e - 1] = a;
|
|
123
|
-
else if (d > 0) {
|
|
124
|
-
const h = new Uint8Array(l.buffer, l.byteOffset, l.byteLength);
|
|
125
|
-
T("onPartData", 0, d, h), d = 0, f("onPartData"), n--;
|
|
126
|
-
}
|
|
127
|
-
break;
|
|
128
|
-
case t.END: break;
|
|
129
|
-
default: throw new Error(`Unexpected state entered: ${i}`);
|
|
130
|
-
}
|
|
131
|
-
b("onHeaderField"), b("onHeaderValue"), b("onPartData"), this.index = e, this.state = i, this.flags = A;
|
|
132
|
-
}
|
|
133
|
-
end() {
|
|
134
|
-
if (this.state === t.HEADER_FIELD_START && this.index === 0 || this.state === t.PART_DATA && this.index === this.boundary.length) this.onPartEnd();
|
|
135
|
-
else if (this.state !== t.END) throw new Error("MultipartParser.end(): stream ended unexpectedly");
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
function $(_) {
|
|
139
|
-
const o = _.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i);
|
|
140
|
-
if (!o) return;
|
|
141
|
-
const n = o[2] || o[3] || "";
|
|
142
|
-
let r = n.slice(n.lastIndexOf("\\") + 1);
|
|
143
|
-
return r = r.replace(/%22/g, "\""), r = r.replace(/&#(\d{4});/g, (d, l) => String.fromCharCode(l)), r;
|
|
144
|
-
}
|
|
145
|
-
E($, "_fileName");
|
|
146
|
-
async function v(_, o) {
|
|
147
|
-
if (!/multipart/i.test(o)) throw new TypeError("Failed to fetch");
|
|
148
|
-
const n = o.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
|
|
149
|
-
if (!n) throw new TypeError("no or bad content-type header, no multipart boundary");
|
|
150
|
-
const r = new M(n[1] || n[2]);
|
|
151
|
-
let d, l, c, m, e, i;
|
|
152
|
-
const A = [], H = new require_node.Zt(), O = E((s) => {
|
|
153
|
-
c += f.decode(s, { stream: !0 });
|
|
154
|
-
}, "onPartData"), y = E((s) => {
|
|
155
|
-
A.push(s);
|
|
156
|
-
}, "appendToFile"), a = E(() => {
|
|
157
|
-
const s = new require_node.Yr(A, i, { type: e });
|
|
158
|
-
H.append(m, s);
|
|
159
|
-
}, "appendFileToFormData"), L = E(() => {
|
|
160
|
-
H.append(m, c);
|
|
161
|
-
}, "appendEntryToFormData"), f = new TextDecoder("utf-8");
|
|
162
|
-
f.decode(), r.onPartBegin = function() {
|
|
163
|
-
r.onPartData = O, r.onPartEnd = L, d = "", l = "", c = "", m = "", e = "", i = null, A.length = 0;
|
|
164
|
-
}, r.onHeaderField = function(s) {
|
|
165
|
-
d += f.decode(s, { stream: !0 });
|
|
166
|
-
}, r.onHeaderValue = function(s) {
|
|
167
|
-
l += f.decode(s, { stream: !0 });
|
|
168
|
-
}, r.onHeaderEnd = function() {
|
|
169
|
-
if (l += f.decode(), d = d.toLowerCase(), d === "content-disposition") {
|
|
170
|
-
const s = l.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i);
|
|
171
|
-
s && (m = s[2] || s[3] || ""), i = $(l), i && (r.onPartData = y, r.onPartEnd = a);
|
|
172
|
-
} else d === "content-type" && (e = l);
|
|
173
|
-
l = "", d = "";
|
|
174
|
-
};
|
|
175
|
-
for await (const s of _) r.write(s);
|
|
176
|
-
return r.end(), H;
|
|
177
|
-
}
|
|
178
|
-
E(v, "toFormData");
|
|
179
|
-
|
|
180
|
-
//#endregion
|
|
181
|
-
exports.toFormData = v
|