@ripwords/myinvois-client 0.1.5 → 0.1.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/package.json +8 -11
- package/.prettierrc +0 -8
- package/CHANGELOG.md +0 -140
- package/bun.lock +0 -460
- package/myinvois-cert.conf.template +0 -23
- package/scripts/gen-cert.sh +0 -159
- package/src/api/platform/platformLogin.ts +0 -34
- package/src/index.ts +0 -530
- package/src/types/classification-codes.d.ts +0 -115
- package/src/types/country-code.d.ts +0 -790
- package/src/types/currencies.d.ts +0 -383
- package/src/types/documents.d.ts +0 -869
- package/src/types/e-invoice.d.ts +0 -41
- package/src/types/index.d.ts +0 -24
- package/src/types/msic/0X.d.ts +0 -408
- package/src/types/msic/1X.d.ts +0 -210
- package/src/types/msic/2X.d.ts +0 -266
- package/src/types/msic/3X.d.ts +0 -114
- package/src/types/msic/4X.d.ts +0 -520
- package/src/types/msic/5X.d.ts +0 -144
- package/src/types/msic/6X.d.ts +0 -200
- package/src/types/msic/7X.d.ts +0 -132
- package/src/types/msic/8X.d.ts +0 -210
- package/src/types/msic/9X.d.ts +0 -186
- package/src/types/msic-codes.d.ts +0 -31
- package/src/types/payment-modes.d.ts +0 -41
- package/src/types/signatures.d.ts +0 -169
- package/src/types/state-codes.d.ts +0 -59
- package/src/types/tax-types.d.ts +0 -39
- package/src/types/unit/1X.d.ts +0 -16
- package/src/types/unit/2X.d.ts +0 -62
- package/src/types/unit/3X.d.ts +0 -17
- package/src/types/unit/4X.d.ts +0 -44
- package/src/types/unit/5X.d.ts +0 -26
- package/src/types/unit/6X.d.ts +0 -12
- package/src/types/unit/7X.d.ts +0 -12
- package/src/types/unit/8X.d.ts +0 -15
- package/src/types/unit/9X.d.ts +0 -11
- package/src/types/unit/AX.d.ts +0 -202
- package/src/types/unit/BX.d.ts +0 -212
- package/src/types/unit/CX.d.ts +0 -238
- package/src/types/unit/DX.d.ts +0 -212
- package/src/types/unit/EX.d.ts +0 -196
- package/src/types/unit/FX.d.ts +0 -236
- package/src/types/unit/GX.d.ts +0 -254
- package/src/types/unit/HX.d.ts +0 -234
- package/src/types/unit/IX.d.ts +0 -28
- package/src/types/unit/JX.d.ts +0 -190
- package/src/types/unit/KX.d.ts +0 -284
- package/src/types/unit/LX.d.ts +0 -228
- package/src/types/unit/MX.d.ts +0 -288
- package/src/types/unit/NX.d.ts +0 -226
- package/src/types/unit/OX.d.ts +0 -34
- package/src/types/unit/PX.d.ts +0 -224
- package/src/types/unit/QX.d.ts +0 -94
- package/src/types/unit/RX.d.ts +0 -28
- package/src/types/unit/SX.d.ts +0 -56
- package/src/types/unit/TX.d.ts +0 -44
- package/src/types/unit/UX.d.ts +0 -14
- package/src/types/unit/VX.d.ts +0 -13
- package/src/types/unit/WX.d.ts +0 -34
- package/src/types/unit/XX.d.ts +0 -825
- package/src/types/unit/YX.d.ts +0 -17
- package/src/types/unit/ZX.d.ts +0 -19
- package/src/types/unit-types.d.ts +0 -86
- package/src/utils/base64.ts +0 -7
- package/src/utils/certificate.ts +0 -60
- package/src/utils/document.ts +0 -852
- package/src/utils/getBaseUrl.ts +0 -5
- package/src/utils/helpers.ts +0 -552
- package/src/utils/signature-diagnostics.ts +0 -583
- package/src/utils/validation.ts +0 -268
- package/test/MyInvoiClientWithRealData.test.ts +0 -40
- package/test/MyInvoisClient.test.ts +0 -204
- package/test/base64.test.ts +0 -43
- package/test/dynamicInvoiceFeatures.test.ts +0 -451
- package/test/signAndSubmitInvoice.test.ts +0 -452
- package/test/signature-diagnostics.test.ts +0 -130
- package/tsconfig.json +0 -39
- package/tsdown.config.ts +0 -31
- package/vitest.config.ts +0 -8
package/src/utils/validation.ts
DELETED
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import type { InvoiceV1_1 } from '../types/documents/index.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* MyInvois Invoice Validation Utilities
|
|
5
|
-
*
|
|
6
|
-
* Provides comprehensive validation for invoice data before document generation
|
|
7
|
-
* and submission to ensure compliance with MyInvois business rules and format requirements.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export interface ValidationResult {
|
|
11
|
-
isValid: boolean
|
|
12
|
-
errors: ValidationError[]
|
|
13
|
-
warnings: ValidationWarning[]
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ValidationError {
|
|
17
|
-
field: string
|
|
18
|
-
code: string
|
|
19
|
-
message: string
|
|
20
|
-
severity: 'error' | 'warning'
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface ValidationWarning extends ValidationError {
|
|
24
|
-
severity: 'warning'
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Validates TIN format based on registration type
|
|
29
|
-
*/
|
|
30
|
-
export const validateTIN = (
|
|
31
|
-
tin: string,
|
|
32
|
-
registrationType?: string,
|
|
33
|
-
): ValidationError[] => {
|
|
34
|
-
const errors: ValidationError[] = []
|
|
35
|
-
|
|
36
|
-
if (!tin) {
|
|
37
|
-
errors.push({
|
|
38
|
-
field: 'tin',
|
|
39
|
-
code: 'TIN_REQUIRED',
|
|
40
|
-
message: 'TIN is required',
|
|
41
|
-
severity: 'error',
|
|
42
|
-
})
|
|
43
|
-
return errors
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// TIN format validation based on type
|
|
47
|
-
if (registrationType === 'BRN' && !tin.startsWith('C')) {
|
|
48
|
-
errors.push({
|
|
49
|
-
field: 'tin',
|
|
50
|
-
code: 'TIN_FORMAT_INVALID',
|
|
51
|
-
message: 'Company TIN should start with "C" for BRN registration',
|
|
52
|
-
severity: 'warning',
|
|
53
|
-
})
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (registrationType === 'NRIC' && !tin.startsWith('IG')) {
|
|
57
|
-
errors.push({
|
|
58
|
-
field: 'tin',
|
|
59
|
-
code: 'TIN_FORMAT_INVALID',
|
|
60
|
-
message: 'Individual TIN should start with "IG" for NRIC registration',
|
|
61
|
-
severity: 'warning',
|
|
62
|
-
})
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Length validation
|
|
66
|
-
if (tin.length > 14) {
|
|
67
|
-
errors.push({
|
|
68
|
-
field: 'tin',
|
|
69
|
-
code: 'TIN_LENGTH_INVALID',
|
|
70
|
-
message: 'TIN cannot exceed 14 characters',
|
|
71
|
-
severity: 'error',
|
|
72
|
-
})
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return errors
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Validates contact number format (E.164 standard)
|
|
80
|
-
*/
|
|
81
|
-
export const validateContactNumber = (
|
|
82
|
-
contactNumber: string,
|
|
83
|
-
): ValidationError[] => {
|
|
84
|
-
const errors: ValidationError[] = []
|
|
85
|
-
|
|
86
|
-
if (!contactNumber || contactNumber === 'NA') {
|
|
87
|
-
return errors // Allow NA for consolidated e-invoices
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// E.164 format validation
|
|
91
|
-
const e164Regex = /^\+[1-9]\d{1,14}$/
|
|
92
|
-
if (!e164Regex.test(contactNumber)) {
|
|
93
|
-
errors.push({
|
|
94
|
-
field: 'contactNumber',
|
|
95
|
-
code: 'CONTACT_FORMAT_INVALID',
|
|
96
|
-
message: 'Contact number must be in E.164 format (e.g., +60123456789)',
|
|
97
|
-
severity: 'error',
|
|
98
|
-
})
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (contactNumber.length < 8) {
|
|
102
|
-
errors.push({
|
|
103
|
-
field: 'contactNumber',
|
|
104
|
-
code: 'CONTACT_LENGTH_INVALID',
|
|
105
|
-
message: 'Contact number must be at least 8 characters',
|
|
106
|
-
severity: 'error',
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return errors
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Validates monetary amounts
|
|
115
|
-
*/
|
|
116
|
-
export const validateMonetaryAmount = (
|
|
117
|
-
amount: number,
|
|
118
|
-
fieldName: string,
|
|
119
|
-
maxDigits = 18,
|
|
120
|
-
maxDecimals = 2,
|
|
121
|
-
): ValidationError[] => {
|
|
122
|
-
const errors: ValidationError[] = []
|
|
123
|
-
|
|
124
|
-
if (amount < 0) {
|
|
125
|
-
errors.push({
|
|
126
|
-
field: fieldName,
|
|
127
|
-
code: 'AMOUNT_NEGATIVE',
|
|
128
|
-
message: `${fieldName} cannot be negative`,
|
|
129
|
-
severity: 'error',
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Check total digits
|
|
134
|
-
const amountStr = amount.toString()
|
|
135
|
-
const [integerPart, decimalPart] = amountStr.split('.')
|
|
136
|
-
|
|
137
|
-
if (integerPart && integerPart.length > maxDigits - maxDecimals) {
|
|
138
|
-
errors.push({
|
|
139
|
-
field: fieldName,
|
|
140
|
-
code: 'AMOUNT_DIGITS_EXCEEDED',
|
|
141
|
-
message: `${fieldName} exceeds maximum ${maxDigits} digits`,
|
|
142
|
-
severity: 'error',
|
|
143
|
-
})
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (decimalPart && decimalPart.length > maxDecimals) {
|
|
147
|
-
errors.push({
|
|
148
|
-
field: fieldName,
|
|
149
|
-
code: 'AMOUNT_DECIMALS_EXCEEDED',
|
|
150
|
-
message: `${fieldName} exceeds maximum ${maxDecimals} decimal places`,
|
|
151
|
-
severity: 'error',
|
|
152
|
-
})
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return errors
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Validates tax calculation consistency
|
|
160
|
-
*/
|
|
161
|
-
export const validateTaxCalculations = (
|
|
162
|
-
invoice: InvoiceV1_1,
|
|
163
|
-
): ValidationError[] => {
|
|
164
|
-
const errors: ValidationError[] = []
|
|
165
|
-
|
|
166
|
-
// Calculate expected totals from line items
|
|
167
|
-
const expectedTaxExclusive = invoice.invoiceLineItems.reduce(
|
|
168
|
-
(sum, item) => sum + item.totalTaxableAmountPerLine,
|
|
169
|
-
0,
|
|
170
|
-
)
|
|
171
|
-
const expectedTaxAmount = invoice.invoiceLineItems.reduce(
|
|
172
|
-
(sum, item) => sum + item.taxAmount,
|
|
173
|
-
0,
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
// Allow small rounding differences (0.01)
|
|
177
|
-
const tolerance = 0.01
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
Math.abs(
|
|
181
|
-
invoice.legalMonetaryTotal.taxExclusiveAmount - expectedTaxExclusive,
|
|
182
|
-
) > tolerance
|
|
183
|
-
) {
|
|
184
|
-
errors.push({
|
|
185
|
-
field: 'legalMonetaryTotal.taxExclusiveAmount',
|
|
186
|
-
code: 'TAX_EXCLUSIVE_MISMATCH',
|
|
187
|
-
message: `Tax exclusive amount (${invoice.legalMonetaryTotal.taxExclusiveAmount}) doesn't match sum of line items (${expectedTaxExclusive})`,
|
|
188
|
-
severity: 'error',
|
|
189
|
-
})
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (Math.abs(invoice.taxTotal.taxAmount - expectedTaxAmount) > tolerance) {
|
|
193
|
-
errors.push({
|
|
194
|
-
field: 'taxTotal.taxAmount',
|
|
195
|
-
code: 'TAX_AMOUNT_MISMATCH',
|
|
196
|
-
message: `Tax amount (${invoice.taxTotal.taxAmount}) doesn't match sum of line item taxes (${expectedTaxAmount})`,
|
|
197
|
-
severity: 'error',
|
|
198
|
-
})
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return errors
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Main validation function for complete invoice
|
|
206
|
-
*/
|
|
207
|
-
export const validateInvoice = (invoice: InvoiceV1_1): ValidationResult => {
|
|
208
|
-
const allErrors: ValidationError[] = []
|
|
209
|
-
|
|
210
|
-
// Core field validations
|
|
211
|
-
allErrors.push(
|
|
212
|
-
...validateTIN(invoice.supplier.tin, invoice.supplier.registrationType),
|
|
213
|
-
)
|
|
214
|
-
allErrors.push(
|
|
215
|
-
...validateTIN(invoice.buyer.tin, invoice.buyer.registrationType),
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
allErrors.push(...validateContactNumber(invoice.supplier.contactNumber))
|
|
219
|
-
allErrors.push(...validateContactNumber(invoice.buyer.contactNumber))
|
|
220
|
-
|
|
221
|
-
// Monetary validations
|
|
222
|
-
allErrors.push(
|
|
223
|
-
...validateMonetaryAmount(
|
|
224
|
-
invoice.legalMonetaryTotal.taxExclusiveAmount,
|
|
225
|
-
'taxExclusiveAmount',
|
|
226
|
-
),
|
|
227
|
-
)
|
|
228
|
-
allErrors.push(
|
|
229
|
-
...validateMonetaryAmount(
|
|
230
|
-
invoice.legalMonetaryTotal.payableAmount,
|
|
231
|
-
'payableAmount',
|
|
232
|
-
),
|
|
233
|
-
)
|
|
234
|
-
allErrors.push(
|
|
235
|
-
...validateMonetaryAmount(invoice.taxTotal.taxAmount, 'taxAmount'),
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
// Line item validations
|
|
239
|
-
invoice.invoiceLineItems.forEach((item, index) => {
|
|
240
|
-
allErrors.push(
|
|
241
|
-
...validateMonetaryAmount(item.unitPrice, `lineItem[${index}].unitPrice`),
|
|
242
|
-
)
|
|
243
|
-
allErrors.push(
|
|
244
|
-
...validateMonetaryAmount(item.taxAmount, `lineItem[${index}].taxAmount`),
|
|
245
|
-
)
|
|
246
|
-
allErrors.push(
|
|
247
|
-
...validateMonetaryAmount(
|
|
248
|
-
item.totalTaxableAmountPerLine,
|
|
249
|
-
`lineItem[${index}].totalTaxableAmountPerLine`,
|
|
250
|
-
),
|
|
251
|
-
)
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
// Business rule validations
|
|
255
|
-
allErrors.push(...validateTaxCalculations(invoice))
|
|
256
|
-
|
|
257
|
-
// Separate errors and warnings
|
|
258
|
-
const errors = allErrors.filter(e => e.severity === 'error')
|
|
259
|
-
const warnings = allErrors.filter(
|
|
260
|
-
e => e.severity === 'warning',
|
|
261
|
-
) as ValidationWarning[]
|
|
262
|
-
|
|
263
|
-
return {
|
|
264
|
-
isValid: errors.length === 0,
|
|
265
|
-
errors,
|
|
266
|
-
warnings,
|
|
267
|
-
}
|
|
268
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { MyInvoisClient } from '../src'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* ⚠️ SECURITY NOTICE: This file uses environment variables for sensitive data.
|
|
6
|
-
* Never hardcode actual TIN, NRIC, certificates, or API credentials in test files.
|
|
7
|
-
* Use .env file for your actual values (already gitignored).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
describe('MyInvoisClientWithRealData', () => {
|
|
11
|
-
it('should verify TIN with real data', async () => {
|
|
12
|
-
// Skip the test if the environment variables are not set
|
|
13
|
-
if (
|
|
14
|
-
!process.env.CLIENT_ID ||
|
|
15
|
-
!process.env.CLIENT_SECRET ||
|
|
16
|
-
!process.env.TIN_VALUE ||
|
|
17
|
-
!process.env.NRIC_VALUE
|
|
18
|
-
) {
|
|
19
|
-
expect
|
|
20
|
-
.soft(false, 'Skipping test: Missing required environment variables')
|
|
21
|
-
.toBe(true)
|
|
22
|
-
return
|
|
23
|
-
}
|
|
24
|
-
const client = new MyInvoisClient(
|
|
25
|
-
process.env.CLIENT_ID!,
|
|
26
|
-
process.env.CLIENT_SECRET!,
|
|
27
|
-
'sandbox',
|
|
28
|
-
process.env.CERTIFICATE!,
|
|
29
|
-
process.env.PRIVATE_KEY!,
|
|
30
|
-
)
|
|
31
|
-
// @ts-ignore - refreshToken is a private method
|
|
32
|
-
await client.refreshToken()
|
|
33
|
-
const result = await client.verifyTin(
|
|
34
|
-
process.env.TIN_VALUE!,
|
|
35
|
-
'NRIC',
|
|
36
|
-
process.env.NRIC_VALUE!,
|
|
37
|
-
)
|
|
38
|
-
expect(result).toBe(true)
|
|
39
|
-
})
|
|
40
|
-
})
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
-
import { MyInvoisClient } from '../src'
|
|
3
|
-
|
|
4
|
-
// Mock global fetch
|
|
5
|
-
const mockFetch = vi.fn()
|
|
6
|
-
vi.stubGlobal('fetch', mockFetch)
|
|
7
|
-
|
|
8
|
-
vi.useFakeTimers()
|
|
9
|
-
|
|
10
|
-
describe('MyInvoisClient', () => {
|
|
11
|
-
let client: MyInvoisClient
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
vi.clearAllMocks()
|
|
15
|
-
client = new MyInvoisClient(
|
|
16
|
-
'test-id',
|
|
17
|
-
'test-secret',
|
|
18
|
-
'sandbox',
|
|
19
|
-
process.env.TEST_CERTIFICATE!,
|
|
20
|
-
process.env.TEST_PRIVATE_KEY!,
|
|
21
|
-
undefined,
|
|
22
|
-
true,
|
|
23
|
-
)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
describe('constructor', () => {
|
|
27
|
-
it('should set sandbox URL when environment is sandbox', () => {
|
|
28
|
-
const sandboxClient = new MyInvoisClient(
|
|
29
|
-
'test-id',
|
|
30
|
-
'test-secret',
|
|
31
|
-
'sandbox',
|
|
32
|
-
process.env.TEST_CERTIFICATE!,
|
|
33
|
-
process.env.TEST_PRIVATE_KEY!,
|
|
34
|
-
undefined,
|
|
35
|
-
true,
|
|
36
|
-
)
|
|
37
|
-
expect((sandboxClient as any).baseUrl).toBe(
|
|
38
|
-
'https://preprod-api.myinvois.hasil.gov.my',
|
|
39
|
-
)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('should set production URL when environment is production', () => {
|
|
43
|
-
const prodClient = new MyInvoisClient(
|
|
44
|
-
'test-id',
|
|
45
|
-
'test-secret',
|
|
46
|
-
'production',
|
|
47
|
-
process.env.TEST_CERTIFICATE!,
|
|
48
|
-
process.env.TEST_PRIVATE_KEY!,
|
|
49
|
-
undefined,
|
|
50
|
-
true,
|
|
51
|
-
)
|
|
52
|
-
expect((prodClient as any).baseUrl).toBe(
|
|
53
|
-
'https://api.myinvois.hasil.gov.my',
|
|
54
|
-
)
|
|
55
|
-
})
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
describe('token management', () => {
|
|
59
|
-
it('should get a new token if token does not exist', async () => {
|
|
60
|
-
const mockToken = {
|
|
61
|
-
access_token: 'test-token',
|
|
62
|
-
expires_in: 3600,
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
mockFetch.mockResolvedValueOnce({
|
|
66
|
-
ok: true,
|
|
67
|
-
json: () => Promise.resolve(mockToken),
|
|
68
|
-
} as Response)
|
|
69
|
-
|
|
70
|
-
await client.verifyTin('123', 'NRIC', '456')
|
|
71
|
-
|
|
72
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
73
|
-
'https://preprod-api.myinvois.hasil.gov.my/connect/token',
|
|
74
|
-
expect.any(Object),
|
|
75
|
-
)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('should get a new token when token is expired', async () => {
|
|
79
|
-
const mockToken = {
|
|
80
|
-
access_token: 'test-token',
|
|
81
|
-
expires_in: 3600,
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
mockFetch
|
|
85
|
-
.mockResolvedValueOnce({
|
|
86
|
-
ok: true,
|
|
87
|
-
json: () => Promise.resolve(mockToken),
|
|
88
|
-
} as Response)
|
|
89
|
-
.mockResolvedValueOnce({
|
|
90
|
-
ok: true,
|
|
91
|
-
json: () => Promise.resolve(undefined),
|
|
92
|
-
} as Response)
|
|
93
|
-
|
|
94
|
-
await client.verifyTin('123', 'NRIC', '456')
|
|
95
|
-
|
|
96
|
-
// Check first call (token request)
|
|
97
|
-
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
98
|
-
1,
|
|
99
|
-
'https://preprod-api.myinvois.hasil.gov.my/connect/token',
|
|
100
|
-
{
|
|
101
|
-
method: 'POST',
|
|
102
|
-
headers: {
|
|
103
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
104
|
-
},
|
|
105
|
-
body: new URLSearchParams({
|
|
106
|
-
grant_type: 'client_credentials',
|
|
107
|
-
client_id: 'test-id',
|
|
108
|
-
client_secret: 'test-secret',
|
|
109
|
-
scope: 'InvoicingAPI',
|
|
110
|
-
}),
|
|
111
|
-
},
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
// Check second call (verifyTin request)
|
|
115
|
-
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
116
|
-
2,
|
|
117
|
-
`https://preprod-api.myinvois.hasil.gov.my/api/v1.0/taxpayer/validate/123?idType=NRIC&idValue=456`,
|
|
118
|
-
{
|
|
119
|
-
method: 'GET',
|
|
120
|
-
headers: {
|
|
121
|
-
Authorization: `Bearer ${mockToken.access_token}`,
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('should reuse existing token if not expired', async () => {
|
|
128
|
-
const mockToken = {
|
|
129
|
-
access_token: 'test-token',
|
|
130
|
-
expires_in: 3600,
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
mockFetch.mockResolvedValueOnce({
|
|
134
|
-
ok: true,
|
|
135
|
-
json: () => Promise.resolve(mockToken),
|
|
136
|
-
} as Response)
|
|
137
|
-
|
|
138
|
-
// First call to get token
|
|
139
|
-
await client.verifyTin('123', 'NRIC', '456')
|
|
140
|
-
|
|
141
|
-
vi.setSystemTime(new Date(Date.now() + 1000))
|
|
142
|
-
|
|
143
|
-
// Second call should reuse token
|
|
144
|
-
await client.verifyTin('123', 'NRIC', '456')
|
|
145
|
-
|
|
146
|
-
// Token endpoint should only be called once
|
|
147
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
148
|
-
'https://preprod-api.myinvois.hasil.gov.my/connect/token',
|
|
149
|
-
expect.any(Object),
|
|
150
|
-
)
|
|
151
|
-
})
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
describe('verifyTin', () => {
|
|
155
|
-
it('should return true when verification succeeds', async () => {
|
|
156
|
-
const mockToken = {
|
|
157
|
-
access_token: 'test-token',
|
|
158
|
-
expires_in: 3600,
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
mockFetch
|
|
162
|
-
.mockResolvedValueOnce({
|
|
163
|
-
ok: true,
|
|
164
|
-
json: () => Promise.resolve(mockToken),
|
|
165
|
-
} as Response)
|
|
166
|
-
.mockResolvedValueOnce({
|
|
167
|
-
ok: true,
|
|
168
|
-
json: () => Promise.resolve(undefined),
|
|
169
|
-
} as Response)
|
|
170
|
-
|
|
171
|
-
await client.verifyTin('123', 'NRIC', '456')
|
|
172
|
-
vi.setSystemTime(new Date(Date.now() + 1000 * 8000))
|
|
173
|
-
await client.verifyTin('123', 'NRIC', '456')
|
|
174
|
-
|
|
175
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
176
|
-
`https://preprod-api.myinvois.hasil.gov.my/api/v1.0/taxpayer/validate/123?idType=NRIC&idValue=456`,
|
|
177
|
-
expect.objectContaining({
|
|
178
|
-
method: 'GET',
|
|
179
|
-
headers: {
|
|
180
|
-
Authorization: 'Bearer test-token',
|
|
181
|
-
},
|
|
182
|
-
}),
|
|
183
|
-
)
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
it('should return false when verification fails', async () => {
|
|
187
|
-
const mockToken = {
|
|
188
|
-
access_token: 'test-token',
|
|
189
|
-
expires_in: 3600,
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
mockFetch
|
|
193
|
-
.mockResolvedValueOnce({
|
|
194
|
-
ok: true,
|
|
195
|
-
json: () => Promise.resolve(mockToken),
|
|
196
|
-
} as Response)
|
|
197
|
-
.mockRejectedValueOnce(new Error('Invalid TIN'))
|
|
198
|
-
|
|
199
|
-
const result = await client.verifyTin('123', 'NRIC', '456')
|
|
200
|
-
|
|
201
|
-
expect(result).toBe(false)
|
|
202
|
-
})
|
|
203
|
-
})
|
|
204
|
-
})
|
package/test/base64.test.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { encodeToBase64 } from '../src/utils/base64' // Adjust path if necessary
|
|
3
|
-
|
|
4
|
-
describe('encodeToBase64', () => {
|
|
5
|
-
it('should correctly encode a simple JSON string', () => {
|
|
6
|
-
const jsonString = '{"key": "value"}'
|
|
7
|
-
const expectedBase64 = 'eyJrZXkiOiAidmFsdWUifQ==' // Buffer.from(jsonString).toString('base64')
|
|
8
|
-
expect(encodeToBase64(jsonString)).toBe(expectedBase64)
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
it('should correctly encode a simple XML string', () => {
|
|
12
|
-
const xmlString = '<root><element>value</element></root>'
|
|
13
|
-
const expectedBase64 =
|
|
14
|
-
'PHJvb3Q+PGVsZW1lbnQ+dmFsdWU8L2VsZW1lbnQ+PC9yb290Pg==' // Buffer.from(xmlString).toString('base64')
|
|
15
|
-
expect(encodeToBase64(xmlString)).toBe(expectedBase64)
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('should correctly encode an empty string', () => {
|
|
19
|
-
const emptyString = ''
|
|
20
|
-
const expectedBase64 = '' // Buffer.from(emptyString).toString('base64')
|
|
21
|
-
expect(encodeToBase64(emptyString)).toBe(expectedBase64)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('should correctly encode a string with special characters', () => {
|
|
25
|
-
const specialString = '!@#$%^&*()_+=-`~[]{}\\|;\'",./<>?'
|
|
26
|
-
const expectedBase64 = 'IUAjJCVeJiooKV8rPS1gfltde31cfDsnIiwuLzw+Pw==' // Corrected value
|
|
27
|
-
expect(encodeToBase64(specialString)).toBe(expectedBase64)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('should correctly encode a string with Unicode characters', () => {
|
|
31
|
-
const unicodeString = '你好世界🌍'
|
|
32
|
-
const expectedBase64 = '5L2g5aW95LiW55WM8J+MjQ==' // Corrected value
|
|
33
|
-
expect(encodeToBase64(unicodeString)).toBe(expectedBase64)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('should correctly encode a longer string', () => {
|
|
37
|
-
const longString =
|
|
38
|
-
'This is a longer string that needs to be encoded to Base64 to ensure it handles more than just a few characters correctly.'
|
|
39
|
-
const expectedBase64 =
|
|
40
|
-
'VGhpcyBpcyBhIGxvbmdlciBzdHJpbmcgdGhhdCBuZWVkcyB0byBiZSBlbmNvZGVkIHRvIEJhc2U2NCB0byBlbnN1cmUgaXQgaGFuZGxlcyBtb3JlIHRoYW4ganVzdCBhIGZldyBjaGFyYWN0ZXJzIGNvcnJlY3RseS4=' // Buffer.from(longString).toString('base64')
|
|
41
|
-
expect(encodeToBase64(longString)).toBe(expectedBase64)
|
|
42
|
-
})
|
|
43
|
-
})
|