@dooer/dooer-test-env 1.3.0 → 1.4.1

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 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
- // It does NOT add customer data (ledger, documents, tasks, …) the account starts empty.
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, inserted_at, updated_at)
99
- VALUES ($1,'SE','AB',$2,$3,$4,$5,'sv','Europe/Stockholm','SEK','Testgatan 1','11111','Stockholm',now(),now())`,
100
- [orgId, name, shortName, orgNumber(), uuid()]
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) {
@@ -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({ local, name: argv.name, ownerUserId: argv.owner, execute: argv.execute })
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
  },
@@ -89,7 +89,10 @@ module.exports = {
89
89
  if (fs.existsSync(rt.OVERRIDE_FILE))
90
90
  ov = yaml.safeLoad(fs.readFileSync(rt.OVERRIDE_FILE, 'utf8')) || { services: {} }
91
91
  ov.services = ov.services || {}
92
- ov.services[a.name] = { image }
92
+ // Keep DOOER_SERVICE_VERSION in sync with the tag: the logging library stamps it on every log
93
+ // line (and the applicationChain), so a bare image bump leaves logs reporting the manifest's
94
+ // stale version — the same drift deploy-staging.sh exists to prevent.
95
+ ov.services[a.name] = { image, environment: { DOOER_SERVICE_VERSION: a.tag } }
93
96
  rt.ensureRunDir()
94
97
  fs.writeFileSync(rt.OVERRIDE_FILE, yaml.safeDump(ov))
95
98
  const st = rt.readState()
@@ -38,6 +38,11 @@ const DEV = {
38
38
  sqlPassword: 'dooer',
39
39
  minioUser: 'minioadmin',
40
40
  minioPassword: 'minioadmin',
41
+ // Redis MUST have a password locally, like staging (the redis-password secret). service-graphql's
42
+ // pubsub.js does `password ? createRedisClient(...).then(c => new RedisPubSub(...)) : async () => null`
43
+ // — with no password it exports a FUNCTION, and `await fn` yields the function itself, so every GraphQL
44
+ // subscription dies on `pubsubInstance.asyncIterator is not a function`. (Jimmy 2026-09-03.)
45
+ redisPassword: 'dooer',
41
46
  }
42
47
 
43
48
  // Applied to EVERY dooer service, last, so they win over the manifest values.
@@ -138,7 +143,7 @@ const OMIT = Symbol('omit-env')
138
143
  function secretDefault(name) {
139
144
  if (name === 'DOOER_SQL_APPLICATION_PASSWORD') return DEV.sqlPassword
140
145
  if (name === 'DOOER_SQL_MIGRATION_PASSWORD') return DEV.sqlPassword
141
- if (name === 'SECRET_DOOER_REDIS_PASSWORD') return '' // local redis has no auth; empty is the correct value
146
+ if (name === 'SECRET_DOOER_REDIS_PASSWORD') return DEV.redisPassword // local redis runs with --requirepass
142
147
  if (/ACCESS_KEY_ID$/.test(name)) return DEV.minioUser
143
148
  if (/SECRET_ACCESS_KEY$/.test(name)) return DEV.minioPassword
144
149
  return OMIT
@@ -231,6 +236,8 @@ function infraServices() {
231
236
  },
232
237
  redis: {
233
238
  image: 'redis:7-alpine',
239
+ // password-protected like staging — see DEV.redisPassword for why it matters (GraphQL subscriptions).
240
+ command: ['redis-server', '--requirepass', DEV.redisPassword],
234
241
  ports: ['6379:6379'],
235
242
  restart: 'unless-stopped',
236
243
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dooer/dooer-test-env",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
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",