@kernhq/module-inventory 0.2.0 → 0.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/README.md +106 -25
- package/dist/contract/capabilities.d.ts +4 -4
- package/dist/contract/capabilities.d.ts.map +1 -1
- package/dist/contract/capabilities.js +51 -9
- package/dist/contract/capabilities.js.map +1 -1
- package/dist/contract/events.d.ts +43 -0
- package/dist/contract/events.d.ts.map +1 -1
- package/dist/contract/events.js +40 -0
- package/dist/contract/events.js.map +1 -1
- package/dist/contract/index.d.ts +1 -0
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +1 -0
- package/dist/contract/index.js.map +1 -1
- package/dist/contract/models.d.ts +345 -0
- package/dist/contract/models.d.ts.map +1 -1
- package/dist/contract/models.js +245 -0
- package/dist/contract/models.js.map +1 -1
- package/dist/contract/notifications.d.ts +24 -0
- package/dist/contract/notifications.d.ts.map +1 -0
- package/dist/contract/notifications.js +72 -0
- package/dist/contract/notifications.js.map +1 -0
- package/dist/contract/permissions.d.ts +52 -2
- package/dist/contract/permissions.d.ts.map +1 -1
- package/dist/contract/permissions.js +55 -2
- package/dist/contract/permissions.js.map +1 -1
- package/dist/contract/router.d.ts +1108 -0
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +253 -1
- package/dist/contract/router.js.map +1 -1
- package/dist/contract/settings.d.ts +2 -0
- package/dist/contract/settings.d.ts.map +1 -1
- package/dist/contract/settings.js +24 -4
- package/dist/contract/settings.js.map +1 -1
- package/dist/server/index.d.ts +8 -4
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +289 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/jobs.d.ts +48 -0
- package/dist/server/jobs.d.ts.map +1 -0
- package/dist/server/jobs.js +358 -0
- package/dist/server/jobs.js.map +1 -0
- package/dist/server/router.d.ts +1392 -0
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +385 -7
- package/dist/server/router.js.map +1 -1
- package/dist/server/schema.d.ts +134 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +169 -4
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/assets.d.ts +69 -3
- package/dist/server/services/assets.d.ts.map +1 -1
- package/dist/server/services/assets.js +199 -31
- package/dist/server/services/assets.js.map +1 -1
- package/dist/server/services/attachments.d.ts +80 -0
- package/dist/server/services/attachments.d.ts.map +1 -0
- package/dist/server/services/attachments.js +182 -0
- package/dist/server/services/attachments.js.map +1 -0
- package/dist/server/services/audience.d.ts +15 -0
- package/dist/server/services/audience.d.ts.map +1 -0
- package/dist/server/services/audience.js +64 -0
- package/dist/server/services/audience.js.map +1 -0
- package/dist/server/services/categories.d.ts +157 -0
- package/dist/server/services/categories.d.ts.map +1 -0
- package/dist/server/services/categories.js +309 -0
- package/dist/server/services/categories.js.map +1 -0
- package/dist/server/services/cursor.d.ts +68 -0
- package/dist/server/services/cursor.d.ts.map +1 -0
- package/dist/server/services/cursor.js +39 -0
- package/dist/server/services/cursor.js.map +1 -0
- package/dist/server/services/custody.d.ts +175 -0
- package/dist/server/services/custody.d.ts.map +1 -0
- package/dist/server/services/custody.js +367 -0
- package/dist/server/services/custody.js.map +1 -0
- package/dist/server/services/db-errors.d.ts +7 -0
- package/dist/server/services/db-errors.d.ts.map +1 -0
- package/dist/server/services/db-errors.js +32 -0
- package/dist/server/services/db-errors.js.map +1 -0
- package/dist/server/services/index.d.ts +16 -0
- package/dist/server/services/index.d.ts.map +1 -1
- package/dist/server/services/index.js +25 -1
- package/dist/server/services/index.js.map +1 -1
- package/dist/server/services/members.d.ts +27 -0
- package/dist/server/services/members.d.ts.map +1 -0
- package/dist/server/services/members.js +39 -0
- package/dist/server/services/members.js.map +1 -0
- package/dist/server/services/notify.d.ts +43 -1
- package/dist/server/services/notify.d.ts.map +1 -1
- package/dist/server/services/notify.js +47 -5
- package/dist/server/services/notify.js.map +1 -1
- package/dist/server/services/offboarding.d.ts +70 -0
- package/dist/server/services/offboarding.d.ts.map +1 -0
- package/dist/server/services/offboarding.js +116 -0
- package/dist/server/services/offboarding.js.map +1 -0
- package/dist/server/services/repairs.d.ts +204 -0
- package/dist/server/services/repairs.d.ts.map +1 -0
- package/dist/server/services/repairs.js +476 -0
- package/dist/server/services/repairs.js.map +1 -0
- package/dist/server/services/search.d.ts +85 -0
- package/dist/server/services/search.d.ts.map +1 -0
- package/dist/server/services/search.js +142 -0
- package/dist/server/services/search.js.map +1 -0
- package/dist/server/services/stats.d.ts +42 -0
- package/dist/server/services/stats.d.ts.map +1 -0
- package/dist/server/services/stats.js +80 -0
- package/dist/server/services/stats.js.map +1 -0
- package/dist/server/services/status.d.ts +102 -0
- package/dist/server/services/status.d.ts.map +1 -0
- package/dist/server/services/status.js +71 -0
- package/dist/server/services/status.js.map +1 -0
- package/migrations/0002_custody_and_categories.sql +23 -0
- package/migrations/0003_repairs.sql +23 -0
- package/migrations/0004_platform_surfaces.sql +51 -0
- package/migrations/0005_repair_dates.sql +35 -0
- package/migrations/0006_workspace_registry_read.sql +50 -0
- package/migrations/0007_history_sequence.sql +83 -0
- package/migrations/0008_category_order_unique.sql +71 -0
- package/migrations/meta/0002_snapshot.json +1054 -0
- package/migrations/meta/0003_snapshot.json +1070 -0
- package/migrations/meta/0004_snapshot.json +1130 -0
- package/migrations/meta/0005_snapshot.json +1135 -0
- package/migrations/meta/_journal.json +49 -0
- package/package.json +2 -1
- package/src/client/api-instance.ts +27 -2
- package/src/client/bidi.test.ts +148 -0
- package/src/client/bidi.ts +85 -0
- package/src/client/components/AssetDetailPanel.svelte +614 -0
- package/src/client/components/AssetFormDialog.svelte +71 -22
- package/src/client/components/AssetPhoto.svelte +178 -0
- package/src/client/components/AttachmentsSection.svelte +327 -0
- package/src/client/components/CustodyDialog.svelte +201 -0
- package/src/client/components/RepairDialog.svelte +271 -0
- package/src/client/components/RepairsSection.svelte +318 -0
- package/src/client/components/Timeline.svelte +347 -0
- package/src/client/components/TimelineText.svelte +124 -0
- package/src/client/core-api.ts +71 -0
- package/src/client/custody.test.ts +31 -0
- package/src/client/custody.ts +34 -0
- package/src/client/errors.test.ts +395 -0
- package/src/client/errors.ts +229 -0
- package/src/client/links.test.ts +74 -0
- package/src/client/links.ts +44 -0
- package/src/client/members.test.ts +132 -0
- package/src/client/members.ts +116 -0
- package/src/client/messages.test.ts +127 -0
- package/src/client/messages.ts +1106 -18
- package/src/client/mock.test.ts +465 -1
- package/src/client/mock.ts +1105 -79
- package/src/client/module.ts +73 -1
- package/src/client/pages/AssetsPage.svelte +317 -45
- package/src/client/permissions.ts +7 -0
- package/src/client/price.test.ts +151 -3
- package/src/client/price.ts +155 -11
- package/src/client/query.ts +36 -0
- package/src/client/reorder.test.ts +100 -0
- package/src/client/reorder.ts +79 -0
- package/src/client/repairs.test.ts +38 -0
- package/src/client/repairs.ts +38 -0
- package/src/client/sequence.test.ts +248 -0
- package/src/client/sequence.ts +185 -0
- package/src/client/settings/CategoriesSettings.svelte +746 -0
- package/src/client/settings/GeneralSettings.svelte +0 -0
- package/src/client/status.ts +29 -0
- package/src/client/timeline.test.ts +175 -0
- package/src/client/timeline.ts +206 -0
- package/src/client/widgets/OverviewWidget.svelte +127 -26
- package/src/client/widgets/RepairsWidget.svelte +124 -0
- package/src/contract/capabilities.ts +53 -9
- package/src/contract/events.ts +49 -0
- package/src/contract/index.ts +1 -0
- package/src/contract/models.ts +270 -0
- package/src/contract/notifications.ts +73 -0
- package/src/contract/permissions.ts +55 -2
- package/src/contract/router.ts +284 -1
- package/src/contract/settings.ts +24 -4
- package/src/module.test.ts +217 -4
- package/src/server/index.ts +311 -4
- package/src/server/inventory.int.test.ts +4109 -19
- package/src/server/jobs.ts +444 -0
- package/src/server/migrations.test.ts +253 -2
- package/src/server/router.ts +486 -6
- package/src/server/schema.ts +171 -3
- package/src/server/services/assets.ts +214 -54
- package/src/server/services/attachments.ts +215 -0
- package/src/server/services/audience.ts +77 -0
- package/src/server/services/categories.ts +337 -0
- package/src/server/services/cursor.ts +104 -0
- package/src/server/services/custody.ts +471 -0
- package/src/server/services/db-errors.ts +42 -0
- package/src/server/services/index.ts +34 -1
- package/src/server/services/members.ts +54 -0
- package/src/server/services/notify.ts +52 -7
- package/src/server/services/offboarding.ts +150 -0
- package/src/server/services/repairs.ts +567 -0
- package/src/server/services/search.ts +166 -0
- package/src/server/services/stats.ts +88 -0
- package/src/server/services/status.test.ts +34 -0
- package/src/server/services/status.ts +143 -0
- package/src/client/settings/core-api.ts +0 -32
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { and, asc, eq, inArray, isNull, sql } from 'drizzle-orm';
|
|
2
|
+
import { MODULE_ID } from '../contract/models.js';
|
|
3
|
+
import { InventorySettings } from '../contract/settings.js';
|
|
4
|
+
import { assets, repairs, workspaces } from './schema.js';
|
|
5
|
+
import { membersWithPermission } from './services/audience.js';
|
|
6
|
+
import { inventoryServices } from './services/index.js';
|
|
7
|
+
import { assetUrl } from './services/search.js';
|
|
8
|
+
/** More rows than any one workspace should reach in a day, and a bound all the same. */
|
|
9
|
+
const SWEEP_LIMIT = 500;
|
|
10
|
+
/**
|
|
11
|
+
* Inventory's scheduled work.
|
|
12
|
+
*
|
|
13
|
+
* **A cron expression fires in UTC, and this module's users do not live there** — `module-hr` says
|
|
14
|
+
* that at the top of its own jobs file, and it is worth saying what follows from it here, because
|
|
15
|
+
* the answer is different. HR's calendar jobs fan out per office and ask each whether *that
|
|
16
|
+
* office's* local boundary has passed, since which month an accrual belongs to changes with the
|
|
17
|
+
* answer. Nothing here has that property:
|
|
18
|
+
*
|
|
19
|
+
* - what these two sweeps *find* is a date comparison against a window thirty days or a fortnight
|
|
20
|
+
* wide, so an item is inside it for weeks and the hour it is noticed cannot change whether it is
|
|
21
|
+
* noticed;
|
|
22
|
+
* - what they *send* is sent exactly once per row ever, because each writes a marker column, so
|
|
23
|
+
* firing on a different hour cannot send anything twice.
|
|
24
|
+
*
|
|
25
|
+
* So a daily UTC cron is honest here where it would have been a bug in HR. The one thing that must
|
|
26
|
+
* not be borrowed from the deployment is **today**: `now()::date` in Postgres is the database
|
|
27
|
+
* session's timezone, which is an accident of how the container was started. Today comes from
|
|
28
|
+
* `todayUtc()` below and is passed into the query as a value.
|
|
29
|
+
*
|
|
30
|
+
* Both handlers are idempotent by construction rather than by scheduling: a retried run re-reads the
|
|
31
|
+
* marker and finds nothing left to say. That matters, because pg-boss retries a throwing handler
|
|
32
|
+
* three times.
|
|
33
|
+
*/
|
|
34
|
+
/** Today, as this module means it: the UTC date. The same clock `RepairService` dates a repair by. */
|
|
35
|
+
const todayUtc = (at = new Date()) => at.toISOString().slice(0, 10);
|
|
36
|
+
/** `YYYY-MM-DD`, `n` days either side. */
|
|
37
|
+
function shiftDays(date, days) {
|
|
38
|
+
const d = new Date(`${date}T00:00:00Z`);
|
|
39
|
+
d.setUTCDate(d.getUTCDate() + days);
|
|
40
|
+
return d.toISOString().slice(0, 10);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Workspaces this module is switched on in.
|
|
44
|
+
*
|
|
45
|
+
* Read from the module's own table rather than asked of core every tick, for the reason
|
|
46
|
+
* `schema.ts` gives at `workspaces`: a sweep that depends on a broker round trip fails whenever
|
|
47
|
+
* core is briefly away, which is exactly the condition an unattended overnight job has to survive.
|
|
48
|
+
*
|
|
49
|
+
* **Unbound on purpose, and the table's policy is what makes that legal.** A scheduler is woken by a
|
|
50
|
+
* clock, so there is no workspace to bind to and `app.workspace_id` is unset here by definition. The
|
|
51
|
+
* per-workspace policy every tenant table carries therefore matches nothing, and `workspaces` — like
|
|
52
|
+
* every other table in this schema — carries `force row level security`, which subjects the schema's
|
|
53
|
+
* **owner** to its policies as well. So this read answered *zero rows, silently, for ever* on any
|
|
54
|
+
* deployment whose application role is not a superuser, and it would have gone on doing so with no
|
|
55
|
+
* error to notice. `0006_workspace_registry_read.sql` adds the one policy that admits it: a select
|
|
56
|
+
* policy on this table alone, for a session with no workspace bound. It is exported so the test
|
|
57
|
+
* suite can run the real enumeration rather than a query that resembles it.
|
|
58
|
+
*/
|
|
59
|
+
export async function activeWorkspaces(kernel) {
|
|
60
|
+
const rows = await kernel.database.db.select({ id: workspaces.workspaceId }).from(workspaces);
|
|
61
|
+
return rows.map((row) => row.id);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Run `fn` for every workspace that still has the module on, logging per-workspace failures rather
|
|
65
|
+
* than propagating them.
|
|
66
|
+
*
|
|
67
|
+
* One workspace's bad data must not stop the sweep for the next one — a job that throws half way
|
|
68
|
+
* through is a job that has notified an arbitrary prefix of the instance and will do it again on the
|
|
69
|
+
* retry. `isModuleEnabled` is asked because the row here outlives a workspace switching the module
|
|
70
|
+
* off: nothing deletes it, deliberately, so switching Inventory back on does not lose the
|
|
71
|
+
* registration.
|
|
72
|
+
*/
|
|
73
|
+
async function forEachWorkspace(kernel, job, fn) {
|
|
74
|
+
for (const workspaceId of await activeWorkspaces(kernel)) {
|
|
75
|
+
if (!(await kernel.isModuleEnabled(workspaceId, MODULE_ID).catch(() => false)))
|
|
76
|
+
continue;
|
|
77
|
+
try {
|
|
78
|
+
await fn(workspaceId);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
kernel.log.warn({ err: err instanceof Error ? err.message : String(err), workspaceId, job, module: MODULE_ID }, 'inventory: scheduled job failed for a workspace');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The three statuses this module derives, and therefore the only three it may overwrite.
|
|
87
|
+
*
|
|
88
|
+
* `reserved`, `lost` and `retired` are set by hand by features that do not exist yet — nothing
|
|
89
|
+
* writes them today — and a reconciliation that stamped over one would silently undo somebody's
|
|
90
|
+
* decision. `deriveStatus` says the same thing from the other side.
|
|
91
|
+
*/
|
|
92
|
+
const DERIVED_STATUSES = ['in_stock', 'assigned', 'under_repair'];
|
|
93
|
+
/**
|
|
94
|
+
* Bring `assets.status` back into step with the facts, for one workspace.
|
|
95
|
+
*
|
|
96
|
+
* **Every write path already does this for the row it touches, and this is for the rows nobody
|
|
97
|
+
* touches.** `status` is stored because every list filter asks for it, and derived from two facts —
|
|
98
|
+
* custody, and an open repair *the workspace still records*. The second one has an input that no
|
|
99
|
+
* row change can be hung off: switching the `repairs` capability off changes the answer for every
|
|
100
|
+
* asset in the workspace at once, and switching it back on changes it back, without a single row
|
|
101
|
+
* being written either time.
|
|
102
|
+
*
|
|
103
|
+
* That is what stranded an asset. With repairs off, `repairs.complete` answers 404 and the row that
|
|
104
|
+
* decides `under_repair` can never be closed, so an item sat in a status nothing could move it out
|
|
105
|
+
* of, behind an archive that refused it for pointing at the same 404. The refusal is withdrawn now
|
|
106
|
+
* and every custody verb re-derives the status it writes — so anything anybody touches heals at
|
|
107
|
+
* once — and this is the sweep for the spare in the cupboard that nobody touches for a month.
|
|
108
|
+
*
|
|
109
|
+
* It runs **in both directions and on every tick**, which is what makes the capability reversible
|
|
110
|
+
* rather than one-way: with repairs off it releases anything still stamped `under_repair`, and with
|
|
111
|
+
* repairs on it stamps back anything that has an open repair and lost the status while the switch
|
|
112
|
+
* was off. Nothing is destroyed either way — the repair rows are untouched, and the status is a
|
|
113
|
+
* derivation, not a record.
|
|
114
|
+
*
|
|
115
|
+
* One statement, and normally it matches nothing: the `is distinct from` is what keeps a nightly
|
|
116
|
+
* job over every workspace from rewriting every asset row it has. Live rows only — an archived
|
|
117
|
+
* asset is out of the register, and churning its `updated_at` would say something moved.
|
|
118
|
+
*/
|
|
119
|
+
export async function reconcileStatuses(kernel, workspaceId, repairsOn) {
|
|
120
|
+
// Correlated to the asset row being updated. Skipped entirely when the workspace does not record
|
|
121
|
+
// repairs, which is the whole point — the same shape as `awayForRepair` in `status.ts`, expressed
|
|
122
|
+
// once over a set instead of once per row.
|
|
123
|
+
const away = repairsOn
|
|
124
|
+
? sql `exists (select 1 from ${repairs}
|
|
125
|
+
where ${repairs.workspaceId} = ${assets.workspaceId}
|
|
126
|
+
and ${repairs.assetId} = ${assets.id}
|
|
127
|
+
and ${repairs.returnedOn} is null)`
|
|
128
|
+
: sql `false`;
|
|
129
|
+
const derived = sql `case
|
|
130
|
+
when ${away} then 'under_repair'
|
|
131
|
+
when ${assets.custodianUserId} is not null then 'assigned'
|
|
132
|
+
else 'in_stock' end`;
|
|
133
|
+
const rows = await kernel.database.withWorkspace(workspaceId, (tx) => tx
|
|
134
|
+
.update(assets)
|
|
135
|
+
.set({ status: derived, updatedAt: new Date() })
|
|
136
|
+
.where(and(eq(assets.workspaceId, workspaceId), isNull(assets.archivedAt), inArray(assets.status, [...DERIVED_STATUSES]), sql `${assets.status} is distinct from (${derived})`))
|
|
137
|
+
.returning({ id: assets.id }));
|
|
138
|
+
return rows.length;
|
|
139
|
+
}
|
|
140
|
+
/** A `date` column reads back as `YYYY-MM-DD`; this is how it is spoken in a sentence. */
|
|
141
|
+
const readableDate = (date) => new Date(`${date}T00:00:00Z`).toLocaleDateString('en-GB', {
|
|
142
|
+
day: 'numeric',
|
|
143
|
+
month: 'short',
|
|
144
|
+
year: 'numeric',
|
|
145
|
+
timeZone: 'UTC',
|
|
146
|
+
});
|
|
147
|
+
export function inventoryJobs() {
|
|
148
|
+
return [
|
|
149
|
+
{
|
|
150
|
+
/**
|
|
151
|
+
* Warranties about to run out.
|
|
152
|
+
*
|
|
153
|
+
* Told to whoever is holding the item, because they are the person who will be standing in
|
|
154
|
+
* front of a broken laptop the week after — and to whoever may replace it when nobody is
|
|
155
|
+
* holding it, since a spare in a cupboard has no other audience. One notice per asset, ever:
|
|
156
|
+
* `warranty_notified_at` is what stops a thirty-day window sending thirty emails, and
|
|
157
|
+
* `assets.update` clears it when the date itself moves.
|
|
158
|
+
*/
|
|
159
|
+
name: 'warranty-sweep',
|
|
160
|
+
cron: '0 7 * * *',
|
|
161
|
+
handler: async (_input, { kernel }) => {
|
|
162
|
+
const svc = inventoryServices(kernel);
|
|
163
|
+
await forEachWorkspace(kernel, 'warranty-sweep', async (workspaceId) => {
|
|
164
|
+
// Settings come from core over the broker, so they are read before any transaction opens
|
|
165
|
+
// — the same rule `AssetService.codeFormat` documents.
|
|
166
|
+
const settings = await kernel.settings.module(workspaceId, MODULE_ID, InventorySettings);
|
|
167
|
+
const today = todayUtc();
|
|
168
|
+
const horizon = shiftDays(today, settings.warrantyNoticeDays);
|
|
169
|
+
const due = await kernel.database.withWorkspace(workspaceId, (tx) => tx
|
|
170
|
+
.select({
|
|
171
|
+
id: assets.id,
|
|
172
|
+
code: assets.code,
|
|
173
|
+
name: assets.name,
|
|
174
|
+
warrantyUntil: assets.warrantyUntil,
|
|
175
|
+
custodianUserId: assets.custodianUserId,
|
|
176
|
+
})
|
|
177
|
+
.from(assets)
|
|
178
|
+
.where(and(eq(assets.workspaceId, workspaceId), isNull(assets.archivedAt), isNull(assets.warrantyNotifiedAt),
|
|
179
|
+
// Inside the window and not already past it. An expiry that came and went is not
|
|
180
|
+
// "about to run out", and saying so weeks later reads as a broken clock.
|
|
181
|
+
sql `${assets.warrantyUntil} is not null
|
|
182
|
+
and ${assets.warrantyUntil} >= ${today}::date
|
|
183
|
+
and ${assets.warrantyUntil} <= ${horizon}::date`))
|
|
184
|
+
.orderBy(asc(assets.warrantyUntil))
|
|
185
|
+
.limit(SWEEP_LIMIT));
|
|
186
|
+
if (!due.length)
|
|
187
|
+
return;
|
|
188
|
+
// Resolved once for the workspace rather than once per asset: it is a handful of broker
|
|
189
|
+
// calls, and a hundred expiring warranties must not become a hundred audience lookups.
|
|
190
|
+
let managers = null;
|
|
191
|
+
const fallback = async () => {
|
|
192
|
+
managers ??= await membersWithPermission(kernel, workspaceId, 'inventory.asset.manage');
|
|
193
|
+
return managers;
|
|
194
|
+
};
|
|
195
|
+
let notified = 0;
|
|
196
|
+
let unheard = 0;
|
|
197
|
+
let undelivered = 0;
|
|
198
|
+
for (const asset of due) {
|
|
199
|
+
const userIds = asset.custodianUserId ? [asset.custodianUserId] : await fallback();
|
|
200
|
+
/**
|
|
201
|
+
* Nobody to tell is **not** the same as told, so the row is left unmarked.
|
|
202
|
+
*
|
|
203
|
+
* Marking it anyway would mean a workspace that has nobody holding
|
|
204
|
+
* `inventory.asset.manage` today silently loses the notice for ever — including for the
|
|
205
|
+
* person who is given that permission tomorrow. The cost of leaving it is one audience
|
|
206
|
+
* lookup per workspace per night, which is already resolved once and cached above.
|
|
207
|
+
*/
|
|
208
|
+
if (!userIds.length) {
|
|
209
|
+
unheard++;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const delivered = await svc.notify.notify({
|
|
213
|
+
workspaceId,
|
|
214
|
+
userIds,
|
|
215
|
+
type: 'inventory.warranty.expiring',
|
|
216
|
+
// The tag leads, because that is what is printed on the sticker.
|
|
217
|
+
title: `${asset.code} is out of warranty on ${readableDate(asset.warrantyUntil)}`,
|
|
218
|
+
body: asset.name,
|
|
219
|
+
object: { module: MODULE_ID, type: 'asset', id: asset.id },
|
|
220
|
+
url: assetUrl(asset.id),
|
|
221
|
+
groupKey: asset.id,
|
|
222
|
+
});
|
|
223
|
+
/**
|
|
224
|
+
* **The marker means "everybody was told", so nothing is marked until everybody was.**
|
|
225
|
+
*
|
|
226
|
+
* `notify` is best-effort by design: a `core.notifications.create` that fails is logged
|
|
227
|
+
* and swallowed, and it used to come back looking exactly like one that succeeded. This
|
|
228
|
+
* column is written once per asset and never cleared, and the notice is sent once per
|
|
229
|
+
* asset ever — so a marker written over a swallowed failure is not a delayed notice, it
|
|
230
|
+
* is a notice nobody will ever receive, for the life of the row. Left unmarked the
|
|
231
|
+
* sweep simply says it again tomorrow, which is the whole reason it runs every day.
|
|
232
|
+
*
|
|
233
|
+
* **A partial delivery is a failure, and it used to be recorded as a success.** A spare
|
|
234
|
+
* with three managers in the audience needed one of the three writes to land for the row
|
|
235
|
+
* to be stamped *told*; the other two were never told and never would be. There is one
|
|
236
|
+
* column and it cannot hold two answers, so it holds the pessimistic one — whoever did
|
|
237
|
+
* hear it hears it again tomorrow, `groupKey` collapses that where a client groups, and
|
|
238
|
+
* nobody is silently left out.
|
|
239
|
+
*/
|
|
240
|
+
if (delivered.delivered < delivered.targeted || !delivered.targeted) {
|
|
241
|
+
undelivered++;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
await kernel.database.withWorkspace(workspaceId, (tx) => tx
|
|
245
|
+
.update(assets)
|
|
246
|
+
.set({ warrantyNotifiedAt: new Date() })
|
|
247
|
+
.where(and(eq(assets.workspaceId, workspaceId), eq(assets.id, asset.id))));
|
|
248
|
+
notified++;
|
|
249
|
+
}
|
|
250
|
+
if (unheard)
|
|
251
|
+
kernel.log.warn({ module: MODULE_ID, workspaceId, silent: unheard }, 'inventory: warranties are running out and nobody in the workspace may replace them');
|
|
252
|
+
if (undelivered)
|
|
253
|
+
kernel.log.warn({ module: MODULE_ID, workspaceId, undelivered }, 'inventory: warranty notices could not be delivered; they stay unmarked and go again tomorrow');
|
|
254
|
+
if (notified)
|
|
255
|
+
kernel.log.info({ module: MODULE_ID, workspaceId, notified }, 'inventory: warranties about to expire');
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
/**
|
|
261
|
+
* Repairs nobody has chased.
|
|
262
|
+
*
|
|
263
|
+
* Told to the person who logged it — they committed the company to the money and they have the
|
|
264
|
+
* repairer's number — and to whoever is still holding the item, who is the one waiting for it
|
|
265
|
+
* back. Neither is a guess: both are columns. Only when the repair carries neither does this
|
|
266
|
+
* fall back to asking who may manage repairs at all.
|
|
267
|
+
*
|
|
268
|
+
* **Asks the capability first, and acts on the answer either way.** A workspace with `repairs`
|
|
269
|
+
* switched off has no repairs surface, so a sweep that notified about repair rows left over
|
|
270
|
+
* from before it was switched off would be the feature answering 404 to a person and sending
|
|
271
|
+
* them email about it. What it *does* still owe that workspace is a register that does not
|
|
272
|
+
* claim `under_repair` for an item behind a procedure that answers 404 — so the status
|
|
273
|
+
* reconciliation runs before the switch is consulted, in both directions. `reconcileStatuses`
|
|
274
|
+
* argues it in full.
|
|
275
|
+
*/
|
|
276
|
+
name: 'repair-overdue',
|
|
277
|
+
cron: '20 7 * * *',
|
|
278
|
+
handler: async (_input, { kernel }) => {
|
|
279
|
+
const svc = inventoryServices(kernel);
|
|
280
|
+
await forEachWorkspace(kernel, 'repair-overdue', async (workspaceId) => {
|
|
281
|
+
const on = await kernel.capabilities(workspaceId, MODULE_ID);
|
|
282
|
+
const repairsOn = on.has('repairs');
|
|
283
|
+
const healed = await reconcileStatuses(kernel, workspaceId, repairsOn);
|
|
284
|
+
if (healed)
|
|
285
|
+
kernel.log.info({ module: MODULE_ID, workspaceId, healed, repairsOn }, 'inventory: brought asset statuses back into step with the repairs capability');
|
|
286
|
+
if (!repairsOn)
|
|
287
|
+
return;
|
|
288
|
+
const settings = await kernel.settings.module(workspaceId, MODULE_ID, InventorySettings);
|
|
289
|
+
const cutoff = shiftDays(todayUtc(), -settings.repairOverdueDays);
|
|
290
|
+
const late = await kernel.database.withWorkspace(workspaceId, (tx) => tx
|
|
291
|
+
.select({
|
|
292
|
+
id: repairs.id,
|
|
293
|
+
assetId: repairs.assetId,
|
|
294
|
+
summary: repairs.summary,
|
|
295
|
+
vendor: repairs.vendor,
|
|
296
|
+
sentOn: repairs.sentOn,
|
|
297
|
+
createdBy: repairs.createdBy,
|
|
298
|
+
code: assets.code,
|
|
299
|
+
name: assets.name,
|
|
300
|
+
custodianUserId: assets.custodianUserId,
|
|
301
|
+
})
|
|
302
|
+
.from(repairs)
|
|
303
|
+
.innerJoin(assets, and(eq(assets.id, repairs.assetId), eq(assets.workspaceId, repairs.workspaceId)))
|
|
304
|
+
.where(and(eq(repairs.workspaceId, workspaceId), isNull(repairs.returnedOn), isNull(repairs.overdueNotifiedAt), sql `${repairs.sentOn} <= ${cutoff}::date`))
|
|
305
|
+
.orderBy(asc(repairs.sentOn))
|
|
306
|
+
.limit(SWEEP_LIMIT));
|
|
307
|
+
if (!late.length)
|
|
308
|
+
return;
|
|
309
|
+
let managers = null;
|
|
310
|
+
const fallback = async () => {
|
|
311
|
+
managers ??= await membersWithPermission(kernel, workspaceId, 'inventory.repair.manage');
|
|
312
|
+
return managers;
|
|
313
|
+
};
|
|
314
|
+
let notified = 0;
|
|
315
|
+
let unheard = 0;
|
|
316
|
+
let undelivered = 0;
|
|
317
|
+
for (const repair of late) {
|
|
318
|
+
const named = [repair.createdBy, repair.custodianUserId].filter((id) => Boolean(id));
|
|
319
|
+
const userIds = named.length ? named : await fallback();
|
|
320
|
+
// Nobody to tell is not the same as told; see the warranty sweep above.
|
|
321
|
+
if (!userIds.length) {
|
|
322
|
+
unheard++;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const delivered = await svc.notify.notify({
|
|
326
|
+
workspaceId,
|
|
327
|
+
userIds,
|
|
328
|
+
type: 'inventory.repair.overdue',
|
|
329
|
+
title: `${repair.code} has been away since ${readableDate(repair.sentOn)}`,
|
|
330
|
+
body: repair.vendor ? `${repair.summary} · ${repair.vendor}` : repair.summary,
|
|
331
|
+
object: { module: MODULE_ID, type: 'asset', id: repair.assetId },
|
|
332
|
+
url: assetUrl(repair.assetId),
|
|
333
|
+
groupKey: repair.id,
|
|
334
|
+
});
|
|
335
|
+
// Anybody left untold means nothing is marked: the same rule, and the same reason, as
|
|
336
|
+
// the warranty sweep above spells out in full.
|
|
337
|
+
if (delivered.delivered < delivered.targeted || !delivered.targeted) {
|
|
338
|
+
undelivered++;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
await kernel.database.withWorkspace(workspaceId, (tx) => tx
|
|
342
|
+
.update(repairs)
|
|
343
|
+
.set({ overdueNotifiedAt: new Date() })
|
|
344
|
+
.where(and(eq(repairs.workspaceId, workspaceId), eq(repairs.id, repair.id))));
|
|
345
|
+
notified++;
|
|
346
|
+
}
|
|
347
|
+
if (unheard)
|
|
348
|
+
kernel.log.warn({ module: MODULE_ID, workspaceId, silent: unheard }, 'inventory: repairs are overdue and nobody in the workspace may chase them');
|
|
349
|
+
if (undelivered)
|
|
350
|
+
kernel.log.warn({ module: MODULE_ID, workspaceId, undelivered }, 'inventory: overdue repair notices could not be delivered; they stay unmarked and go again tomorrow');
|
|
351
|
+
if (notified)
|
|
352
|
+
kernel.log.info({ module: MODULE_ID, workspaceId, notified }, 'inventory: repairs away longer than the workspace allows');
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
];
|
|
357
|
+
}
|
|
358
|
+
//# sourceMappingURL=jobs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobs.js","sourceRoot":"","sources":["../../src/server/jobs.ts"],"names":[],"mappings":"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"}
|