@wirecat/cli-core 0.576.0 → 0.578.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/CHANGELOG.md +32 -1
- package/dist/cli/compact.d.ts +2 -0
- package/dist/cli/compact.js +1 -1
- package/dist/cli/findings-input.js +2 -0
- package/dist/cli/messenger/projects-command.js +2 -0
- package/dist/cli/messenger/tasks-command.js +2 -0
- package/dist/cli/project-status-rows.d.ts +4 -2
- package/dist/cli/project-status-rows.js +13 -29
- package/dist/cli/project-status.d.ts +59 -16
- package/dist/cli/project-status.js +101 -42
- package/dist/domain/external-links.d.ts +13 -0
- package/dist/domain/external-links.js +30 -0
- package/dist/entities/index.d.ts +15 -0
- package/dist/entities/mail-findings.d.ts +6 -0
- package/dist/entities/mail-findings.js +6 -0
- package/dist/entities/task-results.d.ts +20 -0
- package/dist/entities/task-results.js +4 -0
- package/dist/entities/task.d.ts +4 -0
- package/dist/entities/task.js +4 -0
- package/dist/errors/reasons.js +5 -0
- package/dist/mail/cli/index.js +2 -1
- package/dist/mail/cli/marks.d.ts +6 -0
- package/dist/mail/cli/marks.js +69 -1
- package/dist/mail/findings.d.ts +2 -0
- package/dist/mail/findings.js +15 -1
- package/dist/mail/index.d.ts +1 -1
- package/dist/mail/index.js +1 -1
- package/dist/mail/marks.d.ts +29 -0
- package/dist/mail/marks.js +86 -0
- package/dist/meetings/findings-rules.d.ts +4 -0
- package/dist/services/project-status/signals/index.js +0 -2
- package/dist/store/sqlite/email-activity.d.ts +2 -0
- package/dist/store/sqlite/email-activity.js +4 -0
- package/dist/store/sqlite/findings-core.js +2 -0
- package/dist/store/sqlite/manifest.js +1 -0
- package/dist/store/sqlite/migrations.generated.js +10 -0
- package/dist/store/sqlite/orders.d.ts +4 -0
- package/dist/store/sqlite/orders.js +6 -3
- package/dist/store/sqlite/schema.d.ts +64 -0
- package/dist/store/sqlite/schema.js +5 -0
- package/dist/store/sqlite/work-tasks.d.ts +7 -0
- package/dist/store/sqlite/work-tasks.js +10 -0
- package/package.json +1 -1
- package/dist/services/project-status/signals/last-message.d.ts +0 -4
- package/dist/services/project-status/signals/last-message.js +0 -22
|
@@ -26,6 +26,10 @@ export declare const mailFindings: v.StrictObjectSchema<{
|
|
|
26
26
|
readonly due_evidence: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
27
27
|
readonly assignee: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
28
28
|
readonly assignee_evidence: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
29
|
+
/** `<service>:<id>` in the outside service the mail came from, such as `avito:4512345678`. */
|
|
30
|
+
readonly external_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
31
|
+
/** The http or https page the task is about, such as the conversation the mail links to. */
|
|
32
|
+
readonly source_url: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
29
33
|
}, undefined>, v.StrictObjectSchema<{
|
|
30
34
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
31
35
|
readonly evidence: v.ArraySchema<v.StrictObjectSchema<{
|
|
@@ -143,6 +147,8 @@ export declare const mailFindings: v.StrictObjectSchema<{
|
|
|
143
147
|
readonly invoice: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
144
148
|
readonly payment: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
145
149
|
readonly project: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
150
|
+
/** The order's page at the shop, http or https. */
|
|
151
|
+
readonly source_url: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
146
152
|
readonly reason: v.StringSchema<undefined>;
|
|
147
153
|
}, undefined>, v.StrictObjectSchema<{
|
|
148
154
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
@@ -18,6 +18,10 @@ const finding = v.variant("kind", [
|
|
|
18
18
|
due_evidence: v.optional(v.array(reference)),
|
|
19
19
|
assignee: v.optional(reference),
|
|
20
20
|
assignee_evidence: v.optional(v.array(reference)),
|
|
21
|
+
/** `<service>:<id>` in the outside service the mail came from, such as `avito:4512345678`. */
|
|
22
|
+
external_id: v.optional(text),
|
|
23
|
+
/** The http or https page the task is about, such as the conversation the mail links to. */
|
|
24
|
+
source_url: v.optional(text),
|
|
21
25
|
}),
|
|
22
26
|
v.strictObject({
|
|
23
27
|
...base,
|
|
@@ -96,6 +100,8 @@ const finding = v.variant("kind", [
|
|
|
96
100
|
invoice: v.optional(reference),
|
|
97
101
|
payment: v.optional(reference),
|
|
98
102
|
project: v.optional(reference),
|
|
103
|
+
/** The order's page at the shop, http or https. */
|
|
104
|
+
source_url: v.optional(text),
|
|
99
105
|
reason: text,
|
|
100
106
|
}),
|
|
101
107
|
v.strictObject({
|
|
@@ -22,6 +22,10 @@ export declare const taskCard: v.StrictObjectSchema<{
|
|
|
22
22
|
readonly revision: v.NumberSchema<undefined>;
|
|
23
23
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
24
24
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
25
|
+
/** `<service>:<id>`, its id in an outside service, such as `avito:4512345678`. */
|
|
26
|
+
readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
27
|
+
/** The http or https page it is about, such as an Avito ad. */
|
|
28
|
+
readonly source_url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
25
29
|
readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
26
30
|
readonly next_check_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
27
31
|
readonly closed_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
@@ -136,6 +140,10 @@ export declare const taskCreated: v.StrictObjectSchema<{
|
|
|
136
140
|
readonly revision: v.NumberSchema<undefined>;
|
|
137
141
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
138
142
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
143
|
+
/** `<service>:<id>`, its id in an outside service, such as `avito:4512345678`. */
|
|
144
|
+
readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
145
|
+
/** The http or https page it is about, such as an Avito ad. */
|
|
146
|
+
readonly source_url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
139
147
|
readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
140
148
|
readonly next_check_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
141
149
|
readonly closed_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
@@ -251,6 +259,10 @@ export declare const taskLinks: v.UnionSchema<[v.StrictObjectSchema<{
|
|
|
251
259
|
readonly revision: v.NumberSchema<undefined>;
|
|
252
260
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
253
261
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
262
|
+
/** `<service>:<id>`, its id in an outside service, such as `avito:4512345678`. */
|
|
263
|
+
readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
264
|
+
/** The http or https page it is about, such as an Avito ad. */
|
|
265
|
+
readonly source_url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
254
266
|
readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
255
267
|
readonly next_check_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
256
268
|
readonly closed_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
@@ -394,6 +406,10 @@ export declare const taskMerge: v.StrictObjectSchema<{
|
|
|
394
406
|
readonly revision: v.NumberSchema<undefined>;
|
|
395
407
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
396
408
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
409
|
+
/** `<service>:<id>`, its id in an outside service, such as `avito:4512345678`. */
|
|
410
|
+
readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
411
|
+
/** The http or https page it is about, such as an Avito ad. */
|
|
412
|
+
readonly source_url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
397
413
|
readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
398
414
|
readonly next_check_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
399
415
|
readonly closed_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
@@ -502,6 +518,10 @@ export declare const taskUnmerge: v.StrictObjectSchema<{
|
|
|
502
518
|
readonly revision: v.NumberSchema<undefined>;
|
|
503
519
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
504
520
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
521
|
+
/** `<service>:<id>`, its id in an outside service, such as `avito:4512345678`. */
|
|
522
|
+
readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
523
|
+
/** The http or https page it is about, such as an Avito ad. */
|
|
524
|
+
readonly source_url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
505
525
|
readonly due_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
506
526
|
readonly next_check_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
507
527
|
readonly closed_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
@@ -39,6 +39,10 @@ export const taskCard = v.strictObject({
|
|
|
39
39
|
revision: v.number(),
|
|
40
40
|
parent: v.nullable(reference),
|
|
41
41
|
source: v.nullable(text),
|
|
42
|
+
/** `<service>:<id>`, its id in an outside service, such as `avito:4512345678`. */
|
|
43
|
+
external_id: v.nullable(text),
|
|
44
|
+
/** The http or https page it is about, such as an Avito ad. */
|
|
45
|
+
source_url: v.nullable(text),
|
|
42
46
|
due_at: nullableTime,
|
|
43
47
|
next_check_at: nullableTime,
|
|
44
48
|
closed_at: nullableTime,
|
package/dist/entities/task.d.ts
CHANGED
|
@@ -34,6 +34,10 @@ export declare const task: v.StrictObjectSchema<{
|
|
|
34
34
|
readonly chat: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
35
35
|
/** The message it came from. */
|
|
36
36
|
readonly source_message: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
37
|
+
/** `<service>:<id>`, its id in an outside service, such as `avito:4512345678`. */
|
|
38
|
+
readonly external_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
39
|
+
/** The http or https page it is about, such as an Avito ad. */
|
|
40
|
+
readonly source_url: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
37
41
|
/** 0 to 1, higher first; `priority_label` reads it as low, medium, high or critical. */
|
|
38
42
|
readonly priority: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
|
|
39
43
|
readonly priority_label: v.OptionalSchema<v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>, undefined>;
|
package/dist/entities/task.js
CHANGED
|
@@ -32,6 +32,10 @@ export const task = v.strictObject({
|
|
|
32
32
|
chat: v.optional(v.nullable(reference)),
|
|
33
33
|
/** The message it came from. */
|
|
34
34
|
source_message: v.optional(v.nullable(reference)),
|
|
35
|
+
/** `<service>:<id>`, its id in an outside service, such as `avito:4512345678`. */
|
|
36
|
+
external_id: v.optional(v.nullable(text)),
|
|
37
|
+
/** The http or https page it is about, such as an Avito ad. */
|
|
38
|
+
source_url: v.optional(v.nullable(text)),
|
|
35
39
|
/** 0 to 1, higher first; `priority_label` reads it as low, medium, high or critical. */
|
|
36
40
|
priority: v.optional(v.nullable(v.number())),
|
|
37
41
|
priority_label: v.optional(v.nullable(v.picklist(PRIORITY_LABELS))),
|
package/dist/errors/reasons.js
CHANGED
|
@@ -69,6 +69,8 @@ const OTHERS = [
|
|
|
69
69
|
validation("file.locked", "another process holds the file's lock"),
|
|
70
70
|
validation("mail.cursor-ahead", "an acknowledge cursor is past the newest stored email"),
|
|
71
71
|
["mail.holder-not-yours", "permission_error", "a run moves only the mark of the holder its schedule names"],
|
|
72
|
+
["mail.no-mark", "not_found", "the holder has no mark in the mailbox yet; its first review starts it"],
|
|
73
|
+
["mail.reset-owner-only", "permission_error", "only the owner moves a holder's mark back"],
|
|
72
74
|
["mail.source-failed", "provider_error", "the mail server refused or did not complete a request"],
|
|
73
75
|
["mail.search-refused", "provider_error", "the mail server refused to search a folder"],
|
|
74
76
|
[
|
|
@@ -199,6 +201,7 @@ const OTHERS = [
|
|
|
199
201
|
validation("money.quantity", "the quantity is not a decimal above 0"),
|
|
200
202
|
validation("order.duplicate", "the shop's order with this number is already stored; change its status instead"),
|
|
201
203
|
validation("order.field", "a field of the order is not valid"),
|
|
204
|
+
validation("order.source-url", "an order's source URL is not an http or https address"),
|
|
202
205
|
["order.not-found", "not_found", "no stored order of that shop has that number"],
|
|
203
206
|
validation("order.ref", "the reference names no order, or a shop's number names more than one"),
|
|
204
207
|
validation("order.status", "a rejected or deleted order's status does not change"),
|
|
@@ -246,6 +249,8 @@ const OTHERS = [
|
|
|
246
249
|
validation("tag.invalid", "the tag is not valid"),
|
|
247
250
|
["reactions.list-hidden", "permission_error", "the chat does not let its members see who reacted"],
|
|
248
251
|
["reactions.broadcast", "permission_error", "a channel does not show who reacted to its posts"],
|
|
252
|
+
validation("task.external-id", "an external id is <service>:<id>, such as avito:4512345678"),
|
|
253
|
+
validation("task.source-url", "a task's source URL is not an http or https address"),
|
|
249
254
|
validation("task.category-unknown", "the category is not in the task category list"),
|
|
250
255
|
validation("task.claimed", "someone else holds the task"),
|
|
251
256
|
validation("task.closed", "the task is already closed"),
|
package/dist/mail/cli/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import { searchMailServer } from "../../mail/server-search.js";
|
|
|
19
19
|
import { atDetail } from "../../output-detail.js";
|
|
20
20
|
import { sourceKey } from "../../services/server-search.js";
|
|
21
21
|
import { queryFilter, THREAD_COLUMNS } from "../../store/store.js";
|
|
22
|
-
import { acknowledgeCommand } from "./marks.js";
|
|
22
|
+
import { acknowledgeCommand, resetMarkCommand } from "./marks.js";
|
|
23
23
|
import { mailReviewCommand } from "./review.js";
|
|
24
24
|
export { bulkTriageOf, MAIL_BULK_SETTINGS } from "./bulk-settings.js";
|
|
25
25
|
export { mailMark, markItem, markText } from "./marks.js";
|
|
@@ -254,6 +254,7 @@ const messagesCommand = (mail) => {
|
|
|
254
254
|
: itemOf(shown.find((_, at) => emails[at]?.anchor) ?? shown[0], unread ? { meta: { complete: false, issues: [unread] } } : undefined), () => shownText(emails));
|
|
255
255
|
});
|
|
256
256
|
messages.addCommand(acknowledgeCommand(mail));
|
|
257
|
+
messages.addCommand(resetMarkCommand(mail));
|
|
257
258
|
return messages;
|
|
258
259
|
};
|
|
259
260
|
const mailThread = v.strictObject({
|
package/dist/mail/cli/marks.d.ts
CHANGED
|
@@ -26,3 +26,9 @@ export declare const markText: (mark: MailMark) => string;
|
|
|
26
26
|
* schedule names only: an email that talks its reader into acknowledging would otherwise silence another pipeline.
|
|
27
27
|
*/
|
|
28
28
|
export declare const acknowledgeCommand: (mail: MailReads) => Command;
|
|
29
|
+
/**
|
|
30
|
+
* `messages reset`: the owner moves a holder's mark back, so its next reviews give it that mail again — after its
|
|
31
|
+
* rules changed, say. An agent never does: a holder that could move its own mark back could be talked by an email
|
|
32
|
+
* into reading the whole mailbox again, so this is no proposal and the read-only lock refuses it.
|
|
33
|
+
*/
|
|
34
|
+
export declare const resetMarkCommand: (mail: MailReads) => Command;
|
package/dist/mail/cli/marks.js
CHANGED
|
@@ -8,7 +8,8 @@ import { environmentOf, outputFor } from "../../cli/context.js";
|
|
|
8
8
|
import { LOCAL_WRITE, proposesOnly } from "../../cli/effects.js";
|
|
9
9
|
import { itemOf, renderAnswer } from "../../cli/envelope.js";
|
|
10
10
|
import { mailHolderVariable, readOnlyLockOf } from "../../ports/app.js";
|
|
11
|
-
import {
|
|
11
|
+
import { momentOf } from "../../services/moment.js";
|
|
12
|
+
import { acknowledgeMark, rewindMark } from "../marks.js";
|
|
12
13
|
const iso = (ms) => (ms === null ? null : new Date(ms).toISOString());
|
|
13
14
|
/** A holder's mark as `--json`, `mail status` and MCP show it. */
|
|
14
15
|
export const mailMark = v.strictObject({
|
|
@@ -67,3 +68,70 @@ export const acknowledgeCommand = (mail) => answerEntity(proposesOnly(annotate(n
|
|
|
67
68
|
const mark = await mail.read(this, (store, mailbox) => acknowledgeMark(store, mailbox, holder, cursor));
|
|
68
69
|
renderAnswer(this, outputFor(this), itemOf(markItem(mark)), () => markText(mark));
|
|
69
70
|
});
|
|
71
|
+
const mailMarkReset = v.strictObject({
|
|
72
|
+
mark: mailMark,
|
|
73
|
+
/** Where the mark was before. */
|
|
74
|
+
from_id: v.number(),
|
|
75
|
+
/** Emails the holder reads whole again. */
|
|
76
|
+
again: v.number(),
|
|
77
|
+
/** Bulk emails it gets again as excerpts, unless they are surely newsletters. */
|
|
78
|
+
bulk: v.number(),
|
|
79
|
+
/** Emails between the two marks it skips again: drafts, envelopes, suspicious senders, partial mail. */
|
|
80
|
+
skipped: v.number(),
|
|
81
|
+
/** Nothing moved: `--dry-run` only counts. */
|
|
82
|
+
dry_run: v.boolean(),
|
|
83
|
+
});
|
|
84
|
+
const wholeNumber = (value) => {
|
|
85
|
+
const parsed = Number(value);
|
|
86
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1)
|
|
87
|
+
throw new CliError("validation_error", `--last takes a whole number of emails, 1 or more — got ${value}`);
|
|
88
|
+
return parsed;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* `messages reset`: the owner moves a holder's mark back, so its next reviews give it that mail again — after its
|
|
92
|
+
* rules changed, say. An agent never does: a holder that could move its own mark back could be talked by an email
|
|
93
|
+
* into reading the whole mailbox again, so this is no proposal and the read-only lock refuses it.
|
|
94
|
+
*/
|
|
95
|
+
export const resetMarkCommand = (mail) => answerEntity(annotate(new Command("reset"), {
|
|
96
|
+
effects: LOCAL_WRITE,
|
|
97
|
+
examples: [
|
|
98
|
+
`${mail.app.command} personal messages reset --holder mail-triage --last 8 --dry-run`,
|
|
99
|
+
`${mail.app.command} personal messages reset --holder mail-triage --since-time 2026-11-04T00:00:00Z`,
|
|
100
|
+
],
|
|
101
|
+
}), "mail_mark_reset", mailMarkReset)
|
|
102
|
+
.summary("move a holder's processing mark back, so it reads that mail again (owner only)")
|
|
103
|
+
.description("the owner moves a holder's mark back past the mail received since --since-time, or past the last --last " +
|
|
104
|
+
"emails it was given; its next reviews give that mail again. The answer says how many emails it reads " +
|
|
105
|
+
"again. An agent cannot: a holder only moves its own mark forward, with `messages acknowledge`")
|
|
106
|
+
.requiredOption("--holder <name>", "whose mark: lower-case letters, digits and dashes, such as mail-triage")
|
|
107
|
+
.option("--since-time <time>", "read again what was received since this ISO 8601 time, or 2d ago")
|
|
108
|
+
.option("--last <n>", "read again the last n emails the holder was given", wholeNumber)
|
|
109
|
+
.option("--dry-run", "count what would be read again, and move nothing")
|
|
110
|
+
.action(async function () {
|
|
111
|
+
const { holder, sinceTime, last, dryRun } = this.opts();
|
|
112
|
+
if ((sinceTime === undefined) === (last === undefined))
|
|
113
|
+
throw new CliError("validation_error", "give --since-time or --last, one of them");
|
|
114
|
+
const env = environmentOf(this).env ?? process.env;
|
|
115
|
+
if (env[mailHolderVariable(mail.app)]?.trim() || actorOf(this) !== "owner" || readOnlyLockOf(mail.app, env))
|
|
116
|
+
throw new CliError("permission_error", "only the owner moves a holder's mark back; an agent only acknowledges", {
|
|
117
|
+
reason: "mail.reset-owner-only",
|
|
118
|
+
});
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
const to = last === undefined ? { since: momentOf(sinceTime, "--since-time", now) } : { last };
|
|
121
|
+
const reset = await mail.read(this, (store, mailbox) => rewindMark(store, mailbox, holder, to, { now, dry: dryRun === true }));
|
|
122
|
+
const item = {
|
|
123
|
+
mark: markItem(reset.mark),
|
|
124
|
+
from_id: reset.fromId,
|
|
125
|
+
again: reset.again,
|
|
126
|
+
bulk: reset.bulk,
|
|
127
|
+
skipped: reset.skipped,
|
|
128
|
+
dry_run: reset.dry,
|
|
129
|
+
};
|
|
130
|
+
renderAnswer(this, outputFor(this), itemOf(item), () => {
|
|
131
|
+
const mark = reset.mark.throughId === reset.fromId
|
|
132
|
+
? `the mark stays at ${reset.fromId}`
|
|
133
|
+
: `the mark ${reset.dry ? "would move" : "moved"} from ${reset.fromId} to ${reset.mark.throughId}`;
|
|
134
|
+
return (`${holder} ${reset.dry ? "would read" : "reads"} ${reset.again} emails again, and ${reset.bulk} bulk ` +
|
|
135
|
+
`ones as excerpts unless they are newsletters; ${reset.skipped} are skipped again; ${mark}\n`);
|
|
136
|
+
});
|
|
137
|
+
});
|
package/dist/mail/findings.d.ts
CHANGED
|
@@ -99,6 +99,8 @@ export interface MailOrderFinding extends Cited {
|
|
|
99
99
|
invoice?: string;
|
|
100
100
|
payment?: string;
|
|
101
101
|
project?: string;
|
|
102
|
+
/** The order's page at the shop, http or https. */
|
|
103
|
+
sourceUrl?: string;
|
|
102
104
|
reason: string;
|
|
103
105
|
}
|
|
104
106
|
/** A later mail about a stored order, found by its shop and number: shipped, delivered, cancelled, returned. */
|
package/dist/mail/findings.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { EXTERNAL_ID_CHARS, externalIdOf, SOURCE_URL_CHARS, sourceUrlOf } from "../domain/external-links.js";
|
|
2
3
|
import { INVOICE_DIRECTIONS, MONEY_DIRECTIONS } from "../domain/finance.js";
|
|
3
4
|
import { MEMORY_KINDS, MEMORY_SCOPES } from "../domain/knowledge.js";
|
|
4
5
|
import { formatLocator, parseLocator } from "../domain/locator.js";
|
|
@@ -59,8 +60,19 @@ const optional = (key, value, read) => (value === undefined ? {} : { [key]: read
|
|
|
59
60
|
const one = (raw, ids) => {
|
|
60
61
|
if (!raw || typeof raw !== "object")
|
|
61
62
|
return invalid("a finding is an object");
|
|
62
|
-
if (raw.kind === "
|
|
63
|
+
if (raw.kind === "fact")
|
|
63
64
|
return validateFinding(raw, RULES, ids);
|
|
65
|
+
if (raw.kind === "task") {
|
|
66
|
+
const { externalId, sourceUrl, ...task } = raw;
|
|
67
|
+
return {
|
|
68
|
+
...validateFinding(task, RULES, ids),
|
|
69
|
+
...optional("externalId", externalId, (value) => {
|
|
70
|
+
const { provider, id } = externalIdOf(findingText(value, EXTERNAL_ID_CHARS + 40));
|
|
71
|
+
return `${provider}:${id}`;
|
|
72
|
+
}),
|
|
73
|
+
...optional("sourceUrl", sourceUrl, (value) => sourceUrlOf(findingText(value, SOURCE_URL_CHARS))),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
64
76
|
const id = findingId(raw.id, ids);
|
|
65
77
|
const evidence = validEvidence(raw.evidence, RULES);
|
|
66
78
|
const reference = (value, pattern, what) => {
|
|
@@ -247,6 +259,7 @@ const one = (raw, ids) => {
|
|
|
247
259
|
"invoice",
|
|
248
260
|
"payment",
|
|
249
261
|
"project",
|
|
262
|
+
"sourceUrl",
|
|
250
263
|
"reason",
|
|
251
264
|
"evidence",
|
|
252
265
|
], RULES.singular);
|
|
@@ -266,6 +279,7 @@ const one = (raw, ids) => {
|
|
|
266
279
|
...optional("invoice", raw.invoice, (value) => reference(value, INVOICE, "an order's invoice")),
|
|
267
280
|
...optional("payment", raw.payment, (value) => reference(value, PAYMENT, "an order's payment")),
|
|
268
281
|
...optional("project", raw.project, (value) => reference(value, PROJECT, "a project")),
|
|
282
|
+
...optional("sourceUrl", raw.sourceUrl, (value) => sourceUrlOf(findingText(value, SOURCE_URL_CHARS), "order.source-url")),
|
|
269
283
|
reason: findingText(raw.reason, 2000),
|
|
270
284
|
evidence,
|
|
271
285
|
};
|
package/dist/mail/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { dropMissing, type Gone, tooManyGone } from "./gone.js";
|
|
|
9
9
|
export { type AuthVerdicts, HEADER_FIELDS, headersOf, type MailHeaders, mailHeadersOf } from "./headers.js";
|
|
10
10
|
export { type MailIdsInput, mailIds } from "./ids.js";
|
|
11
11
|
export { decodedLabelName, decodeMailboxName, labelName, systemLabel } from "./labels.js";
|
|
12
|
-
export { acknowledgeMark, advancePastSkipped, type BulkExcerpt, cursorOf, HOLD_PARTIAL_MS, HOLDER, MARK_BATCH, type MailMark, type MailWaiting, type MarkRead, mailWaiting, markOf, marksOf, readPastMark, type SkipReason, } from "./marks.js";
|
|
12
|
+
export { acknowledgeMark, advancePastSkipped, type BulkExcerpt, cursorOf, HOLD_PARTIAL_MS, HOLDER, MARK_BATCH, type MailMark, type MailWaiting, type MarkRead, type MarkRewind, mailWaiting, markOf, marksOf, type RewoundMark, readPastMark, rewindMark, type SkipReason, } from "./marks.js";
|
|
13
13
|
export { BULK_TRIAGE_DEFAULTS, type BulkSorted, type BulkTriage, excerptOf, NEWSLETTER_DEFAULTS, NEWSLETTER_SENDERS, NEWSLETTER_WORDS, type NewsletterCues, newsletterSorter, ORDER_WORDS, } from "./newsletter.js";
|
|
14
14
|
export { type MailTurn, mailTurn } from "./owed.js";
|
|
15
15
|
export { type Address, htmlToText, looksLikeHtml, MAX_ATTACHMENTS, MAX_BODY_CHARS, type ParsedMail, parseMail, } from "./parse.js";
|
package/dist/mail/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export { dropMissing, tooManyGone } from "./gone.js";
|
|
|
9
9
|
export { HEADER_FIELDS, headersOf, mailHeadersOf } from "./headers.js";
|
|
10
10
|
export { mailIds } from "./ids.js";
|
|
11
11
|
export { decodedLabelName, decodeMailboxName, labelName, systemLabel } from "./labels.js";
|
|
12
|
-
export { acknowledgeMark, advancePastSkipped, cursorOf, HOLD_PARTIAL_MS, HOLDER, MARK_BATCH, mailWaiting, markOf, marksOf, readPastMark, } from "./marks.js";
|
|
12
|
+
export { acknowledgeMark, advancePastSkipped, cursorOf, HOLD_PARTIAL_MS, HOLDER, MARK_BATCH, mailWaiting, markOf, marksOf, readPastMark, rewindMark, } from "./marks.js";
|
|
13
13
|
export { BULK_TRIAGE_DEFAULTS, excerptOf, NEWSLETTER_DEFAULTS, NEWSLETTER_SENDERS, NEWSLETTER_WORDS, newsletterSorter, ORDER_WORDS, } from "./newsletter.js";
|
|
14
14
|
export { mailTurn } from "./owed.js";
|
|
15
15
|
export { htmlToText, looksLikeHtml, MAX_ATTACHMENTS, MAX_BODY_CHARS, parseMail, } from "./parse.js";
|
package/dist/mail/marks.d.ts
CHANGED
|
@@ -89,3 +89,32 @@ export declare const advancePastSkipped: (store: MessageStore, holder: string, n
|
|
|
89
89
|
* The same cursor twice changes nothing more.
|
|
90
90
|
*/
|
|
91
91
|
export declare const acknowledgeMark: (store: MessageStore, mailbox: AccountKey, holder: string, cursor: string, now?: number) => Promise<MailMark>;
|
|
92
|
+
/** Where the owner moves a holder's mark back to: the emails received since a time, or the last n it was given. */
|
|
93
|
+
export type MarkRewind = {
|
|
94
|
+
since: number;
|
|
95
|
+
} | {
|
|
96
|
+
last: number;
|
|
97
|
+
};
|
|
98
|
+
export interface RewoundMark {
|
|
99
|
+
mark: MailMark;
|
|
100
|
+
/** Where the mark was before. */
|
|
101
|
+
fromId: number;
|
|
102
|
+
/** The emails between the new mark and the old one the holder reads whole again. */
|
|
103
|
+
again: number;
|
|
104
|
+
/** Bulk emails between them: excerpts again, unless they are surely newsletters. */
|
|
105
|
+
bulk: number;
|
|
106
|
+
/** Emails between them the holder skips again: drafts, envelopes, suspicious senders, partial mail. */
|
|
107
|
+
skipped: number;
|
|
108
|
+
/** The mark is moved only when not dry. */
|
|
109
|
+
dry: boolean;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Moves the holder's mark back, so its next reviews give it that mail again: past every email received since
|
|
113
|
+
* `since`, or past the last `last` emails it was given. The owner's alone: a holder only ever moves its own mark
|
|
114
|
+
* forward. The id is the store's insert order, so an older email stored after the earliest one picked is read again too;
|
|
115
|
+
* the answer counts them all.
|
|
116
|
+
*/
|
|
117
|
+
export declare const rewindMark: (store: MessageStore, mailbox: AccountKey, holder: string, to: MarkRewind, { now, dry }?: {
|
|
118
|
+
now?: number;
|
|
119
|
+
dry?: boolean;
|
|
120
|
+
}) => Promise<RewoundMark>;
|
package/dist/mail/marks.js
CHANGED
|
@@ -316,3 +316,89 @@ export const acknowledgeMark = async (store, mailbox, holder, cursor, now = Date
|
|
|
316
316
|
await store.sync.setSyncState(mailbox, key, JSON.stringify(next));
|
|
317
317
|
return { holder, ...next, updatedAt: now };
|
|
318
318
|
};
|
|
319
|
+
/**
|
|
320
|
+
* Moves the holder's mark back, so its next reviews give it that mail again: past every email received since
|
|
321
|
+
* `since`, or past the last `last` emails it was given. The owner's alone: a holder only ever moves its own mark
|
|
322
|
+
* forward. The id is the store's insert order, so an older email stored after the earliest one picked is read again too;
|
|
323
|
+
* the answer counts them all.
|
|
324
|
+
*/
|
|
325
|
+
export const rewindMark = async (store, mailbox, holder, to, { now = Date.now(), dry = false } = {}) => {
|
|
326
|
+
const key = keyOf(holder);
|
|
327
|
+
const account = await accountOf(store, mailbox);
|
|
328
|
+
if (!account)
|
|
329
|
+
throw new CliError("not_found", `no mail of ${mailbox.account} is stored`);
|
|
330
|
+
const mark = await markOf(store, mailbox, holder);
|
|
331
|
+
if (!mark)
|
|
332
|
+
throw new CliError("not_found", `${holder} has no mark in ${mailbox.account}: its first review starts it`, {
|
|
333
|
+
reason: "mail.no-mark",
|
|
334
|
+
});
|
|
335
|
+
if ("last" in to && (!Number.isSafeInteger(to.last) || to.last < 1))
|
|
336
|
+
throw new CliError("validation_error", "--last takes a whole number of emails, 1 or more");
|
|
337
|
+
if ("since" in to && !Number.isFinite(to.since))
|
|
338
|
+
throw new CliError("validation_error", "--since-time takes a time");
|
|
339
|
+
const given = (email) => {
|
|
340
|
+
const said = verdict(email, now);
|
|
341
|
+
return said === "read" || said === "held" || said === "bulk";
|
|
342
|
+
};
|
|
343
|
+
const between = [];
|
|
344
|
+
let through = mark.throughId;
|
|
345
|
+
let before = mark.throughId + 1;
|
|
346
|
+
for (let counted = 0;;) {
|
|
347
|
+
const page = await store.mail.emailsBefore(account.id, before, MARK_BATCH);
|
|
348
|
+
if (page.length === 0) {
|
|
349
|
+
if ("last" in to && between.length > 0)
|
|
350
|
+
through = 0;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
let done = false;
|
|
354
|
+
for (const email of page) {
|
|
355
|
+
if ("last" in to) {
|
|
356
|
+
between.push(email);
|
|
357
|
+
if (given(email))
|
|
358
|
+
counted += 1;
|
|
359
|
+
if (counted === to.last) {
|
|
360
|
+
through = email.id - 1;
|
|
361
|
+
done = true;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
// An email is stored after it arrives, so once stored before the time, none further back arrived after it.
|
|
367
|
+
if (email.storedAt < to.since - CLOCK_SLACK_MS) {
|
|
368
|
+
done = true;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
between.push(email);
|
|
372
|
+
if ((email.at ?? email.storedAt) >= to.since)
|
|
373
|
+
through = email.id - 1;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (done)
|
|
377
|
+
break;
|
|
378
|
+
before = page.at(-1)?.id ?? 0;
|
|
379
|
+
}
|
|
380
|
+
const moved = between.filter((email) => email.id > through);
|
|
381
|
+
const count = (pick) => moved.filter((email) => pick(verdict(email, now))).length;
|
|
382
|
+
const answer = {
|
|
383
|
+
fromId: mark.throughId,
|
|
384
|
+
again: count((said) => said === "read" || said === "held"),
|
|
385
|
+
bulk: count((said) => said === "bulk"),
|
|
386
|
+
skipped: count((said) => said !== "read" && said !== "held" && said !== "bulk"),
|
|
387
|
+
dry,
|
|
388
|
+
};
|
|
389
|
+
if (through >= mark.throughId)
|
|
390
|
+
return { mark, ...answer };
|
|
391
|
+
const [last] = through > 0 ? await store.mail.emailsBefore(account.id, through + 1, 1) : [];
|
|
392
|
+
const at = last?.id === through ? last.at : null;
|
|
393
|
+
const next = {
|
|
394
|
+
throughId: through,
|
|
395
|
+
throughAt: at,
|
|
396
|
+
held: mark.held.filter((id) => id <= through),
|
|
397
|
+
suspicious: mark.suspicious.filter((id) => id <= through),
|
|
398
|
+
read: mark.read,
|
|
399
|
+
skipped: mark.skipped,
|
|
400
|
+
};
|
|
401
|
+
if (!dry)
|
|
402
|
+
await store.sync.setSyncState(mailbox, key, JSON.stringify(next));
|
|
403
|
+
return { mark: { holder, ...next, updatedAt: dry ? mark.updatedAt : now }, ...answer };
|
|
404
|
+
};
|
|
@@ -39,6 +39,10 @@ export interface TaskFinding extends FindingBase {
|
|
|
39
39
|
dueEvidence?: string[];
|
|
40
40
|
assignee?: string;
|
|
41
41
|
assigneeEvidence?: string[];
|
|
42
|
+
/** Mail only: `<service>:<id>` in the outside service the mail came from, such as `avito:4512345678`. */
|
|
43
|
+
externalId?: string;
|
|
44
|
+
/** Mail only: the http or https page the task is about, such as the conversation the mail links to. */
|
|
45
|
+
sourceUrl?: string;
|
|
42
46
|
}
|
|
43
47
|
export type Finding = FactFinding | TaskFinding | PaymentFinding;
|
|
44
48
|
export interface FindingsInput {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { amounts } from "./amounts.js";
|
|
2
2
|
import { children } from "./children.js";
|
|
3
3
|
import { coverage } from "./coverage.js";
|
|
4
|
-
import { lastMessage } from "./last-message.js";
|
|
5
4
|
import { payments } from "./payments.js";
|
|
6
5
|
import { promises } from "./promises.js";
|
|
7
6
|
import { questions } from "./questions.js";
|
|
@@ -9,7 +8,6 @@ import { replyNext } from "./reply-next.js";
|
|
|
9
8
|
import { topics } from "./topics.js";
|
|
10
9
|
/** The registry; its order is the answer's order. A new signal is one file here and its row in the CLI. */
|
|
11
10
|
export const STATUS_SIGNALS = [
|
|
12
|
-
lastMessage,
|
|
13
11
|
replyNext,
|
|
14
12
|
questions,
|
|
15
13
|
promises,
|
|
@@ -67,6 +67,8 @@ export interface MailActivityStore {
|
|
|
67
67
|
* processing mark reads next. The id is the store's insert order, so mail stored late is never behind the mark.
|
|
68
68
|
*/
|
|
69
69
|
emailsAfter(accountId: number, afterId: number, limit: number, ids?: readonly number[]): Promise<MarkedEmail[]>;
|
|
70
|
+
/** The account's live emails stored before `beforeId`, newest first, at most `limit`: what a mark moved back reads. */
|
|
71
|
+
emailsBefore(accountId: number, beforeId: number, limit: number): Promise<MarkedEmail[]>;
|
|
70
72
|
/** The account's newest store id; 0 with no mail. */
|
|
71
73
|
newestEmailId(accountId: number): Promise<number>;
|
|
72
74
|
/** Every `sync_cursors` value of the account whose key starts with `prefix`. */
|
|
@@ -125,6 +125,10 @@ export const emailsAfter = (database, accountId, afterId, limit, ids = []) => {
|
|
|
125
125
|
};
|
|
126
126
|
export const mailActivity = (database) => ({
|
|
127
127
|
emailsAfter: async (accountId, afterId, limit, ids) => emailsAfter(database, accountId, afterId, limit, ids),
|
|
128
|
+
emailsBefore: async (accountId, beforeId, limit) => database
|
|
129
|
+
.prepare(`${MARKED} WHERE e.id < ? AND e.account_id = ? AND e.deleted_at IS NULL ORDER BY e.id DESC LIMIT ?`)
|
|
130
|
+
.all(beforeId, accountId, limit)
|
|
131
|
+
.map(markedOf),
|
|
128
132
|
newestEmailId: async (accountId) => Number(database.prepare("SELECT max(id) AS id FROM emails WHERE account_id = ?").get(accountId)?.id ?? 0),
|
|
129
133
|
statesLike: async (accountId, prefix) => database
|
|
130
134
|
.prepare("SELECT key, value, updated_at FROM sync_cursors WHERE account_id = ? AND substr(key, 1, ?) = ?")
|
|
@@ -93,6 +93,8 @@ export const findingsCoreOver = (context, kind) => {
|
|
|
93
93
|
type: finding.type,
|
|
94
94
|
description: `${finding.commitment}: ${finding.reason}`,
|
|
95
95
|
...(finding.dueAt ? { dueAt: new Date(finding.dueAt) } : {}),
|
|
96
|
+
...(finding.externalId ? { externalId: finding.externalId } : {}),
|
|
97
|
+
...(finding.sourceUrl ? { sourceUrl: finding.sourceUrl } : {}),
|
|
96
98
|
}, agent);
|
|
97
99
|
const id = taskRowOf(database, task.id);
|
|
98
100
|
if (id === undefined)
|
|
@@ -67,6 +67,7 @@ export const MANIFEST = [
|
|
|
67
67
|
{ name: "20261104232153_version-49-relation-amounts", version: 49, minCompatible: 34 },
|
|
68
68
|
{ name: "20261105125314_version-50-orders", version: 50, minCompatible: 34 },
|
|
69
69
|
{ name: "20261105125315_version-50-orders-rules", version: 50, minCompatible: 34 },
|
|
70
|
+
{ name: "20261105144549_version-51-task-links", version: 51, minCompatible: 34 },
|
|
70
71
|
];
|
|
71
72
|
export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
|
|
72
73
|
const migrations = [];
|
|
@@ -974,5 +974,15 @@ export const GENERATED = [
|
|
|
974
974
|
"-- The store's code checks these first and says why; the triggers keep a stray write from another build or a hand\n-- edit from storing an order with an unknown status or purpose, or an amount without its currency.\nCREATE TRIGGER orders_valid_bi BEFORE INSERT ON orders\n WHEN new.customer_type NOT IN ('person', 'organization')\n OR new.purpose NOT IN ('personal', 'business', 'client', 'other')\n OR new.status NOT IN ('placed', 'shipped', 'delivered', 'cancelled', 'returned')\n OR new.review NOT IN ('proposed', 'confirmed', 'rejected')\n OR (new.total IS NULL) <> (new.currency IS NULL) OR new.total < 0 BEGIN\n SELECT RAISE(ABORT, 'orders: a person or organization customer, a known purpose, status and review, an amount with its currency');\nEND;",
|
|
975
975
|
"CREATE TRIGGER orders_valid_bu BEFORE UPDATE ON orders\n WHEN new.customer_type NOT IN ('person', 'organization')\n OR new.purpose NOT IN ('personal', 'business', 'client', 'other')\n OR new.status NOT IN ('placed', 'shipped', 'delivered', 'cancelled', 'returned')\n OR new.review NOT IN ('proposed', 'confirmed', 'rejected')\n OR (new.total IS NULL) <> (new.currency IS NULL) OR new.total < 0 BEGIN\n SELECT RAISE(ABORT, 'orders: a person or organization customer, a known purpose, status and review, an amount with its currency');\nEND;"
|
|
976
976
|
]
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
"name": "20261105144549_version-51-task-links",
|
|
980
|
+
"statements": [
|
|
981
|
+
"ALTER TABLE `orders` ADD `source_url` text;",
|
|
982
|
+
"ALTER TABLE `tasks` ADD `provider` text;",
|
|
983
|
+
"ALTER TABLE `tasks` ADD `external_id` text;",
|
|
984
|
+
"ALTER TABLE `tasks` ADD `source_url` text;",
|
|
985
|
+
"CREATE INDEX `tasks_by_provider_external_id` ON `tasks` (`provider`,`external_id`) WHERE external_id IS NOT NULL;"
|
|
986
|
+
]
|
|
977
987
|
}
|
|
978
988
|
];
|
|
@@ -26,6 +26,8 @@ export interface Order extends Reviewed {
|
|
|
26
26
|
fulfilledDate: string | null;
|
|
27
27
|
invoice: string | null;
|
|
28
28
|
payment: string | null;
|
|
29
|
+
/** The order's page at the shop, http or https. */
|
|
30
|
+
sourceUrl: string | null;
|
|
29
31
|
}
|
|
30
32
|
export interface OrderInput extends Written {
|
|
31
33
|
/** `person:` or `organization:`; the owner's own person when left out. */
|
|
@@ -43,6 +45,8 @@ export interface OrderInput extends Written {
|
|
|
43
45
|
fulfilledDate?: string;
|
|
44
46
|
invoice?: string;
|
|
45
47
|
payment?: string;
|
|
48
|
+
/** The order's page at the shop: an http or https address. */
|
|
49
|
+
sourceUrl?: string;
|
|
46
50
|
}
|
|
47
51
|
export interface OrderFilter {
|
|
48
52
|
status?: OrderStatus;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { sourceUrlOf } from "../../domain/external-links.js";
|
|
2
3
|
import { FINANCE_REVIEWS } from "../../domain/finance.js";
|
|
3
4
|
import { currencyOf, formatDecimal, minorUnits, scaleOf } from "../../domain/money.js";
|
|
4
5
|
import { ORDER_PURPOSES, ORDER_STATUSES, orderTitle } from "../../domain/orders.js";
|
|
@@ -42,6 +43,7 @@ export const orderOperationsOver = (context) => {
|
|
|
42
43
|
fulfilledDate: row.fulfilled_date == null ? null : String(row.fulfilled_date),
|
|
43
44
|
invoice: row.invoice_id == null ? null : `invoice:${row.invoice_id}`,
|
|
44
45
|
payment: row.payment_id == null ? null : `payment:${row.payment_id}`,
|
|
46
|
+
sourceUrl: row.source_url == null ? null : String(row.source_url),
|
|
45
47
|
...reviewedOf(row, "order"),
|
|
46
48
|
};
|
|
47
49
|
};
|
|
@@ -96,6 +98,7 @@ export const orderOperationsOver = (context) => {
|
|
|
96
98
|
refuse("an order's amount is 0 or more", "money.amount");
|
|
97
99
|
const orderDate = isoDate(input.orderDate, "an order's date");
|
|
98
100
|
const fulfilledDate = isoDate(input.fulfilledDate, "an order's delivery date");
|
|
101
|
+
const sourceUrl = input.sourceUrl === undefined ? null : sourceUrlOf(input.sourceUrl, "order.source-url");
|
|
99
102
|
const id = write(database, () => {
|
|
100
103
|
if (number !== null && ordersOfShop(database, supplierName, number).length)
|
|
101
104
|
refuse(`order ${number} from ${supplierName} is already stored; change its status instead`, "order.duplicate");
|
|
@@ -108,9 +111,9 @@ export const orderOperationsOver = (context) => {
|
|
|
108
111
|
const made = Number(database
|
|
109
112
|
.prepare("INSERT INTO orders (customer_type, customer_id, purpose, supplier_type, supplier_id, supplier_name, number, " +
|
|
110
113
|
"currency, total, status, order_date, fulfilled_date, invoice_id, payment_id, project_id, review, " +
|
|
111
|
-
"author_type, author_id, model, confirmed_by_type, confirmed_by_id, source, metadata, created_at, updated_at
|
|
112
|
-
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id")
|
|
113
|
-
.get(customer.type, customer.id, purpose, supplier?.type ?? null, supplier?.id ?? null, supplierName, number, currency, total, status, orderDate, fulfilledDate, invoice, payment, by.project, by.review, by.actor.type, by.actor.id, by.model, by.confirmedType, by.confirmedId, by.source, by.metadata, at, at)?.id);
|
|
114
|
+
"author_type, author_id, model, confirmed_by_type, confirmed_by_id, source, metadata, created_at, updated_at, " +
|
|
115
|
+
"source_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id")
|
|
116
|
+
.get(customer.type, customer.id, purpose, supplier?.type ?? null, supplier?.id ?? null, supplierName, number, currency, total, status, orderDate, fulfilledDate, invoice, payment, by.project, by.review, by.actor.type, by.actor.id, by.model, by.confirmedType, by.confirmedId, by.source, by.metadata, at, at, sourceUrl)?.id);
|
|
114
117
|
keepEvidence("order", made, by);
|
|
115
118
|
return made;
|
|
116
119
|
});
|