@ripwords/myinvois-client 0.1.6 → 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.
Files changed (81) hide show
  1. package/package.json +5 -8
  2. package/.prettierrc +0 -8
  3. package/CHANGELOG.md +0 -152
  4. package/bun.lock +0 -460
  5. package/myinvois-cert.conf.template +0 -23
  6. package/scripts/gen-cert.sh +0 -159
  7. package/src/api/platform/platformLogin.ts +0 -34
  8. package/src/index.ts +0 -530
  9. package/src/types/classification-codes.d.ts +0 -115
  10. package/src/types/country-code.d.ts +0 -790
  11. package/src/types/currencies.d.ts +0 -383
  12. package/src/types/documents.d.ts +0 -869
  13. package/src/types/e-invoice.d.ts +0 -41
  14. package/src/types/index.d.ts +0 -24
  15. package/src/types/msic/0X.d.ts +0 -408
  16. package/src/types/msic/1X.d.ts +0 -210
  17. package/src/types/msic/2X.d.ts +0 -266
  18. package/src/types/msic/3X.d.ts +0 -114
  19. package/src/types/msic/4X.d.ts +0 -520
  20. package/src/types/msic/5X.d.ts +0 -144
  21. package/src/types/msic/6X.d.ts +0 -200
  22. package/src/types/msic/7X.d.ts +0 -132
  23. package/src/types/msic/8X.d.ts +0 -210
  24. package/src/types/msic/9X.d.ts +0 -186
  25. package/src/types/msic-codes.d.ts +0 -31
  26. package/src/types/payment-modes.d.ts +0 -41
  27. package/src/types/signatures.d.ts +0 -169
  28. package/src/types/state-codes.d.ts +0 -59
  29. package/src/types/tax-types.d.ts +0 -39
  30. package/src/types/unit/1X.d.ts +0 -16
  31. package/src/types/unit/2X.d.ts +0 -62
  32. package/src/types/unit/3X.d.ts +0 -17
  33. package/src/types/unit/4X.d.ts +0 -44
  34. package/src/types/unit/5X.d.ts +0 -26
  35. package/src/types/unit/6X.d.ts +0 -12
  36. package/src/types/unit/7X.d.ts +0 -12
  37. package/src/types/unit/8X.d.ts +0 -15
  38. package/src/types/unit/9X.d.ts +0 -11
  39. package/src/types/unit/AX.d.ts +0 -202
  40. package/src/types/unit/BX.d.ts +0 -212
  41. package/src/types/unit/CX.d.ts +0 -238
  42. package/src/types/unit/DX.d.ts +0 -212
  43. package/src/types/unit/EX.d.ts +0 -196
  44. package/src/types/unit/FX.d.ts +0 -236
  45. package/src/types/unit/GX.d.ts +0 -254
  46. package/src/types/unit/HX.d.ts +0 -234
  47. package/src/types/unit/IX.d.ts +0 -28
  48. package/src/types/unit/JX.d.ts +0 -190
  49. package/src/types/unit/KX.d.ts +0 -284
  50. package/src/types/unit/LX.d.ts +0 -228
  51. package/src/types/unit/MX.d.ts +0 -288
  52. package/src/types/unit/NX.d.ts +0 -226
  53. package/src/types/unit/OX.d.ts +0 -34
  54. package/src/types/unit/PX.d.ts +0 -224
  55. package/src/types/unit/QX.d.ts +0 -94
  56. package/src/types/unit/RX.d.ts +0 -28
  57. package/src/types/unit/SX.d.ts +0 -56
  58. package/src/types/unit/TX.d.ts +0 -44
  59. package/src/types/unit/UX.d.ts +0 -14
  60. package/src/types/unit/VX.d.ts +0 -13
  61. package/src/types/unit/WX.d.ts +0 -34
  62. package/src/types/unit/XX.d.ts +0 -825
  63. package/src/types/unit/YX.d.ts +0 -17
  64. package/src/types/unit/ZX.d.ts +0 -19
  65. package/src/types/unit-types.d.ts +0 -86
  66. package/src/utils/base64.ts +0 -7
  67. package/src/utils/certificate.ts +0 -60
  68. package/src/utils/document.ts +0 -852
  69. package/src/utils/getBaseUrl.ts +0 -5
  70. package/src/utils/helpers.ts +0 -552
  71. package/src/utils/signature-diagnostics.ts +0 -583
  72. package/src/utils/validation.ts +0 -268
  73. package/test/MyInvoiClientWithRealData.test.ts +0 -40
  74. package/test/MyInvoisClient.test.ts +0 -204
  75. package/test/base64.test.ts +0 -43
  76. package/test/dynamicInvoiceFeatures.test.ts +0 -451
  77. package/test/signAndSubmitInvoice.test.ts +0 -452
  78. package/test/signature-diagnostics.test.ts +0 -130
  79. package/tsconfig.json +0 -39
  80. package/tsdown.config.ts +0 -31
  81. package/vitest.config.ts +0 -8
@@ -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
- })
@@ -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
- })