@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.
@@ -12,8 +12,9 @@ export default defineConfig([
12
12
  {
13
13
  input: 'src/index.ts',
14
14
  output: {
15
- dir: 'dist',
15
+ dir: 'dist/cjs',
16
16
  format: 'commonjs',
17
+ sourcemap: true,
17
18
  entryFileNames: 'index.cjs',
18
19
  chunkFileNames: '[name]-[hash].cjs',
19
20
  },
@@ -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 = new Date()
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
@@ -26,7 +26,8 @@
26
26
  "include": [
27
27
  "src/**/*.ts",
28
28
  "src/**/*.d.ts",
29
- "rolldown.config.ts"
29
+ "rolldown.config.ts",
30
+ "test/MyInvoisClient.test.ts"
30
31
  ],
31
32
  "exclude": [
32
33
  "node_modules",
@@ -0,0 +1,5 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ root: '.',
5
+ })
@@ -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