@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.
Files changed (198) hide show
  1. package/README.md +106 -25
  2. package/dist/contract/capabilities.d.ts +4 -4
  3. package/dist/contract/capabilities.d.ts.map +1 -1
  4. package/dist/contract/capabilities.js +51 -9
  5. package/dist/contract/capabilities.js.map +1 -1
  6. package/dist/contract/events.d.ts +43 -0
  7. package/dist/contract/events.d.ts.map +1 -1
  8. package/dist/contract/events.js +40 -0
  9. package/dist/contract/events.js.map +1 -1
  10. package/dist/contract/index.d.ts +1 -0
  11. package/dist/contract/index.d.ts.map +1 -1
  12. package/dist/contract/index.js +1 -0
  13. package/dist/contract/index.js.map +1 -1
  14. package/dist/contract/models.d.ts +345 -0
  15. package/dist/contract/models.d.ts.map +1 -1
  16. package/dist/contract/models.js +245 -0
  17. package/dist/contract/models.js.map +1 -1
  18. package/dist/contract/notifications.d.ts +24 -0
  19. package/dist/contract/notifications.d.ts.map +1 -0
  20. package/dist/contract/notifications.js +72 -0
  21. package/dist/contract/notifications.js.map +1 -0
  22. package/dist/contract/permissions.d.ts +52 -2
  23. package/dist/contract/permissions.d.ts.map +1 -1
  24. package/dist/contract/permissions.js +55 -2
  25. package/dist/contract/permissions.js.map +1 -1
  26. package/dist/contract/router.d.ts +1108 -0
  27. package/dist/contract/router.d.ts.map +1 -1
  28. package/dist/contract/router.js +253 -1
  29. package/dist/contract/router.js.map +1 -1
  30. package/dist/contract/settings.d.ts +2 -0
  31. package/dist/contract/settings.d.ts.map +1 -1
  32. package/dist/contract/settings.js +24 -4
  33. package/dist/contract/settings.js.map +1 -1
  34. package/dist/server/index.d.ts +8 -4
  35. package/dist/server/index.d.ts.map +1 -1
  36. package/dist/server/index.js +289 -5
  37. package/dist/server/index.js.map +1 -1
  38. package/dist/server/jobs.d.ts +48 -0
  39. package/dist/server/jobs.d.ts.map +1 -0
  40. package/dist/server/jobs.js +358 -0
  41. package/dist/server/jobs.js.map +1 -0
  42. package/dist/server/router.d.ts +1392 -0
  43. package/dist/server/router.d.ts.map +1 -1
  44. package/dist/server/router.js +385 -7
  45. package/dist/server/router.js.map +1 -1
  46. package/dist/server/schema.d.ts +134 -1
  47. package/dist/server/schema.d.ts.map +1 -1
  48. package/dist/server/schema.js +169 -4
  49. package/dist/server/schema.js.map +1 -1
  50. package/dist/server/services/assets.d.ts +69 -3
  51. package/dist/server/services/assets.d.ts.map +1 -1
  52. package/dist/server/services/assets.js +199 -31
  53. package/dist/server/services/assets.js.map +1 -1
  54. package/dist/server/services/attachments.d.ts +80 -0
  55. package/dist/server/services/attachments.d.ts.map +1 -0
  56. package/dist/server/services/attachments.js +182 -0
  57. package/dist/server/services/attachments.js.map +1 -0
  58. package/dist/server/services/audience.d.ts +15 -0
  59. package/dist/server/services/audience.d.ts.map +1 -0
  60. package/dist/server/services/audience.js +64 -0
  61. package/dist/server/services/audience.js.map +1 -0
  62. package/dist/server/services/categories.d.ts +157 -0
  63. package/dist/server/services/categories.d.ts.map +1 -0
  64. package/dist/server/services/categories.js +309 -0
  65. package/dist/server/services/categories.js.map +1 -0
  66. package/dist/server/services/cursor.d.ts +68 -0
  67. package/dist/server/services/cursor.d.ts.map +1 -0
  68. package/dist/server/services/cursor.js +39 -0
  69. package/dist/server/services/cursor.js.map +1 -0
  70. package/dist/server/services/custody.d.ts +175 -0
  71. package/dist/server/services/custody.d.ts.map +1 -0
  72. package/dist/server/services/custody.js +367 -0
  73. package/dist/server/services/custody.js.map +1 -0
  74. package/dist/server/services/db-errors.d.ts +7 -0
  75. package/dist/server/services/db-errors.d.ts.map +1 -0
  76. package/dist/server/services/db-errors.js +32 -0
  77. package/dist/server/services/db-errors.js.map +1 -0
  78. package/dist/server/services/index.d.ts +16 -0
  79. package/dist/server/services/index.d.ts.map +1 -1
  80. package/dist/server/services/index.js +25 -1
  81. package/dist/server/services/index.js.map +1 -1
  82. package/dist/server/services/members.d.ts +27 -0
  83. package/dist/server/services/members.d.ts.map +1 -0
  84. package/dist/server/services/members.js +39 -0
  85. package/dist/server/services/members.js.map +1 -0
  86. package/dist/server/services/notify.d.ts +43 -1
  87. package/dist/server/services/notify.d.ts.map +1 -1
  88. package/dist/server/services/notify.js +47 -5
  89. package/dist/server/services/notify.js.map +1 -1
  90. package/dist/server/services/offboarding.d.ts +70 -0
  91. package/dist/server/services/offboarding.d.ts.map +1 -0
  92. package/dist/server/services/offboarding.js +116 -0
  93. package/dist/server/services/offboarding.js.map +1 -0
  94. package/dist/server/services/repairs.d.ts +204 -0
  95. package/dist/server/services/repairs.d.ts.map +1 -0
  96. package/dist/server/services/repairs.js +476 -0
  97. package/dist/server/services/repairs.js.map +1 -0
  98. package/dist/server/services/search.d.ts +85 -0
  99. package/dist/server/services/search.d.ts.map +1 -0
  100. package/dist/server/services/search.js +142 -0
  101. package/dist/server/services/search.js.map +1 -0
  102. package/dist/server/services/stats.d.ts +42 -0
  103. package/dist/server/services/stats.d.ts.map +1 -0
  104. package/dist/server/services/stats.js +80 -0
  105. package/dist/server/services/stats.js.map +1 -0
  106. package/dist/server/services/status.d.ts +102 -0
  107. package/dist/server/services/status.d.ts.map +1 -0
  108. package/dist/server/services/status.js +71 -0
  109. package/dist/server/services/status.js.map +1 -0
  110. package/migrations/0002_custody_and_categories.sql +23 -0
  111. package/migrations/0003_repairs.sql +23 -0
  112. package/migrations/0004_platform_surfaces.sql +51 -0
  113. package/migrations/0005_repair_dates.sql +35 -0
  114. package/migrations/0006_workspace_registry_read.sql +50 -0
  115. package/migrations/0007_history_sequence.sql +83 -0
  116. package/migrations/0008_category_order_unique.sql +71 -0
  117. package/migrations/meta/0002_snapshot.json +1054 -0
  118. package/migrations/meta/0003_snapshot.json +1070 -0
  119. package/migrations/meta/0004_snapshot.json +1130 -0
  120. package/migrations/meta/0005_snapshot.json +1135 -0
  121. package/migrations/meta/_journal.json +49 -0
  122. package/package.json +2 -1
  123. package/src/client/api-instance.ts +27 -2
  124. package/src/client/bidi.test.ts +148 -0
  125. package/src/client/bidi.ts +85 -0
  126. package/src/client/components/AssetDetailPanel.svelte +614 -0
  127. package/src/client/components/AssetFormDialog.svelte +71 -22
  128. package/src/client/components/AssetPhoto.svelte +178 -0
  129. package/src/client/components/AttachmentsSection.svelte +327 -0
  130. package/src/client/components/CustodyDialog.svelte +201 -0
  131. package/src/client/components/RepairDialog.svelte +271 -0
  132. package/src/client/components/RepairsSection.svelte +318 -0
  133. package/src/client/components/Timeline.svelte +347 -0
  134. package/src/client/components/TimelineText.svelte +124 -0
  135. package/src/client/core-api.ts +71 -0
  136. package/src/client/custody.test.ts +31 -0
  137. package/src/client/custody.ts +34 -0
  138. package/src/client/errors.test.ts +395 -0
  139. package/src/client/errors.ts +229 -0
  140. package/src/client/links.test.ts +74 -0
  141. package/src/client/links.ts +44 -0
  142. package/src/client/members.test.ts +132 -0
  143. package/src/client/members.ts +116 -0
  144. package/src/client/messages.test.ts +127 -0
  145. package/src/client/messages.ts +1106 -18
  146. package/src/client/mock.test.ts +465 -1
  147. package/src/client/mock.ts +1105 -79
  148. package/src/client/module.ts +73 -1
  149. package/src/client/pages/AssetsPage.svelte +317 -45
  150. package/src/client/permissions.ts +7 -0
  151. package/src/client/price.test.ts +151 -3
  152. package/src/client/price.ts +155 -11
  153. package/src/client/query.ts +36 -0
  154. package/src/client/reorder.test.ts +100 -0
  155. package/src/client/reorder.ts +79 -0
  156. package/src/client/repairs.test.ts +38 -0
  157. package/src/client/repairs.ts +38 -0
  158. package/src/client/sequence.test.ts +248 -0
  159. package/src/client/sequence.ts +185 -0
  160. package/src/client/settings/CategoriesSettings.svelte +746 -0
  161. package/src/client/settings/GeneralSettings.svelte +0 -0
  162. package/src/client/status.ts +29 -0
  163. package/src/client/timeline.test.ts +175 -0
  164. package/src/client/timeline.ts +206 -0
  165. package/src/client/widgets/OverviewWidget.svelte +127 -26
  166. package/src/client/widgets/RepairsWidget.svelte +124 -0
  167. package/src/contract/capabilities.ts +53 -9
  168. package/src/contract/events.ts +49 -0
  169. package/src/contract/index.ts +1 -0
  170. package/src/contract/models.ts +270 -0
  171. package/src/contract/notifications.ts +73 -0
  172. package/src/contract/permissions.ts +55 -2
  173. package/src/contract/router.ts +284 -1
  174. package/src/contract/settings.ts +24 -4
  175. package/src/module.test.ts +217 -4
  176. package/src/server/index.ts +311 -4
  177. package/src/server/inventory.int.test.ts +4109 -19
  178. package/src/server/jobs.ts +444 -0
  179. package/src/server/migrations.test.ts +253 -2
  180. package/src/server/router.ts +486 -6
  181. package/src/server/schema.ts +171 -3
  182. package/src/server/services/assets.ts +214 -54
  183. package/src/server/services/attachments.ts +215 -0
  184. package/src/server/services/audience.ts +77 -0
  185. package/src/server/services/categories.ts +337 -0
  186. package/src/server/services/cursor.ts +104 -0
  187. package/src/server/services/custody.ts +471 -0
  188. package/src/server/services/db-errors.ts +42 -0
  189. package/src/server/services/index.ts +34 -1
  190. package/src/server/services/members.ts +54 -0
  191. package/src/server/services/notify.ts +52 -7
  192. package/src/server/services/offboarding.ts +150 -0
  193. package/src/server/services/repairs.ts +567 -0
  194. package/src/server/services/search.ts +166 -0
  195. package/src/server/services/stats.ts +88 -0
  196. package/src/server/services/status.test.ts +34 -0
  197. package/src/server/services/status.ts +143 -0
  198. 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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"}