@wirecat/cli-core 0.575.0 → 0.577.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 +37 -0
- package/dist/cli/findings-input.js +2 -0
- package/dist/cli/messenger/tasks-command.js +2 -0
- 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/bulk-settings.d.ts +6 -0
- package/dist/mail/cli/bulk-settings.js +65 -0
- package/dist/mail/cli/index.d.ts +4 -0
- package/dist/mail/cli/index.js +3 -1
- package/dist/mail/cli/marks.d.ts +6 -0
- package/dist/mail/cli/marks.js +69 -1
- package/dist/mail/cli/review.js +34 -13
- package/dist/mail/findings.d.ts +2 -0
- package/dist/mail/findings.js +15 -1
- package/dist/mail/index.d.ts +2 -1
- package/dist/mail/index.js +2 -1
- package/dist/mail/marks.d.ts +48 -7
- package/dist/mail/marks.js +158 -16
- package/dist/mail/newsletter.d.ts +52 -0
- package/dist/mail/newsletter.js +189 -0
- package/dist/mail/review.d.ts +2 -0
- package/dist/meetings/findings-rules.d.ts +4 -0
- 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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mail sent to many is often an order, an invoice or a site's lead form that carries List-Unsubscribe. This tells a
|
|
3
|
+
* newsletter from it by words in the subject and the first lines, so a holder can leave out only what is surely a
|
|
4
|
+
* newsletter and give the rest to its agent as an excerpt. No model: it runs on every read past a mark.
|
|
5
|
+
*/
|
|
6
|
+
/** Words that say "advertising"; each matches at the start of a word, any case. */
|
|
7
|
+
export const NEWSLETTER_WORDS = [
|
|
8
|
+
"скидк",
|
|
9
|
+
"акци",
|
|
10
|
+
"распродаж",
|
|
11
|
+
"вебинар",
|
|
12
|
+
"рассылк",
|
|
13
|
+
"промокод",
|
|
14
|
+
"спецпредложени",
|
|
15
|
+
"дайджест",
|
|
16
|
+
"новинк",
|
|
17
|
+
"подборк",
|
|
18
|
+
"кешбэк",
|
|
19
|
+
"кэшбэк",
|
|
20
|
+
"только сегодня",
|
|
21
|
+
"успейте",
|
|
22
|
+
"чёрная пятница",
|
|
23
|
+
"черная пятница",
|
|
24
|
+
"выгодн",
|
|
25
|
+
"newsletter",
|
|
26
|
+
"unsubscribe",
|
|
27
|
+
"% off",
|
|
28
|
+
"sale",
|
|
29
|
+
"discount",
|
|
30
|
+
"webinar",
|
|
31
|
+
"digest",
|
|
32
|
+
"weekly",
|
|
33
|
+
"promo",
|
|
34
|
+
"deal",
|
|
35
|
+
"limited time",
|
|
36
|
+
"free shipping",
|
|
37
|
+
"new arrivals",
|
|
38
|
+
"black friday",
|
|
39
|
+
"cyber monday",
|
|
40
|
+
"coupon",
|
|
41
|
+
"save up to",
|
|
42
|
+
"last chance",
|
|
43
|
+
"don't miss",
|
|
44
|
+
"descuento",
|
|
45
|
+
"oferta",
|
|
46
|
+
"rebaja",
|
|
47
|
+
"boletín",
|
|
48
|
+
"seminario web",
|
|
49
|
+
"novedades",
|
|
50
|
+
"promoción",
|
|
51
|
+
"no te pierdas",
|
|
52
|
+
"últimos días",
|
|
53
|
+
];
|
|
54
|
+
/** Parts of a sender's address that say "a newsletter": `news@…`, `newsletter.example.com`, `….substack.com`. */
|
|
55
|
+
export const NEWSLETTER_SENDERS = [
|
|
56
|
+
"news",
|
|
57
|
+
"newsletter",
|
|
58
|
+
"newsletters",
|
|
59
|
+
"digest",
|
|
60
|
+
"weekly",
|
|
61
|
+
"promo",
|
|
62
|
+
"marketing",
|
|
63
|
+
"offers",
|
|
64
|
+
"deals",
|
|
65
|
+
"sale",
|
|
66
|
+
"substack.com",
|
|
67
|
+
];
|
|
68
|
+
/** Words that say "this is about something of yours"; one of them keeps the email for the agent. */
|
|
69
|
+
export const ORDER_WORDS = [
|
|
70
|
+
"заказ",
|
|
71
|
+
"счёт",
|
|
72
|
+
"счет на",
|
|
73
|
+
"счета",
|
|
74
|
+
"оплат",
|
|
75
|
+
"квитанци",
|
|
76
|
+
"кассовый чек",
|
|
77
|
+
"электронный чек",
|
|
78
|
+
"заявк",
|
|
79
|
+
"форма",
|
|
80
|
+
"доставк",
|
|
81
|
+
"трек",
|
|
82
|
+
"договор",
|
|
83
|
+
"подтвержд",
|
|
84
|
+
"код подтверждения",
|
|
85
|
+
"код для входа",
|
|
86
|
+
"пароль",
|
|
87
|
+
"order",
|
|
88
|
+
"invoice",
|
|
89
|
+
"receipt",
|
|
90
|
+
"payment",
|
|
91
|
+
"shipped",
|
|
92
|
+
"delivery",
|
|
93
|
+
"tracking",
|
|
94
|
+
"booking",
|
|
95
|
+
"reservation",
|
|
96
|
+
"confirm",
|
|
97
|
+
"your account",
|
|
98
|
+
"verification",
|
|
99
|
+
"password",
|
|
100
|
+
"refund",
|
|
101
|
+
"statement",
|
|
102
|
+
"pedido",
|
|
103
|
+
"factura",
|
|
104
|
+
"recibo",
|
|
105
|
+
"pago",
|
|
106
|
+
"envío",
|
|
107
|
+
"reserva",
|
|
108
|
+
"confirmación",
|
|
109
|
+
"contraseña",
|
|
110
|
+
];
|
|
111
|
+
/** A number an order, an invoice or a request is known by, after the word that names it. */
|
|
112
|
+
const REFERENCE_NUMBER = /(?<![\p{L}])(?:заказ|заявк|сч[её]т|квитанци|order|invoice|ticket|pedido|factura)\p{L}*\s*(?:№|#|n[o°]\.?)?\s?\d{3,}/iu;
|
|
113
|
+
/** The top of a newsletter: a link to read it in a browser, or to leave the list. */
|
|
114
|
+
const NEWSLETTER_TOP = /в браузере|веб-верси|view (?:it |this email )?(?:in|on) (?:your |a )?browser|unsubscribe|отписаться|darse de baja|ver en (?:el )?navegador/iu;
|
|
115
|
+
const PICTOGRAPH = /\p{Extended_Pictographic}/u;
|
|
116
|
+
/** A reply or a forward is a conversation, whatever it says. */
|
|
117
|
+
const REPLY = /^\s*(?:re|fw|fwd|aw|wg|ответ|отв|rv|res)\s*(?:\[\d+\])?\s*:/iu;
|
|
118
|
+
export const NEWSLETTER_DEFAULTS = {
|
|
119
|
+
newsletterWords: NEWSLETTER_WORDS,
|
|
120
|
+
newsletterSenders: NEWSLETTER_SENDERS,
|
|
121
|
+
orderWords: ORDER_WORDS,
|
|
122
|
+
threshold: 2,
|
|
123
|
+
excerptChars: 400,
|
|
124
|
+
};
|
|
125
|
+
const escaped = (word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
126
|
+
const matcherOf = (words) => {
|
|
127
|
+
const list = words.map((word) => word.trim().toLowerCase()).filter(Boolean);
|
|
128
|
+
if (list.length === 0)
|
|
129
|
+
return () => [];
|
|
130
|
+
const pattern = new RegExp(`(?<![\\p{L}\\p{N}])(?:${list.map(escaped).join("|")})`, "giu");
|
|
131
|
+
return (text) => [...new Set([...text.toLowerCase().matchAll(pattern)].map(([found]) => found))];
|
|
132
|
+
};
|
|
133
|
+
/** The first lines of an email's own text, at most `chars`, cut at a word. */
|
|
134
|
+
export const excerptOf = (text, chars) => {
|
|
135
|
+
const flat = text
|
|
136
|
+
.split("\n")
|
|
137
|
+
.map((line) => line.trim())
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.join("\n");
|
|
140
|
+
if (flat.length <= chars)
|
|
141
|
+
return flat;
|
|
142
|
+
const cut = flat.slice(0, chars);
|
|
143
|
+
const space = cut.search(/\s\S*$/);
|
|
144
|
+
return `${space > chars / 2 ? cut.slice(0, space) : cut}…`;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Built once per read, so the word lists compile once. A reply, an order word in the subject or an order's number
|
|
148
|
+
* keeps the email. Otherwise a newsletter word counts 2 in the subject and 1 in the first lines, a newsletter's
|
|
149
|
+
* address 2, an emoji in the subject and a browser or unsubscribe link at the top 1 each, and each order word in
|
|
150
|
+
* the first lines takes 2 off. Measured 2026-11-05 on 30 days of the owner's bulk mail: 99 of 100 sampled
|
|
151
|
+
* newsletter verdicts were newsletters, and about half of the newsletters were found.
|
|
152
|
+
*/
|
|
153
|
+
export const newsletterSorter = (cues = NEWSLETTER_DEFAULTS) => {
|
|
154
|
+
const advertising = matcherOf(cues.newsletterWords);
|
|
155
|
+
const ordered = matcherOf(cues.orderWords);
|
|
156
|
+
const senders = cues.newsletterSenders.map((part) => part.trim().toLowerCase()).filter(Boolean);
|
|
157
|
+
const fromNewsletter = (address) => {
|
|
158
|
+
const domain = address.split("@")[1] ?? "";
|
|
159
|
+
const parts = new Set(address.split(/[.@_+-]/));
|
|
160
|
+
return senders.some((one) => (one.includes(".") ? domain === one || domain.endsWith(`.${one}`) : parts.has(one)));
|
|
161
|
+
};
|
|
162
|
+
return ({ subject, text, from }) => {
|
|
163
|
+
const excerpt = excerptOf(text, cues.excerptChars);
|
|
164
|
+
const title = subject ?? "";
|
|
165
|
+
const number = REFERENCE_NUMBER.exec(`${title}\n${excerpt}`);
|
|
166
|
+
const kept = [...(REPLY.test(title) ? ["a reply"] : []), ...ordered(title), ...(number ? [number[0]] : [])];
|
|
167
|
+
if (kept.length > 0)
|
|
168
|
+
return { verdict: "excerpt", score: 0, cues: kept, excerpt };
|
|
169
|
+
const inTitle = advertising(title);
|
|
170
|
+
const signs = [
|
|
171
|
+
...inTitle.map((word) => [word, 2]),
|
|
172
|
+
...advertising(excerpt)
|
|
173
|
+
.filter((word) => !inTitle.includes(word))
|
|
174
|
+
.map((word) => [word, 1]),
|
|
175
|
+
...(PICTOGRAPH.test(title) ? [["an emoji in the subject", 1]] : []),
|
|
176
|
+
...(NEWSLETTER_TOP.test(excerpt) ? [["a browser or unsubscribe link", 1]] : []),
|
|
177
|
+
...(fromNewsletter((from ?? "").toLowerCase()) ? [["a newsletter's address", 2]] : []),
|
|
178
|
+
...ordered(excerpt).map((word) => [`-${word}`, -2]),
|
|
179
|
+
];
|
|
180
|
+
const score = signs.reduce((sum, [, weight]) => sum + weight, 0);
|
|
181
|
+
return {
|
|
182
|
+
verdict: score >= cues.threshold ? "newsletter" : "excerpt",
|
|
183
|
+
score,
|
|
184
|
+
cues: signs.map(([cue]) => cue),
|
|
185
|
+
excerpt,
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
export const BULK_TRIAGE_DEFAULTS = { excerpts: true, perRead: 30, cues: NEWSLETTER_DEFAULTS };
|
package/dist/mail/review.d.ts
CHANGED
|
@@ -18,8 +18,10 @@ export interface MailReviewOptions {
|
|
|
18
18
|
now?: number;
|
|
19
19
|
}
|
|
20
20
|
/** A review of mail: each thread a chat, each email a stored hit with its locator. */
|
|
21
|
+
/** `excerptOnly`: a bulk email a holder gets as its subject and first lines, its `text` cut to them. */
|
|
21
22
|
export type MailHit = StoredHit & {
|
|
22
23
|
marks?: EmailMarks;
|
|
24
|
+
excerptOnly?: true;
|
|
23
25
|
};
|
|
24
26
|
export interface MailReview extends Review {
|
|
25
27
|
chats: (Omit<ReviewChat, "messages"> & {
|
|
@@ -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 {
|
|
@@ -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
|
});
|
|
@@ -12747,6 +12747,54 @@ export declare const tasks: import("drizzle-orm/sqlite-core").SQLiteTableWithCol
|
|
|
12747
12747
|
identity: undefined;
|
|
12748
12748
|
generated: undefined;
|
|
12749
12749
|
}, {}>;
|
|
12750
|
+
provider: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
12751
|
+
name: string;
|
|
12752
|
+
tableName: "tasks";
|
|
12753
|
+
dataType: "string";
|
|
12754
|
+
data: string;
|
|
12755
|
+
driverParam: string;
|
|
12756
|
+
notNull: false;
|
|
12757
|
+
hasDefault: false;
|
|
12758
|
+
isPrimaryKey: false;
|
|
12759
|
+
isAutoincrement: false;
|
|
12760
|
+
hasRuntimeDefault: false;
|
|
12761
|
+
enumValues: [string, ...string[]];
|
|
12762
|
+
baseColumn: never;
|
|
12763
|
+
identity: undefined;
|
|
12764
|
+
generated: undefined;
|
|
12765
|
+
}, {}>;
|
|
12766
|
+
externalId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
12767
|
+
name: string;
|
|
12768
|
+
tableName: "tasks";
|
|
12769
|
+
dataType: "string";
|
|
12770
|
+
data: string;
|
|
12771
|
+
driverParam: string;
|
|
12772
|
+
notNull: false;
|
|
12773
|
+
hasDefault: false;
|
|
12774
|
+
isPrimaryKey: false;
|
|
12775
|
+
isAutoincrement: false;
|
|
12776
|
+
hasRuntimeDefault: false;
|
|
12777
|
+
enumValues: [string, ...string[]];
|
|
12778
|
+
baseColumn: never;
|
|
12779
|
+
identity: undefined;
|
|
12780
|
+
generated: undefined;
|
|
12781
|
+
}, {}>;
|
|
12782
|
+
sourceUrl: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
12783
|
+
name: string;
|
|
12784
|
+
tableName: "tasks";
|
|
12785
|
+
dataType: "string";
|
|
12786
|
+
data: string;
|
|
12787
|
+
driverParam: string;
|
|
12788
|
+
notNull: false;
|
|
12789
|
+
hasDefault: false;
|
|
12790
|
+
isPrimaryKey: false;
|
|
12791
|
+
isAutoincrement: false;
|
|
12792
|
+
hasRuntimeDefault: false;
|
|
12793
|
+
enumValues: [string, ...string[]];
|
|
12794
|
+
baseColumn: never;
|
|
12795
|
+
identity: undefined;
|
|
12796
|
+
generated: undefined;
|
|
12797
|
+
}, {}>;
|
|
12750
12798
|
};
|
|
12751
12799
|
dialect: 'sqlite';
|
|
12752
12800
|
isAlias: false;
|
|
@@ -15326,6 +15374,22 @@ export declare const orders: import("drizzle-orm/sqlite-core").SQLiteTableWithCo
|
|
|
15326
15374
|
identity: undefined;
|
|
15327
15375
|
generated: undefined;
|
|
15328
15376
|
}, {}>;
|
|
15377
|
+
sourceUrl: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
15378
|
+
name: string;
|
|
15379
|
+
tableName: "orders";
|
|
15380
|
+
dataType: "string";
|
|
15381
|
+
data: string;
|
|
15382
|
+
driverParam: string;
|
|
15383
|
+
notNull: false;
|
|
15384
|
+
hasDefault: false;
|
|
15385
|
+
isPrimaryKey: false;
|
|
15386
|
+
isAutoincrement: false;
|
|
15387
|
+
hasRuntimeDefault: false;
|
|
15388
|
+
enumValues: [string, ...string[]];
|
|
15389
|
+
baseColumn: never;
|
|
15390
|
+
identity: undefined;
|
|
15391
|
+
generated: undefined;
|
|
15392
|
+
}, {}>;
|
|
15329
15393
|
};
|
|
15330
15394
|
dialect: 'sqlite';
|
|
15331
15395
|
isAlias: false;
|
|
@@ -1271,6 +1271,9 @@ export const tasks = sqliteTable("tasks", {
|
|
|
1271
1271
|
priorityScore: real("priority_score"),
|
|
1272
1272
|
agent: text("agent"),
|
|
1273
1273
|
revision: integer("revision").notNull().default(1),
|
|
1274
|
+
provider: text("provider"),
|
|
1275
|
+
externalId: text("external_id"),
|
|
1276
|
+
sourceUrl: text("source_url"),
|
|
1274
1277
|
}, (table) => [
|
|
1275
1278
|
unique().on(table.projectId, table.number),
|
|
1276
1279
|
index("tasks_by_title_folded").on(table.titleFolded),
|
|
@@ -1283,6 +1286,7 @@ export const tasks = sqliteTable("tasks", {
|
|
|
1283
1286
|
index("tasks_by_project_id_status_next_check_at").on(table.projectId, table.status, table.nextCheckAt),
|
|
1284
1287
|
index("tasks_by_project_id_source_locator").on(table.projectId, table.sourceLocator),
|
|
1285
1288
|
index("tasks_by_source_group").on(table.sourceGroup),
|
|
1289
|
+
index("tasks_by_provider_external_id").on(table.provider, table.externalId).where(sql `external_id IS NOT NULL`),
|
|
1286
1290
|
index("tasks_by_parent_id").on(table.parentId),
|
|
1287
1291
|
index("tasks_by_closed_by_type_closed_by_id").on(table.closedByType, table.closedById),
|
|
1288
1292
|
index("tasks_by_author_type_author_id").on(table.authorType, table.authorId),
|
|
@@ -1521,6 +1525,7 @@ export const orders = sqliteTable("orders", {
|
|
|
1521
1525
|
createdAt: integer("created_at").notNull(),
|
|
1522
1526
|
updatedAt: integer("updated_at").notNull(),
|
|
1523
1527
|
deletedAt: integer("deleted_at"),
|
|
1528
|
+
sourceUrl: text("source_url"),
|
|
1524
1529
|
}, (table) => [
|
|
1525
1530
|
index("orders_by_customer_type_customer_id").on(table.customerType, table.customerId),
|
|
1526
1531
|
index("orders_by_supplier_type_supplier_id").on(table.supplierType, table.supplierId),
|
|
@@ -39,6 +39,10 @@ export interface TaskFields {
|
|
|
39
39
|
project?: string;
|
|
40
40
|
/** A project task's kind, bug, feature or chore. */
|
|
41
41
|
type?: WorkTaskType;
|
|
42
|
+
/** `<service>:<id>`, the task's id in an outside service, such as `avito:4512345678`; `null` clears it. */
|
|
43
|
+
externalId?: string | null;
|
|
44
|
+
/** The http or https page the task is about, such as an Avito ad; `null` clears it. */
|
|
45
|
+
sourceUrl?: string | null;
|
|
42
46
|
}
|
|
43
47
|
export interface NewWorkTask extends Omit<TaskFields, "reason" | "project" | "type"> {
|
|
44
48
|
/** `project:<id>` or the project's key. */
|
|
@@ -75,6 +79,9 @@ export interface TaskDetails {
|
|
|
75
79
|
parent: string | null;
|
|
76
80
|
/** What a task found in a message points at; null for a task made here. */
|
|
77
81
|
source: string | null;
|
|
82
|
+
/** `<service>:<id>` in an outside service, such as `avito:4512345678`. */
|
|
83
|
+
externalId: string | null;
|
|
84
|
+
sourceUrl: string | null;
|
|
78
85
|
dueAt: string | null;
|
|
79
86
|
nextCheckAt: string | null;
|
|
80
87
|
closedAt: string | null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { timezoneOf } from "@wirecat/cli-base/query";
|
|
3
|
+
import { externalIdOf, externalIdText, sourceUrlOf } from "../../domain/external-links.js";
|
|
3
4
|
import { TASK_TYPES, WORK_TASK_TYPES } from "../../domain/knowledge.js";
|
|
4
5
|
import { checkedScore, isClosedState, legacyPriority, PRIORITY_LABELS, priorityBand, priorityLabel, TASK_STATES, } from "../../tasks/index.js";
|
|
5
6
|
import { folded } from "./folded.js";
|
|
@@ -136,6 +137,8 @@ export const workTaskOperationsOver = (context) => {
|
|
|
136
137
|
revision: Number(row.revision),
|
|
137
138
|
parent: row.parent_id == null ? null : reference(Number(row.parent_id)),
|
|
138
139
|
source: row.source_locator == null ? null : String(row.source_locator),
|
|
140
|
+
externalId: externalIdText(row.provider, row.external_id),
|
|
141
|
+
sourceUrl: row.source_url == null ? null : String(row.source_url),
|
|
139
142
|
dueAt: iso(row.due_at),
|
|
140
143
|
nextCheckAt: iso(row.next_check_at),
|
|
141
144
|
closedAt: iso(row.closed_at),
|
|
@@ -282,6 +285,13 @@ export const workTaskOperationsOver = (context) => {
|
|
|
282
285
|
change("close_reason", reason);
|
|
283
286
|
change("close_reason_folded", folded(reason));
|
|
284
287
|
}
|
|
288
|
+
if (fields.externalId !== undefined) {
|
|
289
|
+
const external = fields.externalId === null ? null : externalIdOf(fields.externalId);
|
|
290
|
+
change("provider", external?.provider ?? null);
|
|
291
|
+
change("external_id", external?.id ?? null);
|
|
292
|
+
}
|
|
293
|
+
if (fields.sourceUrl !== undefined)
|
|
294
|
+
change("source_url", fields.sourceUrl === null ? null : sourceUrlOf(fields.sourceUrl));
|
|
285
295
|
if (fields.category !== undefined)
|
|
286
296
|
change("category", fields.category, "category");
|
|
287
297
|
if (fields.type !== undefined && fields.type !== before.type) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.577.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|