diff --git a/ENV_READS.md b/ENV_READS.md new file mode 100644 index 0000000..8e8045b --- /dev/null +++ b/ENV_READS.md @@ -0,0 +1,17 @@ +# Environment variable reads in src/ + +| file | variable | default | +| --- | --- | --- | +| src/lib/log.js | LOG_LEVEL | `info` | +| src/server.js | PORT | `8080` | +| src/modules/webhooks/service.js | WEBHOOK_TIMEOUT_MS | `5000` | +| src/modules/files/repo.js | FILES_BUCKET | `local-bucket` | +| src/modules/scheduler/service.js | SCHED_TZ | `UTC` | +| src/modules/auth/service.js | AUTH_SECRET | `dev-secret` | +| src/modules/auth/service.js | TOKEN_TTL_S | `3600` | +| src/modules/search/service.js | SEARCH_MAX_RESULTS | `50` | +| src/modules/notifications/service.js | SMTP_HOST | `localhost` | +| src/modules/notifications/service.js | SMTP_PORT | `2525` | +| src/modules/reports/formatters.js | REPORT_LOCALE | `en-US` | +| src/modules/billing/constants.js | TAX_REGION | `US` | +| 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..1e8c9de --- /dev/null +++ b/src/config.js @@ -0,0 +1,20 @@ +// Central place for environment-driven settings. Getters read process.env on +// access, so each setting keeps the fallback semantics (|| vs ??) it had at its +// original call site. +const env = process.env; + +export const config = { + get logLevel() { return env.LOG_LEVEL || 'info'; }, + get port() { return Number(env.PORT ?? 8080); }, + get webhookTimeoutMs() { return Number(env.WEBHOOK_TIMEOUT_MS ?? '5000'); }, + get filesBucket() { return env.FILES_BUCKET || 'local-bucket'; }, + get schedTz() { return env.SCHED_TZ || 'UTC'; }, + get authSecret() { return env.AUTH_SECRET || 'dev-secret'; }, + get tokenTtlS() { return env.TOKEN_TTL_S ?? '3600'; }, + get searchMaxResults() { return parseInt(env.SEARCH_MAX_RESULTS || '50', 10); }, + get smtpHost() { return env.SMTP_HOST || 'localhost'; }, + get smtpPort() { return Number(env.SMTP_PORT || 2525); }, + get reportLocale() { return env.REPORT_LOCALE ?? 'en-US'; }, + get taxRegion() { return env.TAX_REGION || 'US'; }, + 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..7f92f92 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,8 +34,8 @@ 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; +const SECRET = config.authSecret; +const TOKEN_TTL_S = config.tokenTtlS; export function tokenTtlSeconds() { return Number(TOKEN_TTL_S); 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..25e354f 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,9 +32,8 @@ export function setNoticeStatus(id, status) { return row; } -const env = process.env; -const host = env.SMTP_HOST || 'localhost'; -const smtpPort = Number(env.SMTP_PORT || 2525); +const host = config.smtpHost; +const smtpPort = config.smtpPort; export function smtpTarget() { return { host, port: smtpPort }; diff --git a/src/modules/reports/formatters.js b/src/modules/reports/formatters.js index b0b1d6d..c876383 100644 --- a/src/modules/reports/formatters.js +++ b/src/modules/reports/formatters.js @@ -1,8 +1,10 @@ +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; +const locale = config.reportLocale; export function reportLocale() { return locale; 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..62c9263 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 already 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..a4cbcc0 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,7 +32,7 @@ export function setJobStatus(id, status) { return row; } -const tz = process.env.SCHED_TZ || 'UTC'; +const tz = config.schedTz; export function timezone() { return tz; diff --git a/src/modules/search/service.js b/src/modules/search/service.js index 182febb..ba2d795 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,7 +32,7 @@ export function setSavedSearchStatus(id, status) { return row; } -const MAX_RESULTS = parseInt(process.env.SEARCH_MAX_RESULTS || '50', 10); +const MAX_RESULTS = config.searchMaxResults; export function maxResults() { return MAX_RESULTS; 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..9ee1afb 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 does not add tax twice', () => { + // Line a: 1000 - 10% = 900, + 7% tax (63) = 963. + assert.equal(computeRefund({ ...order, discountCode: 'SAVE10' }, ['a']), 963); + // Line a: 1000 - 500 = 500, + 7% tax (35) = 535. + assert.equal(computeRefund({ ...order, discountCode: 'FLAT500' }, ['a']), 535); +}); + test('returns: return window and reasons', () => { assert.equal(canReturn(order, '2026-10-20', 'damaged'), true); assert.equal(canReturn(order, '2026-11-20', 'damaged'), false);