@dooer/dooer-test-env 1.2.3 → 1.4.0
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/lib/account.js +66 -5
- package/lib/command/customer.js +18 -1
- package/lib/command/service.js +2 -1
- package/lib/compose/generate.js +19 -5
- package/lib/compose/manifests.js +6 -1
- package/package.json +1 -1
package/lib/account.js
CHANGED
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
// - service_accounts.subscriptions the same active set Ghost Inspector has on staging
|
|
9
9
|
// - service_accounts."partnerOrganization" the org placed under partner `dooer` by default
|
|
10
10
|
// - service_accounts."tosAcceptance" the current Terms of Service pre-accepted (so sumify doesn't block)
|
|
11
|
-
//
|
|
11
|
+
// - service_ledger.fiscal_year the CURRENT year, + default company information written to
|
|
12
|
+
// service_accounts."temporalKeyValue" both the companies columns and temporalKeyValue
|
|
13
|
+
// (disable the whole block with --no-fiscal-year)
|
|
14
|
+
// It does NOT add customer data (documents, verifications, tasks, …) — the account starts empty.
|
|
12
15
|
|
|
13
16
|
const crypto = require('crypto')
|
|
14
17
|
const { Client } = require('pg')
|
|
@@ -19,6 +22,16 @@ const SUBSCRIPTION_TYPES = ['accounting', 'billing', 'salaries', 'sales', 'prebo
|
|
|
19
22
|
// The partner every new test account is placed under by default (Jimmy 2026-09-01).
|
|
20
23
|
const DEFAULT_PARTNER_DOMAIN = 'dooer'
|
|
21
24
|
|
|
25
|
+
// Company-information defaults, applied unless --no-fiscal-year (Jimmy 2026-09-03). A brand-new account
|
|
26
|
+
// otherwise has no fiscal year and blank registration fields, so nothing can be booked and HQ's
|
|
27
|
+
// /customers/<org>/information page is empty. Each value is written BOTH as a companies column (what that
|
|
28
|
+
// page edits) AND as a service_accounts."temporalKeyValue" row (the temporal source the platform reads),
|
|
29
|
+
// plus the service_ledger.fiscal_year row itself.
|
|
30
|
+
const currentYearDates = () => {
|
|
31
|
+
const year = new Date().getFullYear()
|
|
32
|
+
return { year, start: `${year}-01-01`, end: `${year}-12-31` }
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
const uuid = () => crypto.randomUUID()
|
|
23
36
|
// A plausible unique 10-digit Swedish org number (556XXXXXXX) — dev placeholder, not a real registration.
|
|
24
37
|
const orgNumber = () => `556${String(crypto.randomInt(0, 1e7)).padStart(7, '0')}`
|
|
@@ -44,9 +57,10 @@ async function uniqueSlug(client, name) {
|
|
|
44
57
|
return `${base}-${crypto.randomBytes(3).toString('hex')}`
|
|
45
58
|
}
|
|
46
59
|
|
|
47
|
-
async function createAccount({ local, name, ownerUserId, execute = false } = {}) {
|
|
60
|
+
async function createAccount({ local, name, ownerUserId, execute = false, fiscalYear = true } = {}) {
|
|
48
61
|
if (!name) throw new Error('customer new: a name is required')
|
|
49
62
|
if (!ownerUserId) throw new Error('customer new: --owner <userId> is required')
|
|
63
|
+
const fy = currentYearDates()
|
|
50
64
|
|
|
51
65
|
const client = new Client({ ...local, ssl: local.ssl === false ? undefined : { rejectUnauthorized: false } })
|
|
52
66
|
await client.connect()
|
|
@@ -81,6 +95,7 @@ async function createAccount({ local, name, ownerUserId, execute = false } = {})
|
|
|
81
95
|
ownerType,
|
|
82
96
|
partner: DEFAULT_PARTNER_DOMAIN,
|
|
83
97
|
subscriptions: SUBSCRIPTION_TYPES,
|
|
98
|
+
fiscalYear: fiscalYear ? { start: fy.start, end: fy.end } : null,
|
|
84
99
|
warning,
|
|
85
100
|
}
|
|
86
101
|
if (!execute) {
|
|
@@ -93,11 +108,29 @@ async function createAccount({ local, name, ownerUserId, execute = false } = {})
|
|
|
93
108
|
await client.query(
|
|
94
109
|
// Populate a placeholder address: some org endpoints serialize organization.address as a required
|
|
95
110
|
// OBJECT and return null (schema violation, seen in sumify) when all address columns are empty.
|
|
111
|
+
// The registration/fiscal-year columns are NULL unless the defaults are enabled (--no-fiscal-year).
|
|
96
112
|
`INSERT INTO service_accounts.companies
|
|
97
113
|
(companies_pk, fk_countries_pk, fk_company_types_pk, company_name, short_name, organization_number,
|
|
98
|
-
api_uuid, language, "timeZone", currency, address, postal_code, city,
|
|
99
|
-
|
|
100
|
-
|
|
114
|
+
api_uuid, language, "timeZone", currency, address, postal_code, city,
|
|
115
|
+
bookkeeping_start_date, skv_fiscal_year_start, skv_fiscal_year_end,
|
|
116
|
+
is_registered_for_vat, vat_registration_date, registered_for_f_tax, is_employer,
|
|
117
|
+
inserted_at, updated_at)
|
|
118
|
+
VALUES ($1,'SE','AB',$2,$3,$4,$5,'sv','Europe/Stockholm','SEK','Testgatan 1','11111','Stockholm',
|
|
119
|
+
$6,$7,$8,$9,$10,$11,$12,now(),now())`,
|
|
120
|
+
[
|
|
121
|
+
orgId,
|
|
122
|
+
name,
|
|
123
|
+
shortName,
|
|
124
|
+
orgNumber(),
|
|
125
|
+
uuid(),
|
|
126
|
+
fiscalYear ? fy.start : null, // bookkeeping_start_date
|
|
127
|
+
fiscalYear ? fy.start : null, // skv_fiscal_year_start
|
|
128
|
+
fiscalYear ? fy.end : null, // skv_fiscal_year_end
|
|
129
|
+
fiscalYear ? true : null, // is_registered_for_vat
|
|
130
|
+
fiscalYear ? fy.start : null, // vat_registration_date
|
|
131
|
+
fiscalYear ? true : null, // registered_for_f_tax
|
|
132
|
+
fiscalYear ? true : null, // is_employer
|
|
133
|
+
]
|
|
101
134
|
)
|
|
102
135
|
await client.query(
|
|
103
136
|
`INSERT INTO service_accounts.companies2users
|
|
@@ -131,6 +164,34 @@ async function createAccount({ local, name, ownerUserId, execute = false } = {})
|
|
|
131
164
|
)
|
|
132
165
|
}
|
|
133
166
|
plan.tosAccepted = activeTos.rowCount
|
|
167
|
+
|
|
168
|
+
if (fiscalYear) {
|
|
169
|
+
// The fiscal year itself (service-ledger owns it; HQ's "Räkenskapsår" + all booking depends on it).
|
|
170
|
+
await client.query(
|
|
171
|
+
`INSERT INTO service_ledger.fiscal_year (id, company_id, start_date, end_date, created_at, closed)
|
|
172
|
+
VALUES ($1,$2,$3,$4,now(),false)`,
|
|
173
|
+
[uuid(), orgId, fy.start, fy.end]
|
|
174
|
+
)
|
|
175
|
+
// The temporal source of truth mirroring the columns set above. `fiscalYear` carries the period in
|
|
176
|
+
// both the value and the row's own start/endDate (matching how ibSync writes it); the registration
|
|
177
|
+
// flags are open-ended from the start of the year.
|
|
178
|
+
const tkv = [
|
|
179
|
+
{ key: 'fiscalYear', value: { start: fy.start, end: fy.end }, startDate: fy.start, endDate: fy.end },
|
|
180
|
+
{ key: 'hasVatRegistration', value: true, startDate: fy.start, endDate: null },
|
|
181
|
+
{ key: 'hasCompanyTax', value: true, startDate: fy.start, endDate: null }, // F-skatt
|
|
182
|
+
{ key: 'hasEmployeeRegistration', value: true, startDate: fy.start, endDate: null },
|
|
183
|
+
]
|
|
184
|
+
for (const t of tkv) {
|
|
185
|
+
await client.query(
|
|
186
|
+
`INSERT INTO service_accounts."temporalKeyValue"
|
|
187
|
+
(id, "organizationId", key, value, "startDate", "endDate", "setBySource", "createdAt", "updatedAt")
|
|
188
|
+
VALUES ($1,$2,$3,$4::jsonb,$5,$6,'user',now(),now())`,
|
|
189
|
+
[uuid(), orgId, t.key, JSON.stringify(t.value), t.startDate, t.endDate]
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
plan.temporalKeys = tkv.map((t) => t.key)
|
|
193
|
+
}
|
|
194
|
+
|
|
134
195
|
await client.query('COMMIT')
|
|
135
196
|
return plan
|
|
136
197
|
} catch (e) {
|
package/lib/command/customer.js
CHANGED
|
@@ -87,11 +87,23 @@ module.exports = {
|
|
|
87
87
|
y2
|
|
88
88
|
.positional('name', { type: 'string', describe: 'company name for the new account' })
|
|
89
89
|
.option('owner', { type: 'string', demandOption: true, describe: 'user (users_pk) to set as Owner' })
|
|
90
|
+
.option('fiscal-year', {
|
|
91
|
+
type: 'boolean',
|
|
92
|
+
default: true,
|
|
93
|
+
describe:
|
|
94
|
+
'create the current-year fiscal year + default company information (VAT/F-skatt/employer registered, bookkeeping start). --no-fiscal-year leaves all of it blank',
|
|
95
|
+
})
|
|
90
96
|
.option('port', { type: 'number', default: 55432, describe: 'local Postgres port' })
|
|
91
97
|
.option('execute', { type: 'boolean', default: false, describe: 'actually create (default: dry-run)' }),
|
|
92
98
|
handler: async (argv) => {
|
|
93
99
|
const local = { host: 'localhost', port: argv.port, user: 'dooer', password: 'dooer', database: 'dooer' }
|
|
94
|
-
const res = await createAccount({
|
|
100
|
+
const res = await createAccount({
|
|
101
|
+
local,
|
|
102
|
+
name: argv.name,
|
|
103
|
+
ownerUserId: argv.owner,
|
|
104
|
+
execute: argv.execute,
|
|
105
|
+
fiscalYear: argv.fiscalYear,
|
|
106
|
+
})
|
|
95
107
|
if (argv.execute) {
|
|
96
108
|
console.log(chalk.green(`\ncreated account "${res.name}"`))
|
|
97
109
|
console.log(` org id: ${res.orgId}`)
|
|
@@ -100,6 +112,11 @@ module.exports = {
|
|
|
100
112
|
console.log(` partner: ${res.partner}`)
|
|
101
113
|
console.log(` subscriptions: ${SUBSCRIPTION_TYPES.join(', ')} (active)`)
|
|
102
114
|
console.log(` TOS: ${res.tosAccepted} version(s) pre-accepted`)
|
|
115
|
+
console.log(
|
|
116
|
+
res.fiscalYear
|
|
117
|
+
? ` fiscal year: ${res.fiscalYear.start} → ${res.fiscalYear.end} (+ VAT/F-skatt/employer registered; ${res.temporalKeys.length} temporal keys)`
|
|
118
|
+
: ` fiscal year: none (--no-fiscal-year)`
|
|
119
|
+
)
|
|
103
120
|
}
|
|
104
121
|
if (res.warning) console.log(chalk.yellow(`\n⚠ ${res.warning}`))
|
|
105
122
|
},
|
package/lib/command/service.js
CHANGED
|
@@ -79,7 +79,8 @@ module.exports = {
|
|
|
79
79
|
command: 'deploy <name> <tag>',
|
|
80
80
|
describe: 'switch a service to a published registry version/tag (upgrade/downgrade)',
|
|
81
81
|
handler: (a) => {
|
|
82
|
-
if (!a.tag || typeof a.tag !== 'string')
|
|
82
|
+
if (!a.tag || typeof a.tag !== 'string')
|
|
83
|
+
throw new Error('missing image tag, e.g. `service deploy <name> 1.2.3`')
|
|
83
84
|
const base = imageFor(a.name, rt.SERVICES_DIR)
|
|
84
85
|
if (!base) throw new Error(`unknown service ${a.name} (no manifest image)`)
|
|
85
86
|
const image = `${base.replace(/:[^:/]+$/, '')}:${a.tag}`
|
package/lib/compose/generate.js
CHANGED
|
@@ -18,6 +18,7 @@ const AUTH_PRIVATE_KEY_PEM = Buffer.from(AUTH_PRIVATE_KEY_B64, 'base64').toStrin
|
|
|
18
18
|
|
|
19
19
|
// The narrower `booking` profile — the minimum set to drive a booking end to end.
|
|
20
20
|
const BOOKING_PROFILE_SERVICES = [
|
|
21
|
+
'public-facade', // the browser-facing entrypoint (GraphQL + REST passthrough) — frontends need it
|
|
21
22
|
'service-graphql',
|
|
22
23
|
'service-core-objects',
|
|
23
24
|
'service-accounts',
|
|
@@ -101,6 +102,12 @@ const SERVICE_ENV_OVERRIDES = {
|
|
|
101
102
|
// library's built-in TEST cert (appapi2.test.bankid.com). Present → `up` also sets IS_PRODUCTION=true, so
|
|
102
103
|
// local login uses PRODUCTION BankID exactly like staging (@dooer/config env override beats the
|
|
103
104
|
// per-environment `local:false`). See lib/bankid.js.
|
|
105
|
+
'public-facade': {
|
|
106
|
+
// The manifest ships DOOER_HOST_PROTOCOL="" — an EMPTY value that overrides the config default
|
|
107
|
+
// ("http"), so @dooer/facade builds protocol-less upstream URLs and falls back to port 80
|
|
108
|
+
// (ECONNREFUSED ::1:80 → 502 on every proxied route). Restore the default explicitly.
|
|
109
|
+
DOOER_HOST_PROTOCOL: 'http',
|
|
110
|
+
},
|
|
104
111
|
'service-accounts': {
|
|
105
112
|
// service-accounts is the only token SIGNER — give it the LOCAL dev private key matching the public key
|
|
106
113
|
// forced on every verifier (GLOBAL_OVERRIDES.DOOER_AUTH_PUBLIC_KEY). Non-secret dev material.
|
|
@@ -292,7 +299,11 @@ function infraServices() {
|
|
|
292
299
|
}
|
|
293
300
|
|
|
294
301
|
// Host ports published so the BROWSER (and dev) can reach things on localhost.
|
|
295
|
-
|
|
302
|
+
// public-facade is the browser-facing entrypoint (api.dooer.com / api.s-e032.com): it serves GraphQL AND
|
|
303
|
+
// the REST passthrough (e.g. /v1/companies/:org/documents/:doc/pages/:page image URLs). service-graphql is
|
|
304
|
+
// an internal backend it fronts — NOT the facade — so it gets its own port for direct dev access.
|
|
305
|
+
const FACADE_HOST_PORT = 4000 // public-facade → DOOER_PUBLIC_FACADE_HOST / DOOER_GRAPHQL_HOST
|
|
306
|
+
const GRAPHQL_HOST_PORT = 4001 // service-graphql (direct access; frontends go through the facade)
|
|
296
307
|
const HQ_HOST_PORT = 8080 // frontend-hq
|
|
297
308
|
|
|
298
309
|
// host port for a frontend: HQ is pinned to 8080; the rest get 8081+ in sorted order.
|
|
@@ -327,7 +338,9 @@ const FRONTEND_URL_ENV = {
|
|
|
327
338
|
// cross-frontend base URL → its frontend's own host port. Only rewrites keys already present (from the
|
|
328
339
|
// manifest) whose target frontend has a known port. Applied to services AND frontends.
|
|
329
340
|
function rewriteBrowserUrls(env, portMap) {
|
|
330
|
-
|
|
341
|
+
// Both point at public-facade (on staging both are https://api.s-e032.com) — not at service-graphql.
|
|
342
|
+
if ('DOOER_PUBLIC_FACADE_HOST' in env) env.DOOER_PUBLIC_FACADE_HOST = `http://localhost:${FACADE_HOST_PORT}`
|
|
343
|
+
if ('DOOER_GRAPHQL_HOST' in env) env.DOOER_GRAPHQL_HOST = `http://localhost:${FACADE_HOST_PORT}`
|
|
331
344
|
for (const [key, frontendName] of Object.entries(FRONTEND_URL_ENV)) {
|
|
332
345
|
if (key in env && portMap[frontendName]) env[key] = `http://localhost:${portMap[frontendName]}`
|
|
333
346
|
}
|
|
@@ -357,8 +370,9 @@ function buildCompose(services, frontends, { profile } = {}) {
|
|
|
357
370
|
// (Consul path) which hangs on consul-template.
|
|
358
371
|
command: svc.command || ['./CMD-OPF.sh'],
|
|
359
372
|
environment: rewriteBrowserUrls({ ...buildServiceEnv(svc), ...(SERVICE_ENV_OVERRIDES[svc.name] || {}) }, portMap),
|
|
360
|
-
// publish the
|
|
361
|
-
...(svc.name === '
|
|
373
|
+
// publish the browser-facing facade (4000) and service-graphql (4001) on the host
|
|
374
|
+
...(svc.name === 'public-facade' ? { ports: [`${FACADE_HOST_PORT}:3000`] } : {}),
|
|
375
|
+
...(svc.name === 'service-graphql' ? { ports: [`${GRAPHQL_HOST_PORT}:3000`] } : {}),
|
|
362
376
|
// legacy-name DNS aliases (e.g. service-periods → service-closing), so callers resolving the old name
|
|
363
377
|
// reach this container on the compose network.
|
|
364
378
|
...(SERVICE_ALIASES[svc.name] ? { networks: { default: { aliases: SERVICE_ALIASES[svc.name] } } } : {}),
|
|
@@ -382,7 +396,7 @@ function buildCompose(services, frontends, { profile } = {}) {
|
|
|
382
396
|
// Repoint the facade + every cross-frontend link at the right local ports. Each base URL points at the
|
|
383
397
|
// frontend that SERVES it (e.g. DOOER_PUBLIC_FRONTEND_BASE_URL → frontend-sumify-neue's port), never at
|
|
384
398
|
// this app's own port — that was the bug that sent HQ's "open go.dooer.com" button to localhost:8080.
|
|
385
|
-
if (!('DOOER_PUBLIC_FACADE_HOST' in env)) env.DOOER_PUBLIC_FACADE_HOST = `http://localhost:${
|
|
399
|
+
if (!('DOOER_PUBLIC_FACADE_HOST' in env)) env.DOOER_PUBLIC_FACADE_HOST = `http://localhost:${FACADE_HOST_PORT}`
|
|
386
400
|
rewriteBrowserUrls(env, portMap)
|
|
387
401
|
composeServices[fe.name] = {
|
|
388
402
|
image: fe.image,
|
package/lib/compose/manifests.js
CHANGED
|
@@ -19,6 +19,11 @@ const DEFAULT_SERVICES_DIR = path.resolve(
|
|
|
19
19
|
|
|
20
20
|
const SERVICE_FILE_RE = /^service-.*\.yaml$/
|
|
21
21
|
const FRONTEND_FILE_RE = /^frontend-.*\.yaml$/
|
|
22
|
+
// Backend services whose manifest filename doesn't match `service-*.yaml` but which are real runnable
|
|
23
|
+
// Deployments we must include. public-facade is the browser-facing REST+GraphQL entrypoint
|
|
24
|
+
// (DOOER_PUBLIC_FACADE_HOST); without it, document page images (/v1/…/pages/…) and other REST passthrough
|
|
25
|
+
// 404. (Jimmy 2026-09-03.)
|
|
26
|
+
const EXTRA_SERVICE_FILES = ['public-facade.yaml']
|
|
22
27
|
|
|
23
28
|
// Pull the application container out of a Deployment doc. Containers are named
|
|
24
29
|
// `node` across the fleet; fall back to the first container defensively.
|
|
@@ -67,7 +72,7 @@ function listServices(servicesDir) {
|
|
|
67
72
|
const dir = servicesDir || DEFAULT_SERVICES_DIR
|
|
68
73
|
return fs
|
|
69
74
|
.readdirSync(dir)
|
|
70
|
-
.filter((f) => SERVICE_FILE_RE.test(f))
|
|
75
|
+
.filter((f) => SERVICE_FILE_RE.test(f) || EXTRA_SERVICE_FILES.includes(f))
|
|
71
76
|
.sort()
|
|
72
77
|
.map((f) => parseManifestFile(path.join(dir, f)))
|
|
73
78
|
.filter(Boolean)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dooer/dooer-test-env",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Run the whole Dooer backend locally (staging DB minus customers), copy/purge customers between environments, and shred — one CLI.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": "Dooer/cli-dooer-test-env",
|