@final-commerce/common 1.1.4-beta.4

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 ADDED
@@ -0,0 +1,224 @@
1
+ # @final-commerce/common
2
+
3
+ Shared utilities, types, and constants for Final Commerce applications. This package provides framework-agnostic functionality that works across both frontend (React, Vue, etc.) and backend (Node.js, NestJS) environments.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @final-commerce/common
9
+ ```
10
+
11
+ ## Imports
12
+
13
+ Everything is re-exported from the package root:
14
+
15
+ ```typescript
16
+ import { CurrencyCode, toMinorUnits, formatCurrency, Amount } from '@final-commerce/common';
17
+ ```
18
+
19
+ Sub-path imports also work:
20
+
21
+ ```typescript
22
+ import { toMinorUnits } from '@final-commerce/common/dist/utils/currency.util';
23
+ import { CurrencyCode } from '@final-commerce/common/dist/enums';
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Enums
29
+
30
+ ### `CurrencyCode`
31
+
32
+ ISO 4217 currency codes. 41 currencies across three decimal groups:
33
+
34
+ | Decimal Places | Currencies |
35
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
36
+ | **2** (standard) | USD, EUR, GBP, CAD, AUD, NZD, CHF, CNY, INR, MXN, BRL, ZAR, SGD, HKD, SEK, NOK, DKK, PLN, THB, MYR, PHP, IDR, AED, SAR, ILS, TRY, RUB |
37
+ | **0** (zero-decimal) | JPY, KRW, VND, CLP, ISK, HUF, TWD |
38
+ | **3** (three-decimal) | KWD, BHD, OMR, JOD, TND, LYD, IQD |
39
+
40
+ ## Interfaces
41
+
42
+ ### `Amount`
43
+
44
+ Represents a monetary value stored in the database.
45
+
46
+ ```typescript
47
+ { amount: 3122, currency: 'USD', minorUnits: 2 } // = $31.22
48
+ ```
49
+
50
+ | Field | Type | Description |
51
+ | ------------ | -------------- | --------------------------------- |
52
+ | `amount` | `number` | Value in minor units (e.g. cents) |
53
+ | `currency` | `CurrencyCode` | ISO 4217 code |
54
+ | `minorUnits` | `number` | Decimal places (2, 0, or 3) |
55
+
56
+ ### `CurrencyConfig`
57
+
58
+ | Currency | Minor Units | Symbol | Example |
59
+ | -------- | ----------- | ------ | -------- |
60
+ | USD | 2 | $ | $10.50 |
61
+ | EUR | 2 | € | €10.50 |
62
+ | GBP | 2 | £ | £10.50 |
63
+ | JPY | 0 | ¥ | ¥1000 |
64
+ | KWD | 3 | KD | KD10.500 |
65
+
66
+ | Field | Type | Description |
67
+ | ---------------- | ---------------------- | ------------------------------------ |
68
+ | `minorUnits` | `number` | Decimal places |
69
+ | `symbol` | `string` | Display symbol (`$`, `€`, `¥`, etc.) |
70
+ | `symbolPosition` | `'prefix' \| 'suffix'` | Where the symbol goes |
71
+
72
+ ---
73
+
74
+ ## Utility Functions
75
+
76
+ ### Conversion
77
+
78
+ | Function | In | Out | What it does |
79
+ | ---------------------------------------------- | ------------------ | ------ | ------------------------------------------------------------ |
80
+ | `toMinorUnits(amount, currency)` | `(10.50, 'USD')` | `1050` | Major to minor units |
81
+ | `fromMinorUnits(minorAmount, currency)` | `(1050, 'USD')` | `10.5` | Minor to major units |
82
+ | `stringifyMinorUnits(minorAmount, minorUnits)` | `(1050, 2)` | `10.5` | Minor to major using decimal count directly |
83
+ | `parseAmountToMinorUnits(str, currency)` | `('10.50', 'USD')` | `1050` | Parse string input to minor units. Returns `null` on failure |
84
+
85
+ ### Formatting
86
+
87
+ | Function | In | Out | What it does |
88
+ | --------------------------------------------------- | --------------- | ---------- | ----------------------------------------------------------------------------------------------- |
89
+ | `formatCurrency(minorAmount, currency, options?)` | `(1050, 'USD')` | `'$10.50'` | Display string with symbol and grouping |
90
+ | `formatMinorUnitsToString(minorAmount, minorUnits)` | `(1050, 2)` | `'10.5'` | Plain decimal string (no symbol, no trailing zeros). Useful for API payloads (e.g. WooCommerce) |
91
+
92
+ `formatCurrency` options:
93
+
94
+ - `includeSymbol` (default: `true`) — include currency symbol
95
+ - `useGrouping` (default: `true`) — thousands separators
96
+
97
+ ### Arithmetic
98
+
99
+ All inputs and outputs are in minor units (integers).
100
+
101
+ | Function | Example | Result |
102
+ | ------------------------------------ | ------------- | ------ |
103
+ | `addAmounts(a, b)` | `(1050, 250)` | `1300` |
104
+ | `subtractAmounts(a, b)` | `(1050, 250)` | `800` |
105
+ | `multiplyAmount(amount, multiplier)` | `(1050, 2)` | `2100` |
106
+ | `divideAmount(amount, divisor)` | `(1050, 2)` | `525` |
107
+ | `calculatePercentage(amount, pct)` | `(10000, 15)` | `1500` |
108
+
109
+ `divideAmount` throws on division by zero. `calculatePercentage` takes percentage as a whole number (15 = 15%, not 0.15).
110
+
111
+ ### Config / Validation
112
+
113
+ | Function | What it does |
114
+ | ---------------------------------- | --------------------------------------------------------------------------------------------- |
115
+ | `getCurrencyConfig(currency)` | Returns `CurrencyConfig` for a code. Falls back to 2-decimal default for unknown currencies |
116
+ | `getMinorUnitMultiplier(currency)` | Returns `10^minorUnits` (100 for USD, 1 for JPY, 1000 for KWD) |
117
+ | `getMinorUnits(currency)` | Returns decimal place count for a currency |
118
+ | `isSupportedCurrency(currency)` | `true` if the currency has explicit config |
119
+ | `parseCurrencyCode(value)` | Validates a raw string is a valid `CurrencyCode`. Throws with supported codes list if invalid |
120
+
121
+ ### Amount Object Helpers
122
+
123
+ Used in BuilderHub for converting between user input and the `Amount` storage format.
124
+
125
+ | Function | Example | Result |
126
+ | ------------------------------- | ---------------------------------------------------- | -------------------------------------------------- |
127
+ | `createAmount(value, currency)` | `('31.22', 'USD')` | `{ amount: 3122, currency: 'USD', minorUnits: 2 }` |
128
+ | `amountToString(amount)` | `({ amount: 3122, currency: 'USD', minorUnits: 2 })` | `'31.22'` |
129
+
130
+ `createAmount` accepts `string | number | null | undefined`. Returns `undefined` for null, undefined, empty string, or NaN input.
131
+ `amountToString` returns `''` for null/undefined input.
132
+
133
+ ### Constants
134
+
135
+ `CURRENCY_CONFIG` — a `Record<CurrencyCode | string, CurrencyConfig>` mapping all 41 currencies to their config (symbol, decimal places, symbol position).
136
+
137
+ ---
138
+
139
+ ## Local Development
140
+
141
+ ```bash
142
+ npm install
143
+ npm run build # build with tsup (CJS + ESM + types → dist/)
144
+ npm run dev # watch mode — rebuilds on file changes
145
+ npm test # vitest in watch mode
146
+ npm run test:run # single test run
147
+ npm run test:ui # vitest UI
148
+ npm run test:coverage
149
+ ```
150
+
151
+ ### Link locally to another project
152
+
153
+ Use `npm link` to test changes in a consuming project without publishing:
154
+
155
+ ```bash
156
+ # 1. In this package (common/) — build first so dist/ is up to date
157
+ npm run build
158
+ npm link
159
+
160
+ # 2. In the consuming project (e.g. hub-api/) — clear node_modules to avoid stale/duplicate copies
161
+ rm -rf node_modules/@final-commerce/common
162
+ npm link @final-commerce/common
163
+
164
+ # 3. Verify the symlink points to your local copy
165
+ ls -la node_modules/@final-commerce/common
166
+ # Should show a symlink → /path/to/common
167
+
168
+ # 4. When done, unlink and restore the published version
169
+ npm unlink @final-commerce/common # in consumer
170
+ npm install # reinstall published version
171
+ npm unlink # in common/
172
+ ```
173
+
174
+ > **If you change source files in common**, run `npm run build` again (or use `npm run dev` for watch mode). The symlink means the consumer always reads from common's `dist/`, so it picks up new builds immediately.
175
+
176
+ ### Quick verification with `node -p`
177
+
178
+ After building, verify exports directly from the terminal:
179
+
180
+ ```bash
181
+ # Check the package loads
182
+ node -p "require('./dist/index.js')"
183
+
184
+ # Test a conversion
185
+ node -p "const c = require('./dist/index.js'); c.toMinorUnits(10.50, 'USD')"
186
+ # → 1050
187
+
188
+ # Test formatting
189
+ node -p "const c = require('./dist/index.js'); c.formatCurrency(1050, 'USD')"
190
+ # → $10.50
191
+
192
+ # Test zero-decimal currency
193
+ node -p "const c = require('./dist/index.js'); c.toMinorUnits(1000, 'JPY')"
194
+ # → 1000
195
+
196
+ # Test three-decimal currency
197
+ node -p "const c = require('./dist/index.js'); c.toMinorUnits(10.5, 'KWD')"
198
+ # → 10500
199
+
200
+ # Test Amount helpers
201
+ node -p "const c = require('./dist/index.js'); c.createAmount('31.22', 'USD')"
202
+ # → { amount: 3122, currency: 'USD', minorUnits: 2 }
203
+
204
+ node -p "const c = require('./dist/index.js'); c.amountToString({ amount: 3122, currency: 'USD', minorUnits: 2 })"
205
+ # → 31.22
206
+
207
+ # List all exported keys
208
+ node -p "Object.keys(require('./dist/index.js'))"
209
+ ```
210
+
211
+ ### Verify from a linked project
212
+
213
+ After `npm link`, verify the link works in the consuming project:
214
+
215
+ ```bash
216
+ # In the consuming project directory
217
+ node -p "require('@final-commerce/common')"
218
+ node -p "const c = require('@final-commerce/common'); c.formatCurrency(2500, 'EUR')"
219
+ # → €25.00
220
+ ```
221
+
222
+ ## License
223
+
224
+ Private — Final Commerce internal use only.
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gate-done — pre-push hook.
4
+ *
5
+ * Runs four sweeps inside a 90-second wall-clock budget:
6
+ *
7
+ * 1. npm audit — known CVE check against the installed dependency tree
8
+ * 2. gitleaks — credential / secret scanning (git log + staged)
9
+ * 3. vitest — full test suite with coverage threshold enforcement
10
+ * 4. bundle — size-limit or custom bundle-size script (if configured)
11
+ *
12
+ * Any sweep failure exits 1, blocking the push.
13
+ * Budget overrun also exits 1.
14
+ *
15
+ * Install as Husky pre-push hook:
16
+ * echo "npx gate-done" > .husky/pre-push
17
+ */
18
+
19
+ import { spawnSync } from 'node:child_process';
20
+ import { readFileSync } from 'node:fs';
21
+ import { resolve } from 'node:path';
22
+
23
+ const BUDGET_MS = 90_000;
24
+ const t0 = Date.now();
25
+
26
+ // ── helpers ──────────────────────────────────────────────────────────────────
27
+
28
+ function remaining() {
29
+ return Math.max(0, BUDGET_MS - (Date.now() - t0));
30
+ }
31
+
32
+ function elapsed() {
33
+ return ((Date.now() - t0) / 1000).toFixed(1);
34
+ }
35
+
36
+ function checkBudget(label) {
37
+ if (remaining() <= 0) {
38
+ fail(`90s budget exhausted before "${label}" could complete. Push blocked.`);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Run a command, streaming output. Returns the spawnSync result.
44
+ * Passes remaining budget as the process timeout so a runaway child
45
+ * cannot exceed the wall-clock limit on its own.
46
+ */
47
+ function run(cmd) {
48
+ return spawnSync(cmd, {
49
+ shell: true,
50
+ encoding: 'utf8',
51
+ stdio: 'inherit',
52
+ timeout: remaining(),
53
+ });
54
+ }
55
+
56
+ function capture(cmd) {
57
+ return spawnSync(cmd, { shell: true, encoding: 'utf8', timeout: remaining() });
58
+ }
59
+
60
+ function fail(msg) {
61
+ process.stderr.write(`\x1b[31m✖ [${elapsed()}s] ${msg}\x1b[0m\n`);
62
+ process.exit(1);
63
+ }
64
+
65
+ function warn(msg) {
66
+ process.stdout.write(`\x1b[33m⚠ [${elapsed()}s] ${msg}\x1b[0m\n`);
67
+ }
68
+
69
+ function info(msg) {
70
+ process.stdout.write(`\x1b[36mℹ [${elapsed()}s] ${msg}\x1b[0m\n`);
71
+ }
72
+
73
+ function ok(msg) {
74
+ process.stdout.write(`\x1b[32m✔ [${elapsed()}s] ${msg}\x1b[0m\n`);
75
+ }
76
+
77
+ // ── 1. npm audit ─────────────────────────────────────────────────────────────
78
+
79
+ info('Sweep 1/4 — npm audit (known CVEs)…');
80
+ checkBudget('npm audit');
81
+
82
+ const auditResult = run('npm audit --audit-level=high');
83
+ if (auditResult.status !== 0) {
84
+ fail('npm audit found high or critical vulnerabilities. Run `npm audit` for details.');
85
+ }
86
+ ok('npm audit: no high/critical vulnerabilities.');
87
+
88
+ // ── 2. gitleaks ───────────────────────────────────────────────────────────────
89
+
90
+ info('Sweep 2/4 — gitleaks secrets scan…');
91
+ checkBudget('gitleaks');
92
+
93
+ const hasGitleaks = capture('command -v gitleaks 2>/dev/null').status === 0;
94
+
95
+ if (!hasGitleaks) {
96
+ warn(
97
+ 'gitleaks not found in PATH — secrets scan skipped.\n' +
98
+ ' Install: https://github.com/gitleaks/gitleaks#installing',
99
+ );
100
+ } else {
101
+ // Scan the full git history visible from HEAD — correct mode for a pre-push hook.
102
+ // --no-git-serial scans refs in parallel for speed within the budget.
103
+ const glResult = run('gitleaks git --exit-code 1 .');
104
+ if (glResult.status !== 0) {
105
+ fail('gitleaks detected a potential secret. Remove it and retry the push.');
106
+ }
107
+ ok('gitleaks: no secrets detected.');
108
+ }
109
+
110
+ // ── 3. vitest coverage ────────────────────────────────────────────────────────
111
+
112
+ info('Sweep 3/4 — Vitest test suite + coverage thresholds…');
113
+ checkBudget('vitest');
114
+
115
+ const vitestResult = run('npx vitest run --coverage');
116
+
117
+ if (vitestResult.status !== 0) {
118
+ fail('Vitest coverage gate failed — fix failing tests or low-coverage areas before pushing.');
119
+ }
120
+
121
+ ok('Vitest: all tests passed, coverage thresholds met.');
122
+
123
+ // ── 4. bundle size ────────────────────────────────────────────────────────────
124
+
125
+ info('Sweep 4/4 — bundle size audit…');
126
+ checkBudget('bundle audit');
127
+
128
+ const pkgPath = resolve(process.cwd(), 'package.json');
129
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
130
+
131
+ const hasBundleSizeScript = Boolean(pkg.scripts?.['bundle-size']);
132
+ const hasSizeLimit = Boolean(pkg.devDependencies?.['size-limit']) || Boolean(pkg.dependencies?.['size-limit']);
133
+ const hasBundlesize = Boolean(pkg.devDependencies?.['bundlesize']) || Boolean(pkg.dependencies?.['bundlesize']);
134
+ const hasBuildScript = Boolean(pkg.scripts?.build);
135
+
136
+ if (hasBundleSizeScript) {
137
+ const result = run('npm run bundle-size');
138
+ if (result.status !== 0) fail('bundle-size script reported a violation. Push blocked.');
139
+ ok('Bundle size: within limits.');
140
+ } else if (hasSizeLimit) {
141
+ const result = run('npx size-limit');
142
+ if (result.status !== 0) fail('size-limit reported a violation. Push blocked.');
143
+ ok('Bundle size: size-limit passed.');
144
+ } else if (hasBundlesize) {
145
+ const result = run('npx bundlesize');
146
+ if (result.status !== 0) fail('bundlesize reported a violation. Push blocked.');
147
+ ok('Bundle size: bundlesize passed.');
148
+ } else if (hasBuildScript) {
149
+ // No size tool configured — at minimum verify the build succeeds cleanly.
150
+ info('No size-limit/bundlesize configured — running build as a smoke-check.');
151
+ const result = run('npm run build');
152
+ if (result.status !== 0) fail('Build failed during gate-done. Push blocked.');
153
+ ok('Build: succeeded (no size budget configured).');
154
+ } else {
155
+ info('No build or bundle-size script found — skipping bundle sweep.');
156
+ }
157
+
158
+ // ── done ─────────────────────────────────────────────────────────────────────
159
+
160
+ ok(`gate-done completed in ${elapsed()}s (budget: 90s). Push proceeding.`);
161
+ process.exit(0);