domain-quotes 0.2.1 → 0.3.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
@@ -11,91 +11,250 @@ Domain Quote is a lightweight TypeScript/JavaScript library to compute domain re
11
11
  Includes:
12
12
  - Extension support based on unified registrar price list (OpenProvider/NIRA)
13
13
  - Currency conversion via remote exchange rates
14
- - VAT calculation per currency (US, GB, DE, NG mapping)
15
- - Optional discount codes with max-or-stack policy
14
+ - VAT calculation with configurable rate
15
+ - Flexible discount system with date ranges, extension/transaction filtering, and custom eligibility callbacks
16
16
  - Configurable markup to increase base prices before taxes/discounts
17
+ - Extension normalization (`.com` and `com` are treated identically)
17
18
  - Clean ESM API with TypeScript types
18
19
 
19
- Install
20
+ ## Install
20
21
 
21
22
  ```bash
22
23
  npm i domain-quotes
23
24
  ```
24
25
 
25
- Usage
26
+ ## Usage
26
27
 
27
28
  ```ts
28
29
  import { getDefaultQuote, DomainQuotes, DEFAULT_CONFIG } from 'domain-quotes';
29
30
 
30
- // Quick quote (uses bundled defaults)
31
- const quote = await getDefaultQuote('com', 'USD', { discountCodes: ['SAVE10'] });
32
- // → { extension, currency, basePrice, discount, tax, totalPrice, symbol }
31
+ // Quick quote (uses bundled defaults, no discounts configured by default)
32
+ const quote = await getDefaultQuote('com', 'USD');
33
+ // → { extension, currency, basePrice, discount, tax, totalPrice, symbol, domainTransaction }
33
34
 
34
- // Advanced: custom or explicit config via the class
35
- const dq = new DomainQuotes(DEFAULT_CONFIG); // or provide your own DomainQuoteConfig
36
- const eur = await dq.getQuote('example.com', 'EUR', { discountPolicy: 'stack' });
35
+ // Extensions are normalized - leading dots are stripped
36
+ const quote2 = await getDefaultQuote('.com', 'USD'); // same as 'com'
37
+
38
+ // Custom config with discounts
39
+ const dq = new DomainQuotes({
40
+ ...DEFAULT_CONFIG,
41
+ discounts: {
42
+ SAVE10: {
43
+ rate: 0.1, // 10% discount
44
+ extensions: ['com', 'net', 'org'],
45
+ startAt: '2024-01-01T00:00:00Z',
46
+ endAt: '2024-12-31T23:59:59Z',
47
+ },
48
+ },
49
+ });
50
+ const discounted = await dq.getQuote('com', 'USD', { discountCodes: ['SAVE10'] });
37
51
 
38
52
  // Add a 15% markup before discounts/taxes
39
53
  const withMarkup = new DomainQuotes({
40
54
  ...DEFAULT_CONFIG,
41
55
  markup: { type: 'percentage', value: 0.15 },
42
56
  });
43
- const quoteWithMarkup = await withMarkup.getQuote('example.com', 'USD', { discountCodes: ['SAVE10'] });
57
+ const quoteWithMarkup = await withMarkup.getQuote('com', 'USD');
58
+ ```
59
+
60
+ ## Discounts
61
+
62
+ Discounts are configured via the `discounts` field in `DomainQuoteConfig`. Each discount can be filtered by:
63
+
64
+ - **Date range**: `startAt` and `endAt` (ISO timestamps)
65
+ - **Extensions**: List of eligible extensions (normalized, so `.com` and `com` are equivalent)
66
+ - **Transaction types**: Optional list of transaction types (`create`, `renew`, `restore`, `transfer`)
67
+ - **Custom eligibility**: Optional callback for complex eligibility logic
68
+
69
+ ```ts
70
+ const dq = new DomainQuotes({
71
+ ...DEFAULT_CONFIG,
72
+ discounts: {
73
+ // Basic discount
74
+ WELCOME: {
75
+ rate: 0.1,
76
+ extensions: ['com', 'net'],
77
+ startAt: '2024-01-01T00:00:00Z',
78
+ endAt: '2024-12-31T23:59:59Z',
79
+ },
80
+
81
+ // Discount limited to specific transaction types
82
+ NEWUSER: {
83
+ rate: 0.2,
84
+ extensions: ['com', 'net', 'org'],
85
+ startAt: '2024-01-01T00:00:00Z',
86
+ endAt: '2024-12-31T23:59:59Z',
87
+ transactions: ['create'], // Only applies to new registrations
88
+ },
89
+
90
+ // Discount with custom eligibility callback
91
+ BIGSPENDER: {
92
+ rate: 0.25,
93
+ extensions: ['com', 'net', 'org'],
94
+ startAt: '2024-01-01T00:00:00Z',
95
+ endAt: '2024-12-31T23:59:59Z',
96
+ isEligible: (ctx) => ctx.basePrice >= 50, // Only if base price >= $50
97
+ },
98
+
99
+ // Async eligibility (e.g., check external service)
100
+ VIP: {
101
+ rate: 0.3,
102
+ extensions: ['com'],
103
+ startAt: '2024-01-01T00:00:00Z',
104
+ endAt: '2024-12-31T23:59:59Z',
105
+ isEligible: async (ctx) => {
106
+ // Check if user is VIP via external service
107
+ const isVip = await checkVipStatus(ctx.discountCode);
108
+ return isVip;
109
+ },
110
+ },
111
+ },
112
+ });
113
+
114
+ // Apply discounts
115
+ const quote = await dq.getQuote('com', 'USD', {
116
+ discountCodes: ['WELCOME', 'NEWUSER'],
117
+ discountPolicy: 'max', // default: use highest discount
118
+ });
119
+
120
+ // Stack multiple discounts
121
+ const stacked = await dq.getQuote('com', 'USD', {
122
+ discountCodes: ['WELCOME', 'NEWUSER'],
123
+ discountPolicy: 'stack', // sum all applicable discounts
124
+ });
125
+ ```
126
+
127
+ ### Eligibility Callback Context
128
+
129
+ The `isEligible` callback receives a context object with:
44
130
 
131
+ ```ts
132
+ interface DiscountEligibilityContext {
133
+ extension: string; // Normalized extension (e.g., 'com')
134
+ currency: string; // Currency code (e.g., 'USD')
135
+ transaction: TransactionType; // Transaction type
136
+ basePrice: number; // Base price before discount
137
+ discountCode: string; // The discount code being evaluated
138
+ }
45
139
  ```
46
140
 
47
- API
141
+ The callback is only invoked after all other criteria (date range, extension, transaction type) are satisfied. If the callback throws an error, the discount is skipped.
142
+
143
+ ## API
144
+
145
+ ### Functions
146
+
147
+ - **`getDefaultQuote(extension, currency, options?): Promise<Quote>`**
148
+
149
+ Computes a quote using bundled defaults (no discounts configured by default).
150
+
151
+ ```ts
152
+ const quote = await getDefaultQuote('com', 'USD');
153
+ const withOptions = await getDefaultQuote('.ng', 'NGN', {
154
+ discountCodes: ['SAVE10'],
155
+ transaction: 'renew',
156
+ });
157
+ ```
158
+
159
+ - **`normalizeExtension(extension: string): string`**
160
+
161
+ Normalizes an extension by trimming whitespace, lowercasing, and removing leading dots.
162
+
163
+ ```ts
164
+ normalizeExtension('.COM') // → 'com'
165
+ normalizeExtension('..ng') // → 'ng'
166
+ normalizeExtension(' org ') // → 'org'
167
+ ```
48
168
 
49
- - `getDefaultQuote(extension: string, currency: string, options?: GetQuoteOptions): Promise<Quote>`
50
- - Computes a quote for a TLD/SLD extension (e.g. `com`, `com.ng`, `.org`) using the bundled defaults.
51
- - `options`
52
- - `discountCodes?: string[]` – one or more codes; case-insensitive.
53
- - `now?: number | Date` – inject time for deterministic tests.
54
- - `discountPolicy?: 'stack' | 'max'` – default `'max'` (highest single discount only).
169
+ - **`listSupportedExtensions(): string[]`**
55
170
 
56
- - `class DomainQuotes(config: DomainQuoteConfig)`
57
- - `getQuote(extension: string, currency: string, options?: GetQuoteOptions): Promise<Quote>` – same behavior as above, but uses the provided config.
58
- - `DEFAULT_CONFIG: DomainQuoteConfig` – exported snapshot config used by `getDefaultQuote`.
171
+ Returns all extensions with pricing data.
59
172
 
60
- - `listSupportedExtensions(): string[]`
61
- - All extensions with a non-zero price in the dataset.
173
+ - **`isSupportedExtension(extension: string): boolean`**
62
174
 
63
- - `isSupportedExtension(extOrDomain: string): boolean`
64
- - Accepts an extension or a full domain (resolved by longest-known suffix match against bundled price data).
175
+ Checks if an extension is supported (normalizes input).
65
176
 
66
- - `listSupportedCurrencies(): string[]`
67
- - Currently returns `['USD','GBP','EUR','NGN']`. These map to VAT via country ISO codes.
177
+ - **`listSupportedCurrencies(): string[]`**
68
178
 
69
- - `isSupportedCurrency(code: string): boolean`
179
+ Returns supported currencies (default: `['USD', 'NGN']`).
70
180
 
71
- Types
181
+ - **`isSupportedCurrency(code: string): boolean`**
182
+
183
+ Checks if a currency is supported (case-insensitive).
184
+
185
+ ### Class
186
+
187
+ - **`new DomainQuotes(config: DomainQuoteConfig)`**
188
+
189
+ Creates a quote calculator with custom configuration.
190
+
191
+ ```ts
192
+ const dq = new DomainQuotes({
193
+ ...DEFAULT_CONFIG,
194
+ vatRate: 0.2,
195
+ supportedCurrencies: ['USD', 'NGN', 'EUR', 'GBP'],
196
+ discounts: { /* ... */ },
197
+ });
198
+
199
+ const quote = await dq.getQuote('com', 'EUR', options);
200
+ ```
201
+
202
+ ### Constants
203
+
204
+ - **`DEFAULT_CONFIG: DomainQuoteConfig`**
205
+
206
+ The default configuration with remote pricing data, 7.5% VAT, and no discounts.
207
+
208
+ - **`DEFAULT_VAT_RATE`** = `0.075` (7.5%)
209
+
210
+ ## Types
72
211
 
73
212
  ```ts
213
+ type TransactionType = 'create' | 'renew' | 'restore' | 'transfer';
74
214
  type DiscountPolicy = 'stack' | 'max';
215
+ type MarkupType = 'percentage' | 'fixedUsd';
75
216
 
76
217
  interface GetQuoteOptions {
77
- discountCodes?: string[];
78
- now?: number | Date;
79
- discountPolicy?: DiscountPolicy;
80
- transaction?: 'create' | 'renew' | 'restore' | 'transfer'; // default 'create'
218
+ discountCodes?: string[]; // Discount codes to apply (case-insensitive)
219
+ now?: number | Date; // Override current time for testing
220
+ discountPolicy?: DiscountPolicy; // 'max' (default) or 'stack'
221
+ transaction?: TransactionType; // default: 'create'
81
222
  }
82
223
 
83
- type MarkupType = 'percentage' | 'fixedUsd';
224
+ interface Quote {
225
+ extension: string; // Normalized extension
226
+ currency: string; // Currency code
227
+ basePrice: number; // Price before discount
228
+ discount: number; // Total discount amount
229
+ tax: number; // Tax amount
230
+ totalPrice: number; // Final price (basePrice - discount + tax)
231
+ symbol: string; // Currency symbol
232
+ domainTransaction: TransactionType; // Transaction type
233
+ }
84
234
 
85
235
  interface Markup {
86
- type: MarkupType; // percentage -> 0.2 === +20%, fixedUsd -> +$ value before conversion
87
- value: number;
236
+ type: MarkupType; // 'percentage' or 'fixedUsd'
237
+ value: number; // 0.2 = +20%, or fixed USD amount
88
238
  }
89
239
 
90
- interface Quote {
240
+ interface DiscountEligibilityContext {
91
241
  extension: string;
92
242
  currency: string;
243
+ transaction: TransactionType;
93
244
  basePrice: number;
94
- discount: number;
95
- tax: number;
96
- totalPrice: number;
97
- symbol: string;
98
- transaction: 'create' | 'renew' | 'restore' | 'transfer';
245
+ discountCode: string;
246
+ }
247
+
248
+ type DiscountEligibilityCallback =
249
+ (context: DiscountEligibilityContext) => boolean | Promise<boolean>;
250
+
251
+ interface DiscountConfig {
252
+ rate: number; // e.g., 0.1 for 10%
253
+ extensions: string[]; // Eligible extensions (normalized)
254
+ startAt: string; // ISO timestamp
255
+ endAt: string; // ISO timestamp
256
+ transactions?: TransactionType[]; // Limit to specific transaction types
257
+ isEligible?: DiscountEligibilityCallback; // Custom eligibility logic
99
258
  }
100
259
 
101
260
  interface ExchangeRateData {
@@ -107,38 +266,49 @@ interface ExchangeRateData {
107
266
  inverseRate: number;
108
267
  }
109
268
 
110
- interface DiscountConfig {
111
- rate: number; // e.g. 0.1 for 10%
112
- extensions: string[]; // eligible extensions
113
- startAt: string; // ISO timestamp
114
- endAt: string; // ISO timestamp
115
- }
269
+ type PriceEntry = number | Record<string, number>;
270
+ type PriceTable = Record<string, PriceEntry>;
116
271
 
117
272
  interface DomainQuoteConfig {
118
- createPrices: Record<string, number>; // base USD prices for create
119
- // Optional price tables per transaction type (all USD). Falls back to `createPrices` when absent.
120
- renewPrices?: Record<string, number>;
121
- restorePrices?: Record<string, number>;
122
- transferPrices?: Record<string, number>;
123
- exchangeRates: ExchangeRateData[]; // currency conversion data
124
- vatRate: number; // single VAT rate applied to subtotal
125
- discounts: Record<string, DiscountConfig>; // discount code → config
126
- markup?: Markup; // optional markup applied before conversion
273
+ createPrices: PriceTable; // Base prices for create
274
+ renewPrices?: PriceTable; // Optional prices for renew
275
+ restorePrices?: PriceTable; // Optional prices for restore
276
+ transferPrices?: PriceTable; // Optional prices for transfer
277
+ exchangeRates: ExchangeRateData[]; // Currency conversion data
278
+ vatRate: number; // VAT rate (e.g., 0.075 for 7.5%)
279
+ discounts: Record<string, DiscountConfig>; // Discount configurations
280
+ markup?: Markup; // Optional markup
281
+ supportedCurrencies?: string[]; // Allowed currencies (default: ['USD', 'NGN'])
127
282
  }
128
283
  ```
129
284
 
130
- Errors
285
+ ## Errors
286
+
287
+ - **`UnsupportedExtensionError`** - `code: 'ERR_UNSUPPORTED_EXTENSION'`
288
+ - **`UnsupportedCurrencyError`** - `code: 'ERR_UNSUPPORTED_CURRENCY'`
131
289
 
132
- - `UnsupportedExtensionError` with `code = 'ERR_UNSUPPORTED_EXTENSION'`
133
- - `UnsupportedCurrencyError` with `code = 'ERR_UNSUPPORTED_CURRENCY'`
290
+ ```ts
291
+ import { UnsupportedExtensionError, UnsupportedCurrencyError } from 'domain-quotes';
292
+
293
+ try {
294
+ await getDefaultQuote('invalid-tld', 'USD');
295
+ } catch (err) {
296
+ if (err instanceof UnsupportedExtensionError) {
297
+ console.log(err.code); // 'ERR_UNSUPPORTED_EXTENSION'
298
+ }
299
+ }
300
+ ```
134
301
 
135
- Notes
302
+ ## Notes
136
303
 
137
- - Rounding is to 2 decimal places at each step to keep totals predictable (`base`, `discount`, `tax`, `total`).
138
- - A single VAT rate is applied to the subtotal by default (7.5% in the default config). Override `vatRate` in the config to change it.
139
- - Price and exchange-rate data are fetched from maintained remote sources at import time:
304
+ - **Rounding**: All monetary values are rounded to 2 decimal places at each step.
305
+ - **VAT**: A single VAT rate is applied to the subtotal (base price - discount). Default is 7.5%.
306
+ - **Extension normalization**: Leading dots are stripped and extensions are lowercased. `.COM`, `..com`, and `com` are all equivalent.
307
+ - **Discount order**: The `isEligible` callback is only called after date range, extension, and transaction type checks pass.
308
+ - **Remote data**: Price and exchange-rate data are fetched at import time from:
140
309
  - Prices: `https://raw.githubusercontent.com/namewiz/registrar-pricelist/refs/heads/main/data/unified-create-prices.csv`
141
310
  - Exchange rates: `https://raw.githubusercontent.com/namewiz/registrar-pricelist/refs/heads/main/data/exchange-rates.json`
311
+
142
312
  These are cached in-memory for the life of the process.
143
313
 
144
314
  ## Testing
@@ -147,4 +317,4 @@ Notes
147
317
  npm test
148
318
  ```
149
319
 
150
- The test suite uses Nodes built-in `node:test` runner and builds the library first.
320
+ The test suite uses Node's built-in `node:test` runner and builds the library first.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,iBAAiB,EAEjB,eAAe,EAIf,KAAK,EAEN,MAAM,SAAS,CAAC;AACjB,YAAY,EACV,cAAc,EACd,2BAA2B,EAC3B,0BAA0B,EAC1B,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,MAAM,EACN,UAAU,EACV,UAAU,EACV,UAAU,EACV,KAAK,EACL,eAAe,EAChB,MAAM,SAAS,CAAC;AAEjB,eAAO,MAAM,gBAAgB,QAAQ,CAAC;AAEtC,wBAAgB,uBAAuB,IAAI,MAAM,EAAE,CAGlD;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAKzD;AAED,wBAAgB,uBAAuB,IAAI,MAAM,EAAE,CAGlD;AAuBD,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAK/D;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAK5D;AAoGD,cAAM,gBAAiB,SAAQ,KAAK;IAClC,IAAI,EAAE,MAAM,CAAC;gBACA,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAK3C;AAED,qBAAa,yBAA0B,SAAQ,gBAAgB;gBAChD,GAAG,EAAE,MAAM;CAIzB;AAED,qBAAa,wBAAyB,SAAQ,gBAAgB;gBAC/C,QAAQ,EAAE,MAAM;CAI9B;AA+BD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;gBAE9B,MAAM,EAAE,iBAAiB;IAItC,OAAO,CAAC,YAAY;IAOd,QAAQ,CACZ,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,KAAK,CAAC;CAmHlB;AAGD,eAAO,MAAM,cAAc,EAAE,iBAQ5B,CAAC;AAEF,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,KAAK,CAAC,CAGhB"}
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- var O={SAVE1:{rate:.01,extensions:["com","net"],startAt:"2023-01-01T00:00:00Z",endAt:"2025-12-31T23:59:59Z"},NEWUSER15:{rate:.15,extensions:["com","net","org"],startAt:"2023-01-01T00:00:00Z",endAt:"2024-12-31T23:59:59Z"},FALL20:{rate:.2,extensions:["org","info"],startAt:"2024-09-01T00:00:00Z",endAt:"2024-12-01T00:00:00Z"}};var k=.075;function Z(){return(M.supportedCurrencies??["USD","NGN"]).slice()}function ce(e){if(!e)return!1;let t=e.toUpperCase();return Z().includes(t)}function ae(){let e=N();return Object.keys(e).sort()}function w(e){if(e==null)return;if(typeof e=="number")return!Number.isFinite(e)||e<=0?void 0:{USD:e};let t={};for(let[n,r]of Object.entries(e)){let c=n?.toUpperCase();if(!c||!Number.isFinite(r)||r<=0)continue;let a=t[c];t[c]=a===void 0?r:Math.min(a,r)}return Object.keys(t).length>0?t:void 0}function V(e){return!!w(e)}function ue(e){let t=N(),n=F(e),r=t[n];return V(r)}function F(e){return e&&e.trim().toLowerCase().replace(/^\.+/,"")}function q(e){return e instanceof Date?e.getTime():typeof e=="number"?e:Date.now()}var z="https://raw.githubusercontent.com/namewiz/registrar-pricelist/refs/heads/main/data/unified-create-prices.csv",J="https://raw.githubusercontent.com/namewiz/registrar-pricelist/refs/heads/main/data/exchange-rates.json";async function X(e){let t=await fetch(e);if(!t.ok)throw new Error(`Failed to fetch ${e}: ${t.status} ${t.statusText}`);return t.text()}async function H(e){let t=await fetch(e);if(!t.ok)throw new Error(`Failed to fetch ${e}: ${t.status} ${t.statusText}`);return t.json()}function B(e){let t=e.split(/\r?\n/).map(c=>c.trim()).filter(Boolean);if(t.length===0)return{};let n=t.shift(),r={};for(let c of t){let a=c.split(",");if(a.length<4)continue;let x=a[0]?.trim().toLowerCase(),o=a[2]?.trim().toUpperCase(),E=a[3]?.trim(),d=Number(E);if(!x||!o||!Number.isFinite(d)||d<=0)continue;let i=r[x],u;i===void 0?u={}:typeof i=="number"?u={USD:i}:u=i;let p=u[o];u[o]=p===void 0?d:Math.min(p,d),r[x]=u}return r}async function W(){try{let[e,t]=await Promise.all([X(z).then(B),H(J)]);return[e,t]}catch(e){let t=e instanceof Error?e:new Error(typeof e=="string"?e:"Unknown error");throw t.message=`domain-quotes: failed to load remote pricing data: ${t.message}`,t}}var[Y,K]=await W();function N(){return Y}function ee(){return K}function te(){return O}var D=class extends Error{constructor(t,n){super(n),this.name="DomainQuoteError",this.code=t}},R=class extends D{constructor(t){super("ERR_UNSUPPORTED_EXTENSION",`Unsupported extension: ${t}`),this.name="UnsupportedExtensionError"}},P=class extends D{constructor(t){super("ERR_UNSUPPORTED_CURRENCY",`Unsupported currency: ${t}`),this.name="UnsupportedCurrencyError"}};function ne(){return{countryCode:"US",currencyName:"United States Dollar",currencySymbol:"$",currencyCode:"USD",exchangeRate:1,inverseRate:1}}function m(e){return Number(e.toFixed(2))}function re(e,t){if(!t)return e;let n=typeof t.value=="number"?t.value:0;if(!Number.isFinite(n)||n<=0)return e;switch(t.type){case"percentage":return e+e*n;case"fixedUsd":return e+n;default:return e}}var C=class{constructor(t){this.config=t}findRateInfo(t){if(t==="USD")return ne();let n=this.config.exchangeRates.find(r=>r.currencyCode===t);if(!n)throw new P(t);return n}async getQuote(t,n,r={}){let c=this.config.createPrices,a=typeof this.config.vatRate=="number"?this.config.vatRate:k,x=this.config.discounts,o=F(t),E=r.transaction||"create",d=w(c[o]);if(!d)throw new R(o);let i={...d},u=(()=>{switch(E){case"renew":return this.config.renewPrices;case"restore":return this.config.restorePrices;case"transfer":return this.config.transferPrices;case"create":default:return}})();if(u){let s=w(u[o]);s&&(i={...i,...s})}if(Object.keys(i).length===0)throw new R(o);let p=(n||"").toUpperCase();if(!(this.config.supportedCurrencies??["USD","NGN"]).includes(p))throw new P(n);let T=this.findRateInfo(p),G=T.currencySymbol,f=i.USD;f===void 0&&(f=d.USD);let b=i[p];if(f===void 0&&b!==void 0&&(f=b/T.exchangeRate),f===void 0||f<=0)throw new R(o);let S=re(f,this.config.markup),l;if(b!==void 0&&f>0){let s=b/f;l=m(S*s)}else l=m(S*T.exchangeRate);let I=a,$=Array.from(new Set((r.discountCodes||[]).map(s=>s.toUpperCase()))),U=q(r.now),y=[];for(let s of $){let g=x[s];if(!g)continue;let Q=Date.parse(g.startAt),_=Date.parse(g.endAt);Number.isNaN(Q)||Number.isNaN(_)||U<Q||U>_||g.extensions.includes(o)&&y.push(m(l*g.rate))}let h=0;y.length>0&&(r.discountPolicy==="stack"?h=m(y.reduce((s,g)=>s+g,0)):h=Math.max(...y)),h>l&&(h=l);let A=m(l-h),v=m(A*I),j=m(A+v);return{extension:o,currency:p,basePrice:l,discount:h,tax:v,totalPrice:j,symbol:G,transaction:E}}},M={createPrices:N(),exchangeRates:ee(),vatRate:k,discounts:te(),supportedCurrencies:["USD","NGN"]};async function fe(e,t,n={}){return new C(M).getQuote(e,t,n)}export{M as DEFAULT_CONFIG,k as DEFAULT_VAT_RATE,C as DomainQuotes,P as UnsupportedCurrencyError,R as UnsupportedExtensionError,fe as getDefaultQuote,ce as isSupportedCurrency,ue as isSupportedExtension,Z as listSupportedCurrencies,ae as listSupportedExtensions};
1
+ var fe=.075;function L(){return(O.supportedCurrencies??["USD","NGN"]).slice()}function de(e){if(!e)return!1;let t=e.toUpperCase();return L().includes(t)}function le(){let e=_();return Object.keys(e).sort()}function N(e){if(e==null)return;if(typeof e=="number")return!Number.isFinite(e)||e<=0?void 0:{USD:e};let t={};for(let[n,r]of Object.entries(e)){let a=n?.toUpperCase();if(!a||!Number.isFinite(r)||r<=0)continue;let u=t[a];t[a]=u===void 0?r:Math.min(u,r)}return Object.keys(t).length>0?t:void 0}function j(e){return!!N(e)}function pe(e){let t=_(),n=S(e),r=t[n];return j(r)}function S(e){return e&&e.trim().toLowerCase().replace(/^\.+/,"")}function q(e){return e instanceof Date?e.getTime():typeof e=="number"?e:Date.now()}var X="https://raw.githubusercontent.com/namewiz/registrar-pricelist/refs/heads/main/data/unified-create-prices.csv",H="https://raw.githubusercontent.com/namewiz/registrar-pricelist/refs/heads/main/data/unified-renew-prices.csv",J="https://raw.githubusercontent.com/namewiz/registrar-pricelist/refs/heads/main/data/unified-transfer-prices.csv",W="https://raw.githubusercontent.com/namewiz/registrar-pricelist/refs/heads/main/data/exchange-rates.json";async function D(e){let t=await fetch(e);if(!t.ok)throw new Error(`Failed to fetch ${e}: ${t.status} ${t.statusText}`);return t.text()}async function B(e){let t=await fetch(e);if(!t.ok)throw new Error(`Failed to fetch ${e}: ${t.status} ${t.statusText}`);return t.json()}function y(e){let t=e.split(/\r?\n/).map(a=>a.trim()).filter(Boolean);if(t.length===0)return{};let n=t.shift(),r={};for(let a of t){let u=a.split(",");if(u.length<4)continue;let b=u[0]?.trim().toLowerCase(),i=u[2]?.trim().toUpperCase(),h=u[3]?.trim(),l=Number(h);if(!b||!i||!Number.isFinite(l)||l<=0)continue;let c=r[b],f;c===void 0?f={}:typeof c=="number"?f={USD:c}:f=c;let p=f[i];f[i]=p===void 0?l:Math.min(p,l),r[b]=f}return r}async function Y(){try{let[e,t,n,r]=await Promise.all([D(X).then(y),D(H).then(y),D(J).then(y),B(W)]);return[e,t,n,r]}catch(e){let t=e instanceof Error?e:new Error(typeof e=="string"?e:"Unknown error");throw t.message=`domain-quotes: failed to load remote pricing data: ${t.message}`,t}}var[K,Z,ee,te]=await Y();function _(){return K}function ne(){return Z}function re(){return ee}function ie(){return te}var T=class extends Error{constructor(t,n){super(n),this.name="DomainQuoteError",this.code=t}},R=class extends T{constructor(t){super("ERR_UNSUPPORTED_EXTENSION",`Unsupported extension: ${t}`),this.name="UnsupportedExtensionError"}},w=class extends T{constructor(t){super("ERR_UNSUPPORTED_CURRENCY",`Unsupported currency: ${t}`),this.name="UnsupportedCurrencyError"}};function se(){return{countryCode:"US",currencyName:"United States Dollar",currencySymbol:"$",currencyCode:"USD",exchangeRate:1,inverseRate:1}}function m(e){return Number(e.toFixed(2))}function oe(e,t){if(!t)return e;let n=typeof t.value=="number"?t.value:0;if(!Number.isFinite(n)||n<=0)return e;switch(t.type){case"percentage":return e+e*n;case"fixedUsd":return e+n;default:return e}}var U=class{constructor(t){this.config=t}findRateInfo(t){if(t==="USD")return se();let n=this.config.exchangeRates.find(r=>r.currencyCode===t);if(!n)throw new w(t);return n}async getQuote(t,n,r={}){let a=this.config.createPrices,u=typeof this.config.vatRate=="number"?this.config.vatRate:.075,b=this.config.discounts,i=S(t),h=r.transaction||"create",l=N(a[i]);if(!l)throw new R(i);let c={...l},f=(()=>{switch(h){case"renew":return this.config.renewPrices;case"restore":return this.config.restorePrices;case"transfer":return this.config.transferPrices;default:return}})();if(f){let o=N(f[i]);o&&(c={...c,...o})}if(Object.keys(c).length===0)throw new R(i);let p=(n||"").toUpperCase();if(!(this.config.supportedCurrencies??["USD","NGN"]).includes(p))throw new w(n);let C=this.findRateInfo(p),M=C.currencySymbol,d=c.USD;d===void 0&&(d=l.USD);let P=c[p];if(d===void 0&&P!==void 0&&(d=P/C.exchangeRate),d===void 0||d<=0)throw new R(i);let A=oe(d,this.config.markup),g;if(P!==void 0&&d>0){let o=P/d;g=m(A*o)}else g=m(A*C.exchangeRate);let G=u,$=Array.from(new Set((r.discountCodes||[]).map(o=>o.toUpperCase()))),v=q(r.now),x=[];for(let o of $){let s=b[o];if(!s)continue;let Q=Date.parse(s.startAt),k=Date.parse(s.endAt);if(!(Number.isNaN(Q)||Number.isNaN(k)||v<Q||v>k||!s.extensions.map(S).includes(i))&&!(s.transactions&&s.transactions.length>0&&!s.transactions.includes(h))){if(s.isEligible)try{let z={extension:i,currency:p,transaction:h,basePrice:g,discountCode:o};if(!await Promise.resolve(s.isEligible(z)))continue}catch{continue}x.push(m(g*s.rate))}}let E=0;x.length>0&&(r.discountPolicy==="stack"?E=m(x.reduce((o,s)=>o+s,0)):E=Math.max(...x)),E>g&&(E=g);let I=m(g-E),F=m(I*G),V=m(I+F);return{extension:i,currency:p,basePrice:g,discount:E,tax:F,totalPrice:V,symbol:M,domainTransaction:h}}},O={createPrices:_(),renewPrices:ne(),transferPrices:re(),exchangeRates:ie(),vatRate:.075,discounts:{},supportedCurrencies:["USD","NGN"]};async function ge(e,t,n={}){return new U(O).getQuote(e,t,n)}export{O as DEFAULT_CONFIG,fe as DEFAULT_VAT_RATE,U as DomainQuotes,w as UnsupportedCurrencyError,R as UnsupportedExtensionError,ge as getDefaultQuote,de as isSupportedCurrency,pe as isSupportedExtension,L as listSupportedCurrencies,le as listSupportedExtensions,S as normalizeExtension};
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,KAAK;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,EAAE,eAAe,CAAC;CACpC;AAED,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,UAAU,CAAC;AAEnD,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,uDAAuD;AACvD,MAAM,WAAW,0BAA0B;IACzC,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,2BAA2B;IAC3B,WAAW,EAAE,eAAe,CAAC;IAC7B,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,4GAA4G;AAC5G,MAAM,MAAM,2BAA2B,GAAG,CAAC,OAAO,EAAE,0BAA0B,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAE9G,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,iHAAiH;IACjH,YAAY,CAAC,EAAE,eAAe,EAAE,CAAC;IACjC,2GAA2G;IAC3G,UAAU,CAAC,EAAE,2BAA2B,CAAC;CAC1C;AAED,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,KAAK,CAAC;AAC7C,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,CAAC;AAE1E,MAAM,WAAW,eAAe;IAC9B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B;AAED,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACzD,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAEpD,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,UAAU,CAAC;IAEzB,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,aAAa,CAAC,EAAE,UAAU,CAAC;IAC3B,cAAc,CAAC,EAAE,UAAU,CAAC;IAC5B,aAAa,EAAE,gBAAgB,EAAE,CAAC;IAElC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC"}
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "domain-quotes",
3
- "version": "0.2.1",
3
+ "version": "0.3.2",
4
4
  "description": "Fast multi-currency domain quote checker library across registrars.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
7
8
  "scripts": {
8
- "build": "npx -y esbuild src/index.ts --bundle --minify --format=esm --target=esnext --outdir=dist/ --platform=neutral",
9
+ "build": "npx -y esbuild src/index.ts --bundle --minify --format=esm --target=esnext --outdir=dist/ --platform=neutral && tsc --emitDeclarationOnly",
9
10
  "postbuild": "cp dist/index.js docs/domain-quotes.js",
10
11
  "test": "npm run build && node --test",
11
12
  "start": "npm run build && cp dist/index.js docs/domain-quotes.js && npx serve docs"
@@ -29,11 +30,10 @@
29
30
  "quote-check"
30
31
  ],
31
32
  "exports": {
32
- ".": "./dist/index.js"
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "default": "./dist/index.js"
36
+ }
33
37
  },
34
- "license": "MIT",
35
- "engines": {
36
- "node": "^22.12.0",
37
- "npm": "^10.9.0"
38
- }
38
+ "license": "MIT"
39
39
  }