finprim 0.1.1 → 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/README.md CHANGED
@@ -20,10 +20,15 @@ finprim is the open source version of what your team has already written three t
20
20
  - ✅ UK sort code and account number validation
21
21
  - ✅ BIC/SWIFT validation
22
22
  - ✅ Card number validation (Luhn, network detection, formatting)
23
+ - ✅ EU VAT number format validation (member states)
24
+ - ✅ US ABA routing number validation
25
+ - ✅ Loan/EMI calculation and schedule
26
+ - ✅ Format-only helpers (IBAN, sort code, account number) for display
23
27
  - ✅ Currency validation and formatting with locale support
24
28
  - ✅ Branded types for compile-time correctness
25
29
  - ✅ Zod schemas out of the box
26
30
  - ✅ Optional React hooks for form inputs
31
+ - ✅ Optional NestJS validation pipes
27
32
  - ✅ Zero dependencies at the core
28
33
  - ✅ Tree-shakeable ESM and CJS builds
29
34
  - ✅ Fully typed
@@ -56,6 +61,13 @@ import {
56
61
  validateBIC,
57
62
  validateCardNumber,
58
63
  validateCurrencyCode,
64
+ validateEUVAT,
65
+ validateUSRoutingNumber,
66
+ formatIBAN,
67
+ formatSortCode,
68
+ formatUKAccountNumber,
69
+ calculateEMI,
70
+ getLoanSchedule,
59
71
  } from 'finprim'
60
72
 
61
73
  const iban = validateIBAN('GB29NWBK60161331926819')
@@ -69,6 +81,19 @@ const account = validateUKAccountNumber('31926819')
69
81
 
70
82
  const card = validateCardNumber('4532015112830366')
71
83
  // { valid: true, value: '...', formatted: '4532 0151 1283 0366', network: 'Visa', last4: '0366' }
84
+
85
+ const vat = validateEUVAT('DE123456789')
86
+ // { valid: true, value: 'DE123456789', formatted: 'DE 123456789', countryCode: 'DE' }
87
+
88
+ const routing = validateUSRoutingNumber('021000021')
89
+ // { valid: true, value: '021000021', formatted: '021000021' }
90
+
91
+ formatIBAN('GB29NWBK60161331926819') // 'GB29 NWBK 6016 1331 9268 19'
92
+ formatSortCode('601613') // '60-16-13'
93
+ formatUKAccountNumber('31926819') // '3192 6819'
94
+
95
+ const emi = calculateEMI(100_000, 10, 12) // monthly payment
96
+ const schedule = getLoanSchedule(100_000, 10, 12) // array of { month, payment, principal, interest, balance }
72
97
  ```
73
98
 
74
99
  ### Currency Formatting
@@ -101,7 +126,7 @@ if (iban.valid) {
101
126
  ### Zod Schemas
102
127
 
103
128
  ```ts
104
- import { ibanSchema, sortCodeSchema, accountNumberSchema, currencySchema } from 'finprim/zod'
129
+ import { ibanSchema, sortCodeSchema, accountNumberSchema, currencySchema, vatSchema, routingNumberSchema } from 'finprim/zod'
105
130
 
106
131
  const PaymentSchema = z.object({
107
132
  iban: ibanSchema,
@@ -132,6 +157,20 @@ function PaymentForm() {
132
157
  }
133
158
  ```
134
159
 
160
+ ### NestJS Pipes
161
+
162
+ ```ts
163
+ import { IbanValidationPipe, SortCodeValidationPipe, createValidationPipe } from 'finprim/nest'
164
+ import { validateIBAN } from 'finprim'
165
+
166
+ @Get('iban/:iban')
167
+ findByIban(@Param('iban', IbanValidationPipe) iban: string) {
168
+ return this.service.findByIban(iban)
169
+ }
170
+
171
+ const MyPipe = createValidationPipe(validateIBAN)
172
+ ```
173
+
135
174
  ---
136
175
 
137
176
  ## API Reference
@@ -146,8 +185,25 @@ function PaymentForm() {
146
185
  | `validateCurrencyCode(input)` | `string` | `ValidationResult<CurrencyCode>` |
147
186
  | `validateBIC(input)` | `string` | `ValidationResult<BIC>` |
148
187
  | `validateCardNumber(input)` | `string` | `CardValidationResult` (includes `network`, `last4` when valid) |
188
+ | `validateEUVAT(input)` | `string` | `VATValidationResult` (includes `countryCode` when valid) |
189
+ | `validateUSRoutingNumber(input)` | `string` | `ValidationResult<RoutingNumber>` |
149
190
 
150
- ### Formatting
191
+ ### Formatting & display
192
+
193
+ | Function | Input | Returns |
194
+ |----------|-------|---------|
195
+ | `formatIBAN(input)` | `string` | `string` (space-separated, no validation) |
196
+ | `formatSortCode(input)` | `string` | `string` (XX-XX-XX) |
197
+ | `formatUKAccountNumber(input)` | `string` | `string` (XXXX XXXX) |
198
+
199
+ ### Loan
200
+
201
+ | Function | Input | Returns |
202
+ |----------|-------|---------|
203
+ | `calculateEMI(principal, annualRatePercent, months)` | `number`, `number`, `number` | `number` |
204
+ | `getLoanSchedule(principal, annualRatePercent, months)` | `number`, `number`, `number` | `LoanScheduleEntry[]` |
205
+
206
+ ### Formatting (currency)
151
207
 
152
208
  | Function | Input | Returns |
153
209
  |----------|-------|---------|
@@ -165,6 +221,7 @@ Validation results include a `formatted` string when valid (e.g. IBAN and card n
165
221
  | `finprim` | Core validators and formatters | none |
166
222
  | `finprim/zod` | Zod schemas | `zod` |
167
223
  | `finprim/react` | React hooks | `react` |
224
+ | `finprim/nest` | NestJS validation pipes | `@nestjs/common` |
168
225
 
169
226
  ---
170
227
 
@@ -182,8 +239,11 @@ Validation results include a `formatted` string when valid (e.g. IBAN and card n
182
239
 
183
240
  - [x] SWIFT / BIC validation
184
241
  - [x] Luhn algorithm for card number validation
185
- - [ ] EU VAT number validation
186
- - [ ] NestJS pipe integration
242
+ - [x] EU VAT number validation
243
+ - [x] NestJS pipe integration
244
+ - [x] US routing number validation
245
+ - [x] Loan/EMI calculation
246
+ - [x] Format-only helpers
187
247
  - [ ] More locale coverage
188
248
 
189
249
  ---
@@ -202,6 +262,15 @@ npm run dev
202
262
 
203
263
  ---
204
264
 
265
+ ## Security
266
+
267
+ - **Input length**: All string validators reject input longer than 256 characters to limit memory and CPU use.
268
+ - **Type checking**: Validators require non-empty strings; numeric helpers (e.g. loan/currency) require finite numbers and sane bounds.
269
+ - **No sensitive logging**: The library does not log or persist input; use it in a way that avoids logging full card or account numbers.
270
+ - **Format helpers**: `formatIBAN`, `formatSortCode`, and `formatUKAccountNumber` cap input length and accept only strings to avoid abuse.
271
+
272
+ ---
273
+
205
274
  ## License
206
275
 
207
276
  MIT
@@ -8,6 +8,8 @@ type AccountNumber = Brand<string, 'AccountNumber'>;
8
8
  type CurrencyCode = Brand<string, 'CurrencyCode'>;
9
9
  type BIC = Brand<string, 'BIC'>;
10
10
  type CardNumber = Brand<string, 'CardNumber'>;
11
+ type VATNumber = Brand<string, 'VATNumber'>;
12
+ type RoutingNumber = Brand<string, 'RoutingNumber'>;
11
13
  type SupportedCurrency = 'GBP' | 'EUR' | 'USD' | 'JPY' | 'CHF' | 'CAD' | 'AUD' | 'NZD';
12
14
  type ValidationSuccess<T> = {
13
15
  valid: true;
@@ -20,6 +22,7 @@ type ValidationFailure = {
20
22
  };
21
23
  type ValidationResult<T> = ValidationSuccess<T> | ValidationFailure;
22
24
  declare function isValidationSuccess<T>(result: ValidationResult<T>): result is ValidationSuccess<T>;
25
+ declare function isValidationFailure<T>(result: ValidationResult<T>): result is ValidationFailure;
23
26
  type IBANValidationSuccess = ValidationSuccess<IBAN> & {
24
27
  countryCode: string;
25
28
  };
@@ -33,6 +36,10 @@ type MoneyResult = {
33
36
  valid: false;
34
37
  error: string;
35
38
  };
39
+ type VATValidationSuccess = ValidationSuccess<VATNumber> & {
40
+ countryCode: string;
41
+ };
42
+ type VATValidationResult = VATValidationSuccess | ValidationFailure;
36
43
 
37
44
  type CardNetwork = 'Visa' | 'Mastercard' | 'Amex' | 'Discover' | 'Unknown';
38
45
  type CardValidationResult = {
@@ -47,4 +54,4 @@ type CardValidationResult = {
47
54
  };
48
55
  declare function validateCardNumber(input: string): CardValidationResult;
49
56
 
50
- export { type AccountNumber as A, type BIC as B, type CurrencyCode as C, type IBANValidationResult as I, type MoneyResult as M, type SortCode as S, type ValidationResult as V, type SupportedCurrency as a, type CardNetwork as b, type CardNumber as c, type CardValidationResult as d, type IBAN as e, type IBANValidationSuccess as f, type ValidationFailure as g, type ValidationSuccess as h, isValidationSuccess as i, validateCardNumber as v };
57
+ export { type AccountNumber as A, type BIC as B, type CurrencyCode as C, type IBANValidationResult as I, type MoneyResult as M, type RoutingNumber as R, type SortCode as S, type ValidationResult as V, type SupportedCurrency as a, type VATValidationResult as b, type CardNetwork as c, type CardNumber as d, type CardValidationResult as e, type IBAN as f, type IBANValidationSuccess as g, type VATNumber as h, type VATValidationSuccess as i, type ValidationFailure as j, type ValidationSuccess as k, isValidationFailure as l, isValidationSuccess as m, validateCardNumber as v };
@@ -8,6 +8,8 @@ type AccountNumber = Brand<string, 'AccountNumber'>;
8
8
  type CurrencyCode = Brand<string, 'CurrencyCode'>;
9
9
  type BIC = Brand<string, 'BIC'>;
10
10
  type CardNumber = Brand<string, 'CardNumber'>;
11
+ type VATNumber = Brand<string, 'VATNumber'>;
12
+ type RoutingNumber = Brand<string, 'RoutingNumber'>;
11
13
  type SupportedCurrency = 'GBP' | 'EUR' | 'USD' | 'JPY' | 'CHF' | 'CAD' | 'AUD' | 'NZD';
12
14
  type ValidationSuccess<T> = {
13
15
  valid: true;
@@ -20,6 +22,7 @@ type ValidationFailure = {
20
22
  };
21
23
  type ValidationResult<T> = ValidationSuccess<T> | ValidationFailure;
22
24
  declare function isValidationSuccess<T>(result: ValidationResult<T>): result is ValidationSuccess<T>;
25
+ declare function isValidationFailure<T>(result: ValidationResult<T>): result is ValidationFailure;
23
26
  type IBANValidationSuccess = ValidationSuccess<IBAN> & {
24
27
  countryCode: string;
25
28
  };
@@ -33,6 +36,10 @@ type MoneyResult = {
33
36
  valid: false;
34
37
  error: string;
35
38
  };
39
+ type VATValidationSuccess = ValidationSuccess<VATNumber> & {
40
+ countryCode: string;
41
+ };
42
+ type VATValidationResult = VATValidationSuccess | ValidationFailure;
36
43
 
37
44
  type CardNetwork = 'Visa' | 'Mastercard' | 'Amex' | 'Discover' | 'Unknown';
38
45
  type CardValidationResult = {
@@ -47,4 +54,4 @@ type CardValidationResult = {
47
54
  };
48
55
  declare function validateCardNumber(input: string): CardValidationResult;
49
56
 
50
- export { type AccountNumber as A, type BIC as B, type CurrencyCode as C, type IBANValidationResult as I, type MoneyResult as M, type SortCode as S, type ValidationResult as V, type SupportedCurrency as a, type CardNetwork as b, type CardNumber as c, type CardValidationResult as d, type IBAN as e, type IBANValidationSuccess as f, type ValidationFailure as g, type ValidationSuccess as h, isValidationSuccess as i, validateCardNumber as v };
57
+ export { type AccountNumber as A, type BIC as B, type CurrencyCode as C, type IBANValidationResult as I, type MoneyResult as M, type RoutingNumber as R, type SortCode as S, type ValidationResult as V, type SupportedCurrency as a, type VATValidationResult as b, type CardNetwork as c, type CardNumber as d, type CardValidationResult as e, type IBAN as f, type IBANValidationSuccess as g, type VATNumber as h, type VATValidationSuccess as i, type ValidationFailure as j, type ValidationSuccess as k, isValidationFailure as l, isValidationSuccess as m, validateCardNumber as v };
package/dist/index.d.mts CHANGED
@@ -1,8 +1,11 @@
1
- import { I as IBANValidationResult, V as ValidationResult, A as AccountNumber, S as SortCode, a as SupportedCurrency, M as MoneyResult, C as CurrencyCode, B as BIC } from './card-D3WC2rzV.mjs';
2
- export { b as CardNetwork, c as CardNumber, d as CardValidationResult, e as IBAN, f as IBANValidationSuccess, g as ValidationFailure, h as ValidationSuccess, i as isValidationSuccess, v as validateCardNumber } from './card-D3WC2rzV.mjs';
1
+ import { I as IBANValidationResult, V as ValidationResult, A as AccountNumber, S as SortCode, a as SupportedCurrency, M as MoneyResult, C as CurrencyCode, B as BIC, b as VATValidationResult, R as RoutingNumber } from './card-D2-7wbam.mjs';
2
+ export { c as CardNetwork, d as CardNumber, e as CardValidationResult, f as IBAN, g as IBANValidationSuccess, h as VATNumber, i as VATValidationSuccess, j as ValidationFailure, k as ValidationSuccess, l as isValidationFailure, m as isValidationSuccess, v as validateCardNumber } from './card-D2-7wbam.mjs';
3
3
 
4
+ declare function formatIBAN(input: string): string;
4
5
  declare function validateIBAN(input: string): IBANValidationResult;
5
6
 
7
+ declare function formatSortCode(input: string): string;
8
+ declare function formatUKAccountNumber(input: string): string;
6
9
  declare function validateUKSortCode(input: string): ValidationResult<SortCode>;
7
10
  declare function validateUKAccountNumber(input: string): ValidationResult<AccountNumber>;
8
11
 
@@ -13,4 +16,18 @@ declare function parseMoney(input: string): MoneyResult;
13
16
 
14
17
  declare function validateBIC(input: string): ValidationResult<BIC>;
15
18
 
16
- export { AccountNumber, BIC, CurrencyCode, IBANValidationResult, MoneyResult, SUPPORTED_CURRENCIES, SortCode, SupportedCurrency, ValidationResult, formatCurrency, parseMoney, validateBIC, validateCurrencyCode, validateIBAN, validateUKAccountNumber, validateUKSortCode };
19
+ declare function validateEUVAT(input: string): VATValidationResult;
20
+
21
+ declare function validateUSRoutingNumber(input: string): ValidationResult<RoutingNumber>;
22
+
23
+ type LoanScheduleEntry = {
24
+ month: number;
25
+ payment: number;
26
+ principal: number;
27
+ interest: number;
28
+ balance: number;
29
+ };
30
+ declare function calculateEMI(principal: number, annualRatePercent: number, months: number): number;
31
+ declare function getLoanSchedule(principal: number, annualRatePercent: number, months: number): LoanScheduleEntry[];
32
+
33
+ export { AccountNumber, BIC, CurrencyCode, IBANValidationResult, type LoanScheduleEntry, MoneyResult, RoutingNumber, SUPPORTED_CURRENCIES, SortCode, SupportedCurrency, VATValidationResult, ValidationResult, calculateEMI, formatCurrency, formatIBAN, formatSortCode, formatUKAccountNumber, getLoanSchedule, parseMoney, validateBIC, validateCurrencyCode, validateEUVAT, validateIBAN, validateUKAccountNumber, validateUKSortCode, validateUSRoutingNumber };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,11 @@
1
- import { I as IBANValidationResult, V as ValidationResult, A as AccountNumber, S as SortCode, a as SupportedCurrency, M as MoneyResult, C as CurrencyCode, B as BIC } from './card-D3WC2rzV.js';
2
- export { b as CardNetwork, c as CardNumber, d as CardValidationResult, e as IBAN, f as IBANValidationSuccess, g as ValidationFailure, h as ValidationSuccess, i as isValidationSuccess, v as validateCardNumber } from './card-D3WC2rzV.js';
1
+ import { I as IBANValidationResult, V as ValidationResult, A as AccountNumber, S as SortCode, a as SupportedCurrency, M as MoneyResult, C as CurrencyCode, B as BIC, b as VATValidationResult, R as RoutingNumber } from './card-D2-7wbam.js';
2
+ export { c as CardNetwork, d as CardNumber, e as CardValidationResult, f as IBAN, g as IBANValidationSuccess, h as VATNumber, i as VATValidationSuccess, j as ValidationFailure, k as ValidationSuccess, l as isValidationFailure, m as isValidationSuccess, v as validateCardNumber } from './card-D2-7wbam.js';
3
3
 
4
+ declare function formatIBAN(input: string): string;
4
5
  declare function validateIBAN(input: string): IBANValidationResult;
5
6
 
7
+ declare function formatSortCode(input: string): string;
8
+ declare function formatUKAccountNumber(input: string): string;
6
9
  declare function validateUKSortCode(input: string): ValidationResult<SortCode>;
7
10
  declare function validateUKAccountNumber(input: string): ValidationResult<AccountNumber>;
8
11
 
@@ -13,4 +16,18 @@ declare function parseMoney(input: string): MoneyResult;
13
16
 
14
17
  declare function validateBIC(input: string): ValidationResult<BIC>;
15
18
 
16
- export { AccountNumber, BIC, CurrencyCode, IBANValidationResult, MoneyResult, SUPPORTED_CURRENCIES, SortCode, SupportedCurrency, ValidationResult, formatCurrency, parseMoney, validateBIC, validateCurrencyCode, validateIBAN, validateUKAccountNumber, validateUKSortCode };
19
+ declare function validateEUVAT(input: string): VATValidationResult;
20
+
21
+ declare function validateUSRoutingNumber(input: string): ValidationResult<RoutingNumber>;
22
+
23
+ type LoanScheduleEntry = {
24
+ month: number;
25
+ payment: number;
26
+ principal: number;
27
+ interest: number;
28
+ balance: number;
29
+ };
30
+ declare function calculateEMI(principal: number, annualRatePercent: number, months: number): number;
31
+ declare function getLoanSchedule(principal: number, annualRatePercent: number, months: number): LoanScheduleEntry[];
32
+
33
+ export { AccountNumber, BIC, CurrencyCode, IBANValidationResult, type LoanScheduleEntry, MoneyResult, RoutingNumber, SUPPORTED_CURRENCIES, SortCode, SupportedCurrency, VATValidationResult, ValidationResult, calculateEMI, formatCurrency, formatIBAN, formatSortCode, formatUKAccountNumber, getLoanSchedule, parseMoney, validateBIC, validateCurrencyCode, validateEUVAT, validateIBAN, validateUKAccountNumber, validateUKSortCode, validateUSRoutingNumber };
package/dist/index.js CHANGED
@@ -1,5 +1,42 @@
1
1
  'use strict';
2
2
 
3
+ // src/_guard.ts
4
+ var MAX_SAFE_INPUT_LENGTH = 256;
5
+ function guardStringInput(input, label = "Input") {
6
+ if (input == null || typeof input !== "string") {
7
+ return { ok: false, error: `${label} must be a non-empty string` };
8
+ }
9
+ if (input.length === 0) {
10
+ return { ok: false, error: `${label} must be a non-empty string` };
11
+ }
12
+ if (input.length > MAX_SAFE_INPUT_LENGTH) {
13
+ return {
14
+ ok: false,
15
+ error: `${label} must not exceed ${MAX_SAFE_INPUT_LENGTH} characters`
16
+ };
17
+ }
18
+ return { ok: true, value: input };
19
+ }
20
+ function guardNumber(value, options) {
21
+ const label = options.label ?? "Value";
22
+ if (typeof value !== "number" || !Number.isFinite(value)) {
23
+ return { ok: false, error: `${label} must be a finite number` };
24
+ }
25
+ if (Number.isNaN(value)) {
26
+ return { ok: false, error: `${label} must not be NaN` };
27
+ }
28
+ if (options.integer && !Number.isInteger(value)) {
29
+ return { ok: false, error: `${label} must be an integer` };
30
+ }
31
+ if (options.min !== void 0 && value < options.min) {
32
+ return { ok: false, error: `${label} must be at least ${options.min}` };
33
+ }
34
+ if (options.max !== void 0 && value > options.max) {
35
+ return { ok: false, error: `${label} must be at most ${options.max}` };
36
+ }
37
+ return { ok: true, value };
38
+ }
39
+
3
40
  // src/iban.ts
4
41
  var IBAN_LENGTHS = {
5
42
  AL: 28,
@@ -86,11 +123,15 @@ function ibanToDigits(iban) {
86
123
  function formatIBANString(iban) {
87
124
  return iban.replace(/(.{4})/g, "$1 ").trim();
88
125
  }
126
+ function formatIBAN(input) {
127
+ if (typeof input !== "string") return "";
128
+ const cleaned = input.replace(/\s/g, "").toUpperCase().slice(0, 34);
129
+ return formatIBANString(cleaned);
130
+ }
89
131
  function validateIBAN(input) {
90
- if (!input || typeof input !== "string") {
91
- return { valid: false, error: "Input must be a non-empty string" };
92
- }
93
- const cleaned = input.replace(/\s/g, "").toUpperCase();
132
+ const guarded = guardStringInput(input);
133
+ if (!guarded.ok) return { valid: false, error: guarded.error };
134
+ const cleaned = guarded.value.replace(/\s/g, "").toUpperCase();
94
135
  if (cleaned.length < 4) {
95
136
  return { valid: false, error: "IBAN is too short" };
96
137
  }
@@ -124,11 +165,22 @@ function validateIBAN(input) {
124
165
  }
125
166
 
126
167
  // src/sortcode.ts
168
+ function formatSortCode(input) {
169
+ if (typeof input !== "string") return "";
170
+ const digits = input.replace(/[-\s]/g, "").slice(0, 6);
171
+ if (digits.length < 6) return digits;
172
+ return `${digits.slice(0, 2)}-${digits.slice(2, 4)}-${digits.slice(4, 6)}`;
173
+ }
174
+ function formatUKAccountNumber(input) {
175
+ if (typeof input !== "string") return "";
176
+ const digits = input.replace(/\s/g, "").slice(0, 8);
177
+ if (digits.length < 8) return digits;
178
+ return `${digits.slice(0, 4)} ${digits.slice(4, 8)}`;
179
+ }
127
180
  function validateUKSortCode(input) {
128
- if (!input || typeof input !== "string") {
129
- return { valid: false, error: "Input must be a non-empty string" };
130
- }
131
- const cleaned = input.replace(/[-\s]/g, "");
181
+ const guarded = guardStringInput(input);
182
+ if (!guarded.ok) return { valid: false, error: guarded.error };
183
+ const cleaned = guarded.value.replace(/[-\s]/g, "");
132
184
  if (!/^\d{6}$/.test(cleaned)) {
133
185
  return {
134
186
  valid: false,
@@ -143,10 +195,9 @@ function validateUKSortCode(input) {
143
195
  };
144
196
  }
145
197
  function validateUKAccountNumber(input) {
146
- if (!input || typeof input !== "string") {
147
- return { valid: false, error: "Input must be a non-empty string" };
148
- }
149
- const cleaned = input.replace(/\s/g, "");
198
+ const guarded = guardStringInput(input);
199
+ if (!guarded.ok) return { valid: false, error: guarded.error };
200
+ const cleaned = guarded.value.replace(/\s/g, "");
150
201
  if (!/^\d{8}$/.test(cleaned)) {
151
202
  return {
152
203
  valid: false,
@@ -190,14 +241,13 @@ var SYMBOL_MAP = {
190
241
  "CHF": "CHF"
191
242
  };
192
243
  function validateCurrencyCode(input) {
193
- if (!input || typeof input !== "string") {
194
- return { valid: false, error: "Input must be a non-empty string" };
195
- }
196
- const upper = input.toUpperCase();
244
+ const guarded = guardStringInput(input);
245
+ if (!guarded.ok) return { valid: false, error: guarded.error };
246
+ const upper = guarded.value.toUpperCase();
197
247
  if (!SUPPORTED_CURRENCIES.includes(upper)) {
198
248
  return {
199
249
  valid: false,
200
- error: `Unsupported currency code: ${input}. Supported: ${SUPPORTED_CURRENCIES.join(", ")}`
250
+ error: `Unsupported currency code: ${upper}. Supported: ${SUPPORTED_CURRENCIES.join(", ")}`
201
251
  };
202
252
  }
203
253
  return {
@@ -207,6 +257,9 @@ function validateCurrencyCode(input) {
207
257
  };
208
258
  }
209
259
  function formatCurrency(amount, currency, locale) {
260
+ if (typeof amount !== "number" || !Number.isFinite(amount)) {
261
+ return "";
262
+ }
210
263
  const resolvedLocale = locale ?? CURRENCY_LOCALES[currency] ?? "en-GB";
211
264
  return new Intl.NumberFormat(resolvedLocale, {
212
265
  style: "currency",
@@ -216,11 +269,10 @@ function formatCurrency(amount, currency, locale) {
216
269
  }).format(amount);
217
270
  }
218
271
  function parseMoney(input) {
219
- if (!input || typeof input !== "string") {
220
- return { valid: false, error: "Input must be a non-empty string" };
221
- }
272
+ const guarded = guardStringInput(input);
273
+ if (!guarded.ok) return { valid: false, error: guarded.error };
222
274
  let currency;
223
- let cleaned = input.trim();
275
+ let cleaned = guarded.value.trim();
224
276
  for (const [symbol, code] of Object.entries(SYMBOL_MAP)) {
225
277
  if (cleaned.startsWith(symbol) || cleaned.endsWith(symbol)) {
226
278
  currency = code;
@@ -247,10 +299,9 @@ function parseMoney(input) {
247
299
  // src/bic.ts
248
300
  var BIC_REGEX = /^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$/;
249
301
  function validateBIC(input) {
250
- if (!input || typeof input !== "string") {
251
- return { valid: false, error: "Input must be a non-empty string" };
252
- }
253
- const cleaned = input.replace(/\s/g, "").toUpperCase();
302
+ const guarded = guardStringInput(input);
303
+ if (!guarded.ok) return { valid: false, error: guarded.error };
304
+ const cleaned = guarded.value.replace(/\s/g, "").toUpperCase();
254
305
  if (cleaned.length !== 8 && cleaned.length !== 11) {
255
306
  return {
256
307
  valid: false,
@@ -289,10 +340,9 @@ function formatCardNumber(digits, network) {
289
340
  return digits.replace(/(.{4})/g, "$1 ").trim();
290
341
  }
291
342
  function validateCardNumber(input) {
292
- if (!input || typeof input !== "string") {
293
- return { valid: false, error: "Input must be a non-empty string" };
294
- }
295
- const digits = input.replace(/[\s-]/g, "");
343
+ const guarded = guardStringInput(input);
344
+ if (!guarded.ok) return { valid: false, error: guarded.error };
345
+ const digits = guarded.value.replace(/[\s-]/g, "");
296
346
  if (!/^\d+$/.test(digits)) {
297
347
  return { valid: false, error: "Card number must contain only digits" };
298
348
  }
@@ -326,20 +376,148 @@ function validateCardNumber(input) {
326
376
  };
327
377
  }
328
378
 
379
+ // src/vat.ts
380
+ var EU_VAT_PATTERNS = {
381
+ AT: /^ATU\d{8}$/,
382
+ BE: /^BE0?\d{9}$/,
383
+ BG: /^BG\d{9,10}$/,
384
+ CY: /^CY\d{8}[A-Z]$/,
385
+ CZ: /^CZ\d{8,10}$/,
386
+ DE: /^DE\d{9}$/,
387
+ DK: /^DK\d{8}$/,
388
+ EE: /^EE\d{9}$/,
389
+ EL: /^EL\d{9}$/,
390
+ ES: /^ES[A-Z0-9]\d{7}[A-Z0-9]$/,
391
+ FI: /^FI\d{8}$/,
392
+ FR: /^FR[A-HJ-NP-Z0-9]{2}\d{9}$/,
393
+ GR: /^GR\d{9}$/,
394
+ HR: /^HR\d{11}$/,
395
+ HU: /^HU\d{8}$/,
396
+ IE: /^IE\d[A-Z0-9]\d{5}[A-Z]$|^IE\d{7}[A-W][A-I0-9]?$/,
397
+ IT: /^IT\d{11}$/,
398
+ LT: /^LT\d{9}$|^LT\d{12}$/,
399
+ LU: /^LU\d{8}$/,
400
+ LV: /^LV\d{11}$/,
401
+ MT: /^MT\d{8}$/,
402
+ NL: /^NL\d{9}B\d{2}$/,
403
+ PL: /^PL\d{10}$/,
404
+ PT: /^PT\d{9}$/,
405
+ RO: /^RO\d{2,10}$/,
406
+ SE: /^SE\d{12}$/,
407
+ SI: /^SI\d{8}$/,
408
+ SK: /^SK\d{10}$/
409
+ };
410
+ function validateEUVAT(input) {
411
+ const guarded = guardStringInput(input);
412
+ if (!guarded.ok) return { valid: false, error: guarded.error };
413
+ const cleaned = guarded.value.replace(/\s/g, "").toUpperCase();
414
+ if (cleaned.length < 4) {
415
+ return { valid: false, error: "VAT number is too short" };
416
+ }
417
+ const countryCode = cleaned.slice(0, 2);
418
+ const pattern = EU_VAT_PATTERNS[countryCode];
419
+ if (!pattern) {
420
+ return { valid: false, error: `Unsupported EU VAT country code: ${countryCode}` };
421
+ }
422
+ if (!pattern.test(cleaned)) {
423
+ return { valid: false, error: `Invalid VAT format for ${countryCode}` };
424
+ }
425
+ const formatted = `${countryCode} ${cleaned.slice(2)}`;
426
+ return {
427
+ valid: true,
428
+ value: cleaned,
429
+ formatted,
430
+ countryCode
431
+ };
432
+ }
433
+
434
+ // src/routing.ts
435
+ function routingChecksum(digits) {
436
+ if (digits.length !== 9) return false;
437
+ const sum = 3 * (Number(digits[0]) + Number(digits[3]) + Number(digits[6])) + 7 * (Number(digits[1]) + Number(digits[4]) + Number(digits[7])) + Number(digits[2]) + Number(digits[5]) + Number(digits[8]);
438
+ return sum % 10 === 0;
439
+ }
440
+ function validateUSRoutingNumber(input) {
441
+ const guarded = guardStringInput(input);
442
+ if (!guarded.ok) return { valid: false, error: guarded.error };
443
+ const cleaned = guarded.value.replace(/\s/g, "");
444
+ if (!/^\d{9}$/.test(cleaned)) {
445
+ return {
446
+ valid: false,
447
+ error: "US routing number must be exactly 9 digits"
448
+ };
449
+ }
450
+ if (!routingChecksum(cleaned)) {
451
+ return { valid: false, error: "US routing number checksum is invalid" };
452
+ }
453
+ return {
454
+ valid: true,
455
+ value: cleaned,
456
+ formatted: cleaned
457
+ };
458
+ }
459
+
460
+ // src/loan.ts
461
+ var MAX_LOAN_MONTHS = 3600;
462
+ function calculateEMI(principal, annualRatePercent, months) {
463
+ const p = guardNumber(principal, { min: 0, label: "Principal" });
464
+ const m = guardNumber(months, { min: 1, max: MAX_LOAN_MONTHS, integer: true, label: "Months" });
465
+ const rate = guardNumber(annualRatePercent, { min: 0, label: "Annual rate" });
466
+ if (!p.ok || !m.ok || !rate.ok) return 0;
467
+ if (rate.value === 0) return p.value / m.value;
468
+ const r = rate.value / 100 / 12;
469
+ return p.value * (r * (1 + r) ** m.value) / ((1 + r) ** m.value - 1);
470
+ }
471
+ function getLoanSchedule(principal, annualRatePercent, months) {
472
+ const emi = calculateEMI(principal, annualRatePercent, months);
473
+ if (emi === 0) return [];
474
+ const p = guardNumber(principal, { min: 0, label: "Principal" });
475
+ const m = guardNumber(months, { min: 1, max: MAX_LOAN_MONTHS, integer: true, label: "Months" });
476
+ const rate = guardNumber(annualRatePercent, { min: 0, label: "Annual rate" });
477
+ if (!p.ok || !m.ok || !rate.ok) return [];
478
+ const r = rate.value / 100 / 12;
479
+ const schedule = [];
480
+ let balance = p.value;
481
+ for (let month = 1; month <= m.value; month++) {
482
+ const interest = balance * r;
483
+ const principalPayment = emi - interest;
484
+ balance = Math.max(0, balance - principalPayment);
485
+ schedule.push({
486
+ month,
487
+ payment: emi,
488
+ principal: principalPayment,
489
+ interest,
490
+ balance
491
+ });
492
+ }
493
+ return schedule;
494
+ }
495
+
329
496
  // src/types.ts
330
497
  function isValidationSuccess(result) {
331
498
  return result.valid === true;
332
499
  }
500
+ function isValidationFailure(result) {
501
+ return result.valid === false;
502
+ }
333
503
 
334
504
  exports.SUPPORTED_CURRENCIES = SUPPORTED_CURRENCIES;
505
+ exports.calculateEMI = calculateEMI;
335
506
  exports.formatCurrency = formatCurrency;
507
+ exports.formatIBAN = formatIBAN;
508
+ exports.formatSortCode = formatSortCode;
509
+ exports.formatUKAccountNumber = formatUKAccountNumber;
510
+ exports.getLoanSchedule = getLoanSchedule;
511
+ exports.isValidationFailure = isValidationFailure;
336
512
  exports.isValidationSuccess = isValidationSuccess;
337
513
  exports.parseMoney = parseMoney;
338
514
  exports.validateBIC = validateBIC;
339
515
  exports.validateCardNumber = validateCardNumber;
340
516
  exports.validateCurrencyCode = validateCurrencyCode;
517
+ exports.validateEUVAT = validateEUVAT;
341
518
  exports.validateIBAN = validateIBAN;
342
519
  exports.validateUKAccountNumber = validateUKAccountNumber;
343
520
  exports.validateUKSortCode = validateUKSortCode;
521
+ exports.validateUSRoutingNumber = validateUSRoutingNumber;
344
522
  //# sourceMappingURL=index.js.map
345
523
  //# sourceMappingURL=index.js.map