@ripwords/myinvois-client 0.0.10 → 0.1.2
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/CHANGELOG.md +99 -0
- package/README.md +128 -4
- package/bun.lock +138 -122
- package/dist/0X-CG1S1WKn.d.ts +210 -0
- package/dist/0X-ChfmeAYF.d.cts +211 -0
- package/dist/1X-CmlbG02M.d.ts +111 -0
- package/dist/1X-DFikQbP0.d.cts +20 -0
- package/dist/1X-DbFshzup.d.ts +19 -0
- package/dist/1X-jFrFCOs0.d.cts +112 -0
- package/dist/2X-5cE4ojEX.d.cts +140 -0
- package/dist/2X-DA6J6Wd7.d.ts +139 -0
- package/dist/2X-FWZ1rYVt.d.cts +40 -0
- package/dist/2X-eC_p0QJf.d.ts +39 -0
- package/dist/3X-D9duw0Ro.d.cts +21 -0
- package/dist/3X-DbfkwdkO.d.ts +20 -0
- package/dist/3X-GxuN79Ax.d.ts +61 -0
- package/dist/3X-r9xbWJ2l.d.cts +62 -0
- package/dist/4X-1ZkK-uH5.d.cts +31 -0
- package/dist/4X-CLO9OuKa.d.cts +265 -0
- package/dist/4X-CnHtgO6u.d.ts +30 -0
- package/dist/4X-DYz_nYSD.d.ts +264 -0
- package/dist/5X-BzPEdkbC.d.ts +21 -0
- package/dist/5X-BziH0a09.d.cts +77 -0
- package/dist/5X-Cfe39fFc.d.cts +22 -0
- package/dist/5X-kBF74qx1.d.ts +76 -0
- package/dist/6X-BceErbps.d.cts +105 -0
- package/dist/6X-C_xpeu6J.d.ts +15 -0
- package/dist/6X-DLWEe1bP.d.cts +16 -0
- package/dist/6X-sQZfT0ZK.d.ts +104 -0
- package/dist/7X-Bet2agJ6.d.ts +15 -0
- package/dist/7X-BoKQxMoO.d.ts +70 -0
- package/dist/7X-CucHYQsA.d.cts +16 -0
- package/dist/7X-CwPAqozm.d.cts +71 -0
- package/dist/8X-BIjJ9kaV.d.cts +110 -0
- package/dist/8X-BOEWATU3.d.ts +109 -0
- package/dist/8X-CJpqd5xp.d.cts +19 -0
- package/dist/8X-s5AzkHR-.d.ts +18 -0
- package/dist/9X-BBDP1gZ6.d.ts +14 -0
- package/dist/9X-CP5Xp6bE.d.cts +98 -0
- package/dist/9X-D3aOVCnz.d.cts +15 -0
- package/dist/9X-DAAZV4sr.d.ts +97 -0
- package/dist/AX-CmpMMhII.d.ts +110 -0
- package/dist/AX-DFzpxGOT.d.cts +111 -0
- package/dist/BX-CyiqNE6b.d.cts +115 -0
- package/dist/BX-j78z_ju_.d.ts +114 -0
- package/dist/CX-B6rEhQx7.d.ts +127 -0
- package/dist/CX-BTPvC3Dv.d.cts +128 -0
- package/dist/DX-BD4-hTCh.d.cts +115 -0
- package/dist/DX-CAmMnx5a.d.ts +114 -0
- package/dist/EX-CXhSC85J.d.ts +106 -0
- package/dist/EX-mTHCH0Yk.d.cts +107 -0
- package/dist/FX-DOOMoQl2.d.cts +127 -0
- package/dist/FX-DYVIJ3l0.d.ts +126 -0
- package/dist/GX-8URJ7WHz.d.cts +136 -0
- package/dist/GX-eBOVHqi6.d.ts +135 -0
- package/dist/HX-49z8CzsE.d.cts +126 -0
- package/dist/HX-Dz5T0xXI.d.ts +125 -0
- package/dist/IX-B6kblmBs.d.cts +23 -0
- package/dist/IX-PiKYro5a.d.ts +22 -0
- package/dist/JX-DMwHLGEE.d.cts +104 -0
- package/dist/JX-Dvnt0mSK.d.ts +103 -0
- package/dist/KX-BUb8e0yg.d.ts +150 -0
- package/dist/KX-DaJPkefl.d.cts +151 -0
- package/dist/LX-BnILxjdS.d.cts +123 -0
- package/dist/LX-l5ZBaGov.d.ts +122 -0
- package/dist/MX-BbPO8mTj.d.cts +153 -0
- package/dist/MX-C7C15AWi.d.ts +152 -0
- package/dist/NX-C7nFVC-D.d.ts +121 -0
- package/dist/NX-xy5uw7H3.d.cts +122 -0
- package/dist/OX-CIzARbON.d.cts +26 -0
- package/dist/OX-Dmkvcaij.d.ts +25 -0
- package/dist/PX-BKZ14N7N.d.ts +120 -0
- package/dist/PX-CL8waFgx.d.cts +121 -0
- package/dist/QX-6xdSNtxZ.d.ts +58 -0
- package/dist/QX-BowAYqZb.d.cts +59 -0
- package/dist/RX-DFoiOK5q.d.ts +22 -0
- package/dist/RX-fWoWcHTG.d.cts +23 -0
- package/dist/SX-CZ3e31OT.d.cts +40 -0
- package/dist/SX-D-Szi9wa.d.ts +39 -0
- package/dist/TX-CJhJ-HYX.d.cts +31 -0
- package/dist/TX-DTfkGsqn.d.ts +30 -0
- package/dist/UX-Bw4HHAj4.d.ts +17 -0
- package/dist/UX-P7-u2y70.d.cts +18 -0
- package/dist/VX-C8Eu5gQf.d.cts +17 -0
- package/dist/VX-DCv5x_hi.d.ts +16 -0
- package/dist/WX-BYtJ5_if.d.ts +25 -0
- package/dist/WX-DkiaqB2V.d.cts +26 -0
- package/dist/XX-D4p4iK-a.d.cts +828 -0
- package/dist/XX-Jo1yJO3A.d.ts +827 -0
- package/dist/YX-BvGmEsVG.d.ts +19 -0
- package/dist/YX-Du8zv9qz.d.cts +20 -0
- package/dist/ZX-B4jFD-21.d.cts +22 -0
- package/dist/ZX-B59AJcBY.d.ts +21 -0
- package/dist/api/platform/platformLogin.d.ts +65 -0
- package/dist/api/platform/platformLogin.js +3 -0
- package/dist/certificate-DFK-788s.cjs +62 -0
- package/dist/certificate-DFK-788s.cjs.map +1 -0
- package/dist/certificate-aooIRf9A.js +49 -0
- package/dist/chunk-CUT6urMc.cjs +30 -0
- package/dist/classification-codes-B15PbWxz.d.cts +118 -0
- package/dist/classification-codes-BKxV-rO9.d.ts +117 -0
- package/dist/country-code-CQuaiQm2.d.ts +542 -0
- package/dist/country-code-DPeNFMMi.d.cts +543 -0
- package/dist/currencies-BYJK-m6v.d.ts +207 -0
- package/dist/currencies-S5g1gzBU.d.cts +208 -0
- package/dist/document-DMIMRJV0.cjs +472 -0
- package/dist/document-DMIMRJV0.cjs.map +1 -0
- package/dist/document-NQos5fSr.js +405 -0
- package/dist/documents-BVG7KIKY.d.ts +847 -0
- package/dist/documents-C066EC9m.d.cts +848 -0
- package/dist/e-invoice-BuwtFnlI.d.cts +44 -0
- package/dist/e-invoice-CmbLQkHw.d.ts +43 -0
- package/dist/getBaseUrl-CO7Jp27d.cjs +14 -0
- package/dist/getBaseUrl-CO7Jp27d.cjs.map +1 -0
- package/dist/getBaseUrl-R3IdgCu3.js +7 -0
- package/dist/index-0-EvC6Nv.d.ts +15 -0
- package/dist/index-D2_HVwCz.d.cts +16 -0
- package/dist/index.cjs +256 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +232 -5665
- package/dist/index.js +253 -46
- package/dist/index10.cjs +0 -0
- package/dist/index11.cjs +34 -0
- package/dist/index11.cjs.map +1 -0
- package/dist/index12.cjs +24 -0
- package/dist/index12.cjs.map +1 -0
- package/dist/index13.cjs +0 -0
- package/dist/index14.cjs +13 -0
- package/dist/index14.cjs.map +1 -0
- package/dist/index15.cjs +4 -0
- package/dist/index16.cjs +13 -0
- package/dist/index17.cjs +3 -0
- package/dist/index18.cjs +340 -0
- package/dist/index18.cjs.map +1 -0
- package/dist/index19.cjs +329 -0
- package/dist/index19.cjs.map +1 -0
- package/dist/index2.cjs +62 -0
- package/dist/index2.cjs.map +1 -0
- package/dist/index20.cjs +140 -0
- package/dist/index20.cjs.map +1 -0
- package/dist/index21.cjs +3 -0
- package/dist/index22.cjs +208 -0
- package/dist/index22.cjs.map +1 -0
- package/dist/index23.cjs +109 -0
- package/dist/index23.cjs.map +1 -0
- package/dist/index24.cjs +137 -0
- package/dist/index24.cjs.map +1 -0
- package/dist/index25.cjs +64 -0
- package/dist/index25.cjs.map +1 -0
- package/dist/index26.cjs +267 -0
- package/dist/index26.cjs.map +1 -0
- package/dist/index27.cjs +79 -0
- package/dist/index27.cjs.map +1 -0
- package/dist/index28.cjs +107 -0
- package/dist/index28.cjs.map +1 -0
- package/dist/index29.cjs +73 -0
- package/dist/index29.cjs.map +1 -0
- package/dist/index3.cjs +532 -0
- package/dist/index3.cjs.map +1 -0
- package/dist/index30.cjs +112 -0
- package/dist/index30.cjs.map +1 -0
- package/dist/index31.cjs +100 -0
- package/dist/index31.cjs.map +1 -0
- package/dist/index32.cjs +18 -0
- package/dist/index32.cjs.map +1 -0
- package/dist/index33.cjs +38 -0
- package/dist/index33.cjs.map +1 -0
- package/dist/index34.cjs +19 -0
- package/dist/index34.cjs.map +1 -0
- package/dist/index35.cjs +29 -0
- package/dist/index35.cjs.map +1 -0
- package/dist/index36.cjs +20 -0
- package/dist/index36.cjs.map +1 -0
- package/dist/index37.cjs +14 -0
- package/dist/index37.cjs.map +1 -0
- package/dist/index38.cjs +14 -0
- package/dist/index38.cjs.map +1 -0
- package/dist/index39.cjs +17 -0
- package/dist/index39.cjs.map +1 -0
- package/dist/index4.cjs +196 -0
- package/dist/index4.cjs.map +1 -0
- package/dist/index40.cjs +13 -0
- package/dist/index40.cjs.map +1 -0
- package/dist/index41.cjs +108 -0
- package/dist/index41.cjs.map +1 -0
- package/dist/index42.cjs +113 -0
- package/dist/index42.cjs.map +1 -0
- package/dist/index43.cjs +126 -0
- package/dist/index43.cjs.map +1 -0
- package/dist/index44.cjs +113 -0
- package/dist/index44.cjs.map +1 -0
- package/dist/index45.cjs +105 -0
- package/dist/index45.cjs.map +1 -0
- package/dist/index46.cjs +125 -0
- package/dist/index46.cjs.map +1 -0
- package/dist/index47.cjs +134 -0
- package/dist/index47.cjs.map +1 -0
- package/dist/index48.cjs +124 -0
- package/dist/index48.cjs.map +1 -0
- package/dist/index49.cjs +21 -0
- package/dist/index49.cjs.map +1 -0
- package/dist/index5.cjs +0 -0
- package/dist/index50.cjs +102 -0
- package/dist/index50.cjs.map +1 -0
- package/dist/index51.cjs +149 -0
- package/dist/index51.cjs.map +1 -0
- package/dist/index52.cjs +121 -0
- package/dist/index52.cjs.map +1 -0
- package/dist/index53.cjs +151 -0
- package/dist/index53.cjs.map +1 -0
- package/dist/index54.cjs +120 -0
- package/dist/index54.cjs.map +1 -0
- package/dist/index55.cjs +24 -0
- package/dist/index55.cjs.map +1 -0
- package/dist/index56.cjs +119 -0
- package/dist/index56.cjs.map +1 -0
- package/dist/index57.cjs +54 -0
- package/dist/index57.cjs.map +1 -0
- package/dist/index58.cjs +21 -0
- package/dist/index58.cjs.map +1 -0
- package/dist/index58.cts.map +1 -0
- package/dist/index59.cjs +35 -0
- package/dist/index59.cjs.map +1 -0
- package/dist/index59.cts.map +1 -0
- package/dist/index6.cjs +25 -0
- package/dist/index6.cjs.map +1 -0
- package/dist/index60.cjs +29 -0
- package/dist/index60.cjs.map +1 -0
- package/dist/index60.cts.map +1 -0
- package/dist/index61.cjs +16 -0
- package/dist/index61.cjs.map +1 -0
- package/dist/index61.cts.map +1 -0
- package/dist/index62.cjs +15 -0
- package/dist/index62.cjs.map +1 -0
- package/dist/index62.cts.map +1 -0
- package/dist/index63.cjs +24 -0
- package/dist/index63.cjs.map +1 -0
- package/dist/index63.cts.map +1 -0
- package/dist/index64.cjs +419 -0
- package/dist/index64.cjs.map +1 -0
- package/dist/index64.cts.map +1 -0
- package/dist/index65.cjs +15 -0
- package/dist/index65.cjs.map +1 -0
- package/dist/index65.cts.map +1 -0
- package/dist/index66.cjs +16 -0
- package/dist/index66.cjs.map +1 -0
- package/dist/index66.cts.map +1 -0
- package/dist/index7.cjs +0 -0
- package/dist/index8.cjs +0 -0
- package/dist/index9.cjs +25 -0
- package/dist/index9.cjs.map +1 -0
- package/dist/msic-codes-CPWVNnpq.d.cts +26 -0
- package/dist/msic-codes-DToUqTI6.d.ts +25 -0
- package/dist/payment-modes-DA7uBIGb.d.ts +43 -0
- package/dist/payment-modes-FX88uyhP.d.cts +44 -0
- package/dist/platformLogin-PGzMhw1X.cjs +37 -0
- package/dist/platformLogin-PGzMhw1X.cjs.map +1 -0
- package/dist/platformLogin-f0bNAoZI.js +30 -0
- package/dist/signatures-BKi9DP2K.d.cts +173 -0
- package/dist/signatures-CG175mpg.d.ts +172 -0
- package/dist/state-codes-CNp_6051.d.cts +62 -0
- package/dist/state-codes-dvoTe5pC.d.ts +61 -0
- package/dist/tax-types-CgwxONDS.d.cts +42 -0
- package/dist/tax-types-tosH5I0q.d.ts +41 -0
- package/dist/types/classification-codes.d.ts +2 -0
- package/dist/types/country-code.d.ts +2 -0
- package/dist/types/currencies.d.ts +2 -0
- package/dist/types/documents.d.ts +7 -0
- package/dist/types/e-invoice.d.ts +2 -0
- package/dist/types/index.d.ts +58 -0
- package/dist/types/msic/0X.d.ts +2 -0
- package/dist/types/msic/1X.d.ts +2 -0
- package/dist/types/msic/2X.d.ts +2 -0
- package/dist/types/msic/3X.d.ts +2 -0
- package/dist/types/msic/4X.d.ts +2 -0
- package/dist/types/msic/5X.d.ts +2 -0
- package/dist/types/msic/6X.d.ts +2 -0
- package/dist/types/msic/7X.d.ts +2 -0
- package/dist/types/msic/8X.d.ts +2 -0
- package/dist/types/msic/9X.d.ts +2 -0
- package/dist/types/msic-codes.d.ts +12 -0
- package/dist/types/payment-modes.d.ts +2 -0
- package/dist/types/signatures.d.ts +2 -0
- package/dist/types/state-codes.d.ts +2 -0
- package/dist/types/tax-types.d.ts +2 -0
- package/dist/types/unit/1X.d.ts +2 -0
- package/dist/types/unit/2X.d.ts +2 -0
- package/dist/types/unit/3X.d.ts +2 -0
- package/dist/types/unit/4X.d.ts +2 -0
- package/dist/types/unit/5X.d.ts +2 -0
- package/dist/types/unit/6X.d.ts +2 -0
- package/dist/types/unit/7X.d.ts +2 -0
- package/dist/types/unit/8X.d.ts +2 -0
- package/dist/types/unit/9X.d.ts +2 -0
- package/dist/types/unit/AX.d.ts +2 -0
- package/dist/types/unit/BX.d.ts +2 -0
- package/dist/types/unit/CX.d.ts +2 -0
- package/dist/types/unit/DX.d.ts +2 -0
- package/dist/types/unit/EX.d.ts +2 -0
- package/dist/types/unit/FX.d.ts +2 -0
- package/dist/types/unit/GX.d.ts +2 -0
- package/dist/types/unit/HX.d.ts +2 -0
- package/dist/types/unit/IX.d.ts +2 -0
- package/dist/types/unit/JX.d.ts +2 -0
- package/dist/types/unit/KX.d.ts +2 -0
- package/dist/types/unit/LX.d.ts +2 -0
- package/dist/types/unit/MX.d.ts +2 -0
- package/dist/types/unit/NX.d.ts +2 -0
- package/dist/types/unit/OX.d.ts +2 -0
- package/dist/types/unit/PX.d.ts +2 -0
- package/dist/types/unit/QX.d.ts +2 -0
- package/dist/types/unit/RX.d.ts +2 -0
- package/dist/types/unit/SX.d.ts +2 -0
- package/dist/types/unit/TX.d.ts +2 -0
- package/dist/types/unit/UX.d.ts +2 -0
- package/dist/types/unit/VX.d.ts +2 -0
- package/dist/types/unit/WX.d.ts +2 -0
- package/dist/types/unit/XX.d.ts +2 -0
- package/dist/types/unit/YX.d.ts +2 -0
- package/dist/types/unit/ZX.d.ts +2 -0
- package/dist/types/unit-types.d.ts +37 -0
- package/dist/unit-types-BKZxwVYQ.d.ts +55 -0
- package/dist/unit-types-CaOA5ZDG.d.cts +56 -0
- package/dist/utils/base64.d.ts +5 -0
- package/dist/utils/base64.js +10 -0
- package/dist/utils/certificate.d.ts +22 -0
- package/dist/utils/certificate.js +3 -0
- package/dist/utils/document.d.ts +135 -0
- package/dist/utils/document.js +3 -0
- package/dist/utils/getBaseUrl.d.ts +4 -0
- package/dist/utils/getBaseUrl.js +3 -0
- package/dist/utils/helpers.d.ts +271 -0
- package/dist/utils/helpers.js +330 -0
- package/dist/utils/signature-diagnostics.d.ts +93 -0
- package/dist/utils/signature-diagnostics.js +326 -0
- package/dist/utils/validation.d.ts +46 -0
- package/dist/utils/validation.js +134 -0
- package/myinvois-cert.conf.template +23 -0
- package/package.json +20 -7
- package/scripts/gen-cert.sh +159 -0
- package/src/api/platform/platformLogin.ts +7 -2
- package/src/index.ts +528 -1
- package/src/types/documents.d.ts +862 -0
- package/src/types/index.d.ts +1 -1
- package/src/types/msic-codes.d.ts +1 -7
- package/src/utils/base64.ts +7 -0
- package/src/utils/certificate.ts +60 -0
- package/src/utils/document.ts +852 -0
- package/src/utils/getBaseUrl.ts +1 -1
- package/src/utils/helpers.ts +552 -0
- package/src/utils/signature-diagnostics.ts +583 -0
- package/src/utils/validation.ts +268 -0
- package/test/MyInvoiClientWithRealData.test.ts +11 -2
- package/test/MyInvoisClient.test.ts +23 -9
- package/test/base64.test.ts +43 -0
- package/test/dynamicInvoiceFeatures.test.ts +449 -0
- package/test/signAndSubmitInvoice.test.ts +450 -0
- package/test/signature-diagnostics.test.ts +128 -0
- package/tsconfig.json +4 -1
- package/tsdown.config.ts +31 -0
- package/rolldown.config.ts +0 -26
- package/src/types/documents/index.d.ts +0 -1
- package/src/types/documents/invoice-1_1.d.ts +0 -349
- package/src/utils/MyInvoisClient.ts +0 -88
|
@@ -0,0 +1,268 @@
|
|
|
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,5 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { MyInvoisClient } from '../src
|
|
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
|
+
*/
|
|
3
9
|
|
|
4
10
|
describe('MyInvoisClientWithRealData', () => {
|
|
5
11
|
it('should verify TIN with real data', async () => {
|
|
@@ -19,11 +25,14 @@ describe('MyInvoisClientWithRealData', () => {
|
|
|
19
25
|
process.env.CLIENT_ID!,
|
|
20
26
|
process.env.CLIENT_SECRET!,
|
|
21
27
|
'sandbox',
|
|
28
|
+
process.env.CERTIFICATE!,
|
|
29
|
+
process.env.PRIVATE_KEY!,
|
|
22
30
|
)
|
|
23
|
-
// @ts-ignore
|
|
31
|
+
// @ts-ignore - refreshToken is a private method
|
|
24
32
|
await client.refreshToken()
|
|
25
33
|
const result = await client.verifyTin(
|
|
26
34
|
process.env.TIN_VALUE!,
|
|
35
|
+
'NRIC',
|
|
27
36
|
process.env.NRIC_VALUE!,
|
|
28
37
|
)
|
|
29
38
|
expect(result).toBe(true)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
-
import { MyInvoisClient } from '../src
|
|
2
|
+
import { MyInvoisClient } from '../src'
|
|
3
3
|
|
|
4
4
|
// Mock global fetch
|
|
5
5
|
const mockFetch = vi.fn()
|
|
@@ -12,7 +12,15 @@ describe('MyInvoisClient', () => {
|
|
|
12
12
|
|
|
13
13
|
beforeEach(() => {
|
|
14
14
|
vi.clearAllMocks()
|
|
15
|
-
client = new MyInvoisClient(
|
|
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
|
+
)
|
|
16
24
|
})
|
|
17
25
|
|
|
18
26
|
describe('constructor', () => {
|
|
@@ -21,6 +29,9 @@ describe('MyInvoisClient', () => {
|
|
|
21
29
|
'test-id',
|
|
22
30
|
'test-secret',
|
|
23
31
|
'sandbox',
|
|
32
|
+
process.env.TEST_CERTIFICATE!,
|
|
33
|
+
process.env.TEST_PRIVATE_KEY!,
|
|
34
|
+
undefined,
|
|
24
35
|
true,
|
|
25
36
|
)
|
|
26
37
|
expect((sandboxClient as any).baseUrl).toBe(
|
|
@@ -33,6 +44,9 @@ describe('MyInvoisClient', () => {
|
|
|
33
44
|
'test-id',
|
|
34
45
|
'test-secret',
|
|
35
46
|
'production',
|
|
47
|
+
process.env.TEST_CERTIFICATE!,
|
|
48
|
+
process.env.TEST_PRIVATE_KEY!,
|
|
49
|
+
undefined,
|
|
36
50
|
true,
|
|
37
51
|
)
|
|
38
52
|
expect((prodClient as any).baseUrl).toBe(
|
|
@@ -53,7 +67,7 @@ describe('MyInvoisClient', () => {
|
|
|
53
67
|
json: () => Promise.resolve(mockToken),
|
|
54
68
|
} as Response)
|
|
55
69
|
|
|
56
|
-
await client.verifyTin('123', '456')
|
|
70
|
+
await client.verifyTin('123', 'NRIC', '456')
|
|
57
71
|
|
|
58
72
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
59
73
|
'https://preprod-api.myinvois.hasil.gov.my/connect/token',
|
|
@@ -77,7 +91,7 @@ describe('MyInvoisClient', () => {
|
|
|
77
91
|
json: () => Promise.resolve(undefined),
|
|
78
92
|
} as Response)
|
|
79
93
|
|
|
80
|
-
await client.verifyTin('123', '456')
|
|
94
|
+
await client.verifyTin('123', 'NRIC', '456')
|
|
81
95
|
|
|
82
96
|
// Check first call (token request)
|
|
83
97
|
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
@@ -122,12 +136,12 @@ describe('MyInvoisClient', () => {
|
|
|
122
136
|
} as Response)
|
|
123
137
|
|
|
124
138
|
// First call to get token
|
|
125
|
-
await client.verifyTin('123', '456')
|
|
139
|
+
await client.verifyTin('123', 'NRIC', '456')
|
|
126
140
|
|
|
127
141
|
vi.setSystemTime(new Date(Date.now() + 1000))
|
|
128
142
|
|
|
129
143
|
// Second call should reuse token
|
|
130
|
-
await client.verifyTin('123', '456')
|
|
144
|
+
await client.verifyTin('123', 'NRIC', '456')
|
|
131
145
|
|
|
132
146
|
// Token endpoint should only be called once
|
|
133
147
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
@@ -154,9 +168,9 @@ describe('MyInvoisClient', () => {
|
|
|
154
168
|
json: () => Promise.resolve(undefined),
|
|
155
169
|
} as Response)
|
|
156
170
|
|
|
157
|
-
await client.verifyTin('123', '456')
|
|
171
|
+
await client.verifyTin('123', 'NRIC', '456')
|
|
158
172
|
vi.setSystemTime(new Date(Date.now() + 1000 * 8000))
|
|
159
|
-
await client.verifyTin('123', '456')
|
|
173
|
+
await client.verifyTin('123', 'NRIC', '456')
|
|
160
174
|
|
|
161
175
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
162
176
|
`https://preprod-api.myinvois.hasil.gov.my/api/v1.0/taxpayer/validate/123?idType=NRIC&idValue=456`,
|
|
@@ -182,7 +196,7 @@ describe('MyInvoisClient', () => {
|
|
|
182
196
|
} as Response)
|
|
183
197
|
.mockRejectedValueOnce(new Error('Invalid TIN'))
|
|
184
198
|
|
|
185
|
-
const result = await client.verifyTin('123', '456')
|
|
199
|
+
const result = await client.verifyTin('123', 'NRIC', '456')
|
|
186
200
|
|
|
187
201
|
expect(result).toBe(false)
|
|
188
202
|
})
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
})
|