diff --git a/ENV_READS.md b/ENV_READS.md new file mode 100644 index 0000000..6ca78b5 --- /dev/null +++ b/ENV_READS.md @@ -0,0 +1,15 @@ +| file | variable | default | +| --- | --- | --- | +| src/lib/log.js | LOG_LEVEL | info | +| src/server.js | PORT | 8080 | +| src/modules/auth/service.js | AUTH_SECRET | dev-secret | +| src/modules/auth/service.js | TOKEN_TTL_S | 3600 | +| src/modules/webhooks/service.js | WEBHOOK_TIMEOUT_MS | 5000 | +| src/modules/inventory/service.js | LOW_STOCK_THRESHOLD | 5 | +| src/modules/billing/constants.js | TAX_REGION | US | +| src/modules/search/service.js | SEARCH_MAX_RESULTS | 50 | +| src/modules/reports/formatters.js | REPORT_LOCALE | en-US | +| src/modules/notifications/service.js | SMTP_HOST | localhost | +| src/modules/notifications/service.js | SMTP_PORT | 2525 | +| src/modules/files/repo.js | FILES_BUCKET | local-bucket | +| src/modules/scheduler/service.js | SCHED_TZ | UTC | diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..dffaf91 --- /dev/null +++ b/src/config.js @@ -0,0 +1,27 @@ +const env = process.env; + +// Getters re-read the environment on each access (these settings were read lazily +// before); the other settings are fixed when this module is first loaded. +export const config = { + logLevel: env.LOG_LEVEL || 'info', + get port() { + return Number(process.env.PORT ?? 8080); + }, + authSecret: env.AUTH_SECRET || 'dev-secret', + tokenTtlS: env.TOKEN_TTL_S ?? '3600', + get webhookTimeoutMs() { + return Number(process.env.WEBHOOK_TIMEOUT_MS ?? '5000'); + }, + get lowStockThreshold() { + return Number(process.env.LOW_STOCK_THRESHOLD ?? 5); + }, + taxRegion: env.TAX_REGION || 'US', + searchMaxResults: parseInt(env.SEARCH_MAX_RESULTS || '50', 10), + reportLocale: env.REPORT_LOCALE ?? 'en-US', + smtpHost: env.SMTP_HOST || 'localhost', + smtpPort: Number(env.SMTP_PORT || 2525), + get filesBucket() { + return process.env.FILES_BUCKET || 'local-bucket'; + }, + schedTz: env.SCHED_TZ || 'UTC', +}; diff --git a/src/lib/log.js b/src/lib/log.js index 4068f23..211a2d2 100644 --- a/src/lib/log.js +++ b/src/lib/log.js @@ -1,4 +1,6 @@ -const LEVEL = process.env.LOG_LEVEL || 'info'; +import { config } from '../config.js'; + +const LEVEL = config.logLevel; const ORDER = ['debug', 'info', 'warn', 'error']; export function level() { diff --git a/src/modules/auth/service.js b/src/modules/auth/service.js index 040b43e..6b13736 100644 --- a/src/modules/auth/service.js +++ b/src/modules/auth/service.js @@ -32,14 +32,12 @@ export function setSessionStatus(id, status) { } import { createHmac } from 'node:crypto'; - -const SECRET = process.env['AUTH_SECRET'] || 'dev-secret'; -const { TOKEN_TTL_S = '3600' } = process.env; +import { config } from '../../config.js'; export function tokenTtlSeconds() { - return Number(TOKEN_TTL_S); + return Number(config.tokenTtlS); } export function sign(payload) { - return createHmac('sha256', SECRET).update(JSON.stringify(payload)).digest('hex'); + return createHmac('sha256', config.authSecret).update(JSON.stringify(payload)).digest('hex'); } diff --git a/src/modules/billing/constants.js b/src/modules/billing/constants.js index 0815919..765187a 100644 --- a/src/modules/billing/constants.js +++ b/src/modules/billing/constants.js @@ -1,4 +1,6 @@ -export const TAX_REGION = process.env.TAX_REGION || 'US'; +import { config } from '../../config.js'; + +export const TAX_REGION = config.taxRegion; export const TAX_RATES = { US: 7, EU: 20, UK: 20 }; export const DISCOUNT_CODES = { SAVE10: { pct: 10 }, FLAT500: { cents: 500 } }; export const CURRENCIES = ['USD', 'EUR', 'GBP']; diff --git a/src/modules/files/repo.js b/src/modules/files/repo.js index 12a781a..1bd1289 100644 --- a/src/modules/files/repo.js +++ b/src/modules/files/repo.js @@ -1,4 +1,5 @@ import { nextId } from '../../lib/ids.js'; +import { config } from '../../config.js'; const rows = new Map(); @@ -26,5 +27,5 @@ export function clearUploads() { } export function bucket() { - return process.env.FILES_BUCKET || 'local-bucket'; + return config.filesBucket; } diff --git a/src/modules/inventory/service.js b/src/modules/inventory/service.js index 5cad31c..346d1c1 100644 --- a/src/modules/inventory/service.js +++ b/src/modules/inventory/service.js @@ -4,6 +4,7 @@ import { insertStockItem, findStockItem, allStockItems, saveStockItem } from './ import { validateStockItem } from './validators.js'; import { emitStockItemChanged } from './events.js'; import { STATUSES } from './constants.js'; +import { config } from '../../config.js'; export function createStockItem(input) { const row = insertStockItem(validateStockItem(input)); @@ -32,7 +33,7 @@ export function setStockItemStatus(id, status) { } export function lowStockThreshold() { - return Number(process.env['LOW_STOCK_THRESHOLD'] ?? 5); + return config.lowStockThreshold; } export function isLow(item) { diff --git a/src/modules/notifications/service.js b/src/modules/notifications/service.js index 06be55c..18729a2 100644 --- a/src/modules/notifications/service.js +++ b/src/modules/notifications/service.js @@ -4,6 +4,7 @@ import { insertNotice, findNotice, allNotices, saveNotice } from './repo.js'; import { validateNotice } from './validators.js'; import { emitNoticeChanged } from './events.js'; import { STATUSES } from './constants.js'; +import { config } from '../../config.js'; export function createNotice(input) { const row = insertNotice(validateNotice(input)); @@ -31,10 +32,6 @@ export function setNoticeStatus(id, status) { return row; } -const env = process.env; -const host = env.SMTP_HOST || 'localhost'; -const smtpPort = Number(env.SMTP_PORT || 2525); - export function smtpTarget() { - return { host, port: smtpPort }; + return { host: config.smtpHost, port: config.smtpPort }; } diff --git a/src/modules/reports/formatters.js b/src/modules/reports/formatters.js index b0b1d6d..3c569c6 100644 --- a/src/modules/reports/formatters.js +++ b/src/modules/reports/formatters.js @@ -1,9 +1,9 @@ +import { config } from '../../config.js'; + export function formatReport(row) { return { id: row.id, name: row.name, status: row.status }; } -const { REPORT_LOCALE: locale = 'en-US' } = process.env; - export function reportLocale() { - return locale; + return config.reportLocale; } diff --git a/src/modules/returns/policy.js b/src/modules/returns/policy.js index 4203b3f..8ac4c0b 100644 --- a/src/modules/returns/policy.js +++ b/src/modules/returns/policy.js @@ -1,14 +1,3 @@ -import { taxFor } from '../billing/tax.js'; - -// Adjusts a refund for tax. Discounted orders are taxed on the discounted amount, -// so the refund must carry that tax too. -export function withTaxAdjustment(refund, priced, order) { - if (order.discountCode) { - return refund + taxFor(priced.subtotal - priced.discount, order.region); - } - return refund; -} - export function withinWindow(orderDate, today, days) { return (new Date(today) - new Date(orderDate)) / 86400000 <= days; } diff --git a/src/modules/returns/service.js b/src/modules/returns/service.js index f49d691..e38e1ca 100644 --- a/src/modules/returns/service.js +++ b/src/modules/returns/service.js @@ -1,6 +1,6 @@ import { ValidationError } from '../../lib/errors.js'; import { priceLines } from '../billing/service.js'; -import { withTaxAdjustment, withinWindow } from './policy.js'; +import { withinWindow } from './policy.js'; import { RETURN_WINDOW_DAYS, REASONS } from './constants.js'; // Refund for the returned lines of an order, in integer cents. @@ -8,7 +8,7 @@ export function computeRefund(order, returnedLineIds) { const lines = order.lines.filter((l) => returnedLineIds.includes(l.id)); if (lines.length === 0) throw new ValidationError('no returned lines'); const priced = priceLines(lines, { discountCode: order.discountCode, region: order.region }); - return withTaxAdjustment(priced.total, priced, order); + return priced.total; // total already includes tax on the discounted amount } export function canReturn(order, today, reason) { diff --git a/src/modules/scheduler/service.js b/src/modules/scheduler/service.js index 46d57be..1e72ded 100644 --- a/src/modules/scheduler/service.js +++ b/src/modules/scheduler/service.js @@ -4,6 +4,7 @@ import { insertJob, findJob, allJobs, saveJob } from './repo.js'; import { validateJob } from './validators.js'; import { emitJobChanged } from './events.js'; import { STATUSES } from './constants.js'; +import { config } from '../../config.js'; export function createJob(input) { const row = insertJob(validateJob(input)); @@ -31,8 +32,6 @@ export function setJobStatus(id, status) { return row; } -const tz = process.env.SCHED_TZ || 'UTC'; - export function timezone() { - return tz; + return config.schedTz; } diff --git a/src/modules/search/service.js b/src/modules/search/service.js index 182febb..0c59abe 100644 --- a/src/modules/search/service.js +++ b/src/modules/search/service.js @@ -4,6 +4,7 @@ import { insertSavedSearch, findSavedSearch, allSavedSearches, saveSavedSearch } import { validateSavedSearch } from './validators.js'; import { emitSavedSearchChanged } from './events.js'; import { STATUSES } from './constants.js'; +import { config } from '../../config.js'; export function createSavedSearch(input) { const row = insertSavedSearch(validateSavedSearch(input)); @@ -31,13 +32,11 @@ export function setSavedSearchStatus(id, status) { return row; } -const MAX_RESULTS = parseInt(process.env.SEARCH_MAX_RESULTS || '50', 10); - export function maxResults() { - return MAX_RESULTS; + return config.searchMaxResults; } export function searchAll(items, term) { const t = String(term ?? '').toLowerCase(); - return items.filter((x) => JSON.stringify(x).toLowerCase().includes(t)).slice(0, MAX_RESULTS); + return items.filter((x) => JSON.stringify(x).toLowerCase().includes(t)).slice(0, config.searchMaxResults); } diff --git a/src/modules/webhooks/service.js b/src/modules/webhooks/service.js index b5faf53..a5d3203 100644 --- a/src/modules/webhooks/service.js +++ b/src/modules/webhooks/service.js @@ -4,6 +4,7 @@ import { insertEndpoint, findEndpoint, allEndpoints, saveEndpoint } from './repo import { validateEndpoint } from './validators.js'; import { emitEndpointChanged } from './events.js'; import { STATUSES } from './constants.js'; +import { config } from '../../config.js'; export function createEndpoint(input) { const row = insertEndpoint(validateEndpoint(input)); @@ -32,5 +33,5 @@ export function setEndpointStatus(id, status) { } export function timeoutMs() { - return Number(process.env.WEBHOOK_TIMEOUT_MS ?? '5000'); + return config.webhookTimeoutMs; } diff --git a/src/server.js b/src/server.js index 939998d..f5e458c 100644 --- a/src/server.js +++ b/src/server.js @@ -1,3 +1,4 @@ +import { config } from './config.js'; import { createRouter } from './lib/router.js'; import * as accounts from './modules/accounts/index.js'; import * as auth from './modules/auth/index.js'; @@ -20,7 +21,7 @@ const MODULES = [accounts, auth, billing, catalog, inventory, orders, shipping, notifications, reports, search, audit, webhooks, scheduler, tenants, files]; export function port() { - return Number(process.env.PORT ?? 8080); + return config.port; } export function createApp() { diff --git a/tests/returns.test.js b/tests/returns.test.js index 21088b2..de588d3 100644 --- a/tests/returns.test.js +++ b/tests/returns.test.js @@ -11,6 +11,11 @@ test('returns: refund for one line, no discount', () => { assert.equal(computeRefund(order, ['a']), 1070); }); +test('returns: discounted refund includes tax once, not twice', () => { + // line a: 1000 - 10% = 900, tax 7% = 63 -> 963 + assert.equal(computeRefund({ ...order, discountCode: 'save10' }, ['a']), 963); +}); + test('returns: return window and reasons', () => { assert.equal(canReturn(order, '2026-10-20', 'damaged'), true); assert.equal(canReturn(order, '2026-11-20', 'damaged'), false);