diff --git a/ENV_READS.md b/ENV_READS.md new file mode 100644 index 0000000..907c734 --- /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/notifications/service.js | SMTP_HOST | localhost | +| src/modules/notifications/service.js | SMTP_PORT | 2525 | +| src/modules/webhooks/service.js | WEBHOOK_TIMEOUT_MS | 5000 | +| src/modules/files/repo.js | FILES_BUCKET | local-bucket | +| src/modules/billing/constants.js | TAX_REGION | US | +| src/modules/auth/service.js | AUTH_SECRET | dev-secret | +| src/modules/auth/service.js | TOKEN_TTL_S | 3600 | +| src/modules/scheduler/service.js | SCHED_TZ | UTC | +| src/modules/reports/formatters.js | REPORT_LOCALE | en-US | +| src/modules/search/service.js | SEARCH_MAX_RESULTS | 50 | +| src/modules/inventory/service.js | LOW_STOCK_THRESHOLD | 5 | diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..bd42416 --- /dev/null +++ b/src/config.js @@ -0,0 +1,18 @@ +// Single place where environment variables are read. Getters keep reads lazy. +const env = process.env; + +export const config = { + get logLevel() { return env.LOG_LEVEL || 'info'; }, + get port() { return Number(env.PORT ?? 8080); }, + get smtpHost() { return env.SMTP_HOST || 'localhost'; }, + get smtpPort() { return Number(env.SMTP_PORT || 2525); }, + get webhookTimeoutMs() { return Number(env.WEBHOOK_TIMEOUT_MS ?? '5000'); }, + get filesBucket() { return env.FILES_BUCKET || 'local-bucket'; }, + get taxRegion() { return env.TAX_REGION || 'US'; }, + get authSecret() { return env.AUTH_SECRET || 'dev-secret'; }, + get tokenTtlS() { return env.TOKEN_TTL_S ?? '3600'; }, + get schedTz() { return env.SCHED_TZ || 'UTC'; }, + get reportLocale() { return env.REPORT_LOCALE ?? 'en-US'; }, + get searchMaxResults() { return parseInt(env.SEARCH_MAX_RESULTS || '50', 10); }, + get lowStockThreshold() { return Number(env.LOW_STOCK_THRESHOLD ?? 5); }, +}; 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..3d21c4c 100644 --- a/src/modules/auth/service.js +++ b/src/modules/auth/service.js @@ -1,3 +1,4 @@ +import { config } from '../../config.js'; import { NotFoundError } from '../../lib/errors.js'; import { log } from '../../lib/log.js'; import { insertSession, findSession, allSessions, saveSession } from './repo.js'; @@ -33,13 +34,10 @@ 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; - 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..8455cf1 100644 --- a/src/modules/files/repo.js +++ b/src/modules/files/repo.js @@ -1,3 +1,4 @@ +import { config } from '../../config.js'; import { nextId } from '../../lib/ids.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..80fe87d 100644 --- a/src/modules/inventory/service.js +++ b/src/modules/inventory/service.js @@ -1,3 +1,4 @@ +import { config } from '../../config.js'; import { NotFoundError } from '../../lib/errors.js'; import { log } from '../../lib/log.js'; import { insertStockItem, findStockItem, allStockItems, saveStockItem } from './repo.js'; @@ -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..f6dd548 100644 --- a/src/modules/notifications/service.js +++ b/src/modules/notifications/service.js @@ -1,3 +1,4 @@ +import { config } from '../../config.js'; import { NotFoundError } from '../../lib/errors.js'; import { log } from '../../lib/log.js'; import { insertNotice, findNotice, allNotices, saveNotice } from './repo.js'; @@ -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..e85f29d 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,8 @@ 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); + // priced.total is already net of the discount and includes tax on the discounted amount. + return priced.total; } export function canReturn(order, today, reason) { diff --git a/src/modules/scheduler/service.js b/src/modules/scheduler/service.js index 46d57be..6604f0c 100644 --- a/src/modules/scheduler/service.js +++ b/src/modules/scheduler/service.js @@ -1,3 +1,4 @@ +import { config } from '../../config.js'; import { NotFoundError } from '../../lib/errors.js'; import { log } from '../../lib/log.js'; import { insertJob, findJob, allJobs, saveJob } from './repo.js'; @@ -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..7959c33 100644 --- a/src/modules/search/service.js +++ b/src/modules/search/service.js @@ -1,3 +1,4 @@ +import { config } from '../../config.js'; import { NotFoundError } from '../../lib/errors.js'; import { log } from '../../lib/log.js'; import { insertSavedSearch, findSavedSearch, allSavedSearches, saveSavedSearch } from './repo.js'; @@ -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..0d47043 100644 --- a/src/modules/webhooks/service.js +++ b/src/modules/webhooks/service.js @@ -1,3 +1,4 @@ +import { config } from '../../config.js'; import { NotFoundError } from '../../lib/errors.js'; import { log } from '../../lib/log.js'; import { insertEndpoint, findEndpoint, allEndpoints, saveEndpoint } from './repo.js'; @@ -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..152d1dc 100644 --- a/tests/returns.test.js +++ b/tests/returns.test.js @@ -11,6 +11,13 @@ test('returns: refund for one line, no discount', () => { assert.equal(computeRefund(order, ['a']), 1070); }); +test('returns: discounted refund includes tax once and never exceeds what was paid', () => { + const discounted = { ...order, discountCode: 'SAVE10' }; + // 1000 - 10% = 900, plus 7% tax on 900 = 63 -> 963 + assert.equal(computeRefund(discounted, ['a']), 963); + assert.ok(computeRefund(discounted, ['a']) <= computeRefund(order, ['a'])); +}); + test('returns: return window and reasons', () => { assert.equal(canReturn(order, '2026-10-20', 'damaged'), true); assert.equal(canReturn(order, '2026-11-20', 'damaged'), false);